Human-in-the-Loop Approvals

A policy rule with action: step_up holds the agent's tool call until a human decides. On interactive coding agents (Claude Code, Copilot, Qwen Code) the question appears inline at the keyboard. A headless agent — a LangChain, CrewAI, browser-use, or OpenAI Agents worker with no human attached — posts a pending approval request to the control plane instead, and waits. Someone on your team approves or denies from the console, from Slack, or from anything you point a webhook at, and the agent continues or fails closed.

Every path fails closed: timeout, denial, network error, or no reviewer configured all resolve to a block, never a silent allow.

The flow

  1. A guarded tool call matches a step_up rule.
  2. The runtime posts the request (tool, arguments preview, rule, severity, session) to POST /api/approvals, deduplicated by fingerprint so a retrying agent coalesces onto one request instead of spamming the queue.
  3. Approval routes pick who gets told (see below). The request also appears in the console under Approvals.
  4. A teammate decides. The decision settles once — two approvers racing cannot both win, and an expired request cannot be approved after the agent has already given up.
  5. The agent's next poll sees the decision. Approved → the tool call runs. Anything else → blocked, with the outcome written to the signed audit trail.

Two ways to require approval

A policy rule with action: step_up — the rule-level route, for a class of action ("any destructive command in production").

A single tool, from the console. Open a blocked or risky call in a session or on the Threats page, and in Apply a rule pick Ask → this session / agent / device / whole org. Tick which policy profiles it should land in — a rule like "kubectl_delete needs approval" usually belongs in every profile governing production, and applying it one profile at a time leaves the fleet half-covered. The staged diff names every policy that will change before you commit.

Either way the verdict is the same, and so are the guarantees: approval is strictly softer than a deny and never softer than an allow. If any rule says block, no one is asked. Denial, expiry, timeout and network errors all fail closed. And like a deny it is categorised agent-control, so a local mode="observe" cannot drop an approval requirement the org set.

Tool-level step-up needs prismor >= 1.38.0 on the device. Earlier runtimes write the entry but skip it.

Routing: who gets asked

Approval routes (console → Approvals → Routing) are evaluated first-match- wins by priority. A route matches on any combination of severity, tool pattern, rule id, agent pattern, or agent tag, and targets a Slack channel or DM. A route with no matchers is your catch-all; the console warns when requests are queueing without one, because an unrouted request notifies nobody and the agent fails closed after its timeout.

Slack setup lives in the route panel: connect the workspace, set the signing secret, and the Approve/Deny buttons work from the channel. A Slack click is resolved back to a Prismor account and must belong to an ADMIN or OWNER of the org — the button is a convenience, not a bypass.

Webhooks: bring your own approver

Register an endpoint (console → Approvals → Webhooks) and Prismor POSTs signed JSON for approval.requested, approval.decided, and approval.expired. Each payload for a new request carries single-purpose approve_url / deny_url links: one POST settles the request, so a pager, an internal portal, or a ten-line bot can act as the approval surface with no further API integration. Signing the links needs a key: APPROVAL_LINK_SECRET if you set one, otherwise a key derived from BETTER_AUTH_SECRET — the session secret itself is never used directly, so an approval link and a session cookie never share a signing key. With neither set the links are omitted from the payload, never sent unsigned.

Verify deliveries with the per-endpoint secret (shown once at creation):

X-Prismor-Event:     approval.requested
X-Prismor-Delivery:  whd_...            # idempotency key — retries reuse it
X-Prismor-Timestamp: 1754190000
X-Prismor-Signature: v1=HMAC_SHA256(secret, "<timestamp>.<raw body>")

Compare timing-safe and reject timestamps older than 300 seconds. Failed deliveries retry with backoff (max 5 attempts); the console shows recent delivery outcomes per endpoint.

Runtime configuration

KnobWhereBehavior
action: step_uppolicy ruleHold this class of action for a human decision
Ask in Apply-a-ruleone tool, chosen policiesHold this tool for a human decision (needs prismor >= 1.38.0)
PRISMOR_APPROVALS=0env, fleet-wideDisable escalation: step_up fails closed with no control-plane round trip
approvals=Falseadapter keywordOpt one guard out of escalation (guard_tools, guard_controller, guard_agent, ...)
PRISMOR_APPROVAL_TIMEOUTenvSeconds the agent waits for a decision (default 300)
PRISMOR_APPROVAL_POLLenvPoll interval in seconds (default 3)

The async adapters (LangChain's coroutine path, browser-use, OpenAI Agents) wait in a worker thread, so a pending approval never stalls the event loop running the rest of your agent — concurrent tools, streams, and the browser CDP connection keep servicing while the human decides.

Requires

Approvals need an enrolled device or agent key (connect to the platform) and prismor ≥ 1.36.0 for the configuration knobs above.