microsoft / microsoft/TypeAgent
Action safety: user-approvable ACL / permission gating for dispatched actions
- Dominant language
- TypeScript
- Stars
- 740
- Forks
- 106
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 84
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` / `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` (`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.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.
Contributor guide
No contributing guide indexed for this repository
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.
Assessment
- Tech stack
- typescript
- Domain
- authorization, backend-api-design, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100