What Exactly Did the Person Approve?
A reviewed action resumes after its target changes. Keep the approval, current object version and receiver’s action ledger together before allowing another write.
16 sections · 8 questions · 1 coding challenge
The arrow keys move between sections too.
Transaction Review Had to Describe the Actual Transaction
BOTIE
Agent frameworks provide ways to pause a proposed tool call and resume after review. That mechanism is useful, but the surrounding application still owns identity, current permission and the transaction. A resumed model conversation is not itself evidence that the reviewed customer and payload remain unchanged.
Sources for this history
The Draft Was Approved Before the Customer Changed
Botie prepares an inspection booking for one customer. A person reviews it. Before the worker resumes, the proposed customer changes or somebody else edits the job. Should the earlier approval still authorise the write?
An approval needs to refer to something concrete. We will keep the reviewed arguments with the decision, then check the current object and any previous receipt before allowing the application to act.
These are invented records and deterministic policy checks. The browser runs no agent, shell command, MCP server or network request. Passing these fixtures establishes only the stated policy behaviour.
Step 1 of 2
Three reasons to stop before another write
The proposed action is action-7 on job-7 version 2.
| Observed state | Meaning | Route |
|---|---|---|
| Approval names a different payload | The person did not review this action | Hold for approval |
| Stored job is now version 3; no receipt | State changed after preparation | Refresh and review |
| Matching receiver receipt exists | This action already happened | Return the recorded outcome |
Approval names a different payload
- Meaning
- The person did not review this action
- Route
- Hold for approval
Stored job is now version 3; no receipt
- Meaning
- State changed after preparation
- Route
- Refresh and review
Matching receiver receipt exists
- Meaning
- This action already happened
- Route
- Return the recorded outcome
Step 2 of 2
Put the last decision beside the write
A production handler must keep this sequence atomic where its storage supports it.
- 1
Authenticate caller
Current object permission - 2
Bind approval
Exact intended effect - 3
Check version and action ID
Detect stale state or replay - 4
Commit effect and receipt
One protected operation
The write boundary joins approval and current state
A receipt, an approval and a version comparison answer different questions.
Follow one approved action through a retry
Bind approval to the proposal, check object version separately, then consult the receiver ledger.
BOTIE
Application action route
apply once atomically
Follow the operations
1. Proposed action
{ "id": "action-7", "record": "job-7", "version": 2, "newValue": "scheduled" }2. Server approval record
{ "id": "action-7", "record": "job-7", "version": 2, "newValue": "scheduled", "approved": true, "expires": 100 }3. Current time
50
4. Compare the reviewed action
true
5. Stored object
{ "id": "job-7", "version": 2 }6. Compare the stored version
true
7. Receiver ledger
[]
8. Reconcile before another write
apply once atomically
9. Result
apply once atomically
These are invented records and deterministic policy checks. The browser runs no agent, shell command, MCP server or network request. Passing these fixtures establishes only the stated policy behaviour.
1. Design tools around understandable effects
A tool called update_everything hides too much. A contract such as prepare_inspection with a specific asset, date and requested status makes the consequence reviewable. Separate preparing a draft from applying it when the effect needs approval.
Validate argument shape, then validate business meaning against trusted records. A correctly typed customer ID can still name the wrong customer. A date can be syntactically valid and outside the allowed booking period.
Schema validation is the beginning of checking a request.
2. Show the person the fields that determine the effect
Present the target, changed fields, destination and relevant cost or impact. Explain uncertainty and show the original values when they matter. Render model-written descriptions as untrusted content so the review interface cannot be altered by the proposal itself.
Avoid a vague Approve everything button. Batch review can work when every action and its scope are visible. The stored decision needs to identify which actions were accepted, not just that someone clicked during the conversation.
The person should be able to tell what will happen before agreeing to it.
3. Store review outside model-controlled state
Keep the reviewer identity, authorised scope, exact action data, policy version and validity period in application-owned storage. If using a digest, define an unambiguous canonical representation and include every effect-bearing field.
Do not accept an approval Boolean returned by the model or embedded in a tool result. The workshop lets us edit a server-record fixture to explore the rule. A deployed API must retrieve that record through an authenticated control path.
An approval record belongs to the service’s trust boundary.
4. Use framework pauses without surrendering the boundary
Pydantic AI supports deferred tool approval. LangChain’s human-in-the-loop middleware can interrupt configured calls and use persisted state for later decisions. These mechanisms coordinate a pause; your application authenticates the reviewer and controls who can resume it.
After an edited proposal, rerun the policy and argument checks. Bind any new approval to the edited operation. Keep durable checkpoint identifiers scoped to the right user and tenant, and consult the documentation for the library version you deploy.
A callback saying approved is meaningful only through the application that owns it.
Check yourself
Questions on Parts 1 to 4
Question 1 of 3
What should the application require?
Use the information below.
A person approved job-7, but the resumed proposal names job-9.5. Recheck current state before committing
The object may have changed while review was open. Compare its stored version with the version used to prepare the action. Recheck current access and business constraints at the same execution boundary.
Use a transaction, conditional write or another supported concurrency mechanism to prevent a gap between checking state and changing it. Otherwise two workers can both observe the old version and both proceed. The browser exercise models the decision, not the locking or isolation mechanism.
A check made earlier in the conversation cannot protect a later concurrent write.
Recognise a changed object version
The proposal refers to the state the person reviewed.
proposal = {"record":"job-7", "version":2}
stored = {"id":"job-7", "version":3}
print(proposal["version"] == stored["version"])FalseThe proposal needs refresh and review unless a matching receipt explains a previously completed action.
6. Give the receiver a durable action identity
The receiver should store an idempotency key bound to the intended operation and its result. A retry with the same key and payload can return the existing receipt. Reusing the key for a different payload should be rejected as a conflict.
Coordinate the effect and receipt atomically where possible. A local dictionary or a check-then-insert sequence does not provide this guarantee across workers. An external service needs its own idempotency or reconciliation contract.
Exactly-once business effects cannot be inferred from exactly one client response.
7. Keep the application interface from adding another bypass
Protect the approval and tool endpoints with normal web controls: authenticated sessions, appropriate request-forgery protection, object access checks and secure rendering. Do not expose a privileged backend route merely because the main UI hides its button.
An agent-generated link or rich response can carry unintended destinations or active markup. Apply context-specific output handling and review data release separately from permission to read. Guardrails must cover ordinary HTTP routes as well as model tool calls.
Agent integration does not replace the application’s existing security responsibilities.
8. Define what happens during an outage
If a required approval store or current-permission check is unavailable, hold the effect under this policy. Preserve the pending proposal and explain the missing evidence. Do not substitute a classifier’s confidence for the unavailable record.
If the write may already have happened, reconcile with the receiver. Cancellation should stop new scheduling while leaving uncertain operations visible. Some effects require compensation rather than rollback, and some disclosures cannot be undone.
An unavailable result is a state to handle, not permission to guess.
9. Test race and replay cases at the real boundary
Use concurrent integration tests to contend for the same action ID and object version. Exercise expired review, changed payload, revoked access, duplicate delivery and a lost reply after commit. Verify both the stored effect and the receipt.
Keep the fixture tests for fast diagnosis. They describe intended routing, while integration tests establish whether the storage and worker implementation preserve it under contention.
The next handoff will carry state to another agent. Its authority should become narrower, not larger.
Check yourself
Questions on Parts 1 to 9
Question 1 of 5
A version comparison without an atomic write mechanism prevents both updates.
Consider this example.
Two workers read version 2 before either writes.Try it with Botie
Build with Botie
Write and run Python