Challenge the assuranceAgent securityHuman approval
What exactly did you approve?
A reviewer edits an agent's tool call. The tool gets the replacement; the transcript keeps the original. Which record shows what was authorized, what changed and what actually happened?
Pinned code · tests run + dispatch capturePrepared October 22, 202611 min readRethinking Agents
What this rests on
Reported by others: LangChain's human-in-the-loop middleware, agent factory and unit tests at one pinned revision, plus maintainer documentation. Observed by us: a static trace of that code, two local runs of the pinned middleware test file (53 passed each time), and a scripted approve/edit/reject capture in the framework's own fixtures showing from outside the model that the tool received the reviewer's replacement while history kept the original proposal. No application, reviewer interface, persistent backend or real sender was exercised. Proposed: the four-record map, the binding design and the acceptance cases—all NOT RUN.
Sources checked October 23, 2026. Read the source record and limitations.
For engineers adding approval to an agent that sends or publishes something, and the reviewers asked to trust its activity view.
A reviewer changes an agent's proposed tool call. The tool receives the replacement, but the conversation still contains the original arguments.
That is intentional behavior in the LangChain human-in-the-loop middleware revision inspected for this article. The middleware associates the replacement with the tool-call ID, applies it before invoking its handler, and can annotate the result. Preserving the model's proposal and applying a reviewer's correction are different jobs. The code supports both. Pinned middleware, September 20, 2026 (opens in a new tab)
For an engineer adding approval to an agent, this creates a concrete review question: does your activity view show what the model proposed, what the human authorized, or what the system submitted? Those records can disagree without any of them being corrupt. Calling all three “the action” hides the distinction precisely when a correction matters.
Follow one useful action
Consider an illustrative support integration. An agent drafts an SDK migration note from an internal investigation. A maintainer approves the cleaned note for a particular customer, and a service account sends it. The intended benefit is less preparation work; the human decides what leaves the organization. That benefit has not been measured here.
Suppose the model proposes:
send_document(
recipient="engineering@example.invalid",
document="migration-note.md"
)
The reviewer replaces the recipient with sdk-review@example.invalid and removes an internal incident reference from the document. These are fictional addresses and content, not a reported incident.
There are now two separate changes. The recipient is a tool argument. The content is behind a filename. Replacing the first does not establish which bytes the tool later reads from the second. A screen can accurately display approved arguments while still leaving the content's identity unresolved.
This is where a general instruction to “put a human in the loop” stops helping. The engineering question is which consequence that human is authorizing, and how the integration preserves that decision until the consequence occurs.
Four records, four questions
For this external send, preserve four connected records:
| Record | What it should let someone establish |
|---|---|
| Proposal | What did the model ask to do? |
| Review | What destination, content and operation did the reviewer actually see? |
| Decision | Who approved or changed that specific proposal, under which authority? |
| Submission and receipt | What did the sender submit, and what does the receiving service confirm? |
The first is useful for debugging the model. The second helps examine whether the interface disclosed enough. The third expresses authorization. The fourth supports incident review and recovery. A clean conversation transcript is not a substitute for all four.
“Submitted” and “delivered” also need separate meanings. A local tool returning a success-shaped string does not, by itself, explain what an external service accepted. Decide which receipt your application needs: an API operation identifier, a resulting draft, a published object version, or another observable effect. Do not promise stronger confirmation than the receiving system exposes.
These are application requirements proposed here. They are not extra guarantees we found in the framework.
What the implementation establishes
The inspected agent factory connects tool middleware to a ToolNode. Its routing also explains rejection: a call with an existing matching tool message is excluded from pending execution. The approval middleware supplies such a message for a rejection. Keeping the original proposal in history therefore does not mean that rejected call is dispatched. Pinned factory (opens in a new tab)
The upstream tests include fake-tool assertions for both corrected arguments reaching a tool and a rejected call never reaching one. We read those tests, and we ran that test file at the pinned revision in an isolated environment on October 22 and again on October 23: 53 passed each time, none failed. That demonstrates the framework's intended behavior inside its own fake-model fixtures. It does not show how a reader's application behaves. Upstream test specifications (opens in a new tab)
Following the dependency version selected by that checkout's lockfile, ToolNode passes the wrapped request toward tool.invoke or tool.ainvoke, with runtime argument injection. That is a meaningful boundary to inspect. It is still upstream of the custom sender, its credentials, its serialization and its network operation. Lockfile (opens in a new tab), langgraph-prebuilt 1.1.0 source distribution record (opens in a new tab)
Our static trace is therefore:
model proposal → review decision → stored replacement
→ wrapped tool request → tool invocation
↓
application sender and service
remain outside this inspection
The source resolves a narrow question about dispatch. It does not establish authenticated reviewer identity, database recovery, or equivalence between displayed content and delivered content. Those remain integration evidence to collect.
What we ran
On October 23 we drove the pinned middleware with a scripted model that makes no model call and proposes one send_document call, and a fake tool that records the arguments it actually receives. We approved, edited and rejected that call and observed each run from outside the model.
The result matched the static trace. On approve, the tool received the proposed arguments. On edit, it received the reviewer's replacement while message history kept the model's original arguments under the same tool-call ID. On reject, the tool was never invoked; a paired error result sat beside the retained proposal.
The shared tool-call ID is the only structural link between proposal and dispatch. History shows the replacement only as text in a default notice that configuration can turn off. The structured replacement sits in a private state key: absent from the agent's returned output, readable from the checkpointer, and still present after the run ends. These were the framework's own fixture conditions, not a production application. Capture method and results
Make the approved object stable
The controlled-release article examines which facts may leave and who decides. Here we follow how that decision survives the implementation path.
For the migration note, the simplest constructive design may be a sender that accepts a prepared, immutable document revision and an explicit destination. The review screen displays that revision. Immediately before sending, the executor verifies that it is using the reviewed revision and destination.
A content digest can help detect a changed artifact, provided the executor compares the bytes it will actually submit. Hashing a filename, an earlier draft or only the visible text while omitting attachments answers a different question. A digest also cannot determine whether the document was appropriate to release. That judgment still needs an authorized reviewer or an independently justified policy.
Record the sender identity as well. Sending the same note from a draft-only integration and from a customer-facing account can have different consequences. An approval should not silently migrate between those execution contexts.
A minimal record for this case binds:
- The particular pending action and reviewed revision.
- The operation, destination and sending identity.
- The approved bytes or immutable reference, including attachments.
- The reviewer, their decision and the applicable authority.
- Conditions that invalidate the decision, including changed content or destination.
- The eventual submission attempt and receiving-service evidence.
The companion action record turns this into a worked example and blank worksheet. It is a proposed review aid, not implemented enforcement.
A pause does not freeze the outside world
An approval may sit overnight. The document, account permissions or customer's intended recipient may change during that interval. The integration needs to decide which changes invalidate approval and which do not.
Checkpointing solves a different problem: retaining execution state so work can resume. The official documentation distinguishes thread checkpoints from other persistent data and warns that in-memory checkpoints do not survive process restarts. None of that makes a saved decision an indefinitely valid authorization. LangGraph persistence documentation (opens in a new tab)
There is a useful counterexample in the upstream tests. A recorded reviewer edit redirects a send operation to a draft operation. The test expects that replacement to remain effective even when the configuration later disallows new edit decisions. Falling back to the model's original send would defeat the reviewer's correction. This test addresses preservation of a completed decision; it is not a general revocation mechanism. Configuration-change test (opens in a new tab)
A sound application response to revoked authority is to stop the pending action and explain why. It should not discard the correction and proceed with the earlier proposal. Conversely, requiring reapproval after every harmless metadata change would create unnecessary work. Name the conditions that matter for this action instead of expiring every decision indiscriminately.
Retrying an attempt is not permission for another effect
Suppose the send times out. Perhaps the service rejected it. Perhaps the service accepted it and its response was lost. The reviewer intended one external send; a retry must preserve that intention.
Two framework behaviors matter when designing that recovery. LangGraph documents that an interrupted node restarts from its beginning on resumption, so side effects before the interrupt need particular care. Separately, the pinned runtime retry loop can re-invoke a task under a matching retry policy; an interrupt is propagated through a distinct path. These are conditional behaviors, not a claim that every interrupted tool is automatically retried. Interrupt documentation (opens in a new tab), langgraph 1.2.11 release artifacts (opens in a new tab)
For our synthetic sender, define a stable operation identifier and use the receiving API's deduplication mechanism if it provides one. Verify its semantics, including retention and what happens if the same identifier accompanies different content. A locally recorded identifier has no power to deduplicate an external API that ignores it.
If the receiving service offers neither usable deduplication nor a way to reconcile an uncertain send, an ambiguous outcome should remain ambiguous. Route it to investigation before resubmitting. That costs time, but reporting “failed” and trying again can conceal a duplicate effect. Measure that recovery work as part of the agent's operating cost.
Stronger approval is not always the right control
Now change the task. The agent edits a local migration-note draft in a disposable workspace; there is no external recipient or production account. A maintainer later reviews the file through an ordinary diff before publication.
Demanding a recipient-bound authorization record for every draft edit would solve a problem this workflow does not have. It could turn an agent into an expensive typing assistant. A restricted draft tool, clear file boundaries and normal version review may be proportionate. Stronger release binding belongs at the transition into an external consequence.
This counterexample also suggests a design improvement for the original integration: separate preparation from sending. Let the agent revise drafts freely within its allowed workspace. Ask the human to approve the completed external operation once the relevant evidence is available. The benefit is conditional on the preparation stage lacking another route to produce the same external effect.
Approval quality should be evaluated alongside useful work retained: can the maintainer complete the legitimate send, how much review is required, and how often does recovery need manual intervention? A control that prevents all sends cannot establish that it safely supports approved sends.
The next review to run
Pick one real consequential tool in your integration. Follow one proposed action through reviewer display, decision storage, final tool invocation and external receipt. Then change one property between review and execution: the referenced document, the recipient, or the approver's authority.
Write down the expected response before running the exercise. Include a rejected action, an edited action and an ambiguous external response. Retain the submitted payload and resulting receipt with appropriate data minimization. These are proposed acceptance tests, not results obtained for this article.
The question is not whether a confirmation dialog exists. It is whether your evidence can explain what was authorized, what changed, and what actually happened—while still letting the useful action succeed.
Evidence note
Inspected September 21, 2026 and re-checked October 22, 2026. LangChain revision 4af7ab8fcb98c2dc211d9e34cddedffa23eba4d9: the pinned middleware, factory, test file and lockfile were fetched again on October 22 and their SHA-256 digests match the September inspection. Dependency inspection used lock-selected langgraph-prebuilt 1.1.0 and langgraph 1.2.11 artifacts, hash-verified and read without installation in September; on October 22 the pinned middleware test file was run once in an isolated environment (53 passed, 0 failed). On October 23 it was run again with the same result, and the dispatch capture described above was run in the same environment. The source register records provenance, exact functions, the test command and remaining gaps. This is not a vulnerability report, a runtime certification or an assurance about a particular LangChain deployment. The example and proposed controls are original, synthetic design analysis.
Source register: provenance, exact functions and the test run. Companion worksheet: Action approval record.
Found an error or a missing boundary? Send a correction or question. Please omit private traces and credentials.