Deskbot approval gate (proposal → approve, plan-gated mutation)
Generated from
pattern-library/registry.json— do not edit by hand; change the registry and runbun run patterns:build.
Category: AI · Tier: deep · Maturity: proven (verified 2026-10-10 @ 91f61a76-dirty) · Risk: high — the safety boundary for AI mutation
Write/destructive desk tools never mutate inside the agent loop; they return a requiresApproval sentinel that becomes a proposal (PlanCard), executed only via an approve-route replay that records the real approver and a receipt per step — and a plan that ran can be undone once, step by step, by compare-and-swap compensation.
When to use: Mandatory companion to any mutating AI tool: it converts 'the model wants to write' into an auditable human decision.
Requires: postgres, ai-provider
Avoid when
- Every tool is read-only or a reversible create — requiresApproval gates write and destructive risk only, so nothing would become a proposal
- The agent runs unattended with no human to approve — proposals would sit pending and the mutation would never execute
Alternatives
- Mutate in the loop and rely on reversal (soft delete, pre-image revision) — prefer when every mutation is cheaply reversible, as desk creates are, so an auto-approved in-loop run loses nothing
Docs
- docs/blueprint/ai/harness-lens.md — Where the gate sits in the harness (GitHub · GitLab)
- docs/blueprint/ai/desk-integration.md — Proposal lifecycle in the desk (GitHub · GitLab)
- docs/blueprint/ai/surfaces.md — The one door, receipts, and undo (GitHub · GitLab)
Code
src/lib/server/ai/tools/propose-plan.ts— Proposal creation (GitHub · GitLab)src/lib/server/ai/tools/desk-execute.ts— executeDeskToolCall — the one door (GitHub · GitLab)src/lib/server/ai/policy/governor.ts— requiresApproval(risk): write/destructive gated, read/create not (GitHub · GitLab)src/routes/api/ai/proposals/[id]/approve/+server.ts— Approve-route replay (GitHub · GitLab)src/lib/server/ai/proposals/execute-proposal.ts— Replay: receipt + recovery point per step, fenced (GitHub · GitLab)src/lib/server/ai/proposals/revert-proposal.ts— Undo: chained compare-and-swap compensation with revert receipts (GitHub · GitLab)src/routes/api/ai/proposals/[id]/revert/+server.ts— Undo route — one revert per proposal (GitHub · GitLab)
Tests
src/lib/server/ai/tools/desk-gated.test.ts(GitHub · GitLab)src/lib/server/ai/tools/desk-execute.test.ts(GitHub · GitLab)src/lib/server/ai/policy/governor.test.ts(GitHub · GitLab)src/lib/server/ai/proposals/execute-proposal.pglite.test.ts(GitHub · GitLab)src/lib/server/ai/proposals/revert-proposal.pglite.test.ts(GitHub · GitLab)src/lib/components/desk/panels/bot/desk-bot-session.svelte.test.ts(GitHub · GitLab)
Invariants
- The hard gate is at the tool layer — write/destructive tools return a requiresApproval sentinel; requiresApproval(risk) is the single rule.
- Planning is soft guidance, not the gate — shouldRequirePlan only decides whether to instruct the model to plan.
- Approval binds execution — the persisted args replay under the scopes frozen at proposal time, against the reviewed baseline, and no model turn follows: the door writes a deterministic execution receipt message.
- A run healed to failed('interrupted') is fenced: each step's transaction locks the proposal and refuses unless it is still executing, so a stalled executor cannot commit behind the terminal status.
- Undo is compensation, not rollback: one compensating step per receipt, in reverse, each a compare-and-swap against the state the plan left (chained per file) — work the user did since is reported as a conflict, never overwritten.
- Undo never widens the model's surface: restores go to the desk domain directly as the asking user, outside executeDeskToolCall and the tool manifest.
- The one undo a plan has belongs to the owner of its conversation — checked in the claim itself, so no door can spend it for someone else; a plan whose steps recorded no recovery point is refused rather than 'undone' as a no-op.
Emulation notes
- An earlier design (inline ConfirmCard with a model-minted confirmed=false→true handshake) was superseded and never shipped — do not emulate it; the proposal→approve flow is the proven shape.
- Record who approved and when at the approve route — the audit trail is the point.
- Capture what an undo needs when the step runs (its recovery point, on the receipt, in the same transaction) — reconstructing it later from the current file is guessing.
- Key the revert receipt by the forward step it undoes, with a foreign key onto that receipt: a revert of a step that never ran, or a second undo of one step, then cannot be written at all.
Depends on
- AI tool manifest & harness split (tool defs, risk metadata, registry)
- AI surfaces (chatbot vs deskbot split over one guard)
- Durable step execution (claim, lease, fence, receipts)
Machine-readable record: deskbot-approval-gate in pattern-library/registry.json.