Required approvals
Required approvals for Q integrations.
Q1.7 binds an approval to a tenant, authenticated principal, run, action ID and immutable action version. PostgreSQL retains pending decisions and terminal outcomes across worker restarts. Approval does not bypass workspace, sandbox, tool or retrieval permissions.
Select the policy
For POST /v1/answer, plan_approval_policy accepts none, advisory and required. Legacy plan_approval: true means required and takes precedence over a weaker policy. A tenant's existing observability plan_approval/plan_approval_default requirement also takes precedence. A policy-store failure stops execution instead of silently disabling approval.
PLAN execution always requires approval, including when loading a plan whose stored status is approved. For a draft-only preview, set mode: "plan", extra_context: {"plan_only": true} and plan_approval_policy: "advisory"; expiry returns the draft without executing its tasks. Optional planning pauses also apply to the legacy planner path. ReAct, deep and multitask paths that cannot honour a requested plan pause fail with plan_approval_requires_plan_mode; use PLAN mode for a structured execution plan. Required and advisory policies never fall through on missing or unavailable state. Only an explicitly advisory plan's positively observed expiry may continue automatically.
enable_tool_approval: true on answer requests or POST /v1/agents/{agent_id}/runs requires approval for WRITE, EXECUTE and DANGEROUS tools. Auto-review, allowlists and sandbox availability cannot satisfy that requirement. The requirement follows supported agent workers, child runs and resume/branch operations. A configured always_ask auto-review policy likewise asks for a human decision before WRITE+ actions. Hard-budget extensions always require an explicit decision.
Receive and decide
approve_plan/plan_ready and tool_approval_required events include an approval ID, action version and policy. Tool events are emitted before the server waits. Plan events carry the exact plan; tool summaries are intentionally compact. Inspect the full authorised proposal with:
GET /v1/answer/{run_id}/approvals
Authorization: Bearer <issuing-principal-token>The response contains up to 100 most recent approvals for this caller/run, including immutable proposal, action version, policy, status, decision and expiry. Another tenant, principal or run cannot discover or decide these approvals. An API key and a JWT, two different keys, or JWTs from different issuers have distinct identities. Shared-secret authentication represents one service principal within a tenant; it does not identify separate human approvers. No cross-principal approval delegation is implied.
Approve/reject an action using the ID from its event. Include the version to verify the reviewed proposal:
POST /v1/answer/{run_id}/tool-approval/{approval_id}
Content-Type: application/json
Authorization: Bearer <issuing-principal-token>
{"approved":true,"action_version":"<version-from-event>"}The same body works at POST /v1/agents/{agent_id}/runs/{run_id}/tool-approval/{approval_id} after the agent/run association is checked. Budget extensions use POST /v1/answer/{run_id}/budget-pause/{pause_id}. A missing body, omitted decision, integer or string boolean is rejected with 422. reason remains accepted for compatibility but is not stored; decision metadata records identity, outcome and time without free-text audit content. All routes require ai:answer and the original principal.
Use POST /v1/answer/{run_id}/approve or /reject for a plan. /approve explicitly approves the immutable plan as-is; an optional {"plan": {...}} verifies that it exactly matches the proposal. In-place plan editing now returns approval_action_changed; submit changed work as a new run for review. Reusing an action slot with different arguments also conflicts. Internal callers must provide a verified principal and a stable, non-empty run/call ID.
Repeating the same decision is safe, including after consumption. Opposite decisions return approval_decision_conflict (409), expired approvals return approval_expired (409), and unknown/foreign approvals return approval_not_found (404). Store failures return approval_store_unavailable (503). A duplicate decision acknowledges the stored outcome; it does not issue another execution permit. Inspect after uncertain HTTP delivery before deciding what to retry. Version is optional only because the proposal ID is immutable; supplying it is recommended.
Execution and recovery
States are pending, approved, rejected, expired and consumed. Each approve/reject and consume operation is atomic. The database clock determines expiry; a decision made before expiry cannot be dispatched after expiry. Full query/inputs, tool schema/tier, workspace and delegated data context are checked after hooks immediately before dispatch. A changed argument, even outside the display summary, is blocked. Required approval never turns into automatic approval.
A pending or approved proposal can be read by another worker with the same authorised context. Stream disconnection does not delete it. Existing workflow recovery must restore that exact action/context and current resource permissions; this change does not automatically restart interrupted answer streams or entire agent runs. Expired or rejected actions require new work with a fresh review, not resetting a database row.
Consumption authorises one dispatch attempt. A crash after consumption or an uncertain commit leaves the action unavailable for automatic retry, even if no side effect occurred. Inspect the external system and reconcile the result before creating a new run. This conservative rule prevents duplicated effects; it does not promise exactly-once execution. Existing idempotency and provider-side retry rules still apply.
Rollout
- Drain or cancel old pending Redis/in-memory approvals and old workers. Do not copy unbound snapshots into the new table or operate mixed approval protocols.
- Apply migration
20261007_200000_approvals. Give the restricted runtime role USAGE onq_serviceand SELECT/INSERT/UPDATE onq_service.approvals; retain forced RLS and a non-superuser, non-BYPASSRLS role. - Deploy the matching API and worker code together. Migrate clients that edited plans in-place or omitted tool/budget decision bodies. Pending approval data from previous code is intentionally not trusted.
- Verify authorised decisions, foreign-principal rejection, expiry, disconnection and restart in staging. Run representative concurrency/latency testing before production qualification.
- Preserve approval rows and run/action identity coherently across backup/restore and rollback. Do not delete consumed rows while a workflow could reuse their action IDs. Downgrading the table while actions can resume destroys the guarantee; prefer a forward fix.
Proposals are bounded to 256 KiB; waits are at most one hour (tool/budget defaults: five minutes). Polling is every 200 ms with bounded database operations. Retained terminal records are not automatically purged; plan capacity and an audited archival policy before large-scale operation. Store full proposals and backups as private tenant data. Monitor approval error codes, outstanding age and database capacity; this local evidence is not a production SLO.