Skip to content
Press / to search

Tutorial 2: Bind a destructive tool through the Tool Gateway

Onboard a destructive capability with approval-mode binding, an approval gate, and a frozen evidence snapshot.

TutorialLast reviewed: Edit on GitHub
At a glance

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

rendering diagram…
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
Destructive tool flow with GATE_FINANCE_APPROVAL and frozen evidence snapshot
1
Declare the capability

The adapter declares the highest mode the capability can produce. Policy may select a lower effective mode for a specific bounded request when the declared maximum allows it, but it cannot exceed that maximum.

{
  "adapter_id": "adp_payments",
  "type": "OPENAPI",
  "endpoint_ref": "internal://payments",
  "capabilities": ["issue_refund"],
  "approval_mode": "destructive"
}

Capability classification: act (mutates external state). Distinct from verify / observe even when both can carry network mode.

2
Constrain the arguments

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}$" }
  }
}
3
Author the approval gate

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
    }
  ]
}
4
Bind the rule to the gate

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"
}
5
Watch the propose → approve → execute sub-sequence

The Tool Gateway routes network / delegated / destructive calls through three steps:

  1. Propose — the Critic emits a frozen evidence snapshot (sha256 of evidence_refs).
  2. Approve — the named approver responds with their assertion (WebAuthn, etc.).
  3. Execute — only if proposed_action_hash == executed_action_hash. Divergence aborts.

The ToolResultEnvelope carries the audit:

{
  "tool_call_id": "tc_118",
  "run_id": "run_refund_2026_05_18",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "capability_id": "adp_payments.issue_refund",
  "status": "completed",
  "approval_mode_effective": "destructive",
  "policy_decision_id": "pol_9901",
  "metadata": {
    "approver": "user_finance_lead_77",
    "approval_evidence_snapshot_hash": "sha256:b2a1..."
  },
  "mutations": [{ "mutation_ref": "tool:adp_payments.issue_refund:tc_118" }],
  "citations": ["refund:rf_118"]
}

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.