Skip to content

Python: [BREAKING] Bind AG-UI workflow agent executors to one authorization scope - #10035

Open
Eduard van Valkenburg (eavanvalkenburg) wants to merge 7 commits into
microsoft:mainfrom
eavanvalkenburg:workflow-caller-isolation
Open

Eduard van Valkenburg (eavanvalkenburg) wants to merge 7 commits into
microsoft:mainfrom
eavanvalkenburg:workflow-caller-isolation

Conversation

@eavanvalkenburg

@eavanvalkenburg Eduard van Valkenburg (eavanvalkenburg) commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Motivation & Context

An AgentExecutor keeps one AgentSession between runs and also tracks the approval requests it has raised. Conversation history, pending approvals, and standing "always approve" rules therefore belong to the executor, not to a caller. When one Workflow, or one WorkflowAgent, that holds an AgentExecutor serves several AG-UI authorization scopes, the second scope's model request contains the first scope's history, and the first scope's standing approvals apply to it. An ordinary agent that exposes a WorkflowAgent through as_tool() reaches the same shared workflow, and the tool wrapper hides it from the endpoint.

add_agent_framework_fastapi_endpoint selects that sharing when it receives a bare Workflow, although the authorization_scope_resolver documentation says it scopes workflow instances. The hosting README and the local_responses_workflow sample have the same shape: WorkflowState(builder, cache_target=False) returns a new Workflow for each request, but WorkflowBuilder.build() reuses the executors registered on the builder, so a module-level AgentExecutor carried its session from one request to the next.

Description & Review Guide

  • What are the major changes?
    • AG-UI: the new _session_ownership.claim_agent_executors binds every AgentExecutor of a workflow, and the AgentSession each one holds, to the first authorization scope that runs it. That includes the executors in nested WorkflowExecutor workflows and in the workflow behind a WorkflowAgent, also when A2UI runners wrap that agent. A run without a scope owns what it runs in the same way. AgentFrameworkWorkflow.run and the WorkflowAgent branch of the agent stream call it after a snapshot replay has returned and before any workflow runs, so a read-only replay never takes ownership. A run in another scope ends with RUN_STARTED and RUN_ERROR (code WORKFLOW_SCOPE_CONFLICT) and never reaches the model. The server log says to use workflow_factory or single_user=True. The registry keys objects by identity (id() with a weak reference) and never calls their __hash__ or __eq__, so an unhashable session or executor subclass works and two objects that compare equal stay apart.
    • AG-UI does not support WorkflowAgent.as_tool(), so it rejects it instead of claiming the workflow. add_agent_framework_fastapi_endpoint raises ValueError for an agent or workflow that exposes a WorkflowAgent as a tool. A run ends with RUN_STARTED and RUN_ERROR (code WORKFLOW_AGENT_TOOL_UNSUPPORTED) before the model when a workflow factory builds one, or when a tool is added after registration. This also applies with single_user=True. The new workflow_agent_tools follows the tools that as_tool() made into sub-agents and into the agents of every AgentExecutor, with a cycle guard and through A2UI runners. Ordinary Agent.as_tool() is still served.
    • Core: BaseAgent.as_tool() keeps the agent it wraps in a private _wrapped_agent attribute of the tool, which AG-UI reads. Serialization skips it, and a copy of the tool keeps pointing at the same agent, as the tool function does. Nothing else changes.
    • Hosting: the README, the WorkflowState docstring, and the local_responses_workflow sample now create the workflow and its executors inside a per-request factory instead of a module-level builder. They also explain why a builder target with cache_target=False still shares its executors.
    • Tests: 94 AG-UI tests. Most run through the real endpoint with two principals, with authorization_scope_resolver and snapshot_scope_resolver, with and without snapshot persistence, and with the same and different thread ids. They cover a shared workflow, a shared WorkflowAgent (also behind A2UI), nested workflows, factories, a shared injected session, unhashable session and executor subclasses, snapshot replays, runs without a scope, single_user, a constant scope, and a standing approval. Further tests cover the rejection of a WorkflowAgent tool in seven runner forms, at registration and at run time, and show that an ordinary agent tool is still served. A separate module tests the identity registry on its own, and another tests the search for workflow tools (sub-agents, executor agents, nested workflows, cycles, A2UI). Four hosting tests cover factory and builder targets for streaming and non-streaming runs, and two core tests cover the as_tool() link.
    • Docs: AG-UI README and AGENTS.md (including what the rule does not cover), the endpoint and workflow_factory docstrings, the AgentExecutor, WorkflowAgent, and ToolApprovalMiddleware docstrings, and two Python builder skill references. They now say that an executor keeps its session between runs, because a checkpoint restore replaces it.
  • What is the impact of these changes? [BREAKING] An AG-UI endpoint that served several authorization scopes from one shared AgentExecutor (a shared Workflow or WorkflowAgent, or a workflow_factory that returns shared workflows, executors, or an injected AgentSession) now fails the second scope with WORKFLOW_SCOPE_CONFLICT instead of sharing history and approvals with it. A workflow that ran without a scope refuses a later scope in the same way, and the reverse. An AG-UI endpoint also no longer serves an agent, or a workflow, that exposes a WorkflowAgent as a tool (WorkflowAgent.as_tool()), also for one scope or with single_user=True. Core gains one private attribute on the tools that as_tool() makes, and its runtime behavior does not change.
    • Migration: pass AgentFrameworkWorkflow(workflow_factory=...) with a factory that builds a new workflow, including new AgentExecutor and AgentSession instances, for every call, or register the endpoint with single_user=True when every caller is the same principal. Factories that already build new executors and sessions for every call, single_user=True, constant-scope setups, and workflows without an AgentExecutor keep working. For a WorkflowAgent tool, serve the workflow with AgentFrameworkWorkflow(workflow_factory=...), or build the workflow inside the tool function for every call. Release note: the AG-UI check relies on the new private attribute in core, so the agent-framework-core floor of agent-framework-ag-ui should move to the first core release that includes it; with an older core the check finds nothing, as before this change.
  • What do you want reviewers to focus on? The ownership rule is enforced at run time and not rejected at registration, because existing tests and code deliberately support a shared workflow with one constant scope and fence its pending interrupts to the live owner. A registration-time rejection would break those single-principal setups. The rule follows live objects only. It does not see state that a factory loads from storage into a new object (for example AgentSession.from_dict() or SessionStore.get()), executors that are not AgentExecutor, or state that sessions share outside the session object. The docs say so. Whether to close the stored-state gap, for example by passing the scope to factories as the scoped-construction question in Python: [Bug]: AG-UI shares one AgentSession across authorization scopes when a bare Workflow is passed to add_agent_framework_fastapi_endpoint #9811 asks, is left open for the maintainers. The threads of one scope still share state in shared mode. WorkflowAgent.as_tool() is rejected instead of claimed, because it is not supported in any form. The rejection cannot see a tool function of your own that calls a workflow, or a workflow tool that a context provider adds while a run is prepared. Those stay unsupported, the docs say so, and AG-UI gives their callers no isolation. A guard at the tool-invocation boundary was left out, because the method is not supported. The change is marked breaking because a second-scope request that used to be accepted now ends with a RUN_ERROR, and because an agent that exposes a WorkflowAgent as a tool is no longer served.

Related Issue

Related to #9811 (not a closing reference). The direction for that issue is still under planning, so this PR proposes one for discussion. No other open PR covers this change.

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: GitHub Copilot assisted with the implementation, tests, documentation, and this PR description.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

An AgentExecutor owns one AgentSession for its lifetime, so a shared Workflow
or WorkflowAgent that serves several authorization scopes shares that
conversation and approval state between them.

Bind every AgentExecutor of a workflow, including nested workflows, to the
first authorization scope that runs it. A run in another scope now ends with
a WORKFLOW_SCOPE_CONFLICT RUN_ERROR before it reaches the model. Workflow
factories that build new executors for every call, single-user endpoints, and
workflows without an AgentExecutor keep working unchanged.
WorkflowBuilder.build() reuses the executors registered on the builder, so a
builder target with cache_target=False returns a new Workflow for each request
that still shares one AgentExecutor and its AgentSession.

Document that in the hosting README and the WorkflowState docstring, create the
workflow and its executors inside a per-request factory in the Responses
workflow sample, add tests for both target shapes, and note the rule in the
Python builder skill references.
Clarify in the AgentExecutor, WorkflowAgent, and ToolApprovalMiddleware
docstrings that an instance keeps one session, with its history, pending
approvals, and standing approval rules, for its whole lifetime and must not
serve callers that need independent state.
Copilot AI balanced review requested due to automatic review settings November 3, 2026 12:00
@agent-framework-automation agent-framework-automation Bot added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Nov 3, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (3 commit(s)): 0a4f457c0265, f51585ddf6f5, 2ebcbd2cd015
Model: gpt-5.6-sol

Overview

The PR adds a process-wide, weak-keyed ownership claim for built-in AgentExecutor graphs, validates endpoint scopes, rejects conflicts before workflow execution, and covers direct, nested, factory, race, and approval scenarios extensively. The guard still misses supported A2UI wrappers and shared injected sessions, allows prior unscoped state to become owned by a later authenticated scope, and lets read-only hydration consume the executor's lifetime ownership. Because the conflict behavior intentionally rejects requests previously accepted by a stable package, the migration also requires breaking-change classification.

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
5 verified findings remained after source verification (3 high, 2 medium) across 3 files. Details are attached to the affected lines below.

Affected areas: python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py, python/packages/ag-ui/agent_framework_ag_ui/_session_ownership.py, python/packages/ag-ui/agent_framework_ag_ui/_workflow.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

With snapshot persistence enabled, hydration-only requests incorrectly claim executors in both workflow paths, blocking another scope’s first actual run with WORKFLOW_SCOPE_CONFLICT.

2 open issues
What changed in this PR

This PR addresses cross-scope session sharing in Python AG-UI workflows by binding workflow-owned AgentExecutor instances to one authorization scope. It also updates hosting guidance to create fresh executors for independent requests.

Changes:

  • Add ownership checks for shared workflows, nested workflows, and WorkflowAgent execution.
  • Add regression coverage for scope isolation, approval state, and factory-versus-builder behavior.
  • Update documentation and the hosting sample to clarify session ownership and per-request construction.
File Description
skills/​agent-framework-builder-python/​references/​workflow-builder.md Explain executor reuse across builder calls.
skills/​agent-framework-builder-python/​references/​hosting-and-deployment.md Clarify per-request executor construction.
python/​samples/​04-hosting/​af-hosting/​local_responses_workflow/​README.md Document factory-based request isolation.
python/​samples/​04-hosting/​af-hosting/​local_responses_workflow/​app.py Create workflows and executors inside a factory.
python/​packages/​hosting/​tests/​hosting/​test_state.py Test factory isolation and builder reuse.
python/​packages/​hosting/​tests/​hosting/​_workflow_fixtures.py Add session-history recording helpers.
python/​packages/​hosting/​README.md Explain independent workflow construction.
python/​packages/​hosting/​agent_framework_hosting/​_state.py Clarify target caching and executor ownership in docstrings.
python/​packages/​core/​agent_framework/​_workflows/​_agent.py Document WorkflowAgent state-sharing constraints.
python/​packages/​core/​agent_framework/​_workflows/​_agent_executor.py Document executor session lifetime.
python/​packages/​core/​agent_framework/​_harness/​_tool_approval.py Explain session-owned standing approvals.
python/​packages/​ag-ui/​tests/​ag_ui/​workflow_scope_helpers.py Add endpoint and approval test helpers.
python/​packages/​ag-ui/​tests/​ag_ui/​test_workflow_scope_ownership.py Test ownership enforcement and isolation.
python/​packages/​ag-ui/​README.md Document scope conflicts and factory usage.
python/​packages/​ag-ui/​AGENTS.md Describe ownership enforcement boundaries.
python/​packages/​ag-ui/​agent_framework_ag_ui/​_workflow.py Check executor ownership before workflow execution.
python/​packages/​ag-ui/​agent_framework_ag_ui/​_session_ownership.py Add locked, weak-reference ownership tracking.
python/​packages/​ag-ui/​agent_framework_ag_ui/​_endpoint.py Document shared-workflow authorization behavior.
python/​packages/​ag-ui/​agent_framework_ag_ui/​_agent_run.py Check ownership for wrapped workflow agents.

Review effort: Balanced

Look through A2UI runners when claiming the workflow behind a WorkflowAgent.
Bind the AgentSession that an AgentExecutor holds as well as the executor, so
new executors that inject one shared session are refused for another scope.
Treat a run without a scope as an owner of its own, so a workflow that ran
without a scope is not handed to an authorization scope later, and the
reverse. Claim only after a snapshot replay has returned, so a read-only
replay does not take ownership.
@eavanvalkenburg Eduard van Valkenburg (eavanvalkenburg) changed the title Python: Bind AG-UI workflow agent executors to one authorization scope Python: [BREAKING] Bind AG-UI workflow agent executors to one authorization scope Nov 3, 2026
@agent-framework-automation agent-framework-automation Bot added the breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible label Nov 3, 2026
@eavanvalkenburg
Eduard van Valkenburg (eavanvalkenburg) marked this pull request as ready for review November 3, 2026 12:36

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 2

Result: Findings reported
Scope: 1 net-new commit(s): e253d29142eb
Model: gpt-5.6-sol

Overview

This iteration closes the earlier A2UI-wrapper, shared-session-object, unscoped-run, and hydration-order gaps with recursive traversal, atomic ownership claims, and broad endpoint tests. The remaining registry still treats a session's transient Python object as its identity, so restored copies of one durable session can cross scopes, and its weak-key lookup rejects otherwise valid unhashable session subclasses.

Reviewed the supplied incremental change set across correctness, security/reliability, architecture, and failure behavior.
2 verified findings remained after source verification (1 high, 1 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: python/packages/ag-ui/agent_framework_ag_ui/_session_ownership.py

The ownership registry used a WeakKeyDictionary, which hashes its keys. An
AgentSession or AgentExecutor subclass that defines __eq__ is unhashable, so
every AG-UI run on it failed, and two distinct objects that compare equal could
share one entry.

Key the registry by id() with a weak reference, so user-defined hashing and
equality never run and an entry disappears when its object is collected.

Document what the ownership rule does not cover, such as state that a factory
loads from storage into a new object, and say that an executor keeps its
session between runs because a checkpoint restore replaces it.

@jpalvarezl Jose Alvarez (jpalvarezl) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One isolation gap remains in the agent-as-tool path. I left an inline comment because the top-level claim does not cover a shared WorkflowAgent.as_tool(), so separate authorization scopes can still reach the same workflow executors.

AG-UI does not support WorkflowAgent.as_tool(): every caller of an agent that
exposes a workflow as a tool reaches the same workflow and its executor
sessions, and the tool wrapper hides that workflow from the ownership claim.

Record the agent that as_tool() wraps in a private attribute of the tool, keep
it by reference when the tool is copied, and follow those links from AG-UI.
The endpoint raises ValueError for an agent or workflow that exposes a
WorkflowAgent as a tool, and a run ends with a WORKFLOW_AGENT_TOOL_UNSUPPORTED
RUN_ERROR when a workflow factory builds one or a tool is added later. The
rejection also applies with single_user=True.

@jpalvarezl Jose Alvarez (jpalvarezl) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direct, nested, factory, copied-tool, and unhashable-session cases look addressed. I followed up in the existing workflow-agent-tool thread because a supported ContextProvider can still inject that tool after the AG-UI validation has already run, leaving the cross-scope path open.

Say in the AG-UI README and AGENTS.md, the WorkflowAgent and as_tool()
docstrings, and the ownership module that exposing a WorkflowAgent as a tool is
not supported, including the forms that AG-UI cannot detect: a tool that a
context provider adds while a run is prepared, and a tool function that calls a
workflow from custom code. AG-UI gives the callers of those no isolation.

@jpalvarezl Jose Alvarez (jpalvarezl) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core cross-scope isolation issue and the concrete reachable WorkflowAgent tool paths are addressed. The remaining provider/custom-tool cases are now explicitly outside the supported WorkflowAgent.as_tool() contract, and I did not find another material correctness or security regression in the latest changes.

This branch was successfully deployed

1 active deployment
github-app-auth — 68b7ad43 Deployed Nov 4, 2026 by eavanvalkenburg via add_label #27068
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants