Approvals
A human sign-off gate for destructive and irreversible tool calls. Every tool declares a risk level in its manifest; the workspace policy decides which levels demand explicit approval, who can grant it, when it escalates, and what happens when nobody answers in time.
Overview
Approvals can be resolved via the operational dashboard, the chat
interface (using the x_ask_user.question SSE frame), the
API (POST /v1/exec/approvals/:id/approve|reject), or
xeroctl approvals approve|reject.
Risk Levels
Every tool declares one of four risk levels in its manifest entry. The risk level determines the default approval behavior:
read write
Dispatch without a human in the loop.
destructive irreversible
Block until a delegated reviewer resolves the request.
| Level | Description | Default Policy |
|---|---|---|
read |
Observes state without modification (e.g. kubectl_get). |
Auto-approve |
write |
Writes data in a recoverable way (e.g. kubectl_label). |
Auto-approve |
destructive |
Destroys data or state that may be difficult to reverse (e.g. kubectl_drain). |
Require approval |
irreversible |
Cannot be undone under any circumstances (e.g. openstack_server_delete). |
Require approval |
Risk levels are ordered read < write <
destructive < irreversible. See the
MCP exec reference for how risk levels
interact with tool dispatch.
Approval Policies
Approval policies are configured per workspace via the
approval_policies table. Each policy defines timeout
durations, timeout behaviors, and escalation chains for destructive
and irreversible risk levels independently.
// schemaPolicy Fields
| Field | Type | Default | Description |
|---|---|---|---|
policy_name |
string | - | Unique policy name within the project + workspace. |
timeout_seconds_destructive |
integer | 900 | Seconds to wait before timeout for destructive operations (15 min). |
timeout_seconds_irreversible |
integer | 3600 | Seconds to wait before timeout for irreversible operations (1 hour). |
on_timeout_destructive |
string | escalate |
Action on destructive timeout: escalate, reject, or approve. |
on_timeout_irreversible |
string | escalate |
Action on irreversible timeout: escalate, reject, or approve. |
escalation_chain_json |
JSON | null | Ordered list of escalation stages (see Escalation Chains). |
// timeoutTimeout Behaviors
| Behavior | Effect | Outcome |
|---|---|---|
escalate |
Advance to the next stage in the escalation chain. If no stages remain, the approval stays in pending_approval until manually resolved. |
Holds and advances |
reject |
Auto-reject the approval and inform the model. The invocation fails. | Fails |
approve |
Auto-approve and dispatch. Use only when missing reviewers must not block work. | Dispatches |
block (legacy) |
Unknown on_timeout values coerce to escalate at policy-load time; a warning is logged. The legacy block value travels this path. It is not first-class. |
Coerced to escalate |
SLA & Timeouts
The approval SLA worker runs on the router and periodically checks
for approvals that have exceeded their timeout. When an approval
times out, the configured on_timeout behavior is
applied:
- The worker scans for approvals in
pending_approvalpast their deadline. - For each timed-out approval, it applies the policy's
on_timeoutaction. - If the action is
escalate, it advances the escalation chain and fires anexec.approval_escalatedwebhook event. - If the action is
reject, it auto-rejects and fires anexec.approval_timed_outwebhook event. - If the action is
approve, the approval is resolved as if a delegated reviewer had approved it and the tool call is dispatched.
SLO tracking for approval latency uses the
exec_approval_latency_ms metric, measuring time from
approval_requested to resolution.
Escalation Chains
An escalation chain is an ordered JSON array of stages stored on
the policy row. Each stage carries exactly one target (a role, a
user, or a webhook) and a delay_seconds budget before
the next stage fires.
[
{
"target": { "role": "owner" },
"delay_seconds": 300
},
{
"target": { "user": "usr_01HCCCC" },
"delay_seconds": 600
},
{
"target": { "webhook": "https://pagerduty.example.com/xerotier" },
"delay_seconds": 900
}
]
The exec.approval_escalated webhook event fires at each
escalation, carrying the current stage index. If the policy row has
no chain configured, the router falls back to a single owner-role
stage whose delay matches the policy timeout.
Delegation
Project owners can delegate approval authority to team members via the
project_role_delegations table. A delegation grants a
member the ability to approve operations up to a specified risk level
within a specific workspace.
// schemaDelegation Fields
| Field | Description |
|---|---|
project_id | Project scope. |
workspace_id | Workspace scope (null = all workspaces). |
user_id | Delegated user. |
can_approve_risk | Maximum risk level the user can approve: destructive or irreversible. |
granted_by_user_id | User who granted the delegation. |
granted_at | When the delegation was granted. |
expires_at | Optional expiration timestamp. |
revoked_at | If set, the delegation is no longer active. |
Delegations are checked at approval time, not at invocation time. A member without delegation can still invoke destructive tools, they just cannot approve them. The approval gate ensures a second person reviews the operation.
Approval Flow
The end-to-end approval flow works as follows:
- The inference model emits a tool call with a risk level that requires approval.
- The router creates an approval record and emits
exec.approval_requestedwebhook + SSE frame. - Eligible approvers are notified via dashboard, webhook, or escalation chain.
- An approver resolves the approval via API, CLI, or dashboard.
- If approved, the router dispatches the tool call to the XEM.
- If rejected, the router fails the invocation and informs the inference model.
- If timed out, the configured
on_timeoutbehavior applies.
Notifications
Approvers can be notified through multiple channels:
- Dashboard, the Approvals tab in the
/opsdashboard shows pending approvals with live updates via SSE. - Chat, in-chat users see the approval prompt
via the
x_ask_user.questionSSE frame. - Webhook, subscribe to
exec.approval_requestedevents. Commonly used to post to a Slack channel with approve/reject buttons. - PagerDuty / Opsgenie, via the escalation chain,
configured to fire
exec.approval_escalatedevents to a webhook target. - xeroctl, use
xeroctl approvals watchto poll for pending approvals from the terminal.
API Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /v1/exec/approvals |
List approvals (filter by status, risk level). |
| GET | /v1/exec/approvals/stream |
SSE stream of approval events. |
| GET | /v1/exec/approvals/:id |
Get approval details. |
| POST | /v1/exec/approvals/:id/approve |
Approve a pending request. |
| POST | /v1/exec/approvals/:id/reject |
Reject a pending request. |