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.
After authoring read-only pack fragments in Tutorial 1, we add the destructive capability adp_payments.issue_refund and define the Tool Gateway path with required approval before execution.
This is a worked contract, not a live payment integration. Before work, admit exact required profiles and actual features. Production supplies trusted brokers, actual containment/data enforcement and atomic approval/operation/budget stores; passing the reference alone cannot establish those controls.
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.
ToolPermission binds this workflow to a capability under arg_constraints; current role/purpose/resource authority is verified separately. Write calls require a stable envelope idempotency key under this workflow; the provider argument below is adapter-specific. The base v1 envelope field remains optional for compatibility.
{
"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 belongs in policy_layer.approval_gates, alongside policy_bundles, and its when condition uses the declared evaluation context. The fragment below shows that member, not a complete PolicyLayer. The compiler’s gate selection is not stored approval validation or atomic redemption.
{
"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"
},
"decision_binding": "support.refund.execute"
}This policy selects a named approval gate, so the Tool Gateway uses three steps:
- Propose — store the complete
ActionBindingand canonical digest: exact arguments, actor/tenant, purpose, operation/proposal, resource/destination, business amount in explicit minor units and evidence/policy/profile/release pins. Hashing reference names alone is insufficient. - Approve — validate the current eligible approver’s authenticated response against the exact stored proposal/request, interval, distinct quorum and separation policy; atomically redeem it for the same stable operation. An assertion method or matching hash alone is not authority.
- Execute — immediately recheck current grants/ancestor revocation, policy, exact approval, observed boundary/data route and aggregate reservation. Atomically claim the operation under current revision/generation, dispatch, then retain verified effect and postcondition observations. Changed binding requires new authorization/approval.
An applicable policy-expression error stops compilation with structured ContextCompileError. A false expression follows its legitimate false/else path; neither a compiler error nor a required-control failure can be overridden by a human click.
The ToolResultEnvelope carries result evidence; approval details may be retained in its metadata and are canonical in the resulting DecisionRecord.approvals:
{
"envelope_version": "contextos.tool_result.v1",
"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": "confirmed" },
"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
}The example assumes verified provider/postcondition evidence; its displayed status and short hash cannot establish that evidence. If an acknowledgement is lost, retain unknown under the same operation and keep uncertain reservations. Retry only under the bounded configured recovery strategy with current verified provider-specific idempotency guarantees or definitive absence; otherwise reconcile/escalate. Cancel/resume preserves original limits and effect uncertainty. Compensation is a new governed operation, not a guaranteed reversal. See Execution Contract.
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.