Put a person in front of a consequential agent action
One decision, bound to one exact payload, spendable once — and nothing calls the agent back.
On this page
The message in a channel, the standing approval, and the queue nobody reads
The first implementation is nearly always a webhook into a chat channel and a person typing yes. It demonstrates well and it evidences nothing: there is no record binding the approval to the action that ran, the person who typed yes is identified by whoever’s account was logged in, and if the agent proposes a slightly different call two minutes later, the same yes covers it as far as anything in the system can tell.
The second is an approval that authorises a category. Approving refunds rather than this refund of £240 on this order is a standing licence for every refund the agent proposes afterwards, and the agent proposing them is precisely the component most likely to have been talked into it by a paragraph of retrieved text. The same holds for an approval that survives its first use: one human decision then authorises an unbounded number of executions, which is the shape of an incident rather than a control.
The third failure is quieter and it is what makes teams switch the gate off. An agent waiting on a human has no callback to wait for, so it re-submits; a gateway that mints a fresh approval per re-submission buries the reviewer under duplicates of the single thing they are being asked to decide. The queue then fills with rows that are individually valid and collectively unusable, and the honest response is to stop gating anything.
The fourth is the one that will be found during an evaluation, so it is worth stating first. A build of this product once salted the approval hash for a proposed tool call with the trace id — a value minted fresh on every attempt — so no approval raised on that path could ever be redeemed: the operator approved, the agent retried, the hash no longer matched, and a second approval was raised, then a third. It failed closed, so nothing unsafe ever ran, and the documented feature had never once worked. What replaced it is a stable execution-context envelope, which is a fact about the action rather than about the attempt that first proposed it.
How to actually do it
Each step is something you can go and do. Where a step depends on a decision somebody has to make rather than a setting somebody has to change, it says so.
- 01
Choose the small set of actions that deserve a person
Gate the actions that move money, delete data, deploy something or contact a customer, and nothing else. Gating too much produces an approval queue nobody reads, and a queue nobody reads is worse than no gate at all, because it converts a control into a delay with a rubber stamp on the end. Risk-tier the gates so approvals stay rare enough to be read carefully. - 02
Write the rule and run it in shadow first
A require_approval rule in shadow mode raises no approval, parks no trace and pauses no agent — it records that it would have, so the volume of interruptions is a number you have measured rather than a surprise your on-call rota discovers. Where an install has switched the promotion gate on, a rule cannot start enforcing until a backtest against recorded traffic has been acknowledged by a named person. - 03
Set a lifetime you will actually meet
The default is sixty minutes and a policy may set anything from one minute to seven days. Set it against the rota that will answer it: an approval granted at 09:00 with a sixty-minute lifetime must not still authorise the action at 17:00, and expiry is terminal after approval as well as before it — which was not always true, because expiry was once checked only while an approval was still pending. - 04
Teach the agent to resume
The refusal carries the trace id, a typed code, the approval id, a retryable flag, a machine-readable status URL and a resume object stating that the payload must match and that the approval is single-use. The whole client-side change is one catch block that re-sends the identical request with the approval id. On the tool path the same facts arrive as a tool result flagged as an error, with the code, retryable flag and approval id in machine-readable metadata beside the prose the model reads. - 05
Require a reason, and scope who may give it
Deciding needs the operator role or above and an evidence scope covering the trace’s team. The console makes a reason of at least five characters mandatory and keeps both buttons disabled without one; the decision endpoint takes the reason as an optional field of up to 2,000 characters, so an install driving decisions through the API should enforce the same rule at whatever is calling it. - 06
Measure the queue rather than trusting it
The governance report returns requested, approved, denied, expired and pending counts plus the median time to decision over a window, and flags the result as truncated past a 5,000-approval scan rather than quietly reporting a partial number. A rising expired count is the signal that the gate has stopped being oversight and started being an outage with a policy id.
What the approval is bound to, and what is deliberately left out
An approval is bound to the SHA-256 of one canonical JSON object containing the governed action and the execution context it was proposed in, and to nothing else. The action half is the sanitised model request, or the tool name and its arguments, or — where an effect contract governs the call — the contract id, its version, its digest, the idempotency key hash and the action-arguments digest, so an approval cannot survive the contract changing underneath it.
The context half is in the hash because changing any of it after a human reviewed the action changes what was approved. An approval granted to an agent holding one set of roles must not still be redeemable after that agent is granted more. An approval granted for a call made on behalf of one person must not be redeemable for the same call on behalf of another. An approval granted at the end of a two-hop delegation chain must not be redeemable when the chain is different. Each of those is a fact about who is acting, and each is inside the object that is hashed.
Canonicalisation is what makes the comparison meaningful across two separate HTTP attempts: role grants are emitted with their permissions sorted and their action lists deduplicated, teams and on-behalf-of identities are lower-cased, tag sets are sorted, and header names are trimmed and lower-cased. A retry differing only in map ordering hashes identically; a retry differing in substance does not.
What is excluded is as load-bearing as what is included. Request ids, trace ids, credentials and generated transport session ids never enter the hash, because they change between the attempt that raised the approval and the attempt that redeems it — and a reconnect must not invalidate an action a person has already reviewed. Presenting an approval issued to a different agent is treated as an escalation attempt rather than a typo: it is logged as an error and refused as a mismatch, and on the tool gateway that refusal deliberately happens after the trace opens, because it is the one refusal on that path with a named actor behind it.
- The action
- The sanitised model request, or the tool name and its arguments, plus the contract identity where an effect contract governs the call.
- The subject
- Id, kind — agent or seat — team, tags and effective role grants, normalised so ordering cannot change the hash. A permission added to a role after the human decided produces a different hash and therefore a mismatch.
- The chain and the human
- The ordered delegation identities with each hop’s grants, plus the on-behalf-of identity, the caller-supplied session id and tags, the tier hint, and allowlisted headers that change provider semantics.
- Excluded on purpose
- Request ids, trace ids, credentials and generated transport session ids — everything that changes between the attempt that proposed the action and the attempt that redeems the decision.
What happens to the request, the money and the agent while it waits
Creating the approval record and moving its trace from running to awaiting-approval happen in one transaction, guarded on the trace still being a running trace belonging to that agent. An approval has four states — pending, approved, denied, expired — plus one orthogonal fact, the timestamp recording that it has been spent. The states are what a human sets or time sets; the timestamp is what execution sets, and they are separate because approved-but-unredeemed is a real and important state: the person has decided, the action has not happened, and the money is still about to be spent.
Denial is terminal and says so to the agent: the refusal is typed as denied and marked not retryable, and the same transaction that records the decision closes the held trace and clears its estimate. Expiry is terminal too and returns the verdict to require-approval rather than to refused, because nobody said no — nobody said anything — so a fresh request is raised on the agent’s next attempt. Expiry is applied lazily before the queue is listed, before a decision is written and before a presented approval is resolved, so a pending list never shows an approval whose time has already passed.
The money is held for exactly as long as the decision is outstanding. A parked model request keeps its conservatively priced estimate reserved against the agent’s hour, day and month windows, priced across the resolved route and every fallback in its chain, so the queue cannot be used as a way around a ceiling. Each terminal path gives it back in the same transaction as the state change that ended the wait — denial with the decision, expiry with the sweep, consumption with the spend. On the approved retry the atomic admission excludes the original approval trace while writing the new reservation, so the estimate is transferred rather than counted twice, and re-polling a still-pending approval takes no second reservation at all.
That symmetry is a fix rather than an original design, and the failure it repairs is worth knowing because it is the kind an evaluation will not surface. The release path did not exist, so a denied or abandoned approval left its trace parked forever, and because a parked trace counts at the greater of its billed cost and its reservation, that estimate stood against the agent’s budget permanently. What an operator actually met was two surfaces disagreeing by orders of magnitude — the agent’s spend endpoint counting reservations and the spend report counting what was billed — with nothing to say which was right, and the only remedy being to edit the database by hand.
What the reviewer sees, what is recorded, and what the queue does not say
Each card in the queue carries the action summary, the requesting agent, the policy that paused it, when it was requested, when it expires, a live countdown that turns amber at twenty minutes and red at five, the first sixteen characters of the payload fingerprint, and a link to the trace that led there. The queue is treated as the critical read: the display-name lookups for agents and policies are independent requests, so a failed registry call degrades a name to a stable id rather than blanking approvals that still need a human, and decisions are disabled while the queue is showing a stale snapshot so an old pending state cannot be mistaken for a current one.
The decision is written as a compare-and-set on the pending state, which matters more than it looks. Without it, two people choosing the same outcome at the same moment would both be told they had decided and both be written into the audit ledger as the decider. The loser is told what the approval is now, and the decision that stands is recorded in the hash-chained audit log with the decider’s identity, the decision, the reason, the agent, the policy, the trace and the action summary, alongside an event published to whatever webhook, chat or email receivers the deployment has configured.
The console is explicit that a decision is not an instruction. Approving tells the operator that the agent has to retry for it to take effect, because nothing calls the agent back; it used to say the agent had been told, which was not true of any channel the product has. An approved but unspent approval is shown in its own state, because a queue reading approved while the work has not happened is how an operator reasonably concludes the job is done when it is not.
One disclosure belongs here rather than in a data-protection appendix. Where the approval was raised on a tool call the model proposed, the action summary is built from the tool name plus up to 160 characters of that proposal’s arguments, before egress redaction has run on the response — so the approvals table and the approval webhook can carry that much unredacted model-generated text and should be treated at the sensitivity of trace content. Approvals raised on the request path and on the tool gateway carry no arguments at all, only the tool or model name and the policy reason, and the summary is capped at 240 characters in every case.
What this still does not solve
Doing everything above leaves a residue. It is smaller than what you started with and it is not nothing, and knowing its shape in advance is the difference between a control you trust correctly and one you trust too much.
- There is no approver routing. Any user with the operator role whose evidence scope covers the trace’s team can decide any approval in that scope: no per-policy approver list, no escalation path, no delegation and no two-person rule.
- The reviewer reads a summary, not the payload. The action summary is capped at 240 characters and the card shows sixteen characters of the payload fingerprint, so somebody who skims approves what they were shown rather than what will run — the full request is on the linked trace, and reading it is a separate act.
- Offline approvals on a developer seat cannot be single-use. A signed policy bundle records consumption as of the moment it was issued and nothing on the device can change it, so within one bundle’s freshness window a granted approval can be spent twice.
- Nothing pushes the decision to the agent. Approving is a permission the agent redeems by retrying, so an agent that never retries leaves the action undone with an approval nobody spent — and the gate binds only the calls Token Observe sees, which is the tool path plus the proposals a governed model response contains.
If one of those residues is the thing that actually worries you, that is the conversation worth having rather than the one about the steps above it.
Talk it throughThe parts of the platform this uses
Human approvals
One human decision, bound to one exact payload, spendable once.
An approval takes effect only when the agent retries
Policy engine
One deterministic verdict on every governed request: allow, block, redact, or park it for a human.
Injection scoring is nine fixed patterns, not a model
MCP gateway
One endpoint in front of every upstream tool server, and the same evaluator deciding a tool call that decides a model call.
A tool call routed around it is not governed here
Effect contracts
The action leaves once, and success is what a second pinned tool observed.
The downstream must enforce the idempotency key you send
The other jobs
Does approving in the console make the agent carry on?
No. Token Observe has no channel that reaches an agent; event delivery goes to webhooks, chat and email, which are all human channels. An approval is a permission the agent redeems by repeating the identical request with the approval id attached, so for an unmodified coding assistant pointed at the gateway by base URL, clicking approve moves nothing until the agent tries again. The console says so on the confirmation and shows an approved-but-unspent approval in its own state, because a queue that reads approved while the work has not happened is how an operator concludes a job is done when it is not.
What happens if the agent changes the request after approval?
It is refused. The retry is canonicalised and hashed again, and a hash differing from the one stored on the approval is refused as granted for a different action payload, with no execution and no partial credit. That covers changes to the arguments and changes to the context alike: a different on-behalf-of identity, a different delegation chain, or role grants widened between the decision and the retry all produce a different hash. Presenting an approval issued to a different agent is refused separately and logged as an error, because it is an escalation attempt rather than a typo.
Can the same approval be used twice?
No. Consumption is a guarded update that stamps the row only if it is still approved, unspent and unexpired, and the number of rows it changed is the answer: the loser changes nothing, is told the approval was already consumed, and executes nothing. Where the retry continues the original parked trace, that update is issued on its own at read-committed isolation, because under a stricter level the loser would raise a serialisation failure rather than reporting zero rows changed, turning a clean typed refusal into an unmapped server error. The one exception is an offline seat decision, where a signed bundle cannot record a consumption that happens on the device.
What happens if nobody decides in time?
The approval expires and the action does not happen. Expiry is applied lazily before the queue is listed, before any decision is written and before a presented approval is resolved, so a pending list never shows an approval whose time has already passed, and expiring the row and releasing the budget reservation it held are one transaction. The agent is then told a new approval is required rather than that it was refused — nobody said no, nobody said anything — and a fresh request is raised on its next attempt. The default lifetime is sixty minutes; a policy may set one minute to seven days.
How do we avoid an approval queue nobody reads?
Measure the volume before you enforce, and gate less than you are tempted to. Run the rule in shadow mode first, where it raises no approval and parks no trace but records that it would have, so the interruption rate is a number rather than a surprise. Then keep the gate on the actions that are irreversible or costly, and read the governance report’s requested, approved, denied, expired and pending counts and the median time to decision. A rising expired count means the gate has stopped being oversight, and gating everything is how a control becomes a delay with a rubber stamp on the end.
Describe the version of this you actually have.
The steps above are the general shape. Which of them matter, and in what order, depends on what your agents do and which of them worries you — say that and you will get a straight answer, including when the answer is that Token Observe is not what you need for it.
no form · no qualification step · no sales desk · the other three ways in