*.delete
on real data. For those you want a person in the loop: hold the call, let
a human look, then proceed only on a yes. That is exactly what the
pending_approval verdict does.
This page covers the human in the loop agent approval flow end to end:
how a held call surfaces, how a reviewer resolves it from the console or a
webhook, and how the agent re-submits the approved call. For where the
verdict sits in the rule grammar, see
Firewall Rules; for the policy model around it,
see the Firewall overview.
1. What a held call looks like
When a rule resolves topending_approval, the engine enqueues an
approval record and the call does not reach the tool. The relay
returns HTTP 400 with error.code firewall_approval_pending; the
approval id the agent will poll on is carried in the human-readable
error.message:
error.metadata (when present) carries the verdict’s
reason detail — reason_code, factors, risk_score — not the approval
id. Parse the id out of the message, or get it from the SDK helper below.
The hold is immediate — there is no inline long-poll blocking your
request. The agent gets the id back, the call is parked server-side in the
pending state, and resolution happens out-of-band.
A held call is recorded as a firewall event with verdict
pending_approval, so it is filterable in the
events log right alongside deny events
— you can always see what was held and, via the approval record, what was
resolved.2. One concrete example
Author a rule that holds any write to a production connection for a human:Agent calls the tool
The agent issues
db.write against prod. The rule matches, the
engine holds the call, and the relay returns 400 firewall_approval_pending with an approval_id.A human (or your system) reviews
A reviewer resolves the approval — in the console or via a signed
webhook callback (see §3).
Agent polls until resolved
The agent polls the approval id until its state is no longer
pending (see §4).3. Resolving an approval
There are two ways to turn apending approval into approved or
rejected. Both share a first-decision-wins guarantee — the first
resolve to land is applied atomically, and any later resolve (or a
duplicate) is an idempotent no-op returning 200.
Console — a reviewer clicks approve/reject (Developer+)
Console — a reviewer clicks approve/reject (Developer+)
The Approvals tab lists pending holds oldest-first, each with the tool
name and a “Held because…” line naming the policy and the rule clause
that fired. (The raw call arguments are not stored on the approval
record — only the tool name, provenance, and an args hash — so the
reviewer decides from the tool plus the matched clause.) A reviewer
resolves one with:
decision must be approved or rejected. This route is
UserAuth (the reviewer’s console session) and gated to
Developer+ — your reviewer’s identity is the authorization, so no
shared secret is involved. Resolutions are written to the workspace
audit log.Webhook — your own system decides, HMAC-signed
Webhook — your own system decides, HMAC-signed
To wire approvals into an external system (a Slack approval, a ticketing
workflow), configure an approval webhook secret for the workspace,
then POST the decision back:The callback is authenticated by HMAC-SHA256: set the
X-Orca-Signature: sha256=<hex> header to the HMAC of
<approval_id>\n<raw_body> keyed with your workspace’s approval webhook
secret. The id is part of the signed material, so a captured signature
can’t be replayed against a different approval. Without a configured
secret, callback-driven resolution is rejected — resolve via the console
PATCH instead.4. Poll, then re-submit
The agent side is a poll loop followed by one re-submit. Poll the approval state with a firewall-gateway-scoped token:/evaluate and the MCP gateway); a regular relay key gets 403. It
returns the approval doc — wait until state is approved or rejected
rather than pending. A cross-workspace or unknown id returns 404, never
disclosing that it exists to another tenant.
Re-submit once the state is approved: re-issue the same tool call,
carrying the approval id in a single-use header:
rejected approval is never claimable, so the agent should
treat rejection as a terminal deny and pick another path.
5. States and roles at a glance
| State | Meaning | Agent action |
|---|---|---|
pending | Held, awaiting a decision | Keep polling |
approved | Reviewer said yes | Re-submit once with the header |
rejected | Reviewer said no | Treat as a deny |
| Action | Route | Auth · role |
|---|---|---|
| List the queue | GET /api/workspace/firewall/approvals | UserAuth · Developer+ |
| Resolve | PATCH /api/workspace/firewall/approvals/:id | UserAuth · Developer+ |
| Webhook callback | POST /api/v1/firewall/approvals/:id/callback | HMAC-signed |
| Poll state | GET /api/v1/firewall/approvals/:id | Gateway token |
6. Where approvals fit
Apending_approval verdict is one of the
firewall verdicts — it composes with
everything else in a policy. Two interactions worth knowing:
- Skill quarantine escalates to a hold. If a held tool call is owned by
a quarantined skill, anything short of a
deny is escalated to
pending_approvalautomatically — quarantine and approvals are the same review gate from two directions. - Shadow mode flattens it. In
shadow mode a
pending_approvalverdict is downgraded toauditand logged as[shadow] would …, so you can measure how often a hold would fire before it starts gating real traffic.
Where to go next
Verdicts
All six firewall verdicts and the default verdict.
Gateway keys
Mint the firewall-gateway token used to poll approvals.
Shadow mode
Measure a hold before it gates real traffic.
Rule reference
Author the rule that produces a pending_approval verdict.
