pending_approval verdict, the agent’s
tool call is held instead of dispatched — it’s now waiting on a human.
This page is for the reviewer: how to approve agent tool call holds (or
reject them) from the console, what the queue shows you, and how OrcaRouter
keeps two reviewers from colliding on the same decision.
It’s the resolution half of human-in-the-loop. For why a call gets held
and how the held agent re-submits afterward, see
verdicts and the deeper
approvals reference. To resolve from your
own system instead of the console, see
approval webhooks.
1. The held-call lifecycle, from a reviewer’s seat
A held call is a short out-of-band loop. Your job is the middle step:The call is held
A rule resolves to
pending_approval. The relay returns HTTP 400
with code firewall_approval_pending and an approval id; the call
never reaches the tool. The agent starts polling on that id.You resolve it
You open the Approvals queue, read why the call was held, and
approve or reject it — the focus of this page.
Resolving a hold is gated at Developer+ — the same gate as the
firewall Events feed. Lower roles can read firewall policy, settings, and
discovered tools, but only Developer-and-above roles can list the approval
queue or approve/reject a held tool call. See
roles and scope.
2. List the pending queue
The Approvals tab readsGET /api/workspace/firewall/approvals. With no
filter it returns the pending queue, oldest first — so the
longest-waiting call sits at the top and you work the backlog in order.
state is the one filter that matters. The values map to the approval’s
lifecycle:
state | Approvals returned |
|---|---|
pending (default) | Held, awaiting a decision — your work queue. |
approved | Already let through. |
rejected | Already blocked. |
3. Read why the call was held
Each row carries the decision inputs a reviewer needs — the tool name (tool_name), an arguments fingerprint (args_hash, the SHA-256 of
the canonicalized call arguments — the raw argument values are not stored
in the approval), the request id, and a plain-English provenance line
that names the policy, the rule, and the clause that fired:
policy_name — which policy held it
policy_name — which policy held it
The named policy the matched rule belongs to, resolved
workspace-scoped so a stale id can never surface another tenant’s
policy name.
rule_label + matched_clause — the human reason
rule_label + matched_clause — the human reason
The rule’s label and a one-line “why” descriptor — e.g.
command contains rm -rf, or tool matches "http_fetch" for a
glob-only rule. This renders the “Held because…” line in the queue.rule_changed — provenance you can trust
rule_changed — provenance you can trust
true when the matched rule was edited at or after this approval
was created. The live label and clause are then suppressed (they may no
longer reflect what actually held the call), and the queue shows a
“rule since changed” note instead of stale provenance. The tool name
and arguments — the real decision inputs — are always shown.4. Approve or reject
Resolving sendsPATCH /api/workspace/firewall/approvals/:id with a
decision of approved or rejected and an optional reason. The
console does this for you when you click the button; the shape is:
approved→ the held call may proceed. The agent’s next re-submit, carrying the single-use approval header, is let through once.rejected→ the call stays blocked. The agent sees the rejection and can pick another path, ask the user, or stop.
decision is validated against the closed {approved, rejected} set —
anything else is rejected. The reason is recorded with the decision and
written to the firewall audit log alongside the actor, the tool name, and
the request id.
Every resolve writes a workspace audit row naming who decided, the
decision, and the reason. Console resolutions record the human actor;
webhook resolutions record a system
actor. Resolution provenance is never silently dropped.
5. First-writer-wins: no double-resolve
A pending approval can be raced — two reviewers in the console, or a console click and a webhook callback arriving together. OrcaRouter resolves this with a single first-writer-wins rule:- The decision is an atomic conditional update that only fires while
the approval is still
pending. The first writer wins and applies the decision. - Every later writer observes “already resolved” and is treated as an idempotent no-op — it gets HTTP 200 with the already-resolved document, not an error.
resolved: true means your call applied the decision; already_resolved: true means someone (or some webhook) got there first and you’re seeing
their outcome. Either way the queue reconciles to one consistent state.
6. A concrete pass through the queue
A balanced-autonomy workspace holds an agent’sshell.exec call because a
rule matched command contains rm -rf. As the reviewer you:
- Open Approvals — the held
shell.execsits at the top of the oldest-firstpendinglist. - Read the row: tool
shell.exec, theargs_hashfingerprint, request id, and the “Held because…command contains rm -rf” line (rendered from the matched rule’s clause). Norule_changedflag, so the provenance is current. - The target is a scratch directory, so you approve with a reason.
- Your
PATCHreturnsresolved: true; the agent’s next poll seesapproved, re-submits with its single-use header, and the command runs exactly once.
already_resolved: true with their decision — no harm, no double-run.
Where to go next
Approvals reference
The full HITL loop: hold, poll, re-submit, and the single-use header.
Approval webhooks
Resolve holds from your own system with an HMAC-signed callback.
Verdicts
Where pending_approval sits among the six firewall verdicts.
Events log
Confirm a resolved call’s downstream outcome in the feed.
