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).
Resolution also enforces both of these server-side controls:
- Second-party: a human requester cannot resolve their own case. The API returns HTTP 409 with code
is_requester. - Live UMS grant: a non-requester needs
resolveaccess to theapprovalresource. The tenant default is the UMS grant held byg_{tenant}_approvers; membership and grants are managed through the existing UMS surfaces. A missing grant or unavailable UMS returns HTTP 403 with codenot_authorized_group.
Admin status is not an alternative approver path. A martha-super-admin can use break-glass to bypass the group gate, but never the second-party or tenant checks; that use is audited separately.
Approval responses expose viewer_can_resolve and resolve_block_reason so a surface can present the server verdict before a click. They are advisory only: the PUT repeats the same check while holding the pending row lock. The case's required_groups value is a creation-time audit snapshot, not authorization; UMS is consulted live at resolution time.
Legacy pending cases
The migration deliberately does not backfill existing pending cases. A legacy row with NULL required_groups keeps its one-time any-human group-gate exemption, although a captured requester still cannot self-approve. If requester_principal is also NULL, the historical requester is unknowable and the second-party comparison cannot run. Drain the pending queue before rollout when that exemption is unacceptable.
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.