Skip to content

Action safety: user-approvable ACL / permission gating for dispatched actions #2628

Description

@robgruen

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:

  1. Author-declared default (see open investigation below).
  2. Central / admin policy config (maps schema.action -> mode).
  3. 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)

  • 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.actionApproval config. Wire popupQuestion-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-destructive opt-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.tsexecuteAction() chokepoint (and checkAgentReady precedent).
  • ts/packages/agentSdk/src/agentInterface.tsAppAgentManifest, SessionContext.popupQuestion.
  • ts/packages/dispatcher/dispatcher/src/execute/sessionContext.tspopupQuestion 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.tsTaskPolicy / ApprovalFn precedent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions