ADR-0014: Approvals happen in Studio, once, for the state the person saw
- Status: Accepted (supersedes the approval mechanics of ADR-0004 and ADR-0011)
- Date: 2026-10-08
- Related: ADR-0004, ADR-0011, ADR-0012
- Source: Sprint 7 pre-launch security review (findings H1, H2)
Context
ADR-0004/0011 approved gated calls (publish, rollback, delete) through MCP elicitation, with a signed confirmToken for clients that cannot elicit. The review confirmed two flaws. H1: whoever answers the elicitation or holds the token is the agent’s own client — the token was returned to the agent in the tool result, and an auto-accepting client passes elicitation — so a remote agent could approve itself. H2: the approval was bound only to (tool, arguments, actor) and was reusable for ten minutes, so after one approved publish an agent could edit content and publish again with the same approval.
Decision
- Agents with their own credentials (API keys over HTTP) never answer for a person. A gated call creates a
pendingrow inapprovals(migration 0006) and returns “approval pending” with anapprovalId. A signed-in person approves or denies it in Studio (POST /api/sites/:id/approvals/:aid/approve|deny, people only, audited). The agent repeats the same call withapprovalId. - Single use, bound to the exact call and the content. Redeeming is one atomic
UPDATE … WHERE status='approved' AND tool AND args_hash AND actor_id AND state_hash AND not expired.state_hashis a digest of every node’s id, rev and hash at request time (SiteGraphStore.stateHash): any change after the person looked voids the approval. Requests expire after 24 h. - Elicitation remains for people and local use (a session-authenticated person’s MCP client;
cloudhal mcpover stdio on the developer’s machine), now also bound tostate_hash. confirmTokenis off by default; it is an explicit local opt-in only.- Agents are refused on the REST API (security review C1), so approval endpoints are reachable by people only.
Alternatives considered
| Alternative | Pros | Cons | Why not |
|---|---|---|---|
| Keep elicitation for agents | No Studio step | The agent’s client answers it | Not a human control |
| Signed single-use tokens without storage | Stateless | Single use needs storage anyway; the token still reaches the agent | Same flaw |
| Approve by e-mail/Slack link | Reach people anywhere | New channels, identity mapping | Later, on top of the same table |
Consequences
- Positive: “publish stops for a person” holds even against a compromised or careless agent client; every decision is audited with who decided.
- Negative: a remote agent must wait (and poll) for the decision; Studio needs an Approvals screen.
- Follow-up: notifications for pending approvals; per-site approver roles.
Addendum (2026-10-09): one waiting request per call
An agent that repeats a call needing approval (same actor, tool, arguments and site state) gets the request that is already pending instead of a new one (ApprovalStore.request). Retries and lost answers no longer leave copies in Approvals for a person to deny. A call with other arguments, or after the site changed, still files its own request; a request voided by a site change (“the site changed after the approval was requested”) is never reused.