Skip to content

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_user vs approval. ask_user asks "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

  1. Gate — the invocation is intercepted; an ApprovalCase (status pending) is created. The capability does not execute.
  2. Suspend — the agent's turn blocks durably (Temporal) waiting for resolution, with a timeout (24h default). In chat, an awaiting_input tool frame carries the approval_case_id so your surface can render an approval widget.
  3. Resolve — a human resolves it:
    PUT /api/admin/approvals/{case_id}/resolve
    { "decision": "approved", "comment": "..." }      # or "rejected"
  4. 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.

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 username claim is also rejected — every resolution must be attributed (resolved_by / resolved_at are 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 WAIT node. Capability approval (this page) gates an agent's tool invocation at call time; workflow wait nodes pause a workflow graph.

Martha is built by aiaiai-pt.