forbid; the review is a control-plane workflow; the execution is still the gateway’s, under the same authorization it would have applied to any other call.
The gate
The approval gate is a Cedarforbid whose @id ends in needs-approval:
Deny into ApprovalRequired only when all of these hold: the tool is destructive, the environment is prod, the call is not already approved, at least one policy matched, and every matched policy id ends in needs-approval. A deny caused by any other forbid stays a deny. A deny with no matched policy at all stays a deny too — there is no permit for an approval to unblock.
This is a naming convention. A workspace that authors its own approval forbid must name it with the
needs-approval suffix, or its denies will never become tickets.What the agent sees
The call is held, not executed. The gateway records the invocation in stateAwaitingApproval and returns an approval_required error — HTTP 423, JSON-RPC code -32003 — carrying approval_id, tool, expires_at, and an inbox URL when one is configured. Tickets expire 24 hours after creation.
Before the held state is committed, the gateway writes the approval ticket to a dedicated Redis stream, stream:approvals, and awaits its acknowledgement. The lossy usage meter is never used for this. If that write fails, nothing is held and the call stops as not-ready.
Raw arguments travel on the private approval stream and into the request snapshot. They never appear in the public receipt or audit envelope.
The inbox
A control worker drainsstream:approvals through a consumer group every ten seconds. Each entry is validated, bound to a live workspace, virtual key, and active connection, and inserted as an ApprovalRequest. Acknowledgement happens only after the database commit, or immediately for deterministically invalid input that could never become valid on retry. Duplicate valid deliveries are idempotent; a repeated approval_id with a different payload is rejected as a collision.
At ingest the request is matched against the workspace’s active approval policies. If no allow rule matches the tool and environment, the request is closed immediately with default_deny.
Approval policies
Approval policies say which humans may review which tools in which environments. They never authorize vendor execution themselves — that stays with Cedar and the gateway. A rule has this shape:
Matching rules:
- A principal matches a rule when their role is selected or their user id is named.
- An explicit matching reviewer
denydefeats every allow. It excludes that person from reviewing; it is not a decision to reject the request. - Quorum is satisfied when any one matching allow rule reaches its own count. Votes are not pooled across rules, so a broad one-person rule also permits a tool covered by a narrower two-person rule. Avoid overlapping weaker grants.
- A permitted reviewer’s
denyvote closes the request.
approval.policy.manage. Casting a vote is approval.decide, which admins and owners hold (ADR-0037’s role matrix, extended by ADR-0039).
Voting
Each request stores the policy revisions, an immutable snapshot of the rules that matched it, the reviewer votes, and a hash of the complete active policy set. Votes use optimistic revisions plus workspace and request locking. Before a vote is accepted the service rechecks:- the request revision matches the caller’s
expected_revision(otherwise 409) - the request is still pending, and has not expired
- the workspace’s current policy-set hash still equals the one stored on the request
- the caller has at least one eligible allow rule and no matching deny rule
- the caller has not already voted
approved, its execution status becomes pending, and a delivery job is enqueued.
Resume
A control worker runs due delivery jobs every ten seconds. It takes a lease with an attempt fence, then re-verifies everything under the workspace lock before it mints any authority:1
Preflight
The request must still be approved and pending execution, unexpired, in an unfrozen workspace, under the same policy-set hash, with quorum still satisfied by current memberships, and the virtual key must still exist. Any failure marks the job failed with a specific error code —
approval_not_executable, approval_expired, workspace_frozen, policy_changed, reviewer_authority_changed, key_unavailable.2
Mint
Control mints a short-lived HS256 approval token bound to the original approval id, workspace, key, concrete tool, argument hash, and idempotency key.
3
Dispatch
The worker commits, releasing the transaction, and calls the gateway’s service-authenticated
POST /internal/v1/approvals/resume with the held record’s identity, the key hash, the original arguments, the idempotency key, and the token. Only the key hash is sent; no plaintext virtual key is ever reconstructed.4
Execute
The gateway verifies service auth and the token, enforces expiry with no clock-skew leeway, requires the exact durable held record and a matching approval, and then re-applies its current key, workspace, connection, scope, and Cedar state before executing.
Resuming from the agent side
The same held call can also be re-issued by the agent. It repeats the identicaltools/call with its virtual key in Authorization, plus two headers:
obol-control, audience obol-gateway, expiry — and every bound claim matches the live request: the header id, workspace, key id, tool name, canonical hash of the arguments, and effective idempotency key. Any mismatch falls back to approved = false, so Cedar’s gate re-issues a ticket instead of executing.
Boundaries
- Approval is not verification. An approved request may still be queued, blocked, or failed. Only the gateway receipt states the vendor outcome and the evidence class, and a federated-route receipt does not become gateway-observed evidence because a human approved it. See Receipts.
- Reviewer views are closed and credential-free. They show metadata, decision history, and a numeric
amount_usdsummary when the ticket carried one. They do not expose arguments, idempotency keys, request payloads, or approval tokens. Reviewers must confirm the underlying request in their own system of record. - This is workspace-scoped approval permission management, not general IAM. Custom roles, user groups, self-approval exclusion, MFA step-up, notifications, escalation, monetary rule conditions, and policy simulation are out of scope today.