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).

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 resolve access to the approval resource. The tenant default is the UMS grant held by g_{tenant}_approvers; membership and grants are managed through the existing UMS surfaces. A missing grant or unavailable UMS returns HTTP 403 with code not_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 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.