Once an assistant can change production state, someone always proposes the same solution: show the user a dialog, and execute if they click Confirm.
That dialog is worth building. It is also the least interesting component in the system, and treating it as the answer produces something that looks safe in a demo and is not safe in production. Confirmation is not a widget. It is a set of guarantees about what was authorized, by whom, over what data, exactly once — and almost all of those guarantees have to hold on the server, in places the user never sees and the model cannot reach.
The systems this comes from are two agent surfaces over a live pay-per-call marketplace: a partner-facing assistant with roughly 35 registered tools, and an internal operator MCP with roughly 120 typed tools. Both can perform real production mutations. I defined the product behaviour and the safety model — what may be proposed, what must be confirmed, what the user has to see before confirming, and what the assistant is permitted to say afterwards. Coding agents implemented the tool servers, the confirmation machinery, and the front end under my direction and against my acceptance criteria.
Model output is not authorization
The premise everything else follows from: a tool call is a proposal, not a decision.
A model emitting update_campaign_payout(campaign_id, 42.50) has produced a well-formed
suggestion. It has not established that the user wanted that, that the user may do that, that
those arguments are the ones the user saw, that the underlying record still looks the way it did
when the suggestion was formed, or that the action has not already been performed. Each of those
is a separate question with a separate answer, and none of them is answerable by looking at the
model’s output — which is exactly as confident when it is right as when it is wrong.
So the design question is not “how do we ask the user to approve the model’s action.” It is “what has to be true, and verified where, before a proposal becomes an execution.”
What can go wrong
Six failure modes. Each one is a real thing that happens, and each one defeats at least one otherwise-reasonable confirmation design.
Intent is ambiguous, and the model is fluent about it
“Pause the campaign for the auto vertical.” The partner has three. The model will pick one — and it will phrase the confirmation prompt in terms of the one it picked, so the user is asked to approve a specific action described in language that presupposes the disambiguation was correct. The ambiguity has been resolved and then hidden inside the confirmation itself.
A confirmation that restates the model’s interpretation is not a check on the model’s interpretation. It has to restate the effect — this specific record, these current values, these new values — computed by something that is not the model.
The mutation is real
The actions worth having are the ones that change money, routing, or access: adjusting a payout rate, pausing a campaign that is currently taking calls, changing a routing target, updating a partner’s terms. These have consequences that begin the instant they commit, and several of them are visible to third parties before anyone notices a mistake. A campaign paused in error stops paying a publisher immediately. There is no undo for calls that did not route.
The world moves between proposal and approval
This is the failure mode people miss most often, because a demo has no elapsed time in it.
The model proposes a change based on state it read some seconds ago. The user reads the confirmation, gets distracted, comes back, and clicks Confirm. Meanwhile someone else has already changed the record, or the campaign has hit a budget cap, or the buyer has been suspended. The click authorized an action against a world that no longer exists. The user approved the preview they saw; the system is about to execute against a state they never saw.
The same action executes twice
A retried request, a double-click, a reconnecting websocket, a model that re-emits a tool call after an ambiguous response. In a marketplace, “twice” is not a cosmetic problem — a payout adjustment applied twice is a real financial error that then has to be found and reversed. Any confirmation design that treats approval as a fire-and-forget signal will eventually execute one of them more than once.
The model can claim success it never achieved
If the assistant composes its reply from its own intention rather than from the actual outcome, it will report success for actions that were rejected, partially applied, or never attempted. This is the most corrosive failure of the six, because it damages the user’s ability to detect any of the others. A user who has been told “done” has stopped looking.
Cross-tenant leakage is unacceptable
A marketplace assistant serves parties with directly opposed commercial interests. A publisher must never see a buyer’s rates; one partner must never see another’s volume. This is not a preference, and it cannot be enforced by instructing the model to be careful — prompt-level scoping is a request, and any request can be argued with.
The controls that answer them
Seven controls, each answering one or more of the above. The ordering matters; several of them are worthless if an earlier one is missing.
Two-turn confirmation
A mutating tool call never executes on the turn it is proposed. The first turn produces a pending action: the server validates the proposal, computes what would happen, persists that under an identifier, and returns a preview. Only a second, explicit user turn against that identifier can cause execution.
The essential property is not the pause. It is that the thing being confirmed is a server-computed effect on a specific stored record, not a natural-language sentence the model wrote about its own plan.
Server-held action arguments
The arguments echoed back at confirmation time are never trusted. They are not used at all.
When the pending action is created, the server stores the resolved, validated arguments. At confirmation the client sends an identifier and an approval; the server executes from its own stored copy. Anything the model or the client repeats back is ignored.
This is the control that closes the widest gap in a naive design, where the confirmation payload carries the arguments and the server executes what it is handed. In that design a model that drifts between turns — or any client that modifies the payload — can substitute a different action behind an approval the user already gave. Holding the arguments server-side makes the approval refer to one specific, immutable proposal.
Hash and tamper verification
The pending action carries a hash over its meaningful content. Before execution the server verifies that what it is about to run is byte-for-byte the proposal that was previewed. If the stored action has been altered by any path, execution fails rather than proceeding on a best-effort basis.
Belt and braces on top of server-held arguments, and cheap: it converts a whole class of subtle substitution bugs into a loud, specific failure.
Persona and organization scoping, and RBAC
Authorization is evaluated on the server, per tool call, against the authenticated caller’s persona and organization — and again at execution, not only at proposal. Tenancy is a predicate on the query, not a filter on the result: a scoped tool cannot return another organization’s rows because the rows are never in the result set to begin with.
The model is not a participant in this decision. It cannot see what it is not entitled to see, so there is nothing for it to leak, and no prompt can talk it into leaking. Cross-tenant safety that depends on a model’s discretion is not a control; it is a hope with good documentation.
TTL and ownership checks
A pending action expires. It can be consumed only by the same user, in the same session context, within a bounded window. After that, it is dead and the request has to be made again.
TTL is the direct answer to the moving-world problem: it bounds how stale an approval is allowed to be. Ownership is the answer to the obvious attack, which is confirming somebody else’s pending action. Alongside these sit version and state checks — if the underlying record has changed since the preview was computed, the action does not execute against the new state. It fails, and the user gets a fresh preview of the world as it is now. Re-previewing an unexpected state is mildly annoying. Executing against an unexpected state is an incident.
Compare-and-swap consumption
Execution begins with an atomic transition of the pending action from pending to consumed, conditional on it still being pending. Exactly one caller can win that transition; every other attempt observes that the action has already been consumed and does nothing.
Idempotency is a property of the consumption, not of the handler. Making each individual handler idempotent is a much larger and less reliable project than making the gate in front of all of them single-entry. One correct compare-and-swap protects every tool behind it.
Reporting the actual execution outcome
After execution, the server’s real result — committed, rejected, partially applied, failed, with the reason — is what goes back into the model’s context. The assistant composes its reply from that, not from its own expectation of what should have happened.
This is the control that makes the other six auditable. Without it, a rejected action and a successful one can produce the same cheerful sentence, and every guarantee upstream becomes invisible to the person relying on it. With it, “I paused the campaign” means the server paused the campaign.
Every one of these steps also writes an audit record: who proposed, what was previewed, who approved, what was executed, and what came back. An agent that can change production state without leaving a reconstructable trail is not something you can operate — you can only hope about it.
Why the UI is the least interesting part
Look at where those seven controls live. One of them has a visible surface. Six are server-side invariants that a user could not observe if they tried, and that a screenshot cannot demonstrate.
The confirmation card is genuinely important — it is the only place a human sees the effect before it happens, so it must show the computed effect on the named record rather than a paraphrase of the request, and it must be honest about what it does not know. But it is a rendering of a guarantee established elsewhere. Building the card first, and the guarantees later or never, produces a system that asks for permission and then does something adjacent to what it asked about. That is worse than no confirmation, because the dialog manufactures the belief that the question was answered.
The useful test for any agent-confirmation design is to assume the front end is hostile and the model is unreliable, and then ask what is still true. If the answer is “the action that executes is exactly the one the server previewed to an authorized user, against unchanged state, at most once, and the model is told what really happened” — the design is sound, and you can put whatever button you like on top of it.
What it costs
Two turns is slower than one, and users notice. The mitigation is not to weaken the control; it is to be precise about which actions need it. Read-only tools — the large majority of both surfaces — need scoping and audit, not confirmation. Reversible writes need confirmation but can carry a generous TTL. The narrow set that moves money or affects a third party gets the full gauntlet, and there the friction is the point.
The other cost is that the model has to be treated as a component that can be wrong at any time, including in its account of its own behaviour. That is an uncomfortable premise for a product built around a conversational surface. It is also the only premise under which such a product can be given real authority — and it is what makes an assistant that changes production state something you can actually run rather than something you can only demonstrate.