Approvals
How a REQUIRE_APPROVAL verdict holds an operation, who may decide, what is recorded, and what happens when the Console is unreachable.
REQUIRE_APPROVAL is the decision that puts a human in the path of a specific operation
without stopping the agent's whole session. The recorder holds the operation, the Console
asks a person, and the answer — either way — becomes evidence.
What has to be true for a hold to happen
Three things, all of them:
- A rule matched and its decision is
REQUIRE_APPROVAL. - The surface is running in enforce mode. In observe mode the verdict is recorded and the operation proceeds.
- The surface implements the hold loop.
Today the hold loop is implemented in the command wrapper only — sealr run --enforce.
The MCP proxy (sealr proxy mcp --enforce) and the Claude Code hook
(sealr hook pre-tool-use --enforce) enforce BLOCK, but treat a REQUIRE_APPROVAL
verdict as allow: it is recorded, and the call proceeds. If you need a gated operation
on those surfaces today, write the rule as BLOCK.
The wrapper hold, step by step
sealr run --enforce --resource db:prod-main -- psql -c "DROP TABLE audit_log"The guard classifies the statement, the policy returns REQUIRE_APPROVAL, and the
operation record is written before anything is held. The request exists in the
evidence stream whether or not it is ever approved.
The recorder prints sealr: approval required for db:prod-main; waiting for a decision…
and calls the Console's RequestApproval RPC with a fresh approval id.
The Console creates a pending approval, puts it in the Approvals queue for anyone with the right role, and long-polls for a decision until the wait bound elapses.
The recorder writes a lifecycle record with operation approval_decided, naming the
operation record, the outcome, and the approval id. Then it either runs the command,
or exits without running it.
The child process is spawned only after an approval or an allow_warn fallback. A denied
or blocked operation never executes, and sealr run exits with code 20.
What the approver sees
The approver gets the structure of the operation, never its literals:
- the integration, the redacted command line, the verb class and the target resource;
- the guard's reason codes and risk class;
- the guard's structural summary — table names, resource addresses, counts — capped at eight fields and 200 characters each;
- who requested it, and whether that principal was attributed, inferred or unattributed.
The payload itself is not sent. A record carries a salted commitment to the payload, not the payload, and that property does not get suspended because someone is looking at an approval card.
Who may approve
Deciding requires the approver or admin role. Reading the queue is additionally open
to security_analyst and auditor.
Two prohibitions are enforced by the Console, not by convention:
- API keys may not decide. A decision is a signed statement that a named human accepted responsibility. An API key carries a tenant and a role but no attributable identity, so the request is refused rather than recorded against "unknown".
- Self-approval is prohibited. When the requesting principal is attributable and equals the deciding principal, the decision is refused.
An approval that has already been decided cannot be decided again.
min_approvers is carried in the policy, shown in the Console, and part of the signed
bundle, but the Console records an approval as approved on the first approve
decision. A two-person rule is expressed today but not enforced by the approval flow.
The wait is bounded
The recorder asks for a bound of 120 seconds by default and gives up 10 seconds later on its own clock, so a slow network surfaces as "unavailable" rather than as a silent early return. The Console clamps the requested bound to between 1 and 900 seconds and polls for a decision within it.
If nobody decides in time, the Console marks the approval timeout and keeps the row.
"Nobody answered in time" is itself evidence about how the operation was handled.
A guardrail that can hang production forever gets uninstalled, and then it protects nothing. That is why the bound exists and why it is short.
TTL and scope
A decision records a scope — operation for this instance, or a narrow pattern — and,
for pattern scopes, a ttl of at most 24 hours. Both travel back to the recorder in the
signed decision.
The recorder does not yet cache an approval for reuse: every held operation asks again, regardless of the scope and TTL on the previous decision. Scope and TTL are recorded faithfully in the signed statement; they do not currently widen what a single approval covers.
What is recorded
On the Console side, a decision is signed with the console's Ed25519 key over this statement:
{
"approval_id": "01J…",
"decision": "approve",
"approver_principal": "[email protected]",
"scope": "operation",
"ttl_seconds": 0,
"comment": "checked the backup ran at 02:00",
"decided_at": "2026-08-12T09:14:22.104Z"
}On the recorder side, the outcome lands in the hash-chained stream as a lifecycle
record with operation approval_decided:
{
"operation_record_id": "01J…",
"outcome": "approved by [email protected]",
"approval_id": "01J…"
}The operation record and the approval are linked by that lifecycle record. The record
schema also has an approval_ref field for a direct link; the recorder does not populate
it today.
When the Console is unreachable
An unreachable Console is not an approval. What happens instead is the binding's
on_approval_unavailable:
| Value | Behaviour |
|---|---|
block (default) | the operation is blocked; the recorder exits without running the command |
allow_warn | the operation proceeds, and the recorder prints sealr: proceeding WITHOUT approval — <reason> |
Either way, the reason is recorded. The paths that count as unavailable are all of them: the recorder is not enrolled, sync is disabled or no endpoint is configured, the RPC failed, or the wait bound elapsed with no decision.
bindings:
- resources: ["db:prod-*"]
mode: enforce
on_approval_unavailable: block # the default; state it anywayBlocking is the default because an operation important enough to need a human should not
proceed unattended because a network failed. Choose allow_warn deliberately, for
resources where an outage costs more than an unreviewed write.
See Writing policies for where on_approval_unavailable and the
approval block are declared.