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.
One tool call crosses three gates
Section titled “One tool call crosses three gates”- Schema gate: fields match exactly, types are correct, and extra or missing arguments are rejected.
- Policy gate:
read_record=allow,write_record=ask, anddelete_record=deny. - 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.
Run seven security cases
Section titled “Run seven security cases”python3 public/lab-assets/tool-gate.pyThe 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.jsonlEach 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-42touser-99; - change the tool from
write_recordto another ask tool; - set expiry in the past;
- add an undeclared
force=trueargument; - 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 eventApproval 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.
Script and source paths
Section titled “Script and source paths”Appendix: review questions
Section titled “Appendix: review questions”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.