Approval Policy
The approval policy category puts a human in the loop before sensitive agent operations. Use it to require approval before a workflow runs, before specific tools or action types execute, or when the in-flight cost crosses a threshold. Also supports session-start gates (E12) for chat-style agents and auto-approval below a risk level.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
require_approval_for | string[] | ["deploy", "delete", "payment"] | Workflow names, action types, tool names, OR the special "session-start" token |
cost_threshold | number | 100.00 | Trigger approval when context.cost_used exceeds this amount |
approvers | string[] | [] | Emails or group IDs surfaced in violation metadata (used by your approval workflow) |
timeout_minutes | integer | 30 | Deadline -- surfaced in metadata for your approval workflow |
action_on_timeout | string | "block" | block raises PolicyViolationError, warn lets the run continue |
auto_approve_below_risk | string | "low" | Auto-approve actions with risk_level at or below this (none, low, medium, high, critical) |
How It Works
The approval handler runs at before_workflow, mid_execution, and after_workflow.
| Phase | What It Checks | Actions |
|---|---|---|
before_workflow | "session-start" in require_approval_for, or workflow_type/workflow_name is in the list | BLOCK or WARN per action_on_timeout |
mid_execution | Each pending_action against require_approval_for (with auto_approve_below_risk skip), cost threshold, tools_used against the list | BLOCK or WARN |
after_workflow | Audit: which restricted actions actually executed and whether cost exceeded threshold | WARN (audit-only) |
Context Attributes Read
| Attribute | Phase | Purpose |
|---|---|---|
context.workflow_type / context.workflow_name | before_workflow | Match against require_approval_for |
context.pending_actions | mid_execution, after_workflow | List of {type, risk_level} dicts (or strings) the agent wants to run |
context.cost_used | mid_execution, after_workflow | Compare to cost_threshold |
context.tools_used | mid_execution, after_workflow | Tool names checked against require_approval_for |
When a gate fires, the result metadata includes requires_approval: True, approvers, timeout_minutes, and action_on_timeout. The handler does not block on a human response -- your inbox / Cowork session-bootstrap code reads that metadata and routes to the approval flow (see services/session_approval.py for the session-start path).
Example Policy
{
"name": "Production Approval Gates",
"category": "approval",
"rules": {
"require_approval_for": ["deploy", "delete", "send_email", "make_purchase"],
"cost_threshold": 25.00,
"approvers": ["ops-team@acme.com", "finance@acme.com"],
"timeout_minutes": 60,
"action_on_timeout": "block",
"auto_approve_below_risk": "low"
},
"scope": {"agents": ["ops-agent"]},
"enabled": true
}
Session-Start Approval (E12)
{
"name": "Session Bootstrap Approval",
"category": "approval",
"rules": {
"require_approval_for": ["session-start"],
"approvers": ["security@acme.com"],
"timeout_minutes": 15,
"action_on_timeout": "block"
}
}
SDK Integration
import waxell_observe as waxell
waxell.init()
@waxell.observe(agent_name="ops-agent", enforce_policy=True)
async def run_deploy(target: str) -> str:
# before_workflow checks workflow_name against require_approval_for
# mid_execution checks pending_actions / tools_used / cost_used
return await deploy(target)
Observability
| Field | Example |
|---|---|
| Category | approval |
| Action | block |
| Reason | "Action 'payment' requires approval" |
| Metadata | {"requires_approval": true, "approvers": ["finance@acme.com"], "action_type": "payment", "timeout_minutes": 60, "action_on_timeout": "block"} |
| Field | Example (cost trigger) |
|---|---|
| Reason | "Cost ($28.45) exceeds approval threshold ($25.00)" |
Common Gotchas
- The handler does not perform approval -- it raises the signal. A BLOCK result raises
PolicyViolationError. Your calling code (Cowork session bootstrap, inbox, or a custom router) inspectsmetadata["requires_approval"]and runs the actual approval flow. auto_approve_below_riskrequiresrisk_levelon each pending action. If your agent emitspending_actionsas plain strings (no dict), the risk-level skip is never taken and every match inrequire_approval_forfires the gate.session-startis a magic string. It does not match againstworkflow_name-- it triggers if the literal"session-start"appears inrequire_approval_for. The metadata includesapproval_kind: "session-start"so routing code can tell session gates apart from workflow gates.cost_thresholdcheckscontext.cost_used, not the cost of the next action. The check fires after the cost has already been incurred. To gate before spending money, pair this with the Budget Policyper_workflow_cost_limit.require_approval_foris a flat list shared across workflow names, action types, and tool names. A tool calleddeleteand a workflow nameddeleteboth match the same string. Use distinct names if you need different behavior.after_workflowalways returns WARN if any restricted action ran, not BLOCK. It is an audit signal -- the run has already happened. Usemid_executionBLOCK to actually halt.
Next Steps
- Safety Policy --
require_human_approvalandapproval_toolsfor simpler cases - Budget Policy -- Block by cost before the spend happens
- Domain Governance -- Approval for specific domain actions
- Policy Categories