Contents
If you serve an agent over AG-UI with Microsoft Agent Framework, the browser can send “Shared State” with a request, and the server merges it into AgentSession.state. Until 1.22 (released 15 Oct 2026), whether a client could overwrite the state your tool-approval middleware reads depended on whether your agent object happened to expose its middleware. That is the whole bug, and it is worth understanding because the same shape of mistake is easy to write in any server that merges client state into a session.
The merge and the protected set
AG-UI merges client Shared State into the session while skipping a set of server-owned namespaces. According to PR #9368, that protected set was computed from optional attributes on the agent AG-UI receives: middleware, context_providers, and service_session_state_keys. None of them are part of the SupportsAgentRun protocol, so nothing guarantees they exist.
PR #8714, merged on 28 Sep 2026, had already patched one version of this. Custom ToolApprovalMiddleware.source_id namespaces were added to the protected set, found by walking the agent’s middleware. The PR #9368 description names the case it missed: an agent wrapped so that the middleware is not visible. A plain SupportsAgentRun implementation qualifies, and so does a BaseAgent subclass that delegates run() to an inner agent while keeping its own empty middleware. In both, the inner agent still has a ToolApprovalMiddleware reading approval rules from a custom namespace, and the outer object shows AG-UI nothing to protect.
A client that sends Shared State with a key matching that namespace then supplies the approval state the inner middleware trusts. The PR says this includes state replayed from a stored Thread Snapshot, so a poisoned snapshot persists across turns.
I did not run this. Everything here comes from the PR text, so treat the exact exploit path as the maintainers’ description rather than something I reproduced.
Why a deny list by name fails
The design had two properties that combine badly. The deny list was derived from the object graph the server could see, and the thing being protected lived in an object graph the server could not see. Adding the source_id fix extended the list. It could not make the list complete, because completeness depended on how users composed their agents.
The fix in 1.22 keeps the name-based set and adds a check on the shape of the data. From the PR:
_TOOL_APPROVAL_STATE_FIELDSis the tuple("rules", "queued_approval_requests", "collected_approval_responses"). A test pins these names toToolApprovalState, so a rename upstream fails a test instead of silently opening the hole again._carries_tool_approval_authority(value)returns true when the value is a mapping with any of those fields holding a non-empty list. It does not look at how the list items are encoded. The PR notes thatToolApprovalStateaccepts encodings other than JSON objects, such as key/value pair lists, so inspecting items would have been another deny list.- Any key already holding approval state in the restored server session, as a mapping or a
ToolApprovalStateobject, cannot be replaced by a client value of any shape. - The protected keys for a request are now the union of the named set, keys already carrying approval state in the session, and keys whose client value carries approval authority.
When a client key is dropped under this rule, the server logs a warning: “Ignoring client Shared State %s%s: it carries tool approval authority, which is server-owned.” Per the PR, it lists at most five sorted keys, each cut to 120 characters, plus a count of the rest.
The follow-up commit is the instructive part. An automated review flagged a high-severity gap in the first version: object-valued ToolApprovalState stayed replaceable when middleware metadata was hidden. That is the same failure one level down, and it shows how easy it is to fix the dict case and forget the typed case.
What the fix refuses to do
The PR deliberately does not recompute the protected keys when a snapshot is saved. Its reasoning is that a legitimate application key like {"policy": {"rules": [{"id": 1}]}} would then be stored as server-owned and frozen against later client updates. So shape matching runs on incoming client values and on restored server state, not on the persistence path.
This has a cost the PR accepts and marks as breaking. Any Shared State key whose mapping holds a non-empty rules, queued_approval_requests, or collected_approval_responses list is no longer mirrored into AgentSession.state. It stays in Shared State, in snapshots, and in the model context. If your application used one of those field names for its own purposes, the model still sees the data and your server-side code reading session.state does not.
The rest of the 1.22 AG-UI hardening
The same release carries PR #9316, which moves several client-controlled inputs behind server decisions. As described in that PR:
forwardedProps.injectA2UIToolfrom the client is only a per-request selection. The backend must opt in witha2ui_config={"inject_a2ui_tool": true}.- Client Shared State and A2UI context are treated as untrusted user data rather than carrying system-message priority. Trusted instructions belong in server-owned agent instructions.
- Workflow checkpoints from endpoint-owned and builder-owned stores are bound to the owner, and a cold resume without owner metadata fails closed for threaded or scoped requests.
- Scoped approval keys use a length-prefixed encoding instead of delimiters, which removes collisions between scoped and unscoped keys. The PR says this only affects a process-local store.
- Default INFO logs no longer include full tool and run events or approval payloads.
The release notes also carry a persisted-state warning that matters for rollouts. The 1.22 runtime reads records written by earlier releases, but older runtimes cannot read new records containing hosted or merge_mode. The notes say not to run old and new workers against the same affected store during a rolling deployment.
What I would check
If you use AG-UI with Agent Framework, three things are worth ten minutes.
- Do you wrap agents? If a wrapper hides
middleware, you were in the affected case on versions before 1.22.0. Upgrade, and confirm with a request that sends an approval-shaped Shared State key that it is ignored and logged. - Do you persist Thread Snapshots? State replayed from a snapshot was part of the described path, so a snapshot written before the upgrade by a malicious or buggy client is exactly the sort of artifact to scan for approval-shaped keys.
- Do you use
rulesorqueued_approval_requestsas ordinary application keys? Rename them, or expect them to disappear fromsession.state.
The general lesson is smaller than the product. A protected set built from what the server can introspect protects only what composition lets it see. Checking what the data claims to be, and refusing to let a restored authority-bearing key change hands, does not care how the agent was wrapped.
Sources
- Microsoft Agent Framework python-1.22.0 release, 15 Oct 2026: breaking-change list and persisted-state warning.
- PR #9368, Protect AG-UI tool approval state from client Shared State: mechanism, fix, constants and tests, as described in the PR text.
- PR #8714, Protect custom AG-UI approval state namespaces: the earlier name-based fix.
- PR #9316, AG-UI request trust boundaries: the other trust-boundary changes.
I read the PR and release pages but not the diff line by line, and I did not run the framework. Function and constant names are from the PR text.