Skip to content

Python: Fix M365 agent sample startup and anonymous mode - #9598

Draft
Naveen Chatlapalli (1aifanatic) wants to merge 3 commits into
microsoft:mainfrom
1aifanatic:contrib/python-m365-sample-aiohttp-middlewares
Draft

Naveen Chatlapalli (1aifanatic) wants to merge 3 commits into
microsoft:mainfrom
1aifanatic:contrib/python-m365-sample-aiohttp-middlewares

Conversation

@1aifanatic

Copy link
Copy Markdown
Contributor

Motivation & Context

The M365 Agents SDK sample (python/samples/05-end-to-end/m365-agent) does not start, and once startup is fixed, anonymous mode still cannot answer a message. Details and reproduction in #9595:

  1. web.Application(middleware=...) raises TypeError, because aiohttp's keyword is middlewares. The middlewares → middleware rename (Python: [BREAKING]: removed display_name, renamed context_providers, middleware and AggregateContextProvider #3139) appears to have caught it.
  2. FoundryChatClient() is created without a credential, and the README still documents OpenAI variables.
  3. The current M365 Agents SDK (1.9.0) requires a connection manager for CloudAdapter and AgentApplication. ClaimsIdentity now takes (claims, authentication_type, ...), so the sample's "anonymous" identity was treated as authenticated and the adapter tried to get an MSAL token (TENANT_ID is not set).

Description & Review Guide

  • What are the major changes?
    • web.Application(middlewares=[middleware_fn]).
    • FoundryChatClient(credential=AzureCliCredential()). The README now lists FOUNDRY_PROJECT_ENDPOINT / FOUNDRY_MODEL and az login, and the docstring no longer points to a nonexistent .env.example.
    • In anonymous mode, build_connection_manager returns an MsalConnectionManager with an empty SERVICE_CONNECTION, and build_authorization always builds Authorization. Anonymous requests never request tokens, so the placeholder connection is never used.
    • The anonymous middleware injects ClaimsIdentity(authentication_type="Anonymous") with no claims, which ClaimsIdentity.allow_anonymous recognizes.
    • The run-command comment points to samples/05-end-to-end/... instead of samples/demos/....
  • What is the impact of these changes? Sample only. Authenticated mode keeps its existing path: MsalConnectionManager(**agents_sdk_config) plus Authorization.
  • What do you want reviewers to focus on? Whether an empty SERVICE_CONNECTION is an acceptable way to satisfy the SDK in anonymous mode, or whether you would rather the sample require connection settings even for local runs.

Verified with aiohttp's test client: create_app(load_app_config()) in anonymous mode, with the agent replaced by an echo stub and a local endpoint standing in for the channel service:

result
main TypeError at web.Application(middleware=...)
this PR GET /api/health 200, GET /api/messages 200, POST /api/messages 202, and the reply activity reaches the channel

I also checked that the real build_agent() constructs with only FOUNDRY_* set. I could not test authenticated mode, which needs an Azure Bot registration. ruff format --check passes. ruff check reports one pre-existing E501 (the long approval_mode NOTE comment), which is also on main. Pyright's two remaining errors are pre-existing typing nits (an Optional return type and the sync lambda route handler), and both routes respond correctly at runtime.

Related Issue

Fixes #9595

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: Claude Code helped trace the breakages through aiohttp and the M365 Agents SDK, implement the fix, build the test-client verification, and draft this description and the linked issue.

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.

🤖 Generated with Claude Code

- aiohttp's Application takes middlewares=, not middleware= (TypeError at startup).
- FoundryChatClient needs a credential; pass AzureCliCredential and document the
  FOUNDRY_* settings the sample actually reads instead of OpenAI ones.
- The M365 Agents SDK now requires a connection manager for CloudAdapter and
  AgentApplication; give anonymous mode an empty service connection.
- ClaimsIdentity's signature is now (claims, authentication_type, ...); build a
  claim-less anonymous identity so the adapter skips token acquisition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 12:54
@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 Oct 22, 2026

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.

🔵 Needs a closer look

The anonymous identity and empty MSAL connection depend on external SDK behavior that still needs independent verification before approval.

No open issues 🎉

What changed in this PR

This PR fixes startup and anonymous messaging failures in the Python M365 agent sample reported in #9595.

Changes:

  • Corrects aiohttp middleware registration and supplies an Azure CLI credential to FoundryChatClient.
  • Adds a placeholder connection manager and claim-less identity for anonymous mode while preserving the authenticated configuration path.
  • Updates Foundry setup instructions and corrects the run-command and configuration comments.
File Description
python/​samples/​05-end-to-end/​m365-agent/​README.md Replaces OpenAI settings with Foundry configuration and Azure CLI prerequisites.
python/​samples/​05-end-to-end/​m365-agent/​m365_agent_demo/​app.py Fixes startup, configures anonymous SDK components, and corrects usage comments.

Review effort: Balanced

The anonymous-mode identity uses ClaimsIdentity(authentication_type=...),
which arrived in microsoft-agents-hosting-core 1.4.0; on 1.0-1.3 the
constructor requires claims and is_authenticated. Verified the sample's
anonymous message flow on 1.4.0 through 1.10.0.dev1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@1aifanatic

Copy link
Copy Markdown
Contributor Author

Following up on the "needs a closer look" note about relying on M365 Agents SDK behavior, I verified the two anonymous-mode pieces against the SDK source and across releases.

Why the anonymous path never touches the placeholder connection (microsoft-agents-hosting-core 1.9.0):

  • ClaimsIdentity.allow_anonymous (authorization/claims_identity.py:47) is true when the authentication type is empty or "Anonymous" and there are no claims. This PR's middleware injects exactly that identity.
  • ChannelServiceAdapter.process_activity passes claims_identity.allow_anonymous as use_anonymous (channel_service_adapter.py:417). RestChannelServiceClientFactory then returns an anonymous UserTokenClient / AnonymousTokenProvider without calling connection_manager.get_token_provider (rest_channel_service_client_factory.py:165).
  • The empty SERVICE_CONNECTION exists only to satisfy the constructor checks: HttpAdapterBase (_http_adapter_base.py:75), ConnectionManager (authorization/connection_manager.py:98), and AgentApplication building Authorization (app/agent_application.py:209).

Version matrix: the sample's anonymous flow, run end to end with aiohttp's test client (agent stubbed, local channel endpoint capturing the reply):

hosting-core / aiohttp / msal / activity main this PR
1.0.0, 1.2.0, 1.3.0 TypeError (aiohttp middleware=) fails: ClaimsIdentity() still requires claims, is_authenticated
1.4.0, 1.5.0, 1.6.0, 1.7.0, 1.8.0, 1.9.0, 1.10.0.dev1 TypeError (aiohttp middleware=) health 200, /api/messages 202, reply delivered

Because the claim-less ClaimsIdentity(authentication_type=...) form starts at 1.4.0, I pushed b6fcdf3, which adds >=1.4.0 to the four microsoft-agents-* entries in the sample's PEP 723 header. An older environment now fails at resolution instead of at the first message.

Authenticated mode, with USE_ANONYMOUS_MODE=false and CONNECTIONS__SERVICE_CONNECTION__SETTINGS__* set, still builds MsalConnectionManager(**agents_sdk_config) and starts cleanly on 1.4.0, 1.9.0 and 1.10.0.dev1. A real token exchange still needs an Azure Bot registration, which I don't have.

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.

Naveen Chatlapalli (@1aifanatic) Two material sample issues remain on this head:

  1. In python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py:197-198, setting USE_ANONYMOUS_MODE=false does not install any JWT validation middleware. Missing and invalid bearer tokens still reach the agent and receive HTTP 202 responses. Please apply the SDK’s authenticated-request validation path when anonymous mode is disabled, preserve the explicit anonymous path, and add tests proving missing/invalid tokens are rejected before agent invocation.
  2. In python/samples/05-end-to-end/m365-agent/README.md:33-43, the documented uv sync plus plain python m365_agent_demo/app.py flow does not install the script’s PEP 723 dependencies. Please document the executable PEP 723 path, such as uv run samples/05-end-to-end/m365-agent/m365_agent_demo/app.py, so a clean checkout installs the required M365 hosting/authentication packages.

Please address both before merge.

With USE_ANONYMOUS_MODE=false the sample installed no JWT validation, so
requests with a missing or invalid token still reached the agent. Wrap
/api/messages with the SDK's jwt_authorization_decorator and its default
connection configuration, keep the anonymous middleware for anonymous mode,
and document running the PEP 723 script with uv run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@1aifanatic

Copy link
Copy Markdown
Contributor Author

Eduard van Valkenburg (@eavanvalkenburg) Thanks. 3a4cf64 addresses both points.

1. Token validation when anonymous mode is off. create_app now uses the SDK's validation path:

  • app["agent_configuration"] = connection_manager.get_default_connection_configuration()
  • POST /api/messages is wrapped with microsoft_agents.hosting.aiohttp.jwt_authorization_decorator. That decorator calls the same _authorize_request / JwtTokenValidator path as jwt_authorization_middleware.

Anonymous mode keeps its explicit middleware, which is now installed only when USE_ANONYMOUS_MODE is true. I used the decorator instead of the global middleware so GET /api/health keeps working without a token.

New tests in python/tests/samples/end_to_end/test_m365_agent_sample.py load app.py and replace start_agent_process with a recorder. A rejected request therefore shows that the turn never started. JWKS lookup is redirected to a locally generated RSA key, so nothing hits the network.

request (authenticated mode) status turn processed
no Authorization header 401 no
Basic … 401 no
Bearer not-a-jwt 401 no
RS256 token signed by an untrusted key 401 no
trusted key, wrong aud 401 no
trusted key, aud = configured client ID 202 yes

Anonymous mode returns 202 with an allow_anonymous identity. /api/health returns 200 without a token.

All 8 tests pass with microsoft-agents-* 1.4.0, 1.9.0 and 1.10.0.dev1. On the previous head, all 6 authenticated-mode tests fail: every request reaches the turn handler with 202. The workspace environment doesn't include microsoft-agents-hosting-aiohttp or -authentication-msal, so the module skips via pytest.importorskip there. To run it, install those plus the workspace packages.

2. README. It now runs the script with uv run m365_agent_demo/app.py from the sample folder and explains that the PEP 723 header supplies the M365 hosting and authentication packages. uv sync / python app.py are gone from all three places they appeared. From a clean checkout, uv run installed the dependencies and started the app. With USE_ANONYMOUS_MODE=false, /api/health returned 200. /api/messages returned 401 both without a token and with Bearer abc.

This branch was previously deployed

1 inactive deployment
github-app-auth — 3a4cf647 Deployed Oct 27, 2026 by 1aifanatic via add_label #26268
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

Python: [Bug]: M365 agent sample fails at startup and in anonymous mode

3 participants