Skip to content

feat(keycardai-oauth)!: pass error context through AccessContext.access(), rename to get_resource_error#114

Merged
Larry-Osakwe merged 3 commits into
mainfrom
larry/oauth-rich-resource-access-error
May 7, 2026
Merged

feat(keycardai-oauth)!: pass error context through AccessContext.access(), rename to get_resource_error#114
Larry-Osakwe merged 3 commits into
mainfrom
larry/oauth-rich-resource-access-error

Conversation

@Larry-Osakwe
Copy link
Copy Markdown
Contributor

Summary

Companion to keycardai/typescript-sdk#19. Brings python-sdk's AccessContext.access() to the same shape, addressing review feedback from @jerriclynsjohn.

Two changes:

  1. AccessContext.access() now passes error context to ResourceAccessError. The constructor (exceptions.py:251-321) already accepted resource, error_type, available_resources, and error_details. The three throw sites in access() were calling ResourceAccessError() with no args, throwing the context away. They now populate it so middleware can read those attributes (and the details dict on the base OAuthServerError) to surface which resource failed and why.

  2. Rename get_resource_errorsget_resource_error on both keycardai.oauth.server.access_context.AccessContext and the parallel keycardai.fastmcp.provider.AccessContext. Plural method name with a singular return shape was misleading. Breaking on a pre-1.0 API.

Test plan

  • cd packages/oauth && uv run pytest — 221 passed (6 new test_access_context.py cases for the rich-error contract; oauth had no direct AccessContext tests before)
  • cd packages/fastmcp && uv run pytest — 51 passed
  • cd packages/mcp && uv run pytest — 532 passed (16 interactive skips)

Related

  • TS companion: keycardai/typescript-sdk#19, commit 317a38b
  • Cross-sdk parity follow-ups filed: ACC-259, ACC-260, ACC-261, ACC-262, ACC-263

BREAKING CHANGE: AccessContext.get_resource_errors renamed to AccessContext.get_resource_error on both keycardai-oauth and keycardai-fastmcp.

…ss(), rename to get_resource_error

ResourceAccessError is now populated with resource, error_type
('global_error' | 'resource_error' | 'missing_token'), available_resources,
and error_details at the three throw sites in AccessContext.access(). The
constructor already accepted these args; this fills the call sites so
middleware can surface which resource failed and why.

Also rename get_resource_errors -> get_resource_error. Plural method name
with a singular return shape was misleading.

Companion to keycardai/typescript-sdk#19 (TS commit 317a38b).
Addresses review feedback from @jerriclynsjohn.

BREAKING CHANGE: AccessContext.get_resource_errors renamed to
AccessContext.get_resource_error.
… to match keycardai-oauth

Mirrors the keycardai-oauth rename in the fastmcp provider's parallel
AccessContext implementation. Keeps the two surfaces aligned for users
who consume both directly.

BREAKING CHANGE: AccessContext.get_resource_errors renamed to
AccessContext.get_resource_error.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

📦 Release Preview

This analysis shows the expected release impact:

📈 Expected Version Changes

keycardai-oauth: 0.11.0 → 0.12.0 (MINOR)
keycardai-fastmcp: 0.2.0 → 0.3.0 (MINOR)

📋 Package Details

[
  {
    "package_name": "keycardai-oauth",
    "package_dir": "packages/oauth",
    "has_changes": true,
    "current_version": "0.11.0",
    "next_version": "0.12.0",
    "increment": "MINOR"
  },
  {
    "package_name": "keycardai-fastmcp",
    "package_dir": "packages/fastmcp",
    "has_changes": true,
    "current_version": "0.2.0",
    "next_version": "0.3.0",
    "increment": "MINOR"
  }
]

📝 Changelog Preview

Changelog for keycardai:
## Unreleased

## 0.2.0-keycardai (2025-09-10)

## 0.1.0-keycardai (2025-09-07)


- feat(keycardai): initial release
Changelog for keycardai-mcp:
## Unreleased

## 0.23.0-keycardai-mcp (2026-04-28)


- refactor(keycardai-mcp): drop deprecated bearer middleware shims (ACC-235) (#104)
- Removes the keycardai.mcp.server.middleware re-export shims that pointed
at the deprecated BearerAuthMiddleware in keycardai-starlette. Anyone
importing BearerAuthMiddleware should switch to AuthenticationMiddleware
with backend=KeycardAuthBackend(verifier) and on_error=keycard_on_error.
The deprecated symbols themselves stay in keycardai-starlette and come
out in ACC-237.
- keycardai-agents repointed at keycardai.starlette.middleware.bearer so
it keeps building. It still emits the DeprecationWarning shipped in
keycardai-starlette 0.3.0; that goes away when ACC-232 archives the
package.
- Bearer-helper unit tests (_get_bearer_token, _get_oauth_protected_resource_url)
moved from packages/mcp/tests to packages/starlette/tests where the
helpers live.

## 0.22.0-keycardai-mcp (2026-04-24)


- fix(keycardai-mcp): resolve ruff lint errors in provider and test imports

## 0.21.0-keycardai-mcp (2026-03-06)


- build(keycardai-mcp): bump keycardai-oauth dependency to >=0.7.0
- refactor(keycardai-mcp)!: optimize error formatting in token exchange chain
- Restructure error dicts to remove redundancy and improve readability.
Key renames: error->message, error_code->code, error_description->description,
resource_errors->resources. Only include raw_error for non-OAuth exceptions.
- BREAKING CHANGE: Error dict keys renamed: error->message, error_code->code, error_description->description. The get_errors() output key resource_errors is now resources.

## 0.20.1-keycardai-mcp (2026-02-06)


- fix(keycardai-mcp): return prm for resources dynamically

## 0.20.0-keycardai-mcp (2026-01-07)


- feat(keycardai-mcp): Adds PydanticAI integration for MCP frameworks
- - Adds PaydanticAI adapter to client integrations directory
- Support for PydanticAI agents with secure MCP tool access
- Follows established pattern with LangChain and OpenAI integrations
- Adds tests for PydanticAI integration imports

## 0.19.0-keycardai-mcp (2026-01-07)


- feat(keycardai-mcp): Add greater control over OAuth metadata location
- - Refactors `auth_metadata_mount` into it's component parts
- Exposes mounts for individual metadata
- Allows the user to specify exactly where their OAuth metadata is
exposed
- NOTE: This is only for advanced use cases where you know you need
something non-standard. Otherwise, follow the OAuth spec.

## 0.18.0-keycardai-mcp (2025-12-04)


- feat(keycardai-mcp): add CrewAI integration for agent frameworks
- - Add CrewAI adapter to client integrations directory
- Support for CrewAI agents with secure MCP tool access
- No token passing - agents never receive raw API tokens
- Fresh token fetched per API call through Keycard
- Follows established pattern with LangChain and OpenAI integrations
- Deleted separate packages/agents package (not needed)
- Added optional dependencies: crewai and agents extras
- Added tests for CrewAI integration imports

## 0.17.0-keycardai-mcp (2025-11-18)


- feat(keycardai-mcp): session callback notification
- feat(keycardai-mcp): session lifecycle management

## 0.16.0-keycardai-mcp (2025-11-17)


- feat(keycardai-mcp): headless clients
- feat(keycardai-mcp): update oauth deps
- feat(keycardai-mcp): client implementation

## 0.15.0-keycardai-mcp (2025-11-07)


- feat(keycardai-mcp): enable web token eks env

## 0.14.0-keycardai-mcp (2025-11-06)


- feat(keycardai-mcp): configure mcp url via env

## 0.13.0-keycardai-mcp (2025-11-05)


- feat(keycardai-mcp): zone settings via env

## 0.12.0-keycardai-mcp (2025-11-05)


- feat(keycardai-mcp): automatic app cred discovery
- feat(keycardai-mcp): default eks env

## 0.11.0-keycardai-mcp (2025-10-29)


- feat(keycardai-mcp): release latest version
- Release current version of workload identity implementation

## 0.10.0-keycardai-mcp (2025-10-27)


- feat(keycardai-mcp): cach the application credentials
- feat(keycardai-mcp): app credential grant flow

## 0.9.0-keycardai-mcp (2025-10-20)


- refactor(keycardai-mcp): align credential names
- feat(keycardai-mcp): eks workload identity support
- feat(keycardai-mcp): add application authentication

## 0.8.1-keycardai-mcp (2025-10-10)


- fix(keycardai-mcp): wrong base url in auth metadata

## 0.8.0-keycardai-mcp (2025-10-07)


- refactor(keycardai-mcp): improve error messages
- refactor(keycardai-mcp): improves the error messages to provide useful debug information

## 0.7.1-keycardai-mcp (2025-09-29)


- fix(keycardai-mcp): set audience for client assertions

## 0.7.0-keycardai-mcp (2025-09-27)


- feat(keycardai-mcp): lowlevel support for RequestContext

## 0.6.0-keycardai-mcp (2025-09-23)


- feat(keycardai-mcp): enable custom middleware injection

## 0.5.1-keycardai-mcp (2025-09-22)


- fix(keycardai-mcp): support x-forwarded-port header

## 0.5.0-keycardai-mcp (2025-09-22)


- feat(keycardai-mcp): dcr can be toggled on/off
- feat(keycardai-mcp): private key jwt support with global key
- feat(keycardai-mcp): grant decorator exception handling
- feat(keycardai-mcp): private key manager protocol

## 0.4.1-keycardai-mcp (2025-09-18)


- fix(keycardai-mcp): support both sync and async tool calls

## 0.4.0-keycardai-mcp (2025-09-18)


- feat(keycardai-mcp): default domain handling

## 0.3.1-keycardai-mcp (2025-09-17)


- fix(keycardai-mcp): check audience when configured

## 0.3.0-keycardai-mcp (2025-09-16)


- feat(keycardai-mcp): multi-zone mcp routing
- feat(keycardai-mcp): advanced server handlers
- feat(keycardai-mcp): auth provider implementation

## 0.1.0-keycardai-mcp (2025-09-10)
Changelog for keycardai-a2a:
## Unreleased

## 0.3.0-keycardai-a2a (2026-05-01)


- fix(keycardai-a2a)!: align DelegationClient with a2a-sdk 1.x JSONRPC dispatcher (ACC-231) (#107)
- DelegationClient was still speaking 0.x JSON-RPC (“message/send”, old envelope, no A2A-Version), so every call was rejected by real 1.x dispatchers before execution—breaking the entire keycardai-crewai delegation path. Fixed by upgrading to the 1.x contract (SendMessage, proper envelope + headers, new response shape) and adding a real dispatcher test to catch drift.

## 0.2.0-keycardai-a2a (2026-04-29)


- feat(keycardai-a2a): new package split from keycardai-agents (ACC-230) (#105)
- * feat(keycardai-a2a)!: new package split from keycardai-agents (ACC-230)
- Per the KEP "Decompose keycardai-agents", the A2A delegation surface moves
out of keycardai-agents into a new keycardai-a2a package, structurally
analogous to keycardai-mcp. Symbols available at the new namespace:
- - AgentServer, create_agent_card_server, serve_agent
- DelegationClient, DelegationClientSync
- AgentExecutor, SimpleExecutor, LambdaExecutor
- KeycardToA2AExecutorBridge
- ServiceDiscovery
- AgentServiceConfig
- The bearer middleware in server/app.py also migrates from the deprecated
BearerAuthMiddleware to the canonical KeycardAuthBackend +
AuthenticationMiddleware pattern from keycardai-starlette. The
keycardai-mcp dependency drops from this code path.
- Hard cut, no transitional bridge: ACC-232 confirms no known production
users of keycardai.agents.* paths.
- The PKCE user-login client (AgentClient) is dropped entirely. Its
capability already lives in keycardai-oauth as
keycardai.oauth.pkce.authenticate (ACC-229 / #101). The duplicate in
keycardai-agents is removed.
- What stays in keycardai-agents: the CrewAI integration only, with its
imports repointed at keycardai.a2a. ACC-231 will move it to a dedicated
keycardai-crewai; ACC-232 will archive the now-stub source directory.
- BREAKING:
- from keycardai.agents import AgentServer, DelegationClient, ...
  becomes from keycardai.a2a import ... .
- from keycardai.agents.client import AgentClient is gone; use
  keycardai.oauth.pkce.authenticate.
- keycardai-agents 0.3.0 ships with the dependency set reduced to
  keycardai-a2a + pydantic, mirroring its now-CrewAI-only scope.
- * fix(keycardai-a2a): apply migration edits to moved files (ACC-230)
- The first commit on this branch did the git mv's but staged the new
files only; the Edit-tool modifications to the moved files (import
rewrites, server/app.py bearer-wiring migration to KeycardAuthBackend +
AuthenticationMiddleware, example pyproject swap from keycardai-agents
to keycardai-a2a, conftest/tests import repoints, agents/__init__.py
trim, agents/pyproject dep set, crewai integration repoint, top-level
workspace sources, justfile test recipe, uv.lock refresh) all sat
uncommitted. CI rejected the prior commit because the example pyproject
still claimed keycardai-agents at packages/a2a/, conflicting with the
real keycardai-agents at packages/agents/ in the workspace graph.
- This commit lands the actual migration content. Tests pass locally:
keycardai-a2a 60/60, keycardai-agents 16/16, no regression in oauth /
starlette / mcp / mcp-fastmcp / fastmcp; ruff workspace check clean.
- * refactor(keycardai-a2a)!: wrap a2a-sdk 1.x, drop parallel surface (ACC-230)
- Aligns keycardai-a2a with the wrap-do-not-reinvent pattern used in
keycardai-mcp and keycardai-starlette: customers implement a2a-sdk native
async AgentExecutor directly; this package contributes only Keycard auth
wiring, OAuth metadata discovery, and convenience composition.
- Drops the parallel-protocol surface inherited from the keycardai-agents
move:
- AgentExecutor protocol (sync execute(task, inputs)) and SimpleExecutor /
  LambdaExecutor implementations
- KeycardToA2AExecutorBridge (the sync->async adapter that existed only to
  bridge our protocol to a2a-sdk)
- Custom POST /invoke endpoint with bespoke InvokeRequest / InvokeResponse
  Pydantic models alongside the standard A2A JSONRPC interface
- AgentServiceConfig.invoke_url (replaced by jsonrpc_url) and
  AgentServiceConfig.to_agent_card() (the 0.x dict-shape constructor)
- Migrates from a2a-sdk 0.x to 1.x natively:
- pyproject pin a2a-sdk[http-server]>=1.0
- Server composition uses route factories (create_jsonrpc_routes,
  create_agent_card_routes) instead of the gone A2AStarletteApplication
- Request handler is DefaultRequestHandlerV2 (alias DefaultRequestHandler)
- AgentCard built from 1.x protobuf schema (supported_interfaces,
  AgentCapabilities streaming/push_notifications/extended_agent_card)
- Example main.py uses a2a-sdk 1.x Client via create_client + A2ACardResolver
- Adds a KeycardServerCallContextBuilder that subclasses a2a-sdk default
builder and stashes the verified KeycardUser plus access_token into
ServerCallContext.state so AgentExecutor implementations can read the
bearer token from context.call_context.state["access_token"] for
downstream delegated token exchange.
- Tests:
- a2a 44/44 pass
- agents 16/16 pass with crewai extra
- ruff clean
- Note: the high-level @auth.grant decorator parity with keycardai-mcp is
not yet shipped here. Customers use DelegationClient (already in this
package) for explicit server-to-server delegation. The decorator port is
a follow-up.
- * fix(keycardai-a2a): address review findings on PR #105
- Three blockers caught in fresh-eyes review:
- 1. release.yml tag-trigger list was hardcoded; *-keycardai-a2a was missing,
   so the post-merge auto-bump would push the tag but the publish workflow
   would never trigger. Trusted Publisher being registered would have been
   moot.
- 2. DelegationClient.invoke_service hardcoded service_url + /invoke. The
   wrap-aligned server only exposes /a2a/jsonrpc; calling invoke_service
   against any 1.x server returned 404. The CrewAI delegation tool runs
   through this code path. Both async and sync variants now build a
   message/send JSONRPC envelope, POST it to /a2a/jsonrpc, and unwrap the
   result to preserve the legacy {result, delegation_chain} shape so the
   CrewAI integration keeps working unchanged.
- 3. discover_service in both DelegationClient and ServiceDiscovery validated
   the 0.x card shape (required_fields = [name, endpoints, auth]). The 1.x
   protobuf-derived JSON has none of endpoints / auth. Discovery against
   any 1.x server raised ValueError. Validation now requires only "name";
   transport / auth specifics live under supportedInterfaces and the OAuth
   metadata routes.
- Plus four important findings:
- 4. Test mocks across conftest.py, test_a2a_client.py, test_discovery.py,
   and test_crewai_a2a.py used the old shape (endpoints/auth keys). Tests
   passed because the validator wrongly accepted them. Mocks now use the
   1.x JSON shape (supportedInterfaces, capabilities object, skills with
   id/name).
- 5. A2AServiceClient and A2AServiceClientSync backward-compat aliases at
   the bottom of delegation.py contradicted the "hard cut, no transitional
   bridge" stance in the PR description. Removed.
- 6. TestJsonRpcAuthGate.test_jsonrpc_requires_authorization asserted
   status_code in (400, 401). 400 means the JSONRPC dispatcher saw the
   request and bailed on the body shape, not that the auth gate caught
   it. Pinned to == 401 with a WWW-Authenticate header check so the gate
   contract is enforced.
- 7. Zero coverage existed for _KeycardServerCallContextBuilder propagating
   the verified KeycardUser plus access_token into ServerCallContext.state.
   Added two unit tests that build the context directly: one with a
   KeycardUser asserting state["access_token"] is set, one with an
   UnauthenticatedUser asserting state["access_token"] is absent (so an
   executor reading it sees None rather than a stale token).
- Tests:
- a2a 47/47 (was 44; +3 new wrap-coverage tests)
- agents 16/16 with crewai extra
- ruff clean
- * refactor(keycardai-a2a)!: ship primitives, not a server abstraction (ACC-230)
- Per Kamil's review on PR #105: AgentServer / create_agent_card_server /
serve_agent presupposed customers want a fresh Starlette app dedicated to
the agent service. The wrap-don't-reinvent stance, taken seriously,
says: customers already have an a2a-sdk app in their head; we ship
primitives that slot Keycard auth into THAT, not a parallel server.
- Public surface change:
- Dropped:
  AgentServer, create_agent_card_server, serve_agent
Promoted to public (renamed off the underscore prefix):
  EagerKeycardAuthBackend
  KeycardServerCallContextBuilder
  build_agent_card_from_config
- AgentServiceConfig trimmed: dropped agent_executor (DefaultRequestHandler
takes its own), port and host (uvicorn's job), status_url (no /status
in the primitives layer).
- The composed-server flow moves to a runnable example at
packages/a2a/examples/keycard_protected_server/. README quickstart
rewritten to show primitive composition into an existing app; greenfield
users follow the example.
- Tests:
  a2a 44/44 (was 47; net -3 from dropping the /status endpoint tests
            and the port-validation test)
  agents 16/16 with crewai extra
  ruff clean
- This change is breaking, but the package is 0.1.0-pre-publish so no
customer is on these names yet.
- * fix(keycardai-a2a): ruff import-organization auto-fix
- * refactor(keycardai-starlette,keycardai-a2a): collapse EagerKeycardAuthBackend into KeycardAuthBackend kwarg (ACC-230)
- Per Kamil's second review observation on PR #105: with keycardai-a2a
now depending on keycardai-starlette, the question of WHERE these
primitives live matters. EagerKeycardAuthBackend was a 5-line subclass
that flipped one branch of KeycardAuthBackend.authenticate to raise on
missing Authorization, with no a2a-sdk specifics. The behavior is a
policy choice ("this mount requires auth"), not a different kind of
backend.
- Collapsed to a kwarg on the existing class:
-   KeycardAuthBackend(verifier)                              # default,
                                                            # mixed-route
  KeycardAuthBackend(verifier, require_authentication=True) # all-paths-protected
- The OAuth metadata bypass (RFC 9728 §2 / RFC 8414 §3) takes precedence
over the kwarg: even with require_authentication=True, requests to
/.well-known/oauth-* and /.well-known/jwks.json still pass through
anonymously per spec. New parametrized test asserts this.
- Net effect:
- One class instead of two; existing KeycardAuthBackend(verifier) callers
  unchanged.
- keycardai-a2a no longer ships EagerKeycardAuthBackend; the kwarg is
  used directly in tests, the example, and the README quickstart.
- Migration story is zero churn for existing users; new behavior is
  opt-in via the kwarg.
- Tests:
  starlette 40 passed (+2 new tests for the kwarg semantics)
  a2a 44 passed
  agents 16 passed
  ruff clean
Changelog for keycardai-oauth:
## Unreleased


- feat(keycardai-oauth)!: pass error context through AccessContext.access(), rename to get_resource_error
- ResourceAccessError is now populated with resource, error_type
('global_error' | 'resource_error' | 'missing_token'), available_resources,
and error_details at the three throw sites in AccessContext.access(). The
constructor already accepted these args; this fills the call sites so
middleware can surface which resource failed and why.
- Also rename get_resource_errors -> get_resource_error. Plural method name
with a singular return shape was misleading.
- Companion to keycardai/typescript-sdk#19 (TS commit 317a38b).
Addresses review feedback from @jerriclynsjohn.
- BREAKING CHANGE: AccessContext.get_resource_errors renamed to
AccessContext.get_resource_error.

## 0.11.0-keycardai-oauth (2026-04-28)


- feat(keycardai-oauth): add high-level PKCE user-login flow (ACC-229) (#101)
- * feat(keycardai-oauth): add high-level PKCE flow client (ACC-229)
- First step of the keycardai-agents decomposition (ACC-229..232). Per the
revised KEP, the OAuth PKCE user-login flow is generic OAuth code with no
agents-specific concerns and belongs in keycardai-oauth next to the rest
of the OAuth client primitives.
- New module keycardai.oauth.pkce:
- - PKCEClient orchestrates the full authorization-code-with-PKCE flow:
  parse the WWW-Authenticate challenge (RFC 9728), fetch protected resource
  and authorization server metadata (RFC 8414), open the browser at the
  authorize endpoint, capture the redirect via a local callback server,
  and exchange the code at the token endpoint. Returns the token endpoint
  response dict directly.
- OAuthCallbackServer is the loopback redirect catcher (RFC 8252) used by
  PKCEClient; exported separately so callers running their own flow on top
  of the lower-level PKCEGenerator + build_authorize_url primitives can
  reuse the callback machinery.
- 7 new tests cover header parsing, discovery error paths, the happy-path
  flow, and confidential vs public client auth on the token endpoint.
- keycardai-agents changes:
- - AgentClient now composes PKCEClient instead of carrying its own copy of
  the auth flow. AgentClient.authenticate(...) is preserved as a thin shim
  that returns the access_token string and updates the per-service token
  cache, so existing /invoke retry-on-401 behavior is unchanged.
- AgentClient drops ~370 lines of duplicated PKCE/discovery/callback code.
- keycardai.agents.client.oauth re-exports OAuthCallbackServer through a
  module __getattr__ that emits a DeprecationWarning pointing at the new
  canonical import path.
- Stale tests in test_agent_client_oauth.py that exercised AgentClient
  private methods (_extract_resource_metadata_url, _fetch_resource_metadata,
  _fetch_authorization_server_metadata) removed; equivalent contracts now
  live in the keycardai-oauth PKCE test suite.
- Verified: oauth 215/215 (was 208 + 7 new), agents 81/81 (was 85 - 4 removed
implementation tests), mcp 560/560, starlette 49/49, ruff clean.
- Stacked on #100 (ACC-236 a2a-sdk pin) so the agents test suite can run
during validation.
- * fix(keycardai-oauth): address review findings on PKCE move
- Three small fixes from the review of #101:
- 1. PKCEClient now accepts an optional injected httpx.AsyncClient. AgentClient
   passes its existing http_client through, so a single connection pool covers
   both the agent service calls and the OAuth flow. close() only closes the
   client it owns. Restores the one-pool-per-AgentClient behavior from main.
- 2. Drop the no-op rstrip("/") + "/" round-trip in PKCEClient.authenticate
   when building the authorization server discovery URL.
- 3. Assert the discovery URL path in test_authenticate_completes_full_flow.
   The previous test stubbed http_mock.get with side_effects but never
   verified what URLs were passed; a typo from oauth-authorization-server
   to openid-configuration would have gone unnoticed.
- * refactor(keycardai-oauth): collapse PKCEClient into a flow function on AsyncClient
- Per Kamil review feedback (#101): a separate PKCEClient sitting next to
AsyncClient invited "which client do I use?" The OAuth-server-facing
operations belong on the existing AsyncClient.
- Changes:
- - keycardai.oauth.pkce.PKCEClient (class) -> keycardai.oauth.pkce.authenticate
  (module-level async function). One-shot per user login, no state worth
  preserving across calls.
- The function uses AsyncClient internally for server metadata discovery
  (RFC 8414) and code exchange. AsyncClient is now the only thing in
  keycardai.oauth that talks to OAuth servers as a client.
- AsyncClient.exchange_authorization_code (and Client + the underlying
  operations._authorize helpers) gain an optional resource= parameter so
  RFC 8707 tokens still work through the canonical path.
- The pkce module retains the user-flow concerns: RFC 9728 challenge
  parsing, resource metadata fetch (paired with the protected resource,
  not the OAuth server), browser launch, and the loopback callback server
  (RFC 8252).
- AgentClient drops the cached _pkce instance and just calls the function
  per /invoke retry, passing its own httpx.AsyncClient through for the
  resource metadata fetch.
- Tests rewritten for the function shape: 7/7 passing, same coverage
  (header parsing, discovery error paths, happy path with resource
  indicator, public vs confidential auth on the token endpoint).
- Verified: oauth 215/215, agents 81/81, mcp 560/560, starlette 49/49.
ruff clean.

## 0.10.0-keycardai-oauth (2026-04-24)


- fix(keycardai-oauth): fall back to legacy ./mcp_keys dir with deprecation warning
- Switch WebIdentity default storage_dir back to ./server_keys (aligning
with the protocol-agnostic naming from this PR), but transparently fall
back to ./mcp_keys when no storage_dir is passed, ./server_keys does not
exist, and ./mcp_keys does. The fallback emits a DeprecationWarning
pointing at the explicit configuration or migration paths.
- This preserves zero-config upgrades for existing keycardai-mcp services
(they keep finding their existing keys) while giving new installs the
new default. The fallback will be removed in a future release.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fix(keycardai-oauth): preserve mcp storage defaults, move server tests
- Address PR #95 review comments from cmars:
- 1. Revert WebIdentity default storage_dir to "./mcp_keys" and key_id
   prefix to "mcp-server-". Changing these would silently break existing
   keycardai-mcp services on upgrade: they would look for keys in a new
   empty directory and regenerate identity, losing their registered client
   identity with Keycard.
- 2. Move oauth-server-specific tests (test_verifier, test_cache,
   test_application_identity -> test_credentials) from packages/mcp/tests
   to packages/oauth/tests/keycardai/oauth/server/ so coverage lives
   with the canonical oauth.server modules.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fix(keycardai-oauth): address PR review findings
- - Add token_exchange module with exchange_tokens_for_resources()
  orchestration (KEP Tier 1 gap)
- Rename WebIdentity param mcp_server_name -> server_name with
  backward-compatible alias; default storage dir ./mcp_keys -> ./server_keys
- Add mcp_server_url/missing_mcp_server_url backward-compat aliases
  to AuthProviderConfigurationError (prevents breaking fastmcp callers)
- Fix _get_kid_and_algorithm returning list instead of tuple
- feat(keycardai-oauth): add server subpackage with framework-free primitives
- Extract protocol-agnostic server components from keycardai-mcp into
keycardai.oauth.server per the Protocol-Agnostic SDK KEP (Tier 1).
- New keycardai.oauth.server modules:
- access_context: AccessContext for non-throwing token access
- credentials: ApplicationCredential, ClientSecret, WebIdentity, EKSWorkloadIdentity
- verifier: TokenVerifier with local AccessToken model (no MCP dependency)
- exceptions: OAuthServerError base + all framework-free exceptions
- _cache: JWKSCache/JWKSKey for JWKS key caching
- client_factory: ClientFactory protocol + DefaultClientFactory
- private_key: PrivateKeyManager, FilePrivateKeyStorage
- keycardai-mcp changes:
- Server auth modules now re-export from keycardai.oauth.server
- MCPServerError is an alias for OAuthServerError
- MissingContextError stays MCP-specific (references FastMCP Context)
- All existing imports continue to work (no breaking changes)
- Tests updated to patch canonical module paths

## 0.9.0-keycardai-oauth (2026-04-02)


- feat(keycardai-oauth): support for impersonation token exchange
- - Add substitute-user token type and unsigned JWT builder
- Add impersonate method to Client and AsyncClient
- Add user_identifier callback to MCP grant decorator
- Add impersonation token exchange example

## 0.8.0-keycardai-oauth (2026-04-02)


- feat(keycardai-oauth): add authorization code exchange and PKCE support
- - Implement PKCE code verifier, challenge generation, and validation
- Add authorization code exchange operation (sync and async)
- Add build_authorize_url for constructing OAuth authorize URLs
- Add exchange_authorization_code to Client and AsyncClient
- Add get_endpoints/endpoints property to expose resolved endpoints
- Add id_token field to TokenResponse

## 0.7.0-keycardai-oauth (2026-03-06)


- fix(keycardai-oauth): update test to expect OAuthProtocolError for structured error bodies
- feat(keycardai-oauth)!: detailed error reporting
- BREAKING CHANGE: Token exchange HTTP 4xx errors with structured JSON bodies now raise OAuthProtocolError instead of OAuthHttpError. Callers catching OAuthHttpError for these responses must update to catch OAuthProtocolError.

## 0.6.0-keycardai-oauth (2025-11-17)


- feat(keycardai-oauth): client metadata updates

## 0.5.0-keycardai-oauth (2025-09-22)


- feat(keycardai-oauth): client assertion support
- feat(keycardai-oauth): JWKS type support

## 0.4.1-keycardai-oauth (2025-09-17)


- fix(keycardai-oauth): audience checks

## 0.4.0-keycardai-oauth (2025-09-16)


- feat(keycardai-oauth): multi-zone authentication strategy
- feat(keycardai-oauth): jwt capabilities

## 0.2.0-keycardai-oauth (2025-09-10)


- feat(keycardai-oauth): remove the impersonation logic

## 0.1.0-keycardai-oauth (2025-09-07)


- feat(keycardai-oauth): initial release
Changelog for keycardai-fastmcp:
## Unreleased


- refactor(keycardai-fastmcp)!: rename AccessContext.get_resource_error to match keycardai-oauth
- Mirrors the keycardai-oauth rename in the fastmcp provider's parallel
AccessContext implementation. Keeps the two surfaces aligned for users
who consume both directly.
- BREAKING CHANGE: AccessContext.get_resource_errors renamed to
AccessContext.get_resource_error.

## 0.2.0-keycardai-fastmcp (2026-04-27)


- feat(keycardai-fastmcp): rename keycardai-mcp-fastmcp via bridge package (ACC-233) (#102)
- * feat(keycardai-fastmcp): rename keycardai-mcp-fastmcp via bridge package (ACC-233)
- The current name carries a redundant -mcp suffix (FastMCP only speaks MCP,
so the protocol tag adds no information). Renames to keycardai-fastmcp per
the revised KEP, with keycardai-mcp-fastmcp preserved as a deprecation
bridge so the customer in production on the old name keeps working
indefinitely.
- What ships:
- * New keycardai-fastmcp package at packages/fastmcp/, full implementation
  under the keycardai.fastmcp namespace. Tests, examples, README move with
  the source. Wired into the workspace and the release.yml tag filter.
* Deprecated keycardai-mcp-fastmcp now depends only on keycardai-fastmcp
  and re-exports every public symbol at the original
  keycardai.mcp.integrations.fastmcp.* paths. Importing the top-level
  module emits a DeprecationWarning pointing at the canonical name.
* Bridge contract test (test_bridge.py, 4 tests) asserts the
  DeprecationWarning fires and that bridge symbols are identity-equal to
  the canonical ones. The full behavioral suite lives in keycardai-fastmcp
  going forward.
- Customer impact: pip install keycardai-mcp-fastmcp keeps working; the
package transitively pulls keycardai-fastmcp. No forced removal timeline,
the bridge ships until every known caller migrates.
- Verified: ruff clean. fastmcp 51/51, mcp-fastmcp bridge 4/4, mcp 560/560,
oauth 208/208, starlette 49/49.
- Supersedes the canceled ACC-195 (which used the now-rejected
keycardai-fastmcp-mcp name).
- * fix(keycardai-fastmcp): bridge re-exports the full canonical surface
- Review caught that the bridge provider.py only re-exported a hand-enumerated
subset of the canonical surface, dropping documented public symbols
(get_token_debug_info, introspect, INTROSPECT, AuthProviderConfigurationError,
AuthProviderInternalError, AuthProviderRemoteError). Importing any of those
from keycardai.mcp.integrations.fastmcp.provider raised ImportError, breaking
the bridge contract for downstream callers using less common symbols.
- Fixes:
- - Add __all__ to keycardai.fastmcp.provider listing the 28-name public
  surface. Stdlib/typing helpers (logging, os, urlparse, wraps, Any,
  Callable, etc.) are deliberately excluded.
- Replace the bridge provider.py hand-enumeration with
  ``from keycardai.fastmcp.provider import *``, plus a re-export of __all__
  so future symbol additions to the canonical module flow through
  automatically.
- Add test_bridge_provider_exposes_full_public_surface: iterates the
  canonical __all__, asserts every symbol is present at the bridge path
  and identity-equal to the canonical reference. Regression test for the
  symbol-drop class of bug.
- Scrub em dashes from the renamed example READMEs (pre-existing prose,
  but new file paths shipping under our review).
- Verified: fastmcp 51/51, mcp-fastmcp bridge 5/5 (was 4 + 1 new). Smoke:
the six previously-missing symbols now import cleanly from the old path.
- * ci: pin extractions/setup-just version
- The action stopped resolving "latest" sometime today and started failing
with `no release for just matching version specifier`. Pinning unblocks
PR validation and the post-merge bump-and-publish pipeline.
- 1.50.0 is the current stable just release (April 2026).
- * ci: replace extractions/setup-just with the upstream install script
- extractions/setup-just@v2 is currently broken for both unpinned and
explicit-version requests ("no release for just matching version
specifier"). Pinning to 1.50.0 did not help because the action regression
is in its release lookup, not its version resolution.
- Switch to the just.systems install script (the project owners ship and
maintain it). Runs as a plain bash step with no third-party action
dependency and is unaffected by setup-just regressions.
Changelog for keycardai-mcp-fastmcp:
## Unreleased

## 0.21.0-keycardai-mcp-fastmcp (2026-04-27)


- feat(keycardai-mcp-fastmcp): release deprecation bridge for keycardai-fastmcp (#103)
- Empty commit to trigger the auto-bump pipeline for keycardai-mcp-fastmcp.
- The actual bridge code (depends on keycardai-fastmcp, re-exports every
public symbol at the original keycardai.mcp.integrations.fastmcp.* paths,
emits DeprecationWarning on top-level import) shipped in #102. That PR
landed scoped (keycardai-fastmcp), so cz only bumped the new package and
keycardai-mcp-fastmcp on PyPI is still the pre-rename version. This
commit gives cz a (keycardai-mcp-fastmcp)-scoped feat to recognize so
the bridge ships as the next published version and customers on the
old name see the deprecation warning.

## 0.20.0-keycardai-mcp-fastmcp (2026-04-01)


- feat(keycardai-mcp-fastmcp): upgrade to FastMCP 3.0
- Upgrade keycardai-mcp-fastmcp from fastmcp>=2.14.0,<3.0.0 to fastmcp>=3.0.0.
- Key changes:
- ctx.get_state()/ctx.set_state() are now async (FastMCP 3.0 breaking change)
- grant decorator uses await ctx.set_state(..., serializable=False)
- All examples, docs, and tests updated for async state access
- Test mocks updated to use async functions for get_state/set_state

## 0.19.0-keycardai-mcp-fastmcp (2026-03-06)


- refactor(keycardai-mcp-fastmcp)!: optimize error formatting in token exchange chain
- Restructure error dicts to remove redundancy and improve readability.
Key renames: error->message, error_code->code, error_description->description,
resource_errors->resources. Only include raw_error for non-OAuth exceptions.
- BREAKING CHANGE: Error dict keys renamed: error->message, error_code->code, error_description->description. The get_errors() output key resource_errors is now resources.

## 0.18.1-keycardai-mcp-fastmcp (2025-11-23)


- fix(keycardai-mcp-fastmcp): include subject in debug

## 0.18.0-keycardai-mcp-fastmcp (2025-11-20)


- feat(keycardai-mcp-fastmcp): debug information for exchange

## 0.17.0-keycardai-mcp-fastmcp (2025-11-17)


- feat(keycardai-mcp-fastmcp): update oauth deps

## 0.16.0-keycardai-mcp-fastmcp (2025-11-07)


- feat(keycardai-mcp-fastmcp): enable web token eks env

## 0.15.0-keycardai-mcp-fastmcp (2025-11-06)


- feat(keycardai-mcp-fastmcp): configure mcp url via env

## 0.14.0-keycardai-mcp-fastmcp (2025-11-05)


- feat(keycardai-mcp-fastmcp): configure zone setting via env

## 0.13.0-keycardai-mcp-fastmcp (2025-11-05)


- feat(keycardai-mcp-fastmcp): automatic app cred discovery

## 0.12.0-keycardai-mcp-fastmcp (2025-10-29)


- feat(keycardai-mcp-fastmcp): support fastmcp 2.13

## 0.11.0-keycardai-mcp-fastmcp (2025-10-29)


- feat(keycardai-mcp-fastmcp): keycardai mcp dep update
- Reverts the eks workload identity changes

## 0.10.0-keycardai-mcp-fastmcp (2025-10-27)


- feat(keycardai-mcp-fastmcp): use application cred cache

## 0.9.0-keycardai-mcp-fastmcp (2025-10-20)


- feat(keycardai-mcp-fastmcp): EKS workload identity

## 0.8.1-keycardai-mcp-fastmcp (2025-10-07)


- refactor(keycardai-mcp-fastmcp): improve error message with debug context

## 0.8.0-keycardai-mcp-fastmcp (2025-10-01)


- feat(keycardai-mcp-fastmcp): ability to mock internal access context for testing

## 0.7.0-keycardai-mcp-fastmcp (2025-09-27)


- refactor(keycardai-mcp-fastmcp): remove the error codes from AccessContext

## 0.6.0-keycardai-mcp-fastmcp (2025-09-22)


- feat(keycardai-mcp-fastmcp): unify exceptions with keycardai-mcp package

## 0.5.0-keycardai-mcp-fastmcp (2025-09-21)


- feat(keycardai-mcp-fastmcp): client factory and base url update

## 0.4.1-keycardai-mcp-fastmcp (2025-09-19)


- fix(keycardai-mcp-fastmcp): lock the oauth dependency

## 0.4.0-keycardai-mcp-fastmcp (2025-09-18)


- feat(keycardai-mcp-fastmcp): refactor API for the provider

## 0.3.0-keycardai-mcp-fastmcp (2025-09-15)


- feat(keycardai-mcp-fastmcp): unify client arguments

## 0.2.0-keycardai-mcp-fastmcp (2025-09-10)


- fix(keycardai-mcp-fastmcp): pin fastmcp for compatibiity
- feat(keycardai-mcp-fastmcp): allowed to override the client

## 0.1.0-keycardai-mcp-fastmcp (2025-09-07)
Changelog for keycardai-starlette:
## Unreleased

## 0.4.0-keycardai-starlette (2026-05-01)


- refactor(keycardai-starlette)!: remove deprecated BearerAuthMiddleware shims (ACC-237) (#109)
- Removed deprecated BearerAuthMiddleware shims after all consumers migrated to AuthenticationMiddleware + KeycardAuthBackend, eliminating unused auth code. This is a breaking change—imports for BearerAuthMiddleware and verify_bearer_token now fail.

## 0.3.0-keycardai-starlette (2026-04-27)


- feat(keycardai-starlette): emit DeprecationWarning from BearerAuthMiddleware and verify_bearer_token (#99)
- Closes ACC-234. PR #97 retained the legacy bearer surface as docstring-only deprecated shims so keycardai-mcp and keycardai-agents keep working until they migrate (ACC-235, ACC-229..232). Without a runtime signal, non-MCP downstream users importing these symbols get no notice before the symbols disappear.
- Changes:
- BearerAuthMiddleware.__init__ emits DeprecationWarning pointing at AuthenticationMiddleware + KeycardAuthBackend
- verify_bearer_token emits DeprecationWarning pointing at KeycardAuthBackend
- BearerAuthMiddleware.dispatch passes _from_middleware=True so a single middleware instantiation fires exactly one warning total, not one per request
- New tests: warning fires on init, warning fires on direct verify_bearer_token call, dispatch path does not double-warn
- _create_auth_challenge_response is intentionally not warned: it is underscored, not in __all__, and not re-exported by the keycardai-mcp shims, so no external caller can plausibly hit it directly.
- Verified mcp tests still pass (560/560). Agents tests fail on a pre-existing a2a-sdk import error unrelated to this change.

## 0.2.0-keycardai-starlette (2026-04-26)


- feat(keycardai-starlette): new package for Starlette/FastAPI Keycard integration (#97)
- * feat(keycardai-starlette-oauth): new package for Starlette/FastAPI OAuth middleware
- Implements Tier 2 of the Protocol-Agnostic SDK KEP: a new
keycardai-starlette-oauth package that provides Starlette-specific
middleware and route builders without any MCP dependency.
- New package (packages/starlette-oauth/):
- middleware/bearer.py: BearerAuthMiddleware
- handlers/metadata.py: RFC 9728 + RFC 8414 metadata with local
  ProtectedResourceMetadata model (no mcp.shared.auth dependency)
- handlers/jwks.py: JWKS endpoint handler
- routers/metadata.py: Route builders + protected_router()
- provider.py: AuthProvider with install() and @protect() decorator
- shared/starlette.py: Proxy-aware URL helpers
- keycardai-mcp changes:
- Now depends on keycardai-starlette-oauth (starlette removed from
  direct deps since it comes transitively)
- Server middleware/handlers/routers replaced with re-export shims
- protected_mcp_router wraps protected_router with mcp_app kwarg compat
- All existing imports continue to work
- * refactor(keycardai-starlette): rename from keycardai-starlette-oauth
- Per revised KEP naming decisions: drop the OAuth suffix from the
customer-facing package since it will cover more than just OAuth
(token exchange, policy enforcement, vaulted creds, etc.). The
keycardai-oauth package stays as an internal building block.
- Renames:
- packages/starlette-oauth/ → packages/starlette/
- src/keycardai/starlette_oauth/ → src/keycardai/starlette/
- keycardai-starlette-oauth → keycardai-starlette (PyPI name)
- keycardai.starlette_oauth → keycardai.starlette (import path)
- Updated workspace source, MCP dependency, and all MCP shim imports.
Backward-compat shims in keycardai-mcp continue to work.
- * feat(keycardai-starlette): add smoke tests and fix .well-known middleware bypass
- - Add 22 smoke tests covering metadata routes, AuthProvider install/config,
  and a guarantee that keycardai.starlette has no keycardai.mcp imports.
- Fix BearerAuthMiddleware to skip /.well-known/* paths. Without this,
  AuthProvider.install() (which adds the middleware globally) blocked the
  OAuth discovery endpoints it had just registered — clients got 401 trying
  to learn how to authenticate. Metadata discovery per RFC 9728 §2 must
  remain publicly reachable.
- Add fastapi and httpx to the starlette package test extras.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * chore: adjust coverage thresholds after starlette extraction
- - Add keycardai-starlette to test-coverage and test recipes
- Lower mcp threshold from 65% to 60%: the well-tested server auth code
  moved to keycardai-oauth / keycardai-starlette, leaving a higher
  proportion of under-tested client integrations (CrewAI/LangChain/OpenAI
  adapters at 14-25%) in the denominator. Absolute coverage of the
  remaining code is unchanged; the ratio is what shifted.
- Set starlette threshold to 55% (smoke tests cover the surface area;
  provider.py @protect() decorator and async client init are the main
  gap, tracked as a follow-up)
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * fix(scripts): pass --yes to cz bump in version_preview for new packages
- Commitizen prompts "Is this the first tag created?" when it cannot find an
existing tag matching a package's tag_format. For brand-new packages like
keycardai-starlette that have no tag yet, this prompt EOFs in non-TTY CI
runs and causes release-preview to report an error instead of a version
delta.
- --yes auto-confirms the prompt. Existing packages with prior tags never
see the prompt, so their output is unchanged.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * chore: regenerate uv.lock with uv >= 0.9 format
- Older lock file (generated with uv 0.8.x) failed to parse on CI's newer
uv with "Dependency `pytokens` has missing `source` field but has more
than one matching package". The lock format tightened in 0.9+ to require
explicit source annotations when multiple resolution markers are in play.
- Regenerated with uv 0.11.7. Resolution now succeeds under setup-uv@v4
(unpinned, tracks latest). All package test suites still pass
(oauth 208, starlette 22, mcp 560, mcp-fastmcp 51).
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * ci: wire keycardai-starlette into release workflow tag filter
- The release workflow only triggers on tag patterns explicitly listed in
on.push.tags. Without adding *-keycardai-starlette, tags created by
commitizen for the new package (e.g. 0.1.0-keycardai-starlette) would
not trigger the release job, so nothing would publish to PyPI even if a
Trusted Publisher were configured.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * chore: minimize uv.lock diff to just the keycardai-starlette addition
- The previous regeneration pass rebuilt the lock wholesale and produced
a 5-marker resolution format (splitting python_full_version >= '3.14'
into '3.15' and '3.14.*'). CI's uv 0.11.7 could not parse that,
failing with "pytokens has missing source field but has more than one
matching package" during uv sync --all-extras.
- Revert to origin/main's lock and re-run `uv lock --no-upgrade`, which
adds only the keycardai-starlette workspace member (34-line diff) and
leaves the resolution-markers block identical to main. CI parses it
cleanly; all package test suites pass.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * style: trim verbose comments added during review
- Condense the justfile coverage-threshold note and version_preview.py
--yes flag comment to one sentence each.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * fix(keycardai-starlette): address PR review feedback from cmars
- Seven correctness and style fixes:
- 1. bearer.py: tighten the auth-bypass path match. The previous
   `path.startswith("/.well-known/")` exempted ALL well-known URIs (e.g.
   `/.well-known/change-password`, `assetlinks.json`) from bearer auth.
   Replace with an explicit allowlist of OAuth metadata endpoints
   (`oauth-protected-resource`, `oauth-authorization-server`, `jwks.json`),
   matched as exact paths or delimited subpaths. Cite RFC 9728 §2 / RFC
   8414 §3 as the spec basis.
- 2. provider.py `_get_or_create_client`: the parameter was annotated
   `dict[str, str] | None = None` but every line dereferenced it
   unguarded. Drop the Optional from the signature; callers always pass
   a non-None dict.
- 3. provider.py `__init__`: construct `_init_lock = asyncio.Lock()`
   eagerly instead of lazily. The previous `if self._init_lock is None:
   self._init_lock = asyncio.Lock()` was technically safe in pure
   asyncio (no await between check and assign) but reads as a race
   smell. Eager init removes the question. asyncio.Lock can be created
   outside an event loop in Python 3.10+.
- 4. provider.py docstring: rephrase the AuthProvider class docstring to
   describe what the class does instead of what it lacks ("without any
   MCP dependency").
- 5. handlers/metadata.py `protected_resource_metadata`: return
   `JSONResponse(content=dict)` instead of `Response(content=json_string)`.
   The previous implementation served `Content-Type: text/plain`.
- 6. handlers/metadata.py `authorization_server_metadata`: pass an explicit
   `timeout=httpx.Timeout(5.0)` to `httpx.Client` so a slow upstream
   cannot pin a Starlette threadpool worker indefinitely. Switch the
   error responses to JSONResponse for the same Content-Type reason.
- 7. shared/starlette.py `get_base_url`: guard against `None` port. When
   `request_base_url.port` is None (proxy stripped it, missing from
   pydantic parsing), the previous code interpolated `:None` into the
   URL string. Now treat None like the default ports (omit).
- Adds regression tests:
- `/.well-known/change-password` returns 401 (path-specific bypass)
- `/.well-known/oauth-protected-resource/zone-id/path` returns 200
- `_init_lock` is an asyncio.Lock after `__init__`
- `Content-Type` is `application/json` on the metadata response
- `httpx.Client` is constructed with an explicit `timeout=` kwarg
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * refactor(keycardai-starlette): make install() per-route opt-in instead of whole-app lockdown
- The previous install() shape added BearerAuthMiddleware globally so every
route in the FastAPI/Starlette app required a bearer token. A /health or
/version endpoint returned 401, which contradicts the framing in the
Protect Any API guide ("an API that knows which agent is calling") and the
existing per-subtree code patterns the docs already show
(BearerAuthMiddleware on a Mount, protected_mcp_router(...)).
- After this change:
- install(app) adds OAuth metadata routes only (.well-known/oauth-*).
  No global middleware. Routes are public by default.
- @auth.protect() (no args) verifies the bearer token, returns 401 on
  missing/invalid. No delegation, no AccessContext required.
- @auth.protect("resource") verifies + runs delegated token exchange and
  populates an AccessContext as before.
- protected_router() is unchanged. Still the right pattern for protecting
  a whole subtree (MCP transport, internal admin app, etc.).
- Implementation:
- Extract the verification body of BearerAuthMiddleware.dispatch() into a
  free verify_bearer_token(request, verifier) helper that returns either an
  auth_info dict on success or an RFC 6750 challenge Response on failure.
  Both the middleware and the decorator call it.
- The decorator reuses request.state.keycardai_auth_info if the middleware
  already populated it (e.g. inside a protected_router() mount), otherwise
  calls verify_bearer_token itself and returns the 401 directly on failure.
- AccessContext lookup and injection only run when resources is set.
- Test changes:
- Removed test_install_rejects_requests_without_bearer_token (old contract).
- Removed test_install_does_not_bypass_unrelated_well_known_paths (without
  global middleware, /.well-known/change-password is now a 404, which the
  framework provides; nothing for us to assert here).
- Added test_install_does_not_block_unprotected_routes: /health stays 200.
- Added test_install_does_not_add_global_middleware: BearerAuthMiddleware
  is NOT in app.user_middleware after install().
- Added TestProtectDecorator class:
  - no-args form returns 401 without bearer
  - resource form returns 401 without bearer
  - no-args form does not require AccessContext on the function signature
  - decorator reuses request.state when middleware preset it (verify_token
    asserts if called)
- README and module docstrings rewritten to show the new model with three
distinct patterns (decorator no-args, decorator with resource, protected_router).
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * style(keycardai-starlette): drop temporal/historical comments and tighten test names
- The previous refactor commit shipped a few comments framed against the
prior code shape ("Reuse middleware-set auth info if BearerAuthMiddleware
ran ... otherwise verify the bearer token here") and a couple of
section-header style comments restating what the code does. Drop them.
Move the "two-call-sites" framing out of the verify_bearer_token
docstring; describe the present contract.
- Rename test_install_does_not_add_global_middleware to
test_install_leaves_user_middleware_stack_empty and
test_install_does_not_block_unprotected_routes to
test_routes_without_protect_decorator_stay_public for clearer positive
framing.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * Kamil/starlette auth model (#98)
- * align keycardai-starlette with starlette authentication framework
- * add protected_resource_server example for keycardai-starlette
- * prevent transitive load_dotenv from polluting mcp test environment
- * fix(lint): resolve ruff B026 and I001 errors after merging #98
- Three errors flagged by `just check` after the #98 merge:
- - packages/mcp/tests/conftest.py: B026 star-arg unpacking after keyword
  argument. Forward dotenv_path/stream positionally to the real load_dotenv.
- packages/starlette/src/keycardai/starlette/authorization.py: I001 import
  ordering (auto-fixed).
- packages/starlette/src/keycardai/starlette/provider.py: I001 import
  ordering (auto-fixed).
- All test suites still pass: starlette 42, mcp 560, oauth 208, mcp-fastmcp 51.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * refactor(keycardai-starlette): tighten review findings before merge
- - mcp.server.routers re-exports the protected_mcp_router wrapper so the
  mcp_app= kwarg keeps working through the package-level import
- consolidate the RFC 6750 challenge response into one helper shared by
  keycard_on_error and the @requires/@auth.grant decorators
- drop KeycardUser.resource_client_id (was always equal to
  resource_server_url); grant.wrapper reads resource_server_url for both
  auth_info dict keys
- type _get_or_create_client auth_info as dict[str, str | None] so
  zone_id is no longer mistyped as str
- replace test that asserted staticmethod identity with regression tests
  for the well-known bypass: OAuth metadata paths short-circuit, sibling
  paths (change-password, security.txt, oauth-protected-resource-fake,
  openid-configuration) still raise KeycardAuthError
- rewrite test_no_auth_header_returns_none to call the backend directly
  instead of building a FastAPI app and patching middleware kwargs
- ---------
- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Kamil <kamil@keycard.ai>

This comment was automatically generated by the release preview workflow.

Four changes based on code review of the access() rename / rich-error PR:

1. available_resources semantics: for global_error and resource_error,
   details["available_resources"] is now None instead of []. The empty-list
   default implied "zero resources available" rather than "field not applicable".
   Only the missing_token path should carry a list.

2. README sweep for get_resource_error rename: four docs files still called
   get_resource_errors (plural) after the rename — added to PR #114 scope to
   prevent AttributeError for any user following those examples.

3. Test assertion fixes: resource_error and global_error tests now assert
   available_resources is None. missing_token test uses set comparison to
   avoid fragility when token registration order varies.

4. fastmcp unit tests: AccessContext.access() in provider.py is an
   independent implementation with the same three error paths. Add
   tests/test_access_context.py mirroring the oauth test file.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

📦 Release Preview

This analysis shows the expected release impact:

📈 Expected Version Changes

keycardai-oauth: 0.11.0 → 0.12.0 (MINOR)
keycardai-fastmcp: 0.2.0 → 0.3.0 (MINOR)

📋 Package Details

[
  {
    "package_name": "keycardai-oauth",
    "package_dir": "packages/oauth",
    "has_changes": true,
    "current_version": "0.11.0",
    "next_version": "0.12.0",
    "increment": "MINOR"
  },
  {
    "package_name": "keycardai-fastmcp",
    "package_dir": "packages/fastmcp",
    "has_changes": true,
    "current_version": "0.2.0",
    "next_version": "0.3.0",
    "increment": "MINOR"
  }
]

📝 Changelog Preview

Changelog for keycardai:
## Unreleased

## 0.2.0-keycardai (2025-09-10)

## 0.1.0-keycardai (2025-09-07)


- feat(keycardai): initial release
Changelog for keycardai-oauth:
## Unreleased


- fix(keycardai-oauth): address review issues on PR #114
- Four changes based on code review of the access() rename / rich-error PR:
- 1. available_resources semantics: for global_error and resource_error,
   details["available_resources"] is now None instead of []. The empty-list
   default implied "zero resources available" rather than "field not applicable".
   Only the missing_token path should carry a list.
- 2. README sweep for get_resource_error rename: four docs files still called
   get_resource_errors (plural) after the rename — added to PR #114 scope to
   prevent AttributeError for any user following those examples.
- 3. Test assertion fixes: resource_error and global_error tests now assert
   available_resources is None. missing_token test uses set comparison to
   avoid fragility when token registration order varies.
- 4. fastmcp unit tests: AccessContext.access() in provider.py is an
   independent implementation with the same three error paths. Add
   tests/test_access_context.py mirroring the oauth test file.
- feat(keycardai-oauth)!: pass error context through AccessContext.access(), rename to get_resource_error
- ResourceAccessError is now populated with resource, error_type
('global_error' | 'resource_error' | 'missing_token'), available_resources,
and error_details at the three throw sites in AccessContext.access(). The
constructor already accepted these args; this fills the call sites so
middleware can surface which resource failed and why.
- Also rename get_resource_errors -> get_resource_error. Plural method name
with a singular return shape was misleading.
- Companion to keycardai/typescript-sdk#19 (TS commit 317a38b).
Addresses review feedback from @jerriclynsjohn.
- BREAKING CHANGE: AccessContext.get_resource_errors renamed to
AccessContext.get_resource_error.

## 0.11.0-keycardai-oauth (2026-04-28)


- feat(keycardai-oauth): add high-level PKCE user-login flow (ACC-229) (#101)
- * feat(keycardai-oauth): add high-level PKCE flow client (ACC-229)
- First step of the keycardai-agents decomposition (ACC-229..232). Per the
revised KEP, the OAuth PKCE user-login flow is generic OAuth code with no
agents-specific concerns and belongs in keycardai-oauth next to the rest
of the OAuth client primitives.
- New module keycardai.oauth.pkce:
- - PKCEClient orchestrates the full authorization-code-with-PKCE flow:
  parse the WWW-Authenticate challenge (RFC 9728), fetch protected resource
  and authorization server metadata (RFC 8414), open the browser at the
  authorize endpoint, capture the redirect via a local callback server,
  and exchange the code at the token endpoint. Returns the token endpoint
  response dict directly.
- OAuthCallbackServer is the loopback redirect catcher (RFC 8252) used by
  PKCEClient; exported separately so callers running their own flow on top
  of the lower-level PKCEGenerator + build_authorize_url primitives can
  reuse the callback machinery.
- 7 new tests cover header parsing, discovery error paths, the happy-path
  flow, and confidential vs public client auth on the token endpoint.
- keycardai-agents changes:
- - AgentClient now composes PKCEClient instead of carrying its own copy of
  the auth flow. AgentClient.authenticate(...) is preserved as a thin shim
  that returns the access_token string and updates the per-service token
  cache, so existing /invoke retry-on-401 behavior is unchanged.
- AgentClient drops ~370 lines of duplicated PKCE/discovery/callback code.
- keycardai.agents.client.oauth re-exports OAuthCallbackServer through a
  module __getattr__ that emits a DeprecationWarning pointing at the new
  canonical import path.
- Stale tests in test_agent_client_oauth.py that exercised AgentClient
  private methods (_extract_resource_metadata_url, _fetch_resource_metadata,
  _fetch_authorization_server_metadata) removed; equivalent contracts now
  live in the keycardai-oauth PKCE test suite.
- Verified: oauth 215/215 (was 208 + 7 new), agents 81/81 (was 85 - 4 removed
implementation tests), mcp 560/560, starlette 49/49, ruff clean.
- Stacked on #100 (ACC-236 a2a-sdk pin) so the agents test suite can run
during validation.
- * fix(keycardai-oauth): address review findings on PKCE move
- Three small fixes from the review of #101:
- 1. PKCEClient now accepts an optional injected httpx.AsyncClient. AgentClient
   passes its existing http_client through, so a single connection pool covers
   both the agent service calls and the OAuth flow. close() only closes the
   client it owns. Restores the one-pool-per-AgentClient behavior from main.
- 2. Drop the no-op rstrip("/") + "/" round-trip in PKCEClient.authenticate
   when building the authorization server discovery URL.
- 3. Assert the discovery URL path in test_authenticate_completes_full_flow.
   The previous test stubbed http_mock.get with side_effects but never
   verified what URLs were passed; a typo from oauth-authorization-server
   to openid-configuration would have gone unnoticed.
- * refactor(keycardai-oauth): collapse PKCEClient into a flow function on AsyncClient
- Per Kamil review feedback (#101): a separate PKCEClient sitting next to
AsyncClient invited "which client do I use?" The OAuth-server-facing
operations belong on the existing AsyncClient.
- Changes:
- - keycardai.oauth.pkce.PKCEClient (class) -> keycardai.oauth.pkce.authenticate
  (module-level async function). One-shot per user login, no state worth
  preserving across calls.
- The function uses AsyncClient internally for server metadata discovery
  (RFC 8414) and code exchange. AsyncClient is now the only thing in
  keycardai.oauth that talks to OAuth servers as a client.
- AsyncClient.exchange_authorization_code (and Client + the underlying
  operations._authorize helpers) gain an optional resource= parameter so
  RFC 8707 tokens still work through the canonical path.
- The pkce module retains the user-flow concerns: RFC 9728 challenge
  parsing, resource metadata fetch (paired with the protected resource,
  not the OAuth server), browser launch, and the loopback callback server
  (RFC 8252).
- AgentClient drops the cached _pkce instance and just calls the function
  per /invoke retry, passing its own httpx.AsyncClient through for the
  resource metadata fetch.
- Tests rewritten for the function shape: 7/7 passing, same coverage
  (header parsing, discovery error paths, happy path with resource
  indicator, public vs confidential auth on the token endpoint).
- Verified: oauth 215/215, agents 81/81, mcp 560/560, starlette 49/49.
ruff clean.

## 0.10.0-keycardai-oauth (2026-04-24)


- fix(keycardai-oauth): fall back to legacy ./mcp_keys dir with deprecation warning
- Switch WebIdentity default storage_dir back to ./server_keys (aligning
with the protocol-agnostic naming from this PR), but transparently fall
back to ./mcp_keys when no storage_dir is passed, ./server_keys does not
exist, and ./mcp_keys does. The fallback emits a DeprecationWarning
pointing at the explicit configuration or migration paths.
- This preserves zero-config upgrades for existing keycardai-mcp services
(they keep finding their existing keys) while giving new installs the
new default. The fallback will be removed in a future release.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fix(keycardai-oauth): preserve mcp storage defaults, move server tests
- Address PR #95 review comments from cmars:
- 1. Revert WebIdentity default storage_dir to "./mcp_keys" and key_id
   prefix to "mcp-server-". Changing these would silently break existing
   keycardai-mcp services on upgrade: they would look for keys in a new
   empty directory and regenerate identity, losing their registered client
   identity with Keycard.
- 2. Move oauth-server-specific tests (test_verifier, test_cache,
   test_application_identity -> test_credentials) from packages/mcp/tests
   to packages/oauth/tests/keycardai/oauth/server/ so coverage lives
   with the canonical oauth.server modules.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fix(keycardai-oauth): address PR review findings
- - Add token_exchange module with exchange_tokens_for_resources()
  orchestration (KEP Tier 1 gap)
- Rename WebIdentity param mcp_server_name -> server_name with
  backward-compatible alias; default storage dir ./mcp_keys -> ./server_keys
- Add mcp_server_url/missing_mcp_server_url backward-compat aliases
  to AuthProviderConfigurationError (prevents breaking fastmcp callers)
- Fix _get_kid_and_algorithm returning list instead of tuple
- feat(keycardai-oauth): add server subpackage with framework-free primitives
- Extract protocol-agnostic server components from keycardai-mcp into
keycardai.oauth.server per the Protocol-Agnostic SDK KEP (Tier 1).
- New keycardai.oauth.server modules:
- access_context: AccessContext for non-throwing token access
- credentials: ApplicationCredential, ClientSecret, WebIdentity, EKSWorkloadIdentity
- verifier: TokenVerifier with local AccessToken model (no MCP dependency)
- exceptions: OAuthServerError base + all framework-free exceptions
- _cache: JWKSCache/JWKSKey for JWKS key caching
- client_factory: ClientFactory protocol + DefaultClientFactory
- private_key: PrivateKeyManager, FilePrivateKeyStorage
- keycardai-mcp changes:
- Server auth modules now re-export from keycardai.oauth.server
- MCPServerError is an alias for OAuthServerError
- MissingContextError stays MCP-specific (references FastMCP Context)
- All existing imports continue to work (no breaking changes)
- Tests updated to patch canonical module paths

## 0.9.0-keycardai-oauth (2026-04-02)


- feat(keycardai-oauth): support for impersonation token exchange
- - Add substitute-user token type and unsigned JWT builder
- Add impersonate method to Client and AsyncClient
- Add user_identifier callback to MCP grant decorator
- Add impersonation token exchange example

## 0.8.0-keycardai-oauth (2026-04-02)


- feat(keycardai-oauth): add authorization code exchange and PKCE support
- - Implement PKCE code verifier, challenge generation, and validation
- Add authorization code exchange operation (sync and async)
- Add build_authorize_url for constructing OAuth authorize URLs
- Add exchange_authorization_code to Client and AsyncClient
- Add get_endpoints/endpoints property to expose resolved endpoints
- Add id_token field to TokenResponse

## 0.7.0-keycardai-oauth (2026-03-06)


- fix(keycardai-oauth): update test to expect OAuthProtocolError for structured error bodies
- feat(keycardai-oauth)!: detailed error reporting
- BREAKING CHANGE: Token exchange HTTP 4xx errors with structured JSON bodies now raise OAuthProtocolError instead of OAuthHttpError. Callers catching OAuthHttpError for these responses must update to catch OAuthProtocolError.

## 0.6.0-keycardai-oauth (2025-11-17)


- feat(keycardai-oauth): client metadata updates

## 0.5.0-keycardai-oauth (2025-09-22)


- feat(keycardai-oauth): client assertion support
- feat(keycardai-oauth): JWKS type support

## 0.4.1-keycardai-oauth (2025-09-17)


- fix(keycardai-oauth): audience checks

## 0.4.0-keycardai-oauth (2025-09-16)


- feat(keycardai-oauth): multi-zone authentication strategy
- feat(keycardai-oauth): jwt capabilities

## 0.2.0-keycardai-oauth (2025-09-10)


- feat(keycardai-oauth): remove the impersonation logic

## 0.1.0-keycardai-oauth (2025-09-07)


- feat(keycardai-oauth): initial release
Changelog for keycardai-starlette:
## Unreleased

## 0.4.0-keycardai-starlette (2026-05-01)


- refactor(keycardai-starlette)!: remove deprecated BearerAuthMiddleware shims (ACC-237) (#109)
- Removed deprecated BearerAuthMiddleware shims after all consumers migrated to AuthenticationMiddleware + KeycardAuthBackend, eliminating unused auth code. This is a breaking change—imports for BearerAuthMiddleware and verify_bearer_token now fail.

## 0.3.0-keycardai-starlette (2026-04-27)


- feat(keycardai-starlette): emit DeprecationWarning from BearerAuthMiddleware and verify_bearer_token (#99)
- Closes ACC-234. PR #97 retained the legacy bearer surface as docstring-only deprecated shims so keycardai-mcp and keycardai-agents keep working until they migrate (ACC-235, ACC-229..232). Without a runtime signal, non-MCP downstream users importing these symbols get no notice before the symbols disappear.
- Changes:
- BearerAuthMiddleware.__init__ emits DeprecationWarning pointing at AuthenticationMiddleware + KeycardAuthBackend
- verify_bearer_token emits DeprecationWarning pointing at KeycardAuthBackend
- BearerAuthMiddleware.dispatch passes _from_middleware=True so a single middleware instantiation fires exactly one warning total, not one per request
- New tests: warning fires on init, warning fires on direct verify_bearer_token call, dispatch path does not double-warn
- _create_auth_challenge_response is intentionally not warned: it is underscored, not in __all__, and not re-exported by the keycardai-mcp shims, so no external caller can plausibly hit it directly.
- Verified mcp tests still pass (560/560). Agents tests fail on a pre-existing a2a-sdk import error unrelated to this change.

## 0.2.0-keycardai-starlette (2026-04-26)


- feat(keycardai-starlette): new package for Starlette/FastAPI Keycard integration (#97)
- * feat(keycardai-starlette-oauth): new package for Starlette/FastAPI OAuth middleware
- Implements Tier 2 of the Protocol-Agnostic SDK KEP: a new
keycardai-starlette-oauth package that provides Starlette-specific
middleware and route builders without any MCP dependency.
- New package (packages/starlette-oauth/):
- middleware/bearer.py: BearerAuthMiddleware
- handlers/metadata.py: RFC 9728 + RFC 8414 metadata with local
  ProtectedResourceMetadata model (no mcp.shared.auth dependency)
- handlers/jwks.py: JWKS endpoint handler
- routers/metadata.py: Route builders + protected_router()
- provider.py: AuthProvider with install() and @protect() decorator
- shared/starlette.py: Proxy-aware URL helpers
- keycardai-mcp changes:
- Now depends on keycardai-starlette-oauth (starlette removed from
  direct deps since it comes transitively)
- Server middleware/handlers/routers replaced with re-export shims
- protected_mcp_router wraps protected_router with mcp_app kwarg compat
- All existing imports continue to work
- * refactor(keycardai-starlette): rename from keycardai-starlette-oauth
- Per revised KEP naming decisions: drop the OAuth suffix from the
customer-facing package since it will cover more than just OAuth
(token exchange, policy enforcement, vaulted creds, etc.). The
keycardai-oauth package stays as an internal building block.
- Renames:
- packages/starlette-oauth/ → packages/starlette/
- src/keycardai/starlette_oauth/ → src/keycardai/starlette/
- keycardai-starlette-oauth → keycardai-starlette (PyPI name)
- keycardai.starlette_oauth → keycardai.starlette (import path)
- Updated workspace source, MCP dependency, and all MCP shim imports.
Backward-compat shims in keycardai-mcp continue to work.
- * feat(keycardai-starlette): add smoke tests and fix .well-known middleware bypass
- - Add 22 smoke tests covering metadata routes, AuthProvider install/config,
  and a guarantee that keycardai.starlette has no keycardai.mcp imports.
- Fix BearerAuthMiddleware to skip /.well-known/* paths. Without this,
  AuthProvider.install() (which adds the middleware globally) blocked the
  OAuth discovery endpoints it had just registered — clients got 401 trying
  to learn how to authenticate. Metadata discovery per RFC 9728 §2 must
  remain publicly reachable.
- Add fastapi and httpx to the starlette package test extras.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * chore: adjust coverage thresholds after starlette extraction
- - Add keycardai-starlette to test-coverage and test recipes
- Lower mcp threshold from 65% to 60%: the well-tested server auth code
  moved to keycardai-oauth / keycardai-starlette, leaving a higher
  proportion of under-tested client integrations (CrewAI/LangChain/OpenAI
  adapters at 14-25%) in the denominator. Absolute coverage of the
  remaining code is unchanged; the ratio is what shifted.
- Set starlette threshold to 55% (smoke tests cover the surface area;
  provider.py @protect() decorator and async client init are the main
  gap, tracked as a follow-up)
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * fix(scripts): pass --yes to cz bump in version_preview for new packages
- Commitizen prompts "Is this the first tag created?" when it cannot find an
existing tag matching a package's tag_format. For brand-new packages like
keycardai-starlette that have no tag yet, this prompt EOFs in non-TTY CI
runs and causes release-preview to report an error instead of a version
delta.
- --yes auto-confirms the prompt. Existing packages with prior tags never
see the prompt, so their output is unchanged.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * chore: regenerate uv.lock with uv >= 0.9 format
- Older lock file (generated with uv 0.8.x) failed to parse on CI's newer
uv with "Dependency `pytokens` has missing `source` field but has more
than one matching package". The lock format tightened in 0.9+ to require
explicit source annotations when multiple resolution markers are in play.
- Regenerated with uv 0.11.7. Resolution now succeeds under setup-uv@v4
(unpinned, tracks latest). All package test suites still pass
(oauth 208, starlette 22, mcp 560, mcp-fastmcp 51).
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * ci: wire keycardai-starlette into release workflow tag filter
- The release workflow only triggers on tag patterns explicitly listed in
on.push.tags. Without adding *-keycardai-starlette, tags created by
commitizen for the new package (e.g. 0.1.0-keycardai-starlette) would
not trigger the release job, so nothing would publish to PyPI even if a
Trusted Publisher were configured.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * chore: minimize uv.lock diff to just the keycardai-starlette addition
- The previous regeneration pass rebuilt the lock wholesale and produced
a 5-marker resolution format (splitting python_full_version >= '3.14'
into '3.15' and '3.14.*'). CI's uv 0.11.7 could not parse that,
failing with "pytokens has missing source field but has more than one
matching package" during uv sync --all-extras.
- Revert to origin/main's lock and re-run `uv lock --no-upgrade`, which
adds only the keycardai-starlette workspace member (34-line diff) and
leaves the resolution-markers block identical to main. CI parses it
cleanly; all package test suites pass.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * style: trim verbose comments added during review
- Condense the justfile coverage-threshold note and version_preview.py
--yes flag comment to one sentence each.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * fix(keycardai-starlette): address PR review feedback from cmars
- Seven correctness and style fixes:
- 1. bearer.py: tighten the auth-bypass path match. The previous
   `path.startswith("/.well-known/")` exempted ALL well-known URIs (e.g.
   `/.well-known/change-password`, `assetlinks.json`) from bearer auth.
   Replace with an explicit allowlist of OAuth metadata endpoints
   (`oauth-protected-resource`, `oauth-authorization-server`, `jwks.json`),
   matched as exact paths or delimited subpaths. Cite RFC 9728 §2 / RFC
   8414 §3 as the spec basis.
- 2. provider.py `_get_or_create_client`: the parameter was annotated
   `dict[str, str] | None = None` but every line dereferenced it
   unguarded. Drop the Optional from the signature; callers always pass
   a non-None dict.
- 3. provider.py `__init__`: construct `_init_lock = asyncio.Lock()`
   eagerly instead of lazily. The previous `if self._init_lock is None:
   self._init_lock = asyncio.Lock()` was technically safe in pure
   asyncio (no await between check and assign) but reads as a race
   smell. Eager init removes the question. asyncio.Lock can be created
   outside an event loop in Python 3.10+.
- 4. provider.py docstring: rephrase the AuthProvider class docstring to
   describe what the class does instead of what it lacks ("without any
   MCP dependency").
- 5. handlers/metadata.py `protected_resource_metadata`: return
   `JSONResponse(content=dict)` instead of `Response(content=json_string)`.
   The previous implementation served `Content-Type: text/plain`.
- 6. handlers/metadata.py `authorization_server_metadata`: pass an explicit
   `timeout=httpx.Timeout(5.0)` to `httpx.Client` so a slow upstream
   cannot pin a Starlette threadpool worker indefinitely. Switch the
   error responses to JSONResponse for the same Content-Type reason.
- 7. shared/starlette.py `get_base_url`: guard against `None` port. When
   `request_base_url.port` is None (proxy stripped it, missing from
   pydantic parsing), the previous code interpolated `:None` into the
   URL string. Now treat None like the default ports (omit).
- Adds regression tests:
- `/.well-known/change-password` returns 401 (path-specific bypass)
- `/.well-known/oauth-protected-resource/zone-id/path` returns 200
- `_init_lock` is an asyncio.Lock after `__init__`
- `Content-Type` is `application/json` on the metadata response
- `httpx.Client` is constructed with an explicit `timeout=` kwarg
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * refactor(keycardai-starlette): make install() per-route opt-in instead of whole-app lockdown
- The previous install() shape added BearerAuthMiddleware globally so every
route in the FastAPI/Starlette app required a bearer token. A /health or
/version endpoint returned 401, which contradicts the framing in the
Protect Any API guide ("an API that knows which agent is calling") and the
existing per-subtree code patterns the docs already show
(BearerAuthMiddleware on a Mount, protected_mcp_router(...)).
- After this change:
- install(app) adds OAuth metadata routes only (.well-known/oauth-*).
  No global middleware. Routes are public by default.
- @auth.protect() (no args) verifies the bearer token, returns 401 on
  missing/invalid. No delegation, no AccessContext required.
- @auth.protect("resource") verifies + runs delegated token exchange and
  populates an AccessContext as before.
- protected_router() is unchanged. Still the right pattern for protecting
  a whole subtree (MCP transport, internal admin app, etc.).
- Implementation:
- Extract the verification body of BearerAuthMiddleware.dispatch() into a
  free verify_bearer_token(request, verifier) helper that returns either an
  auth_info dict on success or an RFC 6750 challenge Response on failure.
  Both the middleware and the decorator call it.
- The decorator reuses request.state.keycardai_auth_info if the middleware
  already populated it (e.g. inside a protected_router() mount), otherwise
  calls verify_bearer_token itself and returns the 401 directly on failure.
- AccessContext lookup and injection only run when resources is set.
- Test changes:
- Removed test_install_rejects_requests_without_bearer_token (old contract).
- Removed test_install_does_not_bypass_unrelated_well_known_paths (without
  global middleware, /.well-known/change-password is now a 404, which the
  framework provides; nothing for us to assert here).
- Added test_install_does_not_block_unprotected_routes: /health stays 200.
- Added test_install_does_not_add_global_middleware: BearerAuthMiddleware
  is NOT in app.user_middleware after install().
- Added TestProtectDecorator class:
  - no-args form returns 401 without bearer
  - resource form returns 401 without bearer
  - no-args form does not require AccessContext on the function signature
  - decorator reuses request.state when middleware preset it (verify_token
    asserts if called)
- README and module docstrings rewritten to show the new model with three
distinct patterns (decorator no-args, decorator with resource, protected_router).
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * style(keycardai-starlette): drop temporal/historical comments and tighten test names
- The previous refactor commit shipped a few comments framed against the
prior code shape ("Reuse middleware-set auth info if BearerAuthMiddleware
ran ... otherwise verify the bearer token here") and a couple of
section-header style comments restating what the code does. Drop them.
Move the "two-call-sites" framing out of the verify_bearer_token
docstring; describe the present contract.
- Rename test_install_does_not_add_global_middleware to
test_install_leaves_user_middleware_stack_empty and
test_install_does_not_block_unprotected_routes to
test_routes_without_protect_decorator_stay_public for clearer positive
framing.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * Kamil/starlette auth model (#98)
- * align keycardai-starlette with starlette authentication framework
- * add protected_resource_server example for keycardai-starlette
- * prevent transitive load_dotenv from polluting mcp test environment
- * fix(lint): resolve ruff B026 and I001 errors after merging #98
- Three errors flagged by `just check` after the #98 merge:
- - packages/mcp/tests/conftest.py: B026 star-arg unpacking after keyword
  argument. Forward dotenv_path/stream positionally to the real load_dotenv.
- packages/starlette/src/keycardai/starlette/authorization.py: I001 import
  ordering (auto-fixed).
- packages/starlette/src/keycardai/starlette/provider.py: I001 import
  ordering (auto-fixed).
- All test suites still pass: starlette 42, mcp 560, oauth 208, mcp-fastmcp 51.
- Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- * refactor(keycardai-starlette): tighten review findings before merge
- - mcp.server.routers re-exports the protected_mcp_router wrapper so the
  mcp_app= kwarg keeps working through the package-level import
- consolidate the RFC 6750 challenge response into one helper shared by
  keycard_on_error and the @requires/@auth.grant decorators
- drop KeycardUser.resource_client_id (was always equal to
  resource_server_url); grant.wrapper reads resource_server_url for both
  auth_info dict keys
- type _get_or_create_client auth_info as dict[str, str | None] so
  zone_id is no longer mistyped as str
- replace test that asserted staticmethod identity with regression tests
  for the well-known bypass: OAuth metadata paths short-circuit, sibling
  paths (change-password, security.txt, oauth-protected-resource-fake,
  openid-configuration) still raise KeycardAuthError
- rewrite test_no_auth_header_returns_none to call the backend directly
  instead of building a FastAPI app and patching middleware kwargs
- ---------
- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Kamil <kamil@keycard.ai>
Changelog for keycardai-mcp:
## Unreleased

## 0.23.0-keycardai-mcp (2026-04-28)


- refactor(keycardai-mcp): drop deprecated bearer middleware shims (ACC-235) (#104)
- Removes the keycardai.mcp.server.middleware re-export shims that pointed
at the deprecated BearerAuthMiddleware in keycardai-starlette. Anyone
importing BearerAuthMiddleware should switch to AuthenticationMiddleware
with backend=KeycardAuthBackend(verifier) and on_error=keycard_on_error.
The deprecated symbols themselves stay in keycardai-starlette and come
out in ACC-237.
- keycardai-agents repointed at keycardai.starlette.middleware.bearer so
it keeps building. It still emits the DeprecationWarning shipped in
keycardai-starlette 0.3.0; that goes away when ACC-232 archives the
package.
- Bearer-helper unit tests (_get_bearer_token, _get_oauth_protected_resource_url)
moved from packages/mcp/tests to packages/starlette/tests where the
helpers live.

## 0.22.0-keycardai-mcp (2026-04-24)


- fix(keycardai-mcp): resolve ruff lint errors in provider and test imports

## 0.21.0-keycardai-mcp (2026-03-06)


- build(keycardai-mcp): bump keycardai-oauth dependency to >=0.7.0
- refactor(keycardai-mcp)!: optimize error formatting in token exchange chain
- Restructure error dicts to remove redundancy and improve readability.
Key renames: error->message, error_code->code, error_description->description,
resource_errors->resources. Only include raw_error for non-OAuth exceptions.
- BREAKING CHANGE: Error dict keys renamed: error->message, error_code->code, error_description->description. The get_errors() output key resource_errors is now resources.

## 0.20.1-keycardai-mcp (2026-02-06)


- fix(keycardai-mcp): return prm for resources dynamically

## 0.20.0-keycardai-mcp (2026-01-07)


- feat(keycardai-mcp): Adds PydanticAI integration for MCP frameworks
- - Adds PaydanticAI adapter to client integrations directory
- Support for PydanticAI agents with secure MCP tool access
- Follows established pattern with LangChain and OpenAI integrations
- Adds tests for PydanticAI integration imports

## 0.19.0-keycardai-mcp (2026-01-07)


- feat(keycardai-mcp): Add greater control over OAuth metadata location
- - Refactors `auth_metadata_mount` into it's component parts
- Exposes mounts for individual metadata
- Allows the user to specify exactly where their OAuth metadata is
exposed
- NOTE: This is only for advanced use cases where you know you need
something non-standard. Otherwise, follow the OAuth spec.

## 0.18.0-keycardai-mcp (2025-12-04)


- feat(keycardai-mcp): add CrewAI integration for agent frameworks
- - Add CrewAI adapter to client integrations directory
- Support for CrewAI agents with secure MCP tool access
- No token passing - agents never receive raw API tokens
- Fresh token fetched per API call through Keycard
- Follows established pattern with LangChain and OpenAI integrations
- Deleted separate packages/agents package (not needed)
- Added optional dependencies: crewai and agents extras
- Added tests for CrewAI integration imports

## 0.17.0-keycardai-mcp (2025-11-18)


- feat(keycardai-mcp): session callback notification
- feat(keycardai-mcp): session lifecycle management

## 0.16.0-keycardai-mcp (2025-11-17)


- feat(keycardai-mcp): headless clients
- feat(keycardai-mcp): update oauth deps
- feat(keycardai-mcp): client implementation

## 0.15.0-keycardai-mcp (2025-11-07)


- feat(keycardai-mcp): enable web token eks env

## 0.14.0-keycardai-mcp (2025-11-06)


- feat(keycardai-mcp): configure mcp url via env

## 0.13.0-keycardai-mcp (2025-11-05)


- feat(keycardai-mcp): zone settings via env

## 0.12.0-keycardai-mcp (2025-11-05)


- feat(keycardai-mcp): automatic app cred discovery
- feat(keycardai-mcp): default eks env

## 0.11.0-keycardai-mcp (2025-10-29)


- feat(keycardai-mcp): release latest version
- Release current version of workload identity implementation

## 0.10.0-keycardai-mcp (2025-10-27)


- feat(keycardai-mcp): cach the application credentials
- feat(keycardai-mcp): app credential grant flow

## 0.9.0-keycardai-mcp (2025-10-20)


- refactor(keycardai-mcp): align credential names
- feat(keycardai-mcp): eks workload identity support
- feat(keycardai-mcp): add application authentication

## 0.8.1-keycardai-mcp (2025-10-10)


- fix(keycardai-mcp): wrong base url in auth metadata

## 0.8.0-keycardai-mcp (2025-10-07)


- refactor(keycardai-mcp): improve error messages
- refactor(keycardai-mcp): improves the error messages to provide useful debug information

## 0.7.1-keycardai-mcp (2025-09-29)


- fix(keycardai-mcp): set audience for client assertions

## 0.7.0-keycardai-mcp (2025-09-27)


- feat(keycardai-mcp): lowlevel support for RequestContext

## 0.6.0-keycardai-mcp (2025-09-23)


- feat(keycardai-mcp): enable custom middleware injection

## 0.5.1-keycardai-mcp (2025-09-22)


- fix(keycardai-mcp): support x-forwarded-port header

## 0.5.0-keycardai-mcp (2025-09-22)


- feat(keycardai-mcp): dcr can be toggled on/off
- feat(keycardai-mcp): private key jwt support with global key
- feat(keycardai-mcp): grant decorator exception handling
- feat(keycardai-mcp): private key manager protocol

## 0.4.1-keycardai-mcp (2025-09-18)


- fix(keycardai-mcp): support both sync and async tool calls

## 0.4.0-keycardai-mcp (2025-09-18)


- feat(keycardai-mcp): default domain handling

## 0.3.1-keycardai-mcp (2025-09-17)


- fix(keycardai-mcp): check audience when configured

## 0.3.0-keycardai-mcp (2025-09-16)


- feat(keycardai-mcp): multi-zone mcp routing
- feat(keycardai-mcp): advanced server handlers
- feat(keycardai-mcp): auth provider implementation

## 0.1.0-keycardai-mcp (2025-09-10)
Changelog for keycardai-mcp-fastmcp:
## Unreleased

## 0.21.0-keycardai-mcp-fastmcp (2026-04-27)


- feat(keycardai-mcp-fastmcp): release deprecation bridge for keycardai-fastmcp (#103)
- Empty commit to trigger the auto-bump pipeline for keycardai-mcp-fastmcp.
- The actual bridge code (depends on keycardai-fastmcp, re-exports every
public symbol at the original keycardai.mcp.integrations.fastmcp.* paths,
emits DeprecationWarning on top-level import) shipped in #102. That PR
landed scoped (keycardai-fastmcp), so cz only bumped the new package and
keycardai-mcp-fastmcp on PyPI is still the pre-rename version. This
commit gives cz a (keycardai-mcp-fastmcp)-scoped feat to recognize so
the bridge ships as the next published version and customers on the
old name see the deprecation warning.

## 0.20.0-keycardai-mcp-fastmcp (2026-04-01)


- feat(keycardai-mcp-fastmcp): upgrade to FastMCP 3.0
- Upgrade keycardai-mcp-fastmcp from fastmcp>=2.14.0,<3.0.0 to fastmcp>=3.0.0.
- Key changes:
- ctx.get_state()/ctx.set_state() are now async (FastMCP 3.0 breaking change)
- grant decorator uses await ctx.set_state(..., serializable=False)
- All examples, docs, and tests updated for async state access
- Test mocks updated to use async functions for get_state/set_state

## 0.19.0-keycardai-mcp-fastmcp (2026-03-06)


- refactor(keycardai-mcp-fastmcp)!: optimize error formatting in token exchange chain
- Restructure error dicts to remove redundancy and improve readability.
Key renames: error->message, error_code->code, error_description->description,
resource_errors->resources. Only include raw_error for non-OAuth exceptions.
- BREAKING CHANGE: Error dict keys renamed: error->message, error_code->code, error_description->description. The get_errors() output key resource_errors is now resources.

## 0.18.1-keycardai-mcp-fastmcp (2025-11-23)


- fix(keycardai-mcp-fastmcp): include subject in debug

## 0.18.0-keycardai-mcp-fastmcp (2025-11-20)


- feat(keycardai-mcp-fastmcp): debug information for exchange

## 0.17.0-keycardai-mcp-fastmcp (2025-11-17)


- feat(keycardai-mcp-fastmcp): update oauth deps

## 0.16.0-keycardai-mcp-fastmcp (2025-11-07)


- feat(keycardai-mcp-fastmcp): enable web token eks env

## 0.15.0-keycardai-mcp-fastmcp (2025-11-06)


- feat(keycardai-mcp-fastmcp): configure mcp url via env

## 0.14.0-keycardai-mcp-fastmcp (2025-11-05)


- feat(keycardai-mcp-fastmcp): configure zone setting via env

## 0.13.0-keycardai-mcp-fastmcp (2025-11-05)


- feat(keycardai-mcp-fastmcp): automatic app cred discovery

## 0.12.0-keycardai-mcp-fastmcp (2025-10-29)


- feat(keycardai-mcp-fastmcp): support fastmcp 2.13

## 0.11.0-keycardai-mcp-fastmcp (2025-10-29)


- feat(keycardai-mcp-fastmcp): keycardai mcp dep update
- Reverts the eks workload identity changes

## 0.10.0-keycardai-mcp-fastmcp (2025-10-27)


- feat(keycardai-mcp-fastmcp): use application cred cache

## 0.9.0-keycardai-mcp-fastmcp (2025-10-20)


- feat(keycardai-mcp-fastmcp): EKS workload identity

## 0.8.1-keycardai-mcp-fastmcp (2025-10-07)


- refactor(keycardai-mcp-fastmcp): improve error message with debug context

## 0.8.0-keycardai-mcp-fastmcp (2025-10-01)


- feat(keycardai-mcp-fastmcp): ability to mock internal access context for testing

## 0.7.0-keycardai-mcp-fastmcp (2025-09-27)


- refactor(keycardai-mcp-fastmcp): remove the error codes from AccessContext

## 0.6.0-keycardai-mcp-fastmcp (2025-09-22)


- feat(keycardai-mcp-fastmcp): unify exceptions with keycardai-mcp package

## 0.5.0-keycardai-mcp-fastmcp (2025-09-21)


- feat(keycardai-mcp-fastmcp): client factory and base url update

## 0.4.1-keycardai-mcp-fastmcp (2025-09-19)


- fix(keycardai-mcp-fastmcp): lock the oauth dependency

## 0.4.0-keycardai-mcp-fastmcp (2025-09-18)


- feat(keycardai-mcp-fastmcp): refactor API for the provider

## 0.3.0-keycardai-mcp-fastmcp (2025-09-15)


- feat(keycardai-mcp-fastmcp): unify client arguments

## 0.2.0-keycardai-mcp-fastmcp (2025-09-10)


- fix(keycardai-mcp-fastmcp): pin fastmcp for compatibiity
- feat(keycardai-mcp-fastmcp): allowed to override the client

## 0.1.0-keycardai-mcp-fastmcp (2025-09-07)
Changelog for keycardai-fastmcp:
## Unreleased


- refactor(keycardai-fastmcp)!: rename AccessContext.get_resource_error to match keycardai-oauth
- Mirrors the keycardai-oauth rename in the fastmcp provider's parallel
AccessContext implementation. Keeps the two surfaces aligned for users
who consume both directly.
- BREAKING CHANGE: AccessContext.get_resource_errors renamed to
AccessContext.get_resource_error.

## 0.2.0-keycardai-fastmcp (2026-04-27)


- feat(keycardai-fastmcp): rename keycardai-mcp-fastmcp via bridge package (ACC-233) (#102)
- * feat(keycardai-fastmcp): rename keycardai-mcp-fastmcp via bridge package (ACC-233)
- The current name carries a redundant -mcp suffix (FastMCP only speaks MCP,
so the protocol tag adds no information). Renames to keycardai-fastmcp per
the revised KEP, with keycardai-mcp-fastmcp preserved as a deprecation
bridge so the customer in production on the old name keeps working
indefinitely.
- What ships:
- * New keycardai-fastmcp package at packages/fastmcp/, full implementation
  under the keycardai.fastmcp namespace. Tests, examples, README move with
  the source. Wired into the workspace and the release.yml tag filter.
* Deprecated keycardai-mcp-fastmcp now depends only on keycardai-fastmcp
  and re-exports every public symbol at the original
  keycardai.mcp.integrations.fastmcp.* paths. Importing the top-level
  module emits a DeprecationWarning pointing at the canonical name.
* Bridge contract test (test_bridge.py, 4 tests) asserts the
  DeprecationWarning fires and that bridge symbols are identity-equal to
  the canonical ones. The full behavioral suite lives in keycardai-fastmcp
  going forward.
- Customer impact: pip install keycardai-mcp-fastmcp keeps working; the
package transitively pulls keycardai-fastmcp. No forced removal timeline,
the bridge ships until every known caller migrates.
- Verified: ruff clean. fastmcp 51/51, mcp-fastmcp bridge 4/4, mcp 560/560,
oauth 208/208, starlette 49/49.
- Supersedes the canceled ACC-195 (which used the now-rejected
keycardai-fastmcp-mcp name).
- * fix(keycardai-fastmcp): bridge re-exports the full canonical surface
- Review caught that the bridge provider.py only re-exported a hand-enumerated
subset of the canonical surface, dropping documented public symbols
(get_token_debug_info, introspect, INTROSPECT, AuthProviderConfigurationError,
AuthProviderInternalError, AuthProviderRemoteError). Importing any of those
from keycardai.mcp.integrations.fastmcp.provider raised ImportError, breaking
the bridge contract for downstream callers using less common symbols.
- Fixes:
- - Add __all__ to keycardai.fastmcp.provider listing the 28-name public
  surface. Stdlib/typing helpers (logging, os, urlparse, wraps, Any,
  Callable, etc.) are deliberately excluded.
- Replace the bridge provider.py hand-enumeration with
  ``from keycardai.fastmcp.provider import *``, plus a re-export of __all__
  so future symbol additions to the canonical module flow through
  automatically.
- Add test_bridge_provider_exposes_full_public_surface: iterates the
  canonical __all__, asserts every symbol is present at the bridge path
  and identity-equal to the canonical reference. Regression test for the
  symbol-drop class of bug.
- Scrub em dashes from the renamed example READMEs (pre-existing prose,
  but new file paths shipping under our review).
- Verified: fastmcp 51/51, mcp-fastmcp bridge 5/5 (was 4 + 1 new). Smoke:
the six previously-missing symbols now import cleanly from the old path.
- * ci: pin extractions/setup-just version
- The action stopped resolving "latest" sometime today and started failing
with `no release for just matching version specifier`. Pinning unblocks
PR validation and the post-merge bump-and-publish pipeline.
- 1.50.0 is the current stable just release (April 2026).
- * ci: replace extractions/setup-just with the upstream install script
- extractions/setup-just@v2 is currently broken for both unpinned and
explicit-version requests ("no release for just matching version
specifier"). Pinning to 1.50.0 did not help because the action regression
is in its release lookup, not its version resolution.
- Switch to the just.systems install script (the project owners ship and
maintain it). Runs as a plain bash step with no third-party action
dependency and is unaffected by setup-just regressions.
Changelog for keycardai-a2a:
## Unreleased

## 0.3.0-keycardai-a2a (2026-05-01)


- fix(keycardai-a2a)!: align DelegationClient with a2a-sdk 1.x JSONRPC dispatcher (ACC-231) (#107)
- DelegationClient was still speaking 0.x JSON-RPC (“message/send”, old envelope, no A2A-Version), so every call was rejected by real 1.x dispatchers before execution—breaking the entire keycardai-crewai delegation path. Fixed by upgrading to the 1.x contract (SendMessage, proper envelope + headers, new response shape) and adding a real dispatcher test to catch drift.

## 0.2.0-keycardai-a2a (2026-04-29)


- feat(keycardai-a2a): new package split from keycardai-agents (ACC-230) (#105)
- * feat(keycardai-a2a)!: new package split from keycardai-agents (ACC-230)
- Per the KEP "Decompose keycardai-agents", the A2A delegation surface moves
out of keycardai-agents into a new keycardai-a2a package, structurally
analogous to keycardai-mcp. Symbols available at the new namespace:
- - AgentServer, create_agent_card_server, serve_agent
- DelegationClient, DelegationClientSync
- AgentExecutor, SimpleExecutor, LambdaExecutor
- KeycardToA2AExecutorBridge
- ServiceDiscovery
- AgentServiceConfig
- The bearer middleware in server/app.py also migrates from the deprecated
BearerAuthMiddleware to the canonical KeycardAuthBackend +
AuthenticationMiddleware pattern from keycardai-starlette. The
keycardai-mcp dependency drops from this code path.
- Hard cut, no transitional bridge: ACC-232 confirms no known production
users of keycardai.agents.* paths.
- The PKCE user-login client (AgentClient) is dropped entirely. Its
capability already lives in keycardai-oauth as
keycardai.oauth.pkce.authenticate (ACC-229 / #101). The duplicate in
keycardai-agents is removed.
- What stays in keycardai-agents: the CrewAI integration only, with its
imports repointed at keycardai.a2a. ACC-231 will move it to a dedicated
keycardai-crewai; ACC-232 will archive the now-stub source directory.
- BREAKING:
- from keycardai.agents import AgentServer, DelegationClient, ...
  becomes from keycardai.a2a import ... .
- from keycardai.agents.client import AgentClient is gone; use
  keycardai.oauth.pkce.authenticate.
- keycardai-agents 0.3.0 ships with the dependency set reduced to
  keycardai-a2a + pydantic, mirroring its now-CrewAI-only scope.
- * fix(keycardai-a2a): apply migration edits to moved files (ACC-230)
- The first commit on this branch did the git mv's but staged the new
files only; the Edit-tool modifications to the moved files (import
rewrites, server/app.py bearer-wiring migration to KeycardAuthBackend +
AuthenticationMiddleware, example pyproject swap from keycardai-agents
to keycardai-a2a, conftest/tests import repoints, agents/__init__.py
trim, agents/pyproject dep set, crewai integration repoint, top-level
workspace sources, justfile test recipe, uv.lock refresh) all sat
uncommitted. CI rejected the prior commit because the example pyproject
still claimed keycardai-agents at packages/a2a/, conflicting with the
real keycardai-agents at packages/agents/ in the workspace graph.
- This commit lands the actual migration content. Tests pass locally:
keycardai-a2a 60/60, keycardai-agents 16/16, no regression in oauth /
starlette / mcp / mcp-fastmcp / fastmcp; ruff workspace check clean.
- * refactor(keycardai-a2a)!: wrap a2a-sdk 1.x, drop parallel surface (ACC-230)
- Aligns keycardai-a2a with the wrap-do-not-reinvent pattern used in
keycardai-mcp and keycardai-starlette: customers implement a2a-sdk native
async AgentExecutor directly; this package contributes only Keycard auth
wiring, OAuth metadata discovery, and convenience composition.
- Drops the parallel-protocol surface inherited from the keycardai-agents
move:
- AgentExecutor protocol (sync execute(task, inputs)) and SimpleExecutor /
  LambdaExecutor implementations
- KeycardToA2AExecutorBridge (the sync->async adapter that existed only to
  bridge our protocol to a2a-sdk)
- Custom POST /invoke endpoint with bespoke InvokeRequest / InvokeResponse
  Pydantic models alongside the standard A2A JSONRPC interface
- AgentServiceConfig.invoke_url (replaced by jsonrpc_url) and
  AgentServiceConfig.to_agent_card() (the 0.x dict-shape constructor)
- Migrates from a2a-sdk 0.x to 1.x natively:
- pyproject pin a2a-sdk[http-server]>=1.0
- Server composition uses route factories (create_jsonrpc_routes,
  create_agent_card_routes) instead of the gone A2AStarletteApplication
- Request handler is DefaultRequestHandlerV2 (alias DefaultRequestHandler)
- AgentCard built from 1.x protobuf schema (supported_interfaces,
  AgentCapabilities streaming/push_notifications/extended_agent_card)
- Example main.py uses a2a-sdk 1.x Client via create_client + A2ACardResolver
- Adds a KeycardServerCallContextBuilder that subclasses a2a-sdk default
builder and stashes the verified KeycardUser plus access_token into
ServerCallContext.state so AgentExecutor implementations can read the
bearer token from context.call_context.state["access_token"] for
downstream delegated token exchange.
- Tests:
- a2a 44/44 pass
- agents 16/16 pass with crewai extra
- ruff clean
- Note: the high-level @auth.grant decorator parity with keycardai-mcp is
not yet shipped here. Customers use DelegationClient (already in this
package) for explicit server-to-server delegation. The decorator port is
a follow-up.
- * fix(keycardai-a2a): address review findings on PR #105
- Three blockers caught in fresh-eyes review:
- 1. release.yml tag-trigger list was hardcoded; *-keycardai-a2a was missing,
   so the post-merge auto-bump would push the tag but the publish workflow
   would never trigger. Trusted Publisher being registered would have been
   moot.
- 2. DelegationClient.invoke_service hardcoded service_url + /invoke. The
   wrap-aligned server only exposes /a2a/jsonrpc; calling invoke_service
   against any 1.x server returned 404. The CrewAI delegation tool runs
   through this code path. Both async and sync variants now build a
   message/send JSONRPC envelope, POST it to /a2a/jsonrpc, and unwrap the
   result to preserve the legacy {result, delegation_chain} shape so the
   CrewAI integration keeps working unchanged.
- 3. discover_service in both DelegationClient and ServiceDiscovery validated
   the 0.x card shape (required_fields = [name, endpoints, auth]). The 1.x
   protobuf-derived JSON has none of endpoints / auth. Discovery against
   any 1.x server raised ValueError. Validation now requires only "name";
   transport / auth specifics live under supportedInterfaces and the OAuth
   metadata routes.
- Plus four important findings:
- 4. Test mocks across conftest.py, test_a2a_client.py, test_discovery.py,
   and test_crewai_a2a.py used the old shape (endpoints/auth keys). Tests
   passed because the validator wrongly accepted them. Mocks now use the
   1.x JSON shape (supportedInterfaces, capabilities object, skills with
   id/name).
- 5. A2AServiceClient and A2AServiceClientSync backward-compat aliases at
   the bottom of delegation.py contradicted the "hard cut, no transitional
   bridge" stance in the PR description. Removed.
- 6. TestJsonRpcAuthGate.test_jsonrpc_requires_authorization asserted
   status_code in (400, 401). 400 means the JSONRPC dispatcher saw the
   request and bailed on the body shape, not that the auth gate caught
   it. Pinned to == 401 with a WWW-Authenticate header check so the gate
   contract is enforced.
- 7. Zero coverage existed for _KeycardServerCallContextBuilder propagating
   the verified KeycardUser plus access_token into ServerCallContext.state.
   Added two unit tests that build the context directly: one with a
   KeycardUser asserting state["access_token"] is set, one with an
   UnauthenticatedUser asserting state["access_token"] is absent (so an
   executor reading it sees None rather than a stale token).
- Tests:
- a2a 47/47 (was 44; +3 new wrap-coverage tests)
- agents 16/16 with crewai extra
- ruff clean
- * refactor(keycardai-a2a)!: ship primitives, not a server abstraction (ACC-230)
- Per Kamil's review on PR #105: AgentServer / create_agent_card_server /
serve_agent presupposed customers want a fresh Starlette app dedicated to
the agent service. The wrap-don't-reinvent stance, taken seriously,
says: customers already have an a2a-sdk app in their head; we ship
primitives that slot Keycard auth into THAT, not a parallel server.
- Public surface change:
- Dropped:
  AgentServer, create_agent_card_server, serve_agent
Promoted to public (renamed off the underscore prefix):
  EagerKeycardAuthBackend
  KeycardServerCallContextBuilder
  build_agent_card_from_config
- AgentServiceConfig trimmed: dropped agent_executor (DefaultRequestHandler
takes its own), port and host (uvicorn's job), status_url (no /status
in the primitives layer).
- The composed-server flow moves to a runnable example at
packages/a2a/examples/keycard_protected_server/. README quickstart
rewritten to show primitive composition into an existing app; greenfield
users follow the example.
- Tests:
  a2a 44/44 (was 47; net -3 from dropping the /status endpoint tests
            and the port-validation test)
  agents 16/16 with crewai extra
  ruff clean
- This change is breaking, but the package is 0.1.0-pre-publish so no
customer is on these names yet.
- * fix(keycardai-a2a): ruff import-organization auto-fix
- * refactor(keycardai-starlette,keycardai-a2a): collapse EagerKeycardAuthBackend into KeycardAuthBackend kwarg (ACC-230)
- Per Kamil's second review observation on PR #105: with keycardai-a2a
now depending on keycardai-starlette, the question of WHERE these
primitives live matters. EagerKeycardAuthBackend was a 5-line subclass
that flipped one branch of KeycardAuthBackend.authenticate to raise on
missing Authorization, with no a2a-sdk specifics. The behavior is a
policy choice ("this mount requires auth"), not a different kind of
backend.
- Collapsed to a kwarg on the existing class:
-   KeycardAuthBackend(verifier)                              # default,
                                                            # mixed-route
  KeycardAuthBackend(verifier, require_authentication=True) # all-paths-protected
- The OAuth metadata bypass (RFC 9728 §2 / RFC 8414 §3) takes precedence
over the kwarg: even with require_authentication=True, requests to
/.well-known/oauth-* and /.well-known/jwks.json still pass through
anonymously per spec. New parametrized test asserts this.
- Net effect:
- One class instead of two; existing KeycardAuthBackend(verifier) callers
  unchanged.
- keycardai-a2a no longer ships EagerKeycardAuthBackend; the kwarg is
  used directly in tests, the example, and the README quickstart.
- Migration story is zero churn for existing users; new behavior is
  opt-in via the kwarg.
- Tests:
  starlette 40 passed (+2 new tests for the kwarg semantics)
  a2a 44 passed
  agents 16 passed
  ruff clean

This comment was automatically generated by the release preview workflow.

@Larry-Osakwe Larry-Osakwe merged commit 8376ef1 into main May 7, 2026
8 checks passed
@Larry-Osakwe Larry-Osakwe deleted the larry/oauth-rich-resource-access-error branch May 7, 2026 01:43
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.

2 participants