Skip to main content
Approvals gate sensitive agent actions behind a human decision, then execute them server-side so the agent never has to retry.

The gate

Every sensitive route delegates to runGuardedAction(req, { actionType, payload }):
  1. Resolve the subject (tenant_user + Account Kit) and the actor.
  2. resolveApprovalRequirement(kit, actionType) decides if approval is needed: start from the built-in DEFAULT_APPROVAL_ACTIONS set, then apply the per-primitive requiresApproval override on the kit.
  3. Agent + required → freeze the request as a pending row, return 202 { status: "pending_approval", approval_id }.
  4. Human session, or not required → execute immediately.
The same gate runs for MCP tools (via mcpGuard) so an agent can’t bypass it by switching transports.

Deferred replay (single execution path)

There is one executor registry mapping action_type → service call (cards.create → createCard, domains.purchase → purchaseDomain, …). Both the immediate path and the post-approval path dispatch through it, so a frozen action runs exactly as it would have live.

Data

Pending and resolved approvals live in the approvals table, scoped to a tenant_user: action_type, primitive, the frozen payload, requested_by_*, status (pending | executed | failed | denied | expired), result / error, and resolved_by_*. Creating, approving, and denying each write an activity_events row (approval.requested, approval.approved, …) and publish a live event for dashboards.

Approvers

Anyone authenticated for the company can resolve an approval: operators via the dashboard or CLI (any of their tenant users), and end-users via your app (scoped to their own tenant_user). PII-heavy payloads (e.g. KYC members) are stored minimally and re-fetched at execution time.