Skip to content

fix(server): A2A auth middleware populates current_principal contextvars (closes #590)#592

Merged
bokelley merged 1 commit intomainfrom
bokelley/issue-590-a2a-principal-contextvar
May 5, 2026
Merged

fix(server): A2A auth middleware populates current_principal contextvars (closes #590)#592
bokelley merged 1 commit intomainfrom
bokelley/issue-590-a2a-principal-contextvar

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 5, 2026

Summary

Closes #590. A2ABearerAuthMiddleware previously only stashed the principal in scope[\"user\"] / scope[\"auth\"] for a2a-sdk's ServerCallContext path. The contract advertised by the matching MCP middleware (BearerTokenAuthMiddleware) — populating current_principal, current_tenant, and current_principal_metadata ContextVars for the downstream call — silently held only on MCP.

Adopters with tenant policies that read current_principal.get() from a platform method (salesagent's default) saw None on every A2A message/send, locking buyers out of A2A while MCP worked on the same token.

Root cause

Transport ContextVars set by adcp's auth middleware?
MCP BearerTokenAuthMiddleware.dispatch sets all three
A2A A2ABearerAuthMiddleware.__call__ only set scope[\"user\"]

The user's diagnostic in #590 attributed this to a contextvar boundary (asyncio.create_task in a2a-sdk's DefaultRequestHandler), but reproducing the path locally showed contextvars do propagate through asyncio.create_task. The actual mechanism was simpler: the A2A middleware never set the contextvars in the first place, so the platform handler saw the default None regardless of the propagation path.

Fix

Symmetric set + finally-reset in A2ABearerAuthMiddleware.__call__ between auth success and the inner-app await. Behavior now identical across transports for sellers reading current_principal.get() directly from a platform method.

Test plan

  • 3 new unit tests in tests/test_serve_auth_both.py:
    • test_valid_token_populates_contextvars — all three contextvars set during downstream call
    • test_contextvars_reset_after_call — reset to default after success
    • test_contextvars_reset_even_on_inner_exception — reset on exception path
  • Full main suite green: 3727 passed, 17 skipped, 1 xfailed
  • ruff check + mypy clean

Adopter impact

Existing readers of current_principal.get() on A2A start working without code changes. The ctx.caller_identity path through ServerCallContext.user is unchanged — both APIs now reliably hold the same identity on both transports, matching the docstring contract.

🤖 Generated with Claude Code

closes #590

``A2ABearerAuthMiddleware`` previously only stashed the principal in
``scope["user"]`` / ``scope["auth"]`` for a2a-sdk's ``ServerCallContext``
path. The contract advertised by the matching MCP middleware
(``BearerTokenAuthMiddleware``) — populating ``current_principal``,
``current_tenant``, and ``current_principal_metadata`` ContextVars for
the downstream call — silently held only on MCP. Adopters with tenant
policies that read ``current_principal.get()`` from a platform method
(salesagent's default) saw ``None`` on every A2A ``message/send``,
locking buyers out of A2A while MCP worked on the same token.

Symmetric fix: set the same three ContextVars in the A2A middleware
between auth and the inner-app await, with unconditional reset in
``finally`` so a buggy handler can't poison the contextvars for a
shared task.

Adds three regression tests to tests/test_serve_auth_both.py exercising
the populated, reset, and exception-path resets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 5430942 into main May 5, 2026
16 checks passed
@bokelley bokelley deleted the bokelley/issue-590-a2a-principal-contextvar branch May 5, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2A: current_principal contextvar is None inside platform handlers (current_tenant works) — same token works on MCP

1 participant