Skip to content

Lab 02 · Tool gate: approval must bind one action

Use schema validation, allow/ask/deny policy, an HMAC approval token, and an audit log to block argument drift, replay, and unauthorized tools.

Chapter brief

Question to answer

After a user approves write_record, how do we guarantee the same actor, tool, and arguments are executed?

By the end, you can

  • Separate argument validation, policy decision, and execution into independent gates
  • Bind an approval token to actor, tool, arguments, and expiry
  • Detect argument drift, token replay, and deny-policy calls
Read this now if
Engineers designing tool dispatchers, human approval, policy engines, or high-impact tools
Prerequisites
Read structured tool calls; Understand the roles of hashes, expiry, and nonces
Deliverable
A runnable tool gate, seven security cases, and a per-action audit log
Evidence boundary
The example secret and in-memory nonce set are instructional; production needs key management, persistent replay protection, and real identity

The user approved safe-note, but execution changed to secrets.env

Section titled “The user approved safe-note, but execution changed to secrets.env”

The model requested:

{"tool":"write_record","arguments":{"record_id":"safe-note","value":"approved content"}}

After the user clicks Approve, the next context turn or a hostile intermediary changes record_id to secrets.env. If approval is only approved=true, the dispatcher still executes the drifted action.

This lab binds approval to four facts: actor, tool, canonical arguments hash, and expiry, with a nonce preventing reuse.

  1. Schema gate: fields match exactly, types are correct, and extra or missing arguments are rejected.
  2. Policy gate: read_record=allow, write_record=ask, and delete_record=deny.
  3. Approval gate: ask actions require a token that matches the current action, is unexpired, and has not been replayed.

Every rejection enters audit.jsonl. Audit must include failed decisions, not only executions, so operators can distinguish model formatting, policy refusal, and invalid approval.

Terminal window
python3 public/lab-assets/tool-gate.py

The output includes:

{
"approval": "approval_required",
"approved_write": "executed",
"argument_drift": "arguments_drift",
"delete": "denied",
"read": "executed",
"replay": "replay",
"schema": "rejected"
}

Then inspect:

.agent-mechanics-lab/tool-gate/audit.jsonl

Each call includes actor, tool, arguments, status, and reason. The teaching script also records the token; production logs usually store only a digest to avoid exposing replayable credentials.

Modify the lab to test approval boundaries

Section titled “Modify the lab to test approval boundaries”

Add these attacks one at a time:

  • change actor from user-42 to user-99;
  • change the tool from write_record to another ask tool;
  • set expiry in the past;
  • add an undeclared force=true argument;
  • let two processes consume the same nonce concurrently.

The current script rejects the first four. The fifth exposes the limit of an in-memory set: cross-process replay protection needs a database unique constraint or atomic shared-cache operation.

Move the teaching gate into a production dispatcher

Section titled “Move the teaching gate into a production dispatcher”

A production path can be decomposed as:

model output
→ JSON parse
→ schema validation
→ identity and scope lookup
→ policy decision: allow | ask | deny
→ action-bound approval
→ sandbox / execution host
→ result persistence
→ audit event

Approval runs before execution, but it is not the final safety boundary. Even when the approval gate has a bug, the execution host still limits files, network, identity, and resources.

Acceptance: approval means this action, not this class of action

Section titled “Acceptance: approval means this action, not this class of action”

The lab passes when:

  • drifted arguments cannot reuse the original token;
  • one token cannot execute twice;
  • a deny tool cannot execute even with a valid signature;
  • schema failure happens before policy;
  • the audit log covers allow, ask, deny, rejected, and replay.

Automate these five assertions before connecting a UI button and the real executor.

Open the review and extension tasks
  • Why can approval not bind only the tool name?
  • What breaks when canonical JSON differs across languages?
  • When should a nonce become used: before validation, before execution, or after commit?
  • Who blocks a mismatch between UI-displayed arguments and hashed arguments?
  • Extension: replace used_nonces with a SQLite unique table and test two processes racing on one token.