Approvals
An approval is a record flow, not a dialog. When the runtime needs a human
decision before executing a tool action, it appends durable approval records
to the session log and parks the requesting work; the wire model is a fold of
that flow. The durable identity is the approvalId; an approval is pending
while its opening record has no terminal decision record, and the pending set
is a log read that host restarts recompute identically.
Because it is a record flow, everything a dialog needs falls out for free: idle unload is blocked while an approval is pending, resume returns pending approvals as pointers and re-issues the requests, and the first durable decision wins with every other submitter informed by a typed conflict, never raced silently.
Subjects
Section titled “Subjects”The subject says what is being approved. It is an open discriminated union of five kinds — shell, file access, network, process, and generic tool — and clients must render unknown kinds generically and must never auto-approve them: an auto-approval rule keyed on subject kind must fail closed on any kind it does not recognize. Shell subjects are multi-stage: the runtime parses the command into independently satisfiable stages, and the approval’s current requirement id names the stage a decision must target next.
Request, decide, resolved
Section titled “Request, decide, resolved”The cycle has three legs:
- Request. The server sends
approval/request— a server-initiated JSON-RPC request with a connection-scoped id — to the subscribed connection with the subject, the correlation ids, and a server-minted choice menu. Your response to that request is a presentation receipt only — it changes no state, and there is no dismiss-without-deciding on the wire. The same payload also folds onto the view stream as theapproval/requestedview event, which is whatview/pagereturns for catch-up; the frame you answer is always theapproval/requestrequest, never the view event. - Decide. The decision travels as an ordinary idempotent command,
approval/decide, never as the request’s response — a response has no retry handle, while a command gets value-identical replay across reconnects and host crashes. The decide names the approval, the current requirement (the multi-stage race guard), and achoiceIdfrom the server’s menu; clients select choices, never construct them. - Resolved. Once the terminal decision record is durable, the
approval/resolvedview event broadcasts — including to the connection that decided, whose ack was only admission. It is protected-set delivery: never gapped, never dropped, never opted out, so a client that saw the request can always dismiss its dialog.
Here is the decide command and its ack from the recorded round-trip scenario (the requirement id targets stage zero, and the ack’s terminal flag says this decision produced the terminal record):
{"dir":"client","raw":"{\"jsonrpc\":\"2.0\",\"id\":\"b9\",\"method\":\"approval/decide\",\"params\":{\"sessionId\":\"0198f0aa-1111-7000-8000-0000000000aa\",\"commandId\":\"018f6a2a-3333-7abc-8def-00000000d001\",\"approvalId\":\"0198f0ac-7777-7000-8000-0000000000e1\",\"requirementId\":{\"approvalId\":\"0198f0ac-7777-7000-8000-0000000000e1\",\"sourceIndex\":0},\"choiceId\":\"allow_session\"}}"}{"dir":"server","raw":"{\"jsonrpc\":\"2.0\",\"id\":\"b9\",\"result\":{\"commandId\":\"018f6a2a-3333-7abc-8def-00000000d001\",\"status\":\"accepted\",\"approvalId\":\"0198f0ac-7777-7000-8000-0000000000e1\",\"terminal\":true}}"}A decide that loses a race gets the typed
approvalAlreadyResolved
conflict carrying the winning resolution, so the losing dialog settles
without a page read. A stale requirement id gets its own typed
approvalRequirementStale
error carrying the current requirement, and a choiceId that is not on the
server’s menu gets
approvalChoiceInvalid.
Modes, ceilings, and scopes
Section titled “Modes, ceilings, and scopes”Modes.
session/setApprovalMode
switches the session’s enforcement mode among four preconfigured
mode names — the wire spellings
are allowAll, promptUnmatched, onRequest, and denyUnmatched (allow
everything, prompt on unmatched subjects, approve only on request, deny
unmatched subjects). The rule is select, never create: a client may pick a
mode the user’s configuration already defines but can never author a policy
on the wire, because a constructible policy surface would make every client
an administrator of the user’s safety settings. Exposing allowAll is safe
only because of the spawn boundary — the client already holds the user’s
ambient authority. The starting mode is declared with the optional mode field
on session/start, which is
also how a non-interactive run declares its policy: start in a default-deny
mode and unmatched subjects fail their tool call with a typed, model-visible
denial instead of parking the run.
Ceilings. Under the ephemeral profile the mode is bounded by a ceiling:
the comparand is always the sealed startup snapshot, never the currently held
mode, and the authority order is partial — onRequest is incomparable with
both promptUnmatched and denyUnmatched, so “not above” is not the test. A
request above or incomparable to the ceiling is rejected with the
approval-mode-ceiling reason. See
durability profiles.
Scopes. Every choice in the menu carries a scope naming the grant durability it creates: once, session (the session policy overlay), or locally-persistent (the workspace policy store). Session-grant equivalence is defined per kind — for example, session shell grants match only the byte-identical raw command, and a write grant does not imply read. Until a client has schema support for a subject kind, it must treat session-scope choices for that kind as opaque server policy and render the server’s rule preview verbatim.
Decision durability. The decision itself is durable before anything else
moves: the terminal record is the fold’s source of truth, auto-approvals from
session grants still write terminal records and appear as view events, and a
failed persistence of a locally-persistent rule surfaces as a later
approval/updated
while the decision itself stands.
Expiry
Section titled “Expiry”Approvals do not expire in v1: the runtime starts no timer, so a pending approval parks its turn indefinitely, blocks idle unload, and survives restarts. Headless clients that must not park forever declare a default-deny mode instead. User-input prompts are the honest asymmetry: they do time out, because the runtime durably records their auto-resolution deadline.
Crash and restart
Section titled “Crash and restart”A host restart is recovery, not amnesia: on load, recovery replays the
approval streams, closes each still-pending old identity, and durably opens a
fresh request with a deterministic new approvalId and the same action
payload — pre-crash answers are evidence, not authority, and policy stages
are re-derived by the live backend. A decide replayed across the restart is
safe: if its own terminal is in the log the replay returns the original ack,
and otherwise it answers the idempotent already-resolved conflict for the old
identity — its commandId can never migrate to the fresh successor.
Protected writes recover on their own fail-closed path with no silently
revalidated pre-crash grant, and protected-write decisions additionally
require a live reviewer on the host, failing closed with
approvalReviewerUnavailable
when none exists. None of this section exists on an ephemeral host, where
pending approvals simply end with the process.