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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions