new: v0.3 — kubernetes-native sandboxes and the multi-user control plane. read the changelog ›
DocsGovernance

Approvals

The pause in the middle of a run. When the permission gate resolves a tool call to approve, the run enters awaiting_approval, an approval.requested event appears on the timeline, and the tool call blocks until a human decides — or the approval expires.

Decide

ID=$(curl -s "$FLUIDBOX_URL/v1/approvals" \
  -H "Authorization: Bearer $FLUIDBOX_ADMIN_TOKEN" | jq -r '.approvals[0].id')

curl -sX POST "$FLUIDBOX_URL/v1/approvals/$ID/decision" \
  -H "Authorization: Bearer $FLUIDBOX_ADMIN_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"decision":"approved_once"}'

Three decisions:

DecisionEffect
approved_onceThis single call proceeds. The next matching call asks again.
approved_sessionCalls in this approval's scope are allowed for the rest of the run.
deniedThe tool call returns an error to the model — which usually tries a different approach rather than giving up.

GET /v1/approvals lists everything pending; GET /v1/sessions/{id}/approvals scopes to one run. The dashboard's attention strip is the same data.

Scope — how far one "yes" reaches

approved_session remembers by scope key, not blanket tool name where that would over-grant: for Bash the key is the matched command prefix (approving git push covers git push, not all shell); for other tools it is the tool name. The policy chooses once or session as the default scope per rule, and can override the TTL per rule too — see Policies.

Idempotency — safe to double-click, safe across restarts

Decisions are idempotent by (session_id, tool_call_id) and settled by a compare-and-swap: a double-submit, two reviewers racing, or a retried HTTP call produce exactly one decision, and everyone else sees the settled answer. The database row is the source of truth — if the control plane restarts mid-pause, the runner's retry re-attaches to the pending row; nothing duplicates and nothing hangs.

Expiry — absence narrows, never widens

An unanswered approval expires after the policy's TTL (per-rule overridable) and the expiry action is deny. There is no configuration in which nobody-was-watching results in more permission than somebody-said-yes.

Autonomous runs — rewritten, not bypassed

With autonomous: true, a run never waits on a human: an approve verdict is rewritten to the policy's fallback (autonomy.on_approval_rule, or the rule's own on_autonomous override) inside the policy engine, and the ledger records both the original and the rewritten verdict. The permission callback stays wired in every mode — there is no bypass flag anywhere in the system, so the audit trail for an autonomous run reads exactly like a governed one, minus the human.

What an approval can never do

Two hard floors sit above approvals in the gate:

  • Fork-PR trust tier. A run triggered by a pull request from a fork is frozen read-only. No approval widens it — reads only, no writes, no execution, no egress.
  • Frozen tool surface. A tool that wasn't in the run's frozen capability set (or whose brokered binding has gone stale) is denied at the availability stage; approval is never consulted.

Who may decide

Single-admin deployments: the operator. Multi-user deployments (FLUIDBOX_REQUIRE_SSO): role-based — with one deliberate exception. A brokered call riding a personal connection is decidable only by the owner who invoked the run — no role, admin, or operator override, approve or deny. Your credential, your call.

Next