# Policy approval (agents)

Fail-closed: sensitive **`workspace-kit`** operations need explicit approval so automation cannot silently mutate kit-owned state.

**Tier tables and `operationId` rows:** `.ai/AGENT-CLI-MAP.md`  
**Ultra-compact CLI recap:** `.ai/machine-cli-policy.md`

## What counts as approval for `workspace-kit run`

For **policy-sensitive** `workspace-kit run` commands, use one of:

- JSON **`policyApproval`** in the **third CLI argument** (preferred in agents/CI)
- A **valid session grant** for the same `operationId` + `WORKSPACE_KIT_SESSION_ID`
- **Interactive approval** when stdio is a TTY and `WORKSPACE_KIT_INTERACTIVE_APPROVAL` enables the prompt

**Not sufficient:** chat messages, ticket comments, or **`WORKSPACE_KIT_POLICY_APPROVAL` alone** on the `run` path.

## Two approval surfaces (do not mix)

| Surface | When | How |
| --- | --- | --- |
| **`policyApproval` in JSON** | `workspace-kit run <cmd> '<json>'` when the command is policy-sensitive | Third argv includes `"policyApproval":{"confirmed":true,"rationale":"…"}`. Optional `"scope":"session"` for reuse with the same `WORKSPACE_KIT_SESSION_ID`. |
| **`WORKSPACE_KIT_POLICY_APPROVAL` env** | `workspace-kit init`, `upgrade`, **`config`** mutating subcommands | Export env to JSON `{"confirmed":true,"rationale":"…"}`. |

**`workspace-kit run` does not read `WORKSPACE_KIT_POLICY_APPROVAL` for the run path.** Repo helpers that wrap `run` may read the env var only to **inject** `policyApproval` into the third JSON argument.

## Agents and IDE subprocesses (non-TTY)

Assume **no TTY**: use JSON **`policyApproval`** on `workspace-kit run`. **Chat is not approval.**

## Workflow Cannon Dashboard (Cursor extension)

The Dashboard webview drawer is an **approval surface** for gated mutations: the extension still sends JSON **`policyApproval`** on each sensitive **`wk run`** (kit validation unchanged).

| Tier | Operator experience | Trace `rationale` |
| --- | --- | --- |
| **Routine** | Confirm the action; no rationale textarea | Structured auto string (`dashboard\|workflow=…\|command=…\|tier=routine\|…`) from `buildDashboardPolicyApproval` |
| **Elevated** | Drawer shows policy explainer + required rationale field | Same structured prefix + `\|detail=<human text>` |

**Matrix:** `extensions/cursor-workflow-cannon/src/policy/dashboard-policy-tier.ts` (`DASHBOARD_POLICY_TIER_MATRIX`). Elevated examples: critical phase-note dismiss, rewind checkpoint, block/cancel assignment, register subagent. Routine review-item paths (accept, decline, accept_edited) auto-fill policy traces from the drawer.

**Agents / terminal:** Do **not** paste Dashboard auto-rationale boilerplate on CLI runs — supply task-specific prose in JSON **`policyApproval.rationale`** as usual.

## Operator slash / chat vs Tier A/B `wk run`

Cursor slash commands (for example **`/generate-features`**), Composer text, dashboard chat prompts, and ticket comments express **what the human wants done**. They **do not** replace JSON **`policyApproval`** on the **third** argv for policy-sensitive **`pnpm exec wk run …`** — see **`.ai/AGENT-CLI-MAP.md`**. The tool or agent that invokes the CLI must still pass **`policyApproval`** (or a valid session grant) so traces and policy stay auditable.

**Publish / npm / tag:** Phase ship still follows **`.ai/playbooks/phase-closeout-and-release.md`** and the explicit maintainer approval path described from **`.ai/RELEASING.md`** through **`docs/maintainers/RELEASING.md`** — chat-only wording is not the release gate.

## Evidence

- Traces: `.workspace-kit/policy/traces.jsonl`
- Session grants: **`kit_session_grants`** (SQLite) when using `scope":"session"` — list with **`workspace-kit run list-session-grants '{}'`

## Discovering approval-adjacent work (read-only)

- **Improvement `review-item` queue** — `workspace-kit run list-approval-queue '{}'` returns **`ready`** and **`in_progress`** improvement tasks (the same ids **`review-item`** accepts) plus stable relative paths for **`kit_policy_traces`**, **`list-session-grants`**, and **`kit_approval_decisions`** (legacy JSON import-only where noted). Instruction: `src/modules/approvals/instructions/list-approval-queue.md`.
- **Proposed improvements (triage, not `review-item`)** — `workspace-kit run list-tasks '{"type":"improvement","status":"proposed"}'`; playbook **`.ai/playbooks/improvement-triage-top-three.md`**.
- **Rollups** — `workspace-kit run dashboard-summary '{}'` (**`proposedImprovementsSummary`**, **`readyImprovementsSummary`**, maintainer **`pending_decisions`** in **`workspaceStatus`** when present).

Sensitive runs still require JSON **`policyApproval`** on the **`run`** argv object per the table in **`.ai/AGENT-CLI-MAP.md`** — listing the queue does not waive **`review-item`** policy.

## Copy-paste patterns

```bash
workspace-kit run generate-recommendations '{"policyApproval":{"confirmed":true,"rationale":"improvement pass"}}'
```

```bash
export WORKSPACE_KIT_POLICY_APPROVAL='{"confirmed":true,"rationale":"config change"}'
workspace-kit config set some.key value --json
```

**Policy rehearsal:** `generate-recommendations` accepts `"dryRun":true` in JSON; still pass **`policyApproval`** when the command remains gated.

## `operationId` recipes (doc generation + task-engine backfill)

These are **Tier B** `workspace-kit run` commands: approval is JSON **`policyApproval`** on the **third** argv object. **`WORKSPACE_KIT_POLICY_APPROVAL` alone does not satisfy** the `run` path.

### `doc.generate-document`

Dry-run / preview (not sensitive):

```bash
pnpm exec wk run generate-document '{"documentType":"ROADMAP.md","options":{"dryRun":true}}'
```

Real write (example — adjust `documentType` / `options` per `src/modules/documentation/instructions/generate-document.md`):

```bash
pnpm exec wk run generate-document '{"documentType":"ROADMAP.md","options":{"overwriteHuman":true},"policyApproval":{"confirmed":true,"rationale":"regenerate maintainer ROADMAP"}}'
```

### `task-engine.backfill-task-feature-links`

This repository sets `tasks.planningGenerationPolicy` to **`require`**: **both** dry-run and live runs need **`policyApproval`** and **`expectedPlanningGeneration`** (read **`data.planningGeneration`** from **`list-tasks`** / **`get-next-actions`** first).

Dry-run:

```bash
pnpm exec wk run list-tasks '{}'
pnpm exec wk run backfill-task-feature-links '{"dryRun":true,"policyApproval":{"confirmed":true,"rationale":"dry-run backfill"},"expectedPlanningGeneration":123}'
```

Live backfill (replace the integer; **`dryRun`** false or omit when your instruction allows):

```bash
pnpm exec wk run backfill-task-feature-links '{"dryRun":false,"policyApproval":{"confirmed":true,"rationale":"backfill task↔feature junction"},"expectedPlanningGeneration":123}'
```
