Tutorial 2: Bind a destructive tool through the Tool Gateway
Onboard a high-impact capability with native ActionRisk, a conservative v1 mode, an approval gate, and a frozen evidence snapshot.
In Tutorial 1 we shipped a read_only pack. Here we add a destructive capability — adp_payments.issue_refund — and route it through the Tool Gateway with an approval gate that captures a frozen evidence snapshot before execution.
What we are building
view source
sequenceDiagram participant Pl as Planner participant Cr as Critic participant TG as Tool Gateway participant Ap as Approver participant Ad as Adapter Pl->>Cr: Plan with s3 (destructive) Cr->>TG: propose s3 with frozen evidence TG->>Ap: GATE_FINANCE_APPROVAL pending Ap-->>TG: approve(snapshot_hash) TG->>Ad: execute s3 Ad-->>TG: ToolResultEnvelope(completed) TG-->>Cr: audit + evidence_refs
The adapter declares native risk for the capability plus a conservative v1 mode. Policy evaluates every risk dimension and the actual arguments; the model cannot lower the declaration.
{
"adapter_id": "adp_payments",
"type": "OPENAPI",
"endpoint_ref": "internal://payments",
"capabilities": ["issue_refund"],
"capability_risks": {
"issue_refund": {
"effect": "external_state",
"authority": "user_delegated",
"reversibility": "compensatable",
"interaction": "api",
"data_scope": "CONFIDENTIAL",
"decision_ttl_seconds": 120
}
},
"approval_mode": "destructive"
}Capability classification: act (mutates external state). Class describes the kind of operation; ActionRisk describes its effect, authority, reversibility, interaction, and data boundary.
Permissions bind a role to a capability under explicit arg_constraints. idempotency_key is required on every write-class call.
{
"permission_id": "p_refund",
"adapter_id": "adp_payments",
"capability": "issue_refund",
"allow": true,
"arg_constraints": {
"amount_inr": { "min": 1, "max": 50000 },
"currency": { "enum": ["INR"] },
"idempotency_key": { "required": true, "pattern": "^ik_[a-z0-9]{16}$" }
}
}The gate is named in the policy bundle and triggered by a JsonLogic when predicate evaluated against the Run Context.
{
"approval_gates": [
{
"gate_id": "GATE_FINANCE_APPROVAL",
"when": { ">": [{ "var": "request.context.refund_amount" }, 3000] },
"required_approver_role": "finance_lead",
"ttl_seconds": 7200
}
]
}A policy rule fires the gate and binds the capability call to a decision_key from the Decision Catalog.
{
"rule_id": "R_HIGH_VALUE_REQUIRES_APPROVAL",
"applies_to": { "intent": "support.refund" },
"if": {
"and": [
{ "==": [{ "var": "user.role" }, "support_agent"] },
{ ">": [{ "var": "request.context.refund_amount" }, 3000] }
]
},
"then": {
"allow": true,
"approval_mode": "destructive",
"requires_approval_gate": "GATE_FINANCE_APPROVAL",
"arg_constraints": { "refund_amount": { "max": 3000, "unless_approved": true } }
},
"decision_binding": "decision.support.refund.execute"
}This policy selects a named approval gate, so the Tool Gateway uses three steps:
- Propose — the Critic emits a frozen evidence snapshot (sha256 of
evidence_refs). - Approve — the named approver responds with their assertion (WebAuthn, etc.).
- Execute — only if
proposed_action_hash == executed_action_hash. Divergence aborts.
The ToolResultEnvelope carries result evidence; approval details may be retained in its metadata and are canonical in the resulting DecisionRecord.approvals:
{
"tool_call_id": "tc_118",
"run_id": "run_refund_2026_05_18",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"capability_id": "adp_payments.issue_refund",
"status": "completed",
"output": { "refund_id": "rf_118", "status": "accepted" },
"error": null,
"artifacts": [],
"policy_decision_id": "pol_9901",
"metadata": {
"approval_mode_effective": "destructive",
"approver": "user_finance_lead_77",
"approval_evidence_snapshot_hash": "sha256:b2a1..."
},
"mutations": ["tool:adp_payments.issue_refund:tc_118"],
"citations": [{ "evidence_ref": "refund:rf_118" }],
"risk_tags": ["external_state", "user_delegated", "compensatable"],
"latency_ms": 242
}What’s next
Tutorial 3: Wire a typed Decision Catalog entry - author a DecisionSpec with required_evidence so the Critic emits a typed DecisionRecord that audit can replay.