Capability approvals
How to gate a risky capability behind human approval. This is the one place where a human's authority is carried into a specific agent invocation: the call is suspended, a human signs off under their own identity, and only then does it run.
ask_uservs approval.ask_userasks "what did you mean?" (clarification, non-privileged, any session principal answers). Approval asks "may I do this?" (authorization, blocking, human-only). Same wait-gate plumbing, deliberately different auth.
What triggers it
A tenant policy rule with the require_approval action, matched by capability or by risk (risk_tags like destructive_write / payment, or risk_level). When an agent invokes a capability that matches, the policy gate creates an approval case and returns a pending marker without running the capability.
The flow
- Gate — the invocation is intercepted; an
ApprovalCase(statuspending) is created. The capability does not execute. - Suspend — the agent's turn blocks durably (Temporal) waiting for resolution, with a timeout (24h default). In chat, an
awaiting_inputtool frame carries theapproval_case_idso your surface can render an approval widget. - Resolve — a human resolves it:
PUT /api/admin/approvals/{case_id}/resolve { "decision": "approved", "comment": "..." } # or "rejected" - Outcome
- Approved → the capability is re-invoked with a one-time preapproval reference and runs. The gate re-verifies the case against the DB (status
approved, same tenant, same capability) before allowing — a case approved for one capability can't unlock another. - Rejected / expired → a
{error, status:403, policy_blocked:true, decision}result flows back as the tool result; the capability never ran, and the agent degrades gracefully.
- Approved → the capability is re-invoked with a one-time preapproval reference and runs. The gate re-verifies the case against the DB (status
Who can resolve (auth)
PUT …/resolve requires a human Keycloak token with a username:
- Agents, service accounts, and embeds cannot resolve — rejected at the auth layer. This is a cardinal rule: an agent can never approve itself out of a gate.
- A human token missing a
usernameclaim is also rejected — every resolution must be attributed (resolved_by/resolved_atare recorded, and the action is written to the immutable audit log).
Current behavior & limitation
Today, resolution requires a human, with a username, in the tenant — and that's all. Be aware of what is not yet enforced:
- No second-party check — the same human who triggered the gated action can approve it (self-approval is currently possible).
- No designated approver role — any human in the tenant can resolve any case.
So the gate today guarantees a human is in the loop and the agent can't bypass it, but not that it's a different, authorized human. For threat model "agent goes rogue" it's a real wall; for "a person pushes their own risky action through" it's a confirmation they click themselves. Separation-of-duties (requester ≠ approver) and approver-role gating are planned; don't rely on them yet.
See also
- Permissions & access model — identities, human vs service-account, grants.
- Surfaces & capability keys — rendering the approval/question widgets.
- Human-in-the-Loop Workflows — a different mechanism: pausing a workflow on an external event via a
WAITnode. Capability approval (this page) gates an agent's tool invocation at call time; workflow wait nodes pause a workflow graph.