Actions and Approvals
Permissioned execution with single-claim, at-most-once dispatch.
What an action is
Any consequential tool call becomes an action: tool name, sanitized arguments (credential-looking keys are stripped before storage), risk level, tenant/run/session attribution, approval policy, and expiry. Read-only tools may execute automatically; write tools need installation permission; destructive, financial, identity, email-send, and provisioning actions require approval by default.
Lifecycle
pending → approved → executed | failed
pending → rejected | expired
Create with POST /v1/actions (idempotent via Idempotency-Key, scoped to your project — the same key elsewhere is a different action). Decide with POST /v1/actions/:id/approve or /reject. Both decisions are idempotent: replays return the recorded outcome with deduped: true. Terminal states conflict with 409; expired actions return 410 and can never be revived.
Delivery semantics: single-claim, at-most-once
Exactly one approver wins the pending → approved claim; that winner dispatches once, and concurrent approvers get the recorded outcome instead of a second side effect. This is not exactly-once external delivery:
- A crash between the external send and the result write leaves
approvedwith no recorded outcome. - Gmail does not accept our execution key as upstream idempotency.
On an ambiguous action, reconcile against the upstream outbox (Gmail sent folder, MCP server logs) before creating a replacement action with a new key — never reuse a key with a different body (that is 409 idempotency_conflict by design).
Session approvals
Session pauses (turn.paused) resolve through the session endpoints and mirror into the actions index, so one timeline covers both. Approval requires the full tool_results set; the resume replays tool outputs back into the model and continues the turn.

