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 / onPermissionRequest posture 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 in ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts) delegates to ClientIO.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 | deny with ApprovalFn / ApprovalResult and secure-by-default gating (ts/examples/workflow/model/src/taskDefinition.ts, ts/examples/workflow/engine/src/runner.ts).
- Persistence:
CollisionPreferenceStore (collisionPreferences.json in the profile/instance dir) with three-tier resolution (session one-shot -> persisted profile -> interactive) in ts/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.json in 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
deny is 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)
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 (and checkAgentReady precedent).
ts/packages/agentSdk/src/agentInterface.ts — AppAgentManifest, SessionContext.popupQuestion.
ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts — popupQuestion implementation.
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 / ApprovalFn precedent.
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.Motivation
TypeAgent routes natural language into typed actions, many of which have real-world side effects and are irreversible. Examples across current agents:
sendEmail/replyEmail/forwardEmail(email),removeEvent/scheduleEvent/addParticipant(calendar),prMerge/deleteRepo/deleteIssue/deleteGist(github-cli),removeItems/clearList(list),LaunchProgram/CloseProgram(desktop).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
Non-goals
Current state (research)
Enforcement chokepoint.
executeAction()ints/packages/dispatcher/dispatcher/src/execute/actionHandlers.tsis 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 replacementActionResult. An approval/ACL gate belongs right next to it, immediately beforeappAgent.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.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.createPickRememberChoiceResult()(agentSdk helpers).Precedents to mirror.
TaskPolicy=allow | prompt | denywithApprovalFn/ApprovalResultand secure-by-default gating (ts/examples/workflow/model/src/taskDefinition.ts,ts/examples/workflow/engine/src/runner.ts).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).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 existingcheckAgentReadypre-flight. Because additional/queued actions also flow through here, gating is uniform and per-action.2. Policy / ACL model
Resolve a decision
allow | prompt | denyfor each action from a layered policy:schema.action-> mode).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.: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)
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):
@sideEffect,@risk high,@requires email.send). Pros: co-located and precise. Cons: relies on every agent author; needs schema plumbing; unannotated = unknown.schema.action-> risk/mode shipped with the dispatcher. Pros: no agent changes, centrally auditable. Cons: must track every agent/action; drift.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
denyis silent or surfaced; auditing/logging of decisions.Proposed implementation (phased)
executeAction()(allow-all no-op by default) +execution.actionApprovalconfig. WirepopupQuestion-based approval with Allow once / Deny.CollisionPreferenceStore); "Always allow" writes to the profile allowlist; add allowlist config.prompt-destructiveopt-in.Acceptance criteria
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.