approval: always (always true for irreversible tools), when a
policy rule says require_approval, or when a content check does.
Compensations can also require approval (compensation.requires_approval).
The flow
- The agent makes the call. Reversi queues an approval request and blocks the call. The message names the
request id:
approval required: request apr_… is pending an operator decision; retry after it is approved. - An approver decides it: in the console, with
reversi approvals approve <id>, over the HTTP API, through the gateway’s operator endpoint, or from a Slack button. A notification can tell them first. - The agent retries the same call with the same arguments. The approval is consumed, the call runs, and the ledger
records
approved.
What an approval is bound to
- The arguments. A call approval carries the hash of the arguments it was requested for. Approve a refund of 5 and a retry for 500 is a different call and needs its own approval.
- One use. After the call runs the approval is
consumed. Running the same call again queues a new request. - A time limit. Pending and approved requests expire (default one hour,
approval.ttlinreversi.config.yaml, or the timeout set by the manifest or rule). - A decision record. Who decided, when, the basis (role, group or delegation) and the note, in the approval store.
The ledger also gets a record, whichever way the decision is made (CLI, server, gateway or console):
approvedfor approvals,blockedfor denials, with the approver as actor. - Claimed atomically. Consuming or deciding a request is a compare-and-swap in the store, so two processes sharing a database cannot both use one approval or both cast the last vote.
- A denial sticks for that call (same arguments, same step), with the reason shown to the agent.
reversi approvals approve|deny goes through the engine, like the server and console do: the decision is journaled in the
ledger (approved, or blocked for a denial, with the person given by --as as actor), delegations apply, and
notification channels from the project file are told (approval_decided).
Who may approve: routing and quorum
By default anyapprover or above may decide. Name who must decide on the manifest or on a require_approval rule.
tools.yaml
policy.yaml
- Roles and groups. An approver qualifies if they hold a listed role, or belong to a listed group. Groups come from
groupson an API key or, for OIDC, from the values of the role claim. - Several requirements on one call (manifest plus rules) are merged: any listed role may approve, the largest
quorum wins, the shortest timeout wins, and
denyon timeout wins overescalate. - Quorum. Each approver counts once. One denial ends the request. A quorum above 1 needs
roles. While votes are missing the request stayspendingand the message saysneeds 2 approvers from finance. - No self-approval. Whoever owns the run that asked cannot decide the request.
- Deciding as a group member. Over HTTP and in the console your key’s groups are used. With the CLI you state them:
--groups financeand--roles operator(defaultoperator). A person who is not in the group is refused:this request needs an approver from: finance. The CLI trusts the caller, so use it on the host that owns the data. - The open console acts as one role and has no groups, so it can decide only requests that name that role or no one.
Escalation on timeout
Withon_timeout: escalate an unanswered request does not simply expire. When its time is up it is widened once to
escalate_to (or left as is if none is named), given a fresh time limit, an escalated ledger record is written and an
approval_escalated notification is sent. If it expires again it is expired. The check happens when the queue is read,
so it is only as prompt as the alert interval, console views or API reads.
With on_timeout: deny (the default) the request becomes expired. The agent’s next retry is told
approval … expired unanswered and was denied; the call must be requested again, and a retry after that queues a new one.
Delegation
An approver on leave can hand their authority to someone else for a while.- Who and how long.
--from,--to, a reason (required), and an end time at most 90 days away. You cannot delegate to yourself. - Scope.
--group(repeatable) limits it to requests routed to those groups. Without it the delegation covers every routed request, so always pass--group. - What happens. The delegate approves as themselves; the decision records
on_behalf_ofand the basisdelegated by fred (finance). It counts as fred’s vote: fred cannot vote on the same request afterwards, and the delegate cannot vote twice. The delegate still needs theapproverrole to reach the approve endpoint. - Ends at its expiry or when revoked.
- Where. The CLI above, the Approvals page of the console (own authority, hours and reason), or the
/v1/delegationsendpoints, whichreversi-serverserves (admin API). Delegations apply everywhere a decision is made, includingreversi approvals approve --as <delegate>.
Break-glass
An admin can force a pending request through when the usual approvers are unreachable. It skips routing and quorum, and it is loud on purpose.- Admin only over HTTP and in the console (other roles get
403). A reason of at least 10 characters and a pointer to evidence (ticket or incident) are required (400otherwise). The CLI has no login, so it accepts--byas given. - Recorded. The approval keeps
break_glasswith who, why, the evidence reference and the time. The ledger gets anapprovedrecord whose detail startsBREAK-GLASS: … evidence: INC-77. - Notified. A critical
break_glassnotification goes out, and a criticalbreak_glassalert shows in the console.
What the approver sees
The Approvals page lists each pending request with why approval is needed (rule and manifest), who may decide, the argument names and hash, the expiry and quorum progress. Each tool links to an impact preview, also atGET /v1/impact:
what the manifest and policy say about the tool, the verdict, the risk score and how the call could be undone. Previews
are about the tool, not the argument values, which are never stored.
Notifications
Withnotifications: configured, Reversi sends approval_requested, approval_progress
(a quorum vote that is not the last), approval_decided, approval_escalated and break_glass. A Slack channel with
interactive: true adds Approve and Deny buttons.
The older single REVERSI_APPROVAL_WEBHOOK (or approval.webhook) still works: each new request is POSTed as JSON with a
text field, so a Slack or Teams incoming-webhook URL works as is. Set REVERSI_CONSOLE_URL to link the console.
Delivery is best effort with a 5 s timeout and never changes the decision. It goes through the
egress rules, like channels do; a refused URL is logged as approval webhook failed.
Approving from Slack
The Slack buttons post to/integrations/slack/actions, which verifies Slack’s signature (v0, five-minute window) and
maps the Slack user id to a Reversi person. reversi-server enables it when REVERSI_SLACK_SIGNING_SECRET and
REVERSI_SLACK_USERS (a JSON file mapping Slack user ids to name, roles and groups) are set; see
authentication. In code:
approver role or higher, gets “You are not set up as an approver in
Reversi.” The same routing, quorum and self-approval rules apply. Checked here with locally signed requests, not with real Slack.
Pre-approval
An operator can grant standing approval for a tool in a run (POST /v1/runs/{id}/approvals, count from 1 to 100,
any arguments). Use it for compensations you know you will want to run, not for agent calls.
Who can decide
Theapprover role and up. Agents cannot approve anything, including their own calls. See roles.
Storage
Requests live in the data store (approvals.json for files, a table for SQLite and PostgreSQL), so the CLI, server
and gateway see the same queue and a restart does not lose it. Delegations live in the same store.
