Approval gates & circles
An approval gate asks for permission to perform one precisely described action. The gate freezes its approver pools, records one revisioned decision history, and, if approved, issues one action-bound execution capability.
Atomic creation and idempotency
Section titled “Atomic creation and idempotency”Creating a gate commits the gate, frozen approver snapshots, request signal, initial audit events, and any matching circle decision together. A partial approval graph is never externally visible.
Pass an idempotency_key when a create request may be retried:
- the same requester, key, and canonical request return the existing gate;
- the same requester and key with a different action, payload, or policy returns
APPROVAL_IDEMPOTENCY_CONFLICT; - a retry never reproduces a plaintext execution token.
The canonical request includes resolved policy references. Role names and identity names are resolved at creation and stored as concrete snapshot IDs, so later membership or fill changes cannot silently widen an existing approval.
One active voter tier
Section titled “One active voter tier”Every pending gate has exactly one active_tier: primary, fallback, or
escalation. Only unique eligible identities in that tier count.
| Mode | Approval threshold in the active tier |
|---|---|
any |
one approval |
quorum |
min_approvals unique approvals |
all |
every identity in that tier approves |
An exact duplicate vote is an idempotent acknowledgement-loss retry. Reusing the same
identity with a different vote or note returns APPROVAL_DUPLICATE_VOTE. Late votes
cannot alter a terminal gate.
A primary denial either terminates the gate or atomically activates the escalation
tier, according to on_deny. Idle processing can activate fallback or escalation when
the configured pool exists. Tier changes, votes, threshold evaluation, terminal state,
audit events, and token hash issuance share one transaction and one revision fence.
Deadline precedence
Section titled “Deadline precedence”The database clock is authoritative. If the stored deadline has passed, an approval, denial, cancellation, or reaper request all resolve through the same pending-revision check, and timeout wins. Caller clocks cannot force or postpone a deadline or idle-tier transition.
Concurrent approve, deny, cancel, and timeout requests therefore produce one terminal state and one matching deterministic event set.
Execution authorization and durable attempt
Section titled “Execution authorization and durable attempt”MCP and CLI creation use requester-bound execution by default for an agent requesting approval for its own exact operation. SDK and HTTP callers can select it explicitly:
{ "execution_binding": { "mode": "requester_bound", "attempt_id": "stable-id-for-this-action" }}Chorus commits the authenticated requester and attempt ID inside the fingerprinted
action payload. The decisive approval returns no secret. That requester begins through
beginRequesterBoundExecutionAttempt (or the bound MCP/CLI command), and storage
atomically checks the gate, identity, attempt, fingerprint, expiry, and one-time
transition. Finish uses the same identity binding and revision fence. Approval, begin,
and finish emit personal approval invalidations so agents and Console can converge on
Approval → Ready → Running → Done without polling a token handoff.
Bearer mode remains the explicit delegation adapter for a different executor or a pre-binding gate. Existing gates without an execution mode are bearer gates.
For bearer mode, the transaction that first commits approval receives a random plaintext token. Chorus stores only its SHA-256 hash, action fingerprint, database expiry, and consumption state. Reads and idempotent retries never return plaintext.
To call a route protected by requireApproval, send:
X-Approval-Gate-Id: approval_gate:...X-Approval-Token: <plaintext from the winning response>X-Approval-Attempt-Id: <stable id for this exact action request>The TypeScript SDK exports buildApprovalExecutionHeaders to construct these headers.
Choose a new attempt ID for new work; reuse it only when retrying the exact same action
and body.
Token acceptance atomically checks the approved state, token hash, action fingerprint, and database expiry, consumes the token, and creates a durable execution attempt:
- one of competing consumers is accepted;
- a different attempt after consumption gets
APPROVAL_TOKEN_CONSUMED; - the same active attempt gets
APPROVAL_EXECUTION_IN_FLIGHTuntil its lease expires; - a failed or stale attempt can resume with the same ID, token, and fingerprint only before the original token expiry;
- a succeeded attempt gets
APPROVAL_EXECUTION_COMPLETEDand never runs again; - reusing an attempt ID for different work gets
APPROVAL_EXECUTION_ATTEMPT_CONFLICT.
Accepted, resumed, succeeded, and failed transitions are audit events. If a worker crashes after acceptance, the unfinished attempt remains visible and becomes resumable after its lease, but the original token expiry is the absolute resumption deadline. Terminal results and exact finish replays remain observable after that deadline.
At gate creation, Chorus copies the requested execution lease into
action_payload.approval_execution_lease_seconds before computing the action
fingerprint. If the request omits it, Chorus samples
APPROVAL_EXECUTION_LEASE_SECONDS (default five minutes) once and commits that value.
Begin and resume operations use only the committed value; later environment changes
and caller-provided lease values cannot alter it. Legacy approved gates without this
field fail closed and require a fresh gate.
Chorus likewise copies the execution-token lifetime into
action_payload.approval_execution_token_ttl_seconds before fingerprinting. The
value is sampled once from APPROVAL_EXECUTION_TOKEN_TTL_SECONDS (default one hour),
must be a whole 60–604800 seconds, and must cover the committed execution lease.
Storage derives the absolute token/resumption deadline only from that committed
value; it never accepts a caller-supplied TTL or resamples the environment when an
approval later crosses threshold. Legacy gates without the committed lifetime fail
closed and require a fresh gate.
External execution protocol
Section titled “External execution protocol”External workers must use the public begin/finish protocol rather than treating an approved gate as permission by itself:
- Independently derive the expected action fingerprint from the exact local action or candidate manifest.
- Begin one stable attempt ID with the gate ID, capability, and expected fingerprint.
- Perform the action only when the response is
acceptedorresumedandexecution_authorizedistrue. - Finish that same attempt with its returned revision and an outcome of
succeeded,retryable_failure, orterminal_failure.
observed_in_flight and observed_completed are reconciliation observations. They
always have execution_authorized: false and must never start or repeat work. Finish
responses also have execution_authorized: false; finalization records an outcome but
does not grant a second execution.
The CLI enforces this distinction: approvals execution begin-bound needs the gate,
committed attempt, and action fingerprint but no capability file. Bearer
approvals execution begin requires both an
owner-only approval-response file and --expected-action-fingerprint, and exits
nonzero for a non-authorizing observation. For high-risk operations, prefer the REST
header or this file-based CLI path. Bearer RPC and MCP calls carry plaintext in their
request body, so use them only where request bodies and transcripts are not recorded.
Requester-bound MCP calls carry no approval secret.
When requireApproval commits the protected action but cannot record its final state,
it preserves the action response and adds
X-Chorus-Approval-Execution: reconciliation-required plus the attempt ID. Do not
retry the action from that response. Reconcile the durable attempt and finish its
existing revision instead.
Circles
Section titled “Circles”A circle is a bounded standing authorization. During atomic gate creation, Chorus
matches active circles by requester membership in the circle ring, allowed_actions,
and the circle validity window. A matching auto_approve_enabled circle creates the
gate already approved, with its token hash and audit events in the same commit. Only
the winning create response receives plaintext.
Circle auto-approval still creates a gate and complete audit history; it bypasses votes, not accountability. Use circles only for repetitive, tightly bounded actions. Fresh approval gates remain appropriate for spend, customer-facing actions, production changes, and other decisions whose context must be reviewed each time.
GET /circles and client.circles.list() accept limit (default 20, maximum
100). Ring membership and active-window filters run in storage before that limit,
so a caller cannot receive hidden circles or an under-filled page merely because
inaccessible rows sort first. Matching a circle during gate creation is a single
database-clock query with requester membership in the predicate; it does not scan
rings in application code.
Visibility and migration safety
Section titled “Visibility and migration safety”Approval reads deliberately return 404 to callers who are neither the requester,
an eligible snapshot identity, nor an admin. This avoids revealing that a hidden gate
exists.
Migration 045 adds gate revisions, the explicit active tier, unique idempotency scopes,
deterministic event keys, and durable execution attempts. Migration 069 inventories
the retained action binding, quarantines legacy gates whose payload no longer matches
their fingerprint, and separately quarantines pending or approved legacy gates
that do not already commit a valid lease and token-lifetime pair. It never invents
those authority-defining values or recomputes a fingerprint. Terminal denied or
cancelled history can remain when its action binding is valid. Migration 069 also
records executor/finalizer provenance and makes the action binding read-only.
Duplicate scopes, orphaned rows, invalid retained bindings, and dependents of a
quarantined executable gate move to approval_lifecycle_quarantine instead of being
silently accepted into the state machine.