Skip to main content
When a firewall rule returns the 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:
1

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.
2

You resolve it

You open the Approvals queue, read why the call was held, and approve or reject it — the focus of this page.
3

The agent proceeds (or stops)

On approve, the agent re-submits the original call with a single-use X-OrcaRouter-Firewall-Approval header and the gateway lets it through that one time. On reject, the call stays blocked.
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 reads GET /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.
GET /api/workspace/firewall/approvals?state=pending
state is the one filter that matters. The values map to the approval’s lifecycle:
stateApprovals returned
pending (default)Held, awaiting a decision — your work queue.
approvedAlready let through.
rejectedAlready blocked.
This is a console route on your session — configure and review it from the dashboard, not with an sk-orca-… relay key. Relay keys are for /v1/* model calls; firewall management runs under your console login.

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:
The named policy the matched rule belongs to, resolved workspace-scoped so a stale id can never surface another tenant’s policy name.
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.
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.
rule_changed is a deliberate honesty signal, not an error. If someone edits the firewall rule while a call is sitting in the queue, OrcaRouter would rather hide a possibly-wrong reason than show you provenance that no longer matches. Decide on the tool name and the policy name (still shown) in that case.

4. Approve or reject

Resolving sends PATCH /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:
PATCH /api/workspace/firewall/approvals/507f1f77bcf86cd799439011
Content-Type: application/json

{ "decision": "approved", "reason": "verified target host with the on-call" }
  • 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.
The response tells you which side you were on:
{
  "resolved": false,
  "already_resolved": true,
  "approval": { "state": "approved", "decision": "approved", "...": "..." }
}
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.
Because resolution is idempotent, a flaky network or a double-click can’t corrupt a hold or flip a decision. The first approve/reject is the only one that counts; everything after it just reads back the result.

6. A concrete pass through the queue

A balanced-autonomy workspace holds an agent’s shell.exec call because a rule matched command contains rm -rf. As the reviewer you:
  1. Open Approvals — the held shell.exec sits at the top of the oldest-first pending list.
  2. Read the row: tool shell.exec, the args_hash fingerprint, request id, and the “Held because… command contains rm -rf” line (rendered from the matched rule’s clause). No rule_changed flag, so the provenance is current.
  3. The target is a scratch directory, so you approve with a reason.
  4. Your PATCH returns resolved: true; the agent’s next poll sees approved, re-submits with its single-use header, and the command runs exactly once.
Had a teammate approved it a second earlier, your click would have returned 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.
For the risks these holds are meant to catch, see dangerous tool calls and excessive agency.