Skip to content
Press / to search

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.

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 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.

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

This policy selects a named approval gate, so the Tool Gateway uses 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 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.