Action safety: user-approvable ACL / permission gating for dispatched actions
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- typescript
- Domain
- authorization, backend-api-design, security
Research direction
Start with executeAction() in ts/packages/dispatcher/dispatcher/src/execute/actionHandlers.ts, then read TaskPolicy and ApprovalFn in the workflow model and runner for the policy precedent. Review the listed session, agent SDK, schema, and collision-preference files before resolving the open risk-source and precedence questions. Done first means a design note recommending the model and defining its decision and persistence rules.
Written by the indexing model from the issue text.
Description
Summary
Introduce a first-class action safety layer in the dispatcher so that potentially sensitive or destructive typed actions can be gated behind user approval and governed by an ACL-style policy. Today, once an action is translated and validated, it is dispatched straight to the owning agent's executeAction() with no permission check and no user confirmation. This issue proposes a mechanism for users to approve actions (with an "always allow" memory) plus an ACL / policy model so we can ship with action safety.
Scope note: This is separate from the reasoning-loop tool-call approval (the
approveAll/onPermissionRequestposture in the Claude/Copilot reasoning adapters), which is tracked independently. This issue targets the general dispatcher action-execution path that every agent action flows through.
Motivation
TypeAgent routes natural language into typed actions, many of which have real-world side effects and are irreversible. Examples across current agents:
- Destructive / side-effecting:
sendEmail/replyEmail/forwardEmail(email),removeEvent/scheduleEvent/addParticipant(calendar),prMerge/deleteRepo/deleteIssue/deleteGist(github-cli),removeItems/clearList(list),LaunchProgram/CloseProgram(desktop). - Read-only / safe:
findEmail,findEvents,getList,listIssues,browseRepo.
Currently all of these execute without confirmation. As agent capabilities and automation grow, users should be able to review and approve sensitive actions, and we need a safety story we can enable in shipped builds.
Goals
- A single, uniform enforcement point for gating action execution.
- An ACL / policy model that classifies actions and decides allow / prompt / deny.
- Approval UX that works across all surfaces (Electron shell, CLI, VS Code shell) with an "always allow" option.
- Persist approvals at two tiers: current session and per-profile ("always allow this action").
- Allow-by-default, opt-in gating initially: ship the mechanism without regressing behavior, with a clear path toward stricter defaults.
Non-goals
- Reasoning-loop tool-call permission (tracked separately).
- Sandboxing / OS-level isolation of agent code.
- Network / credential / secrets management.
Current state (research)
Enforcement chokepoint. executeAction() in ts/packages/dispatcher/dispatcher/src/execute/actionHandlers.ts is the single function every action passes through (regular, flow-based, and agent-queued additional actions). It already contains a pre-flight readiness gate (checkAgentReady) that runs "as late as possible, right before we invoke the agent" and can short-circuit with a replacement ActionResult. An approval/ACL gate belongs right next to it, immediately before appAgent.executeAction(action, actionContext). Action identity = schemaName + actionName (agent = getAppAgentName(schemaName)).
No security metadata today.
AppAgentManifest(ts/packages/agentSdk/src/agentInterface.ts) has no permission / capability / risk fields.- No per-action risk or side-effect categorization exists in any agent.
Reusable primitives.
SessionContext.popupQuestion(message, choices?, defaultId?): Promise<number>(ts/packages/agentSdk/src/agentInterface.ts; impl ints/packages/dispatcher/dispatcher/src/execute/sessionContext.ts) delegates toClientIO.question()and is already rendered as choice cards in the shell, a terminal menu in the CLI, and a QuickPick in the VS Code shell.- "Remember my choice" pattern via
createPickRememberChoiceResult()(agentSdk helpers).
Precedents to mirror.
- Policy model: the workflow engine's
TaskPolicy=allow | prompt | denywithApprovalFn/ApprovalResultand secure-by-default gating (ts/examples/workflow/model/src/taskDefinition.ts,ts/examples/workflow/engine/src/runner.ts). - Persistence:
CollisionPreferenceStore(collisionPreferences.jsonin the profile/instance dir) with three-tier resolution (session one-shot -> persisted profile -> interactive) ints/packages/dispatcher/dispatcher/src/context/collisionPreferences.ts. An action-approval allowlist can mirror this (e.g.actionApprovals.json). - Schema metadata: action types already carry JSDoc comments parsed into schema (
ts/packages/actionSchema/src/type.ts,parser.ts) — a viable home for author-declared risk tags if we choose that route.
Proposed design
1. Enforcement point
Add an approval/ACL gate inside executeAction() immediately before dispatch, alongside the existing checkAgentReady pre-flight. Because additional/queued actions also flow through here, gating is uniform and per-action.
2. Policy / ACL model
Resolve a decision allow | prompt | deny for each action from a layered policy:
- Author-declared default (see open investigation below).
- Central / admin policy config (maps
schema.action-> mode). - User overrides / ACL allowlist (built from approvals).
Most-specific / most-restrictive layer wins (exact resolution rules TBD).
3. Default posture — allow-by-default, opt-in gating
Ship the mechanism off by default (allow-by-default) so nothing regresses; gating is opt-in via config, with a documented path toward secure-by-default for clearly destructive actions once classification is trustworthy. Add config under the existing execution settings (ts/packages/dispatcher/dispatcher/src/context/session.ts), e.g.:
execution.actionApproval: "off" | "prompt-destructive" | "prompt-all" // default "off"
execution.actionApprovalAllowlist: string[] // "schema.action" auto-approved
4. Approval UX
Reuse popupQuestion / choice cards uniformly across surfaces. When prompting, offer at least Allow once / Deny / Always allow (this action) — the last writes to the persisted allowlist. Render the action (schema.action + key parameters) so the user can decide with context.
5. Approval persistence (two tiers)
- Session tier: in-memory override consumed for the current session.
- Profile tier: persist "always allow this action" to a per-profile store (mirror
CollisionPreferenceStore->actionApprovals.jsonin the instance/profile dir under~/.typeagent/).
6. OPEN INVESTIGATION — where action sensitivity/risk is declared
Intentionally not decided; part of this work is to evaluate the options and pick one (or a combination):
- (A) Author annotations — declare risk on actions via manifest capabilities and/or schema JSDoc tags (e.g.
@sideEffect,@risk high,@requires email.send). Pros: co-located and precise. Cons: relies on every agent author; needs schema plumbing; unannotated = unknown. - (B) Central policy config — a maintained map of
schema.action-> risk/mode shipped with the dispatcher. Pros: no agent changes, centrally auditable. Cons: must track every agent/action; drift. - (C) User-defined ACL — users classify/allow/deny at runtime, building their own allowlist. Pros: user control, no upfront taxonomy. Cons: cold-start; inconsistent defaults.
- (D) Heuristic / inferred — infer side-effects from action verb/name or (future) effect-inference. Pros: zero config. Cons: unreliable.
Likely outcome: a combination (author-declared default + user ACL override, with an optional central policy). Deliverable: a short design note recommending the approach with tradeoffs.
Open questions
- Resolution/precedence rules when layers disagree.
- Risk taxonomy: binary (safe/destructive) vs. graded levels.
- How to treat "unknown risk" actions under allow-by-default.
- Whether
denyis silent or surfaced; auditing/logging of decisions. - Behavior in headless / non-interactive / automated contexts (no user to prompt).
- Scope of "always allow": per action, per action+params, or per agent?
- Telemetry for gated / approved / denied actions.
Proposed implementation (phased)
- Phase 0 — Investigation: Evaluate risk-source options (A–D); produce a short design note + recommendation. Define the ACL decision model and resolution rules.
- Phase 1 — Enforcement scaffold: Add the gate in
executeAction()(allow-all no-op by default) +execution.actionApprovalconfig. WirepopupQuestion-based approval with Allow once / Deny. - Phase 2 — Persistence: Add session + profile approval store (mirror
CollisionPreferenceStore); "Always allow" writes to the profile allowlist; add allowlist config. - Phase 3 — Classification: Implement the chosen risk-source for a first set of agents (email, calendar, github-cli, list, desktop). Enable
prompt-destructiveopt-in. - Phase 4 — Surfaces & polish: Consistent UX across shell / CLI / VS Code shell; render action + params; telemetry; docs.
- Phase 5 — Path to default-on: Once classification is trustworthy, propose secure-by-default for destructive actions.
Acceptance criteria
- A single dispatcher-level gate governs all dispatched actions; disabled by default (no behavior change).
- With gating enabled, destructive actions prompt the user; "Always allow" persists across sessions per profile; "Allow once" is session-scoped.
- Approval UX works on the Electron shell, CLI, and VS Code shell.
- Config documented; decisions logged/traceable; unit tests for the decision model and persistence.
Related code
ts/packages/dispatcher/dispatcher/src/execute/actionHandlers.ts—executeAction()chokepoint (andcheckAgentReadyprecedent).ts/packages/agentSdk/src/agentInterface.ts—AppAgentManifest,SessionContext.popupQuestion.ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts—popupQuestionimplementation.ts/packages/dispatcher/dispatcher/src/context/collisionPreferences.ts— persistence precedent.ts/packages/dispatcher/dispatcher/src/context/session.ts— execution config.ts/packages/actionSchema/src/type.ts,ts/packages/actionSchema/src/parser.ts— schema comment/metadata extraction.ts/examples/workflow/model/src/taskDefinition.ts,ts/examples/workflow/engine/src/runner.ts—TaskPolicy/ApprovalFnprecedent.
- Dominant language
- TypeScript
- Stars
- 744
- Forks
- 107
- Avg merge
- 1d 59m
- Merged PRs (30d)
- 113
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- No pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/TypeAgent
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
Maintainers usually reply within 1 day
-
Add CI validation for schema keyword driftMay be free again @GeorgeNgMsft claimed this 33 days ago, and no pull request is open. Open
microsoft/TypeAgent#2978 · 1 assignee ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 15/100
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
Maintainers usually reply within 1 day
-
dispatcher enhancement reasoning security
Difficulty 4/5 3-5 days Newbie friendliness 58/100
Maintainers usually reply within 1 day
All issues in microsoft/TypeAgent
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
farbenmeer/tapi#531 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
naver/egjs-flicking#971 ·
-
Renderer treats a sub-pixel width difference as a resize, which cancels the `motion()` entranceOpen
Difficulty 1/5 Under an hour Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
Tenant
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
MTES-MCT/Dossier-Facile-Frontend#2061 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
backnotprop/plannotator#1784 ·
Maintainers usually reply within 1 day