Covers: Definitions; Flow; Results; Design rules; Interview questions, with the follow-ups; Common mistakes; What the example does not do.
Definitions
Approval token, binding
Approval token: a signed record that a named human approved one specific action.
Binding: tying the token to the action, arguments, session, approver, expiry and nonce so it is useless for anything else.
HMAC, nonce, single use
HMAC: a keyed hash proving a message was produced by someone holding the key.
Nonce: a random value that makes each token unique.
Single use: the executor remembers used nonces and refuses a second use.
TOCTOU, separation of duties
TOCTOU: the state changes between the check and the action.
Separation of duties: the requester cannot approve their own request; high risk needs two approvers.
Canonical form
Canonical form: one standard serialisation (sorted keys, fixed separators) so equal arguments always hash equally.
Flow
Agent proposes, human approves a rendered prompt, service signs, executor verifies.
Results
Attack
Token scheme
Boolean flag
Forged signature
blocked
not applicable
Amount changed after approval
blocked
succeeded
Extra field added after approval
blocked
succeeded
Token used for a different action
blocked
succeeded
Replay after one use
blocked
succeeded
Another session
blocked
n/a
After expiry
blocked
n/a
Self-approval
blocked
n/a
High risk, one approver
blocked
n/a
State changed since approval
blocked
n/a
Legitimate path
Result
Exact arguments
allowed
Same arguments, different key order
allowed
High risk, two distinct approvers
allowed
Amount sent as 50 instead of 50.0
denied (fail safe)
Ten attacks against the token scheme, four against a boolean flag, and the legitimate paths.
A flag approves a person's intent; a token approves one action.
Single use and expiry turn approval into a short window, not a standing permission.
Rendering is part of the security boundary: if the model writes the prompt, the attacker writes the prompt.
Design rules
Keep signing keys and nonce storage outside the agent, in a service the model cannot call arbitrarily.
Bind everything: action, canonical arguments, session, requester, approver, expiry, nonce, and the state version where it matters.
Single use, short expiry (minutes, not days).
Requester not equal to approver; two distinct approvers for high risk or high value.
Render from the arguments: never show model-written summaries as the authority; escape control characters; call out new recipients, unexpected fields, lookalike domains, and large or irreversible effects.
State the impact plainly (amount, recipient, whether it can be undone).
Re-verify state at execution and deny if it changed.
Log every issue, use and denial with the trace id (Level 9 module 8).
Prefer asymmetric signatures or a separate approval service so the executor cannot mint approvals.
Keep the approval rate meaningful: if nearly everything is approved, the gate is over-flagging (Level 8 module 8).
Interview questions, with the follow-ups
How do you make sure a human approval cannot be abused by a compromised agent?
"The approval is a signed token bound to the action name, a hash of the exact arguments, the session, the approver, an expiry and a one-time nonce. The executor verifies all of it, so changing the amount, replaying it, using it elsewhere or approving your own request all fail. In my tests ten attacks failed on the token scheme while all four I tried succeeded against a boolean flag." Follow-up: "What can still go wrong?" A stolen key, a compromised approval screen, or a human approving something harmful that is correctly described.
What should the approval prompt show?
"A description built from the arguments by trusted code: the action, the amount and recipient, whether it is reversible, and warnings for anything unusual (new recipient, extra field, lookalike domain). Not a model-written summary, because a manipulated model can write a reassuring one. I also escape control characters so a value cannot forge extra lines." Follow-up: "What about long argument lists?" Show a diff or the risky fields first and the rest on demand, and cap the batch size so approvers do not rubber-stamp.
Why single-use, short-lived approvals?
"A long-lived or reusable approval becomes a standing permission an attacker can replay. Short expiry and single use keep the approval tied to the moment and the action. The cost is re-approval for retries." Follow-up: "What about retries after a timeout?" Use idempotency keys (Level 8 module 6) so a retried call is the same action, and reissue approval only if the arguments change.
How do you handle state changing after approval?
"Bind the approval to a version of the relevant state, such as an order's version or etag, and have the executor compare it at execution. If it changed, deny and ask again. Otherwise the human approved a different situation from the one executed." Follow-up: "Is that enough for concurrency?" The check and the action must be atomic, for example in one transaction, or two executions can both pass.
Common mistakes
An approved=True flag in the agent's state.
Approving a model-written summary.
Reusable or never-expiring approvals.
Not binding the arguments, so "approve a $5 refund" authorises any refund.
Requester can approve own request.
Executor shares the signing key with the agent process.
No state check between approval and execution.
Approval prompts with unescaped model text.
Treating approval as a substitute for least privilege (module 2).
What the example does not do
No real approval UI, identity provider or authentication of the approver; "alice" is a string.
Symmetric HMAC: anyone with the key can mint approvals; I did not implement asymmetric signing.
In-memory nonce store, so a restart forgets used tokens and a cluster would need a shared store.
No atomicity between the state-version check and the action.
No tests of timing side channels beyond using a constant-time comparison.
Attacks written by me against my own code.
Optional real experiment (I did not run this): show the rendered prompt to colleagues with and without the warnings, with one malicious request among ten, and compare how many they catch.
Check yourself
A human approves a refund of 50, and the agent executes a refund of 5,000. What stops it? (The approval is bound to the exact action: a signed token covers the action name and a hash of the arguments, so a changed amount fails verification.)
For the curious A token proves what was approved, not that approving was wise
All ten attacks on the token scheme failed, including a replay, self-approval, a high-risk action with one approver and an order that changed after approval. A false denial is the price: 50 and 50.0 hash differently. A stolen signing key, a compromised approval screen, a human who approves everything, and a misleading screen are not solved by tokens. These are my own attacks on my own code.
Draft: confirm how your identity provider and key storage would issue and protect the signing key.
What you must be able to say (one page)
This module is about the integrity of an approval, not the policy. Level 8 module 8 covers which actions to gate, attention fatigue, blocking versus pause-and-resume and timeouts. Here the question is: once a human says yes, what exactly did they approve, and can the agent change it? A boolean "approved" flag in agent-visible state fails every test below.
Bind the approval to the exact action. My ApprovalService signs (HMAC-SHA256) a token containing the action name, a hash of the canonical arguments, the session, requester, approver, expiry, a one-time nonce and optionally a version of the state it was approved against. The executor verifies all of it before running the action. The key never enters the agent's process.
Attacks. Ten attacks on the token scheme all failed: a forged signature, changed amount (50 to 5,000), an extra field added after approval, the token used for a different action, in another session, after expiry, replayed after one use, self-approval, a high-risk action with one approver and an order that changed after approval. Against a plain boolean flag, all four attacks I tried (changed amount, different action, replay, extra field) succeeded. Legitimate calls still worked (exact, reordered keys, two approvers for high risk).
Fail safe has a price. Amount 50 and 50.0 hash differently, so a harmless type change is denied. I prefer a false denial to a silent difference; the price is retries.
What the human sees must come from the arguments, not from the model. Use clear, non-technical language explaining the action's impact. My renderer builds the prompt only from the arguments, formats the amount (5,000.00), prints strings in quoted, escaped form so a newline cannot fake a line such as "APPROVED BY SECURITY", and calls out unexpected fields, new recipients and mixed-script (lookalike) domains.
Separation of duties: requester and approver must differ, and high-risk actions need two distinct approvers.
Time-of-check to time-of-use (TOCTOU): the world can change between approval and execution (order edited, balance changed). Binding to a state version makes the executor deny; I tested it.
Limits that remain: a stolen signing key, a compromised approval UI, a human who approves anything, and approval of a legitimate action sent to the wrong place by a valid but misleading rendering are not solved by tokens. The token proves what was approved, not that approving it was wise.
Honest limits: my own attacks against my own code; no real identity provider; time is passed in as a number; used-nonce storage is in memory.