Skip to content

feat(keycardai-crewai): new package split from keycardai-agents (ACC-231)#106

Closed
Larry-Osakwe wants to merge 5 commits into
mainfrom
larry/acc-231-keycardai-crewai-split
Closed

feat(keycardai-crewai): new package split from keycardai-agents (ACC-231)#106
Larry-Osakwe wants to merge 5 commits into
mainfrom
larry/acc-231-keycardai-crewai-split

Conversation

@Larry-Osakwe
Copy link
Copy Markdown
Contributor

@Larry-Osakwe Larry-Osakwe commented Apr 29, 2026

Summary

Per the KEP Decompose keycardai-agents, extracts the CrewAI-over-A2A integration into a new keycardai-crewai package, sister to keycardai-a2a (#105). After this PR, keycardai-agents ships no source; ACC-232 archives the directory.

The CrewAI-over-MCP-tools integration in keycardai-mcp is a separate concern and is untouched.

Public surface in keycardai.crewai:

  • CrewAIExecutora2a-sdk 1.x AgentExecutor subclass. Pass directly to DefaultRequestHandler(agent_executor=...). Reads the bearer from context.call_context.state["access_token"], runs crew.kickoff() on a worker thread via asyncio.to_thread, enqueues the result as a Message.
  • set_delegation_token(access_token) — public hook for code paths running a crew outside CrewAIExecutor.
  • get_a2a_tools(service_config, delegatable_services) — list of crewai.tools.BaseTool per delegatable service. Each calls the target via keycardai-a2a's DelegationClientSync.invoke_service.
  • create_a2a_tool_for_service(service_config, target_service_url) — single-tool variant.

Five commits on this branch

  1. feat(keycardai-crewai)!: — new package skeleton at packages/crewai/. Body of keycardai.agents.integrations.crewai moves into keycardai.crewai.__init__.py; tests and release.yml allowlist updated; workspace registered in repo-root pyproject.toml.
  2. refactor(keycardai-agents)!: — empty out the package: deletes integrations/, reduces deps to [], replaces README with an upgrade-path table.
  3. refactor(keycardai-crewai)!: — addresses fresh-eyes review. CrewAIExecutor reconciled to subclass AgentExecutor with async execute. README quickstart drops the hand-written bridge wrapper. crew.kickoff runs via asyncio.to_thread. Drops dead code (set_token_context flag, set_token_for_delegation, try/except around the crewai import, Topic :: Security classifier). +8 unit tests.
  4. test(keycardai-crewai): — integration test through the real JSONRPC dispatcher. Sends a SendMessage POST through TestClient against DefaultRequestHandler(agent_executor=CrewAIExecutor(...)) and asserts the crew is driven, the result reaches the response, and the bearer reaches the contextvar.
  5. fix(keycardai-a2a)!:DelegationClient.invoke_service was using the 0.x JSONRPC method (message/send), 0.x message envelope (lowercase role, no messageId), and missing the A2A-Version: 1.0 header. Against any real 1.x server the dispatcher rejected the call. Caught by commit 4. Fixes the wire shape, updates _unwrap_jsonrpc_response for the 1.x SendMessageResponse.message.parts shape, and adds a positive-path JSONRPC test on the keycardai-a2a side so future drift is local.

Hard cut, no transitional bridge

The Explore inventory confirms zero runtime imports of keycardai.agents.integrations.crewai outside packages/agents/ itself. No re-export shim ships.

Reviewer notes

  • The 16 tests previously at packages/agents/tests/integrations/test_crewai_a2a.py move to packages/crewai/tests/test_crewai_a2a.py with imports rewritten. Same coverage, same count.
  • keycardai-agents keeps its [tool.commitizen] block so it can still be tagged for the bump (next bump 0.2.0 → 0.3.0 — breaking, since deps and surface were reduced).
  • The merged packages/a2a/examples/keycard_protected_server/main.py:54-57 references MessageRole.MESSAGE_ROLE_AGENT, which doesn't exist in installed a2a-sdk 1.x (the enum is Role.ROLE_AGENT). Out of scope here; worth a follow-up.

Test plan

  • crewai: 27 passed (24 unit + 3 integration)
  • a2a: 45 passed (was 44, +1 positive-path JSONRPC dispatch)
  • agents: 0 collected (empty)
  • oauth, starlette, mcp, mcp-fastmcp, fastmcp: green
  • uv run ruff check clean
  • Public-surface check: from keycardai.crewai import set_delegation_token, get_a2a_tools, create_a2a_tool_for_service, CrewAIExecutor succeeds; isinstance(CrewAIExecutor(lambda: None), AgentExecutor) is True; from keycardai.agents.integrations.crewai import set_delegation_token raises ModuleNotFoundError

Pre-merge checklist

  • PyPI Trusted Publisher registered for keycardai-crewai in the keycard org. Form fields: project keycardai-crewai, owner keycardai, repo python-sdk, workflow release.yml, environment pypi-release.
  • At squash-merge, edit the merge commit body down to a one-line summary. GitHub pre-fills it with all 5 commit message bodies concatenated, which would balloon the CHANGELOG (see packages/a2a/CHANGELOG.md from feat(keycardai-a2a): new package split from keycardai-agents (ACC-230) #105).

Follow-ups

  • ACC-232: archive the packages/agents/ source directory.
  • Fix the MessageRole reference in packages/a2a/examples/keycard_protected_server/main.py.
  • Consider pinning crewai>=0.86.0,<2.0 defensively (silent-import-break insurance).
  • @auth.grant(resource) decorator parity with keycardai-mcp.
  • Update the keycardai-crewai row in the ACC-216 feature parity matrix.

…-231)

Per the KEP "Decompose keycardai-agents", the CrewAI-over-A2A integration
moves out of keycardai-agents into a new keycardai-crewai package, sister
to keycardai-a2a (#105 / ACC-230).

Public surface in keycardai.crewai:

- CrewAIExecutor — adapter that runs a CrewAI Crew factory and returns
  the result as a string. Intended to be called from inside an a2a-sdk
  AgentExecutor.execute method.
- set_delegation_token(access_token) — stash the inbound bearer in a
  contextvar so synchronous CrewAI tool calls can pick it up at
  delegation time.
- get_a2a_tools(service_config, delegatable_services) — return a list
  of crewai.tools.BaseTool instances, one per delegatable service.
  Each tool calls the target service via keycardai-a2a is
  DelegationClientSync.invoke_service.
- create_a2a_tool_for_service(service_config, target_service_url) —
  single-tool variant for callers that already know the target URL.

The body is the same code that lived at
keycardai.agents.integrations.crewai with three string-level changes:
- ImportError message updated to "pip install keycardai-crewai".
- Docstring usage examples updated to "from keycardai.crewai import ...".
- Module path in test mock targets ("patch(...ServiceDiscovery)")
  updated to "keycardai.crewai.ServiceDiscovery".

Wrap rules (mirrored from keycardai-a2a):
1. Use CrewAI native Crew and BaseTool types directly. No parallel protocol.
2. Use keycardai-a2a native primitives (DelegationClientSync, ServiceDiscovery,
   AgentServiceConfig, KeycardServerCallContextBuilder) directly.
3. The bearer-token contextvar bridges from ServerCallContext.state["access_token"]
   (set by keycardai-a2a is KeycardServerCallContextBuilder) into the
   synchronous CrewAI tool execution path.
4. No new HTTP endpoints, no new wire formats. Customers compose
   CrewAIExecutor with keycardai-a2a primitives in their own Starlette
   or FastAPI app.

The CrewAI-over-MCP-tools integration in keycardai-mcp is a separate
concern (different protocol, different package) and stays put.

Followups:
- Empty out keycardai-agents (next commit on this branch).
- ACC-232 archives the keycardai-agents source directory.
- Refactor CrewAIExecutor to subclass a2a.server.agent_execution.AgentExecutor
  (the 1.x async base) so it can be passed straight to DefaultRequestHandler
  without an outer wrapper.
- Update the keycardai-crewai row in the ACC-216 feature parity matrix.
…e (ACC-231)

Per the KEP "Decompose keycardai-agents", the final piece of code in
this package - the CrewAI-over-A2A integration at
keycardai.agents.integrations.crewai - moved to keycardai-crewai in the
preceding commit. This commit empties out the package.

Deleted:
- packages/agents/src/keycardai/agents/integrations/ (whole directory)
- packages/agents/tests/integrations/ (whole directory)

Reduced packages/agents/pyproject.toml:
- Dropped [project.optional-dependencies].crewai (the crewai>=0.86.0 extra)
- Dropped keycardai-a2a from core deps (no consumer left in this package)
- Dropped pydantic from core deps (same)
- Trimmed test/dev extras to the minimum needed for an empty package

Reduced packages/agents/src/keycardai/agents/__init__.py to a
deprecation-only docstring with no re-exports.

Replaced packages/agents/README.md with an upgrade-path table pointing
the three concerns at their new homes:
- A2A delegation -> keycardai-a2a (#105 / ACC-230)
- PKCE user-login -> keycardai-oauth (#101 / ACC-229)
- CrewAI-over-A2A -> keycardai-crewai (this PR / ACC-231)

Hard cut, no transitional bridge or re-export shim. The Explore
inventory confirms zero runtime imports of
keycardai.agents.integrations.crewai outside packages/agents/ itself,
matching the precedent set by #105.

ACC-232 will archive the packages/agents/ source directory in a
follow-up PR.

BREAKING:
- from keycardai.agents.integrations.crewai import ... is gone; use
  from keycardai.crewai import ... .
- The keycardai-agents[crewai] extra no longer exists; install
  keycardai-crewai directly.
@github-actions

This comment was marked as outdated.

…allel surface (ACC-231)

Addresses fresh-eyes review on PR #106. The previous commit moved
CrewAIExecutor verbatim, which carried the 0.x sync execute(task, inputs)
signature and forced the README quickstart to hand-write the exact
parallel-protocol bridge that #105 was reshaped to remove. Ships as the
0.1.0 PyPI surface, the README example becomes the canonical pattern
users copy, so the gap had to close before publish.

CrewAIExecutor now subclasses a2a.server.agent_execution.AgentExecutor
with async execute(self, context, event_queue) and slots straight into
DefaultRequestHandler(agent_executor=...). The README quickstart drops
the hand-written async wrapper.

execute() does four things in order:
  1. Read context.call_context.state["access_token"] (set by
     keycardai.a2a.KeycardServerCallContextBuilder), call
     set_delegation_token so synchronous CrewAI tools see it; warn if
     absent so misconfigured deployments are visible in logs.
  2. Build a fresh Crew via the factory.
  3. Run crew.kickoff() on a worker thread via asyncio.to_thread.
     crew.kickoff is synchronous and CPU/IO-bound for seconds at a time;
     calling it directly inside an async def starves uvicorn. The
     contextvar set in step 1 propagates into the worker via
     contextvars.copy_context (which asyncio.to_thread does for us), so
     ServiceDelegationTool._run can still read the token. The class
     docstring spells this out so a future reader does not swap to_thread
     for a raw ThreadPoolExecutor.
  4. Wrap the string result in an A2A Message (Role.ROLE_AGENT) and
     enqueue it.

Drops the dead set_token_context flag and set_token_for_delegation
method on CrewAIExecutor: both existed only for the Keycard
AgentExecutor protocol that #105 deleted. Nothing in the new world
calls them.

Drops the dead try/except around `from crewai import Crew`: crewai is
a hard dependency in pyproject.toml, not an extra, so the except branch
is unreachable.

Trims the Topic :: Security classifier from pyproject.toml: copy-paste
from keycardai-a2a, but a CrewAI integration is not security software.

Adds 8 new tests (TestCrewAIExecutor + TestSetDelegationToken):
  - subclass check (isinstance(executor, AgentExecutor))
  - execute runs crew.kickoff with the user input as task
  - execute enqueues a Message with the crew result
  - execute propagates access_token from RequestContext into the
    contextvar, observed from inside crew.kickoff via
    asyncio.to_thread context inheritance
  - execute warns when access_token is absent
  - execute does not block the event loop (probe inside kickoff
    confirms asyncio.get_running_loop() raises in the worker thread)
  - cancel returns None
  - set_delegation_token writes to the public contextvar

Tests:
  crewai 24 passed (was 16; +8)
  a2a 44 passed
  agents 0 collected (empty package, expected)
  ruff clean
@github-actions

This comment was marked as outdated.

…gh DefaultRequestHandler over JSONRPC (ACC-231)

The unit tests in test_crewai_a2a.py prove CrewAIExecutor.execute does
the right thing when called directly with mocked context and event_queue.
They do not prove that DefaultRequestHandler actually invokes execute
when a real JSONRPC SendMessage request lands on the mount. That gap is
the highest-value thing to close before this hits PyPI; the README quickstart
shape (DefaultRequestHandler(agent_executor=CrewAIExecutor(...), ...)) is
what users will copy, so a regression here is invisible until somebody
actually deploys the package.

This module instantiates the headline composition the way the README
shows it, drives it with a real JSONRPC POST through Starlette is
TestClient, and asserts the crew result comes back.

Three assertions:
  1. The user message text reaches crew.kickoff(inputs={"task": ...}).
     If the dispatcher does not invoke our execute method, this fails.
  2. The crew result string appears in the JSONRPC response body. Verifies
     the Message we enqueue is shaped correctly for SendMessageResponse.
  3. The KeycardUser access_token reaches the contextvar by the time
     crew.kickoff runs. End-to-end check of the full chain:
     KeycardUser -> request.scope["user"] -> KeycardServerCallContextBuilder
     -> ServerCallContext.state["access_token"] -> CrewAIExecutor.execute
     -> set_delegation_token -> asyncio.to_thread context inheritance
     -> crew.kickoff observes it.

Auth uses a stub backend that always returns a fixed KeycardUser. Real
auth is exercised in keycardai-starlette is own tests; a real verifier
here would need a reachable JWKS endpoint, and the verifier-to-context
flow is already covered by the propagation tests in keycardai-a2a. The
isolation lets this test focus on the wire-up between the JSONRPC
dispatcher and our executor.

Two non-obvious bits learned while wiring this up, both worth knowing:

- a2a-sdk 1.x JSONRPC method names are CamelCase ("SendMessage"), not the
  0.x slash form ("message/send"). The keycardai-a2a tests at
  packages/a2a/tests/test_agent_card_server.py use "message/send" but
  only assert 401 from the auth gate, so the dispatcher never sees the
  body and the wrong method name was never caught there.
- The dispatcher requires an A2A-Version: 1.0 header on the request. Without
  it the server defaults to 0.3 and validate_version returns a JSONRPC error
  before the executor is ever called.

Both worth following up on the keycardai-a2a side (the 401 tests should
also cover at least one positive path so this kind of dispatcher-shape
drift gets caught), but out of scope for this PR.

Tests:
  crewai 27 passed (24 unit + 3 integration)
  a2a 44 passed
  agents 0 collected (empty package, expected)
  oauth/starlette/mcp/mcp-fastmcp/fastmcp green
  ruff clean
@github-actions

This comment was marked as outdated.

…dispatcher (ACC-231)

DelegationClient.invoke_service was hardcoded to the 0.x JSONRPC method
"message/send", emitted a 0.x message envelope (lowercase role, no
messageId), omitted the A2A-Version header, and unwrapped a 0.x response
shape (result.parts). Against any real 1.x server the dispatcher
rejected the call before the executor ran, so the keycardai-crewai
delegation tools (the entire client-side path that runs through
DelegationClientSync.invoke_service) were dead-on-arrival.

The bug survived because the existing test_a2a_client.py tests mock the
http client and the request body never sees a real dispatcher; they
asserted "method == message/send" against the stubbed wire shape, which
was internally consistent and externally wrong. Caught by the
keycardai-crewai integration test added in commit 4: that test sends a
real JSONRPC POST through Starlette TestClient against the real
a2a-sdk DefaultRequestHandler, so dispatcher contract drift fails fast.

Changes:

- _build_jsonrpc_message_send -> _build_jsonrpc_send_message: method
  name is now "SendMessage" (the 1.x CamelCase form), the message envelope
  carries a messageId (required by the dispatcher) and the canonical
  enum-string role "ROLE_USER".
- Both async and sync invoke_service set the A2A-Version: 1.0 header.
  The header constant comes from a2a.utils.constants so a future a2a-sdk
  rename follows our code automatically.
- _unwrap_jsonrpc_response unwraps result.message.parts[].text, the
  shape SendMessageResponse takes when the executor enqueues a Message.
  Tasks fall back to JSON-stringified for now; full Task lifecycle
  consumers should reach for a2a.client.create_client directly.

Tests:
  test_a2a_client.py mocks updated to the 1.x response shape and now
  assert method == "SendMessage", role == "ROLE_USER", messageId
  presence, and A2A-Version header on the outbound POST.

  Added test_jsonrpc_dispatch.py: a positive-path TestClient test that
  drives a real a2a-sdk DefaultRequestHandler with a stub
  AgentExecutor enqueuing a Message, then asserts the executor saw the
  user input and the access_token from KeycardServerCallContextBuilder.
  The auth-gate tests in test_agent_card_server.py only cover 401, so
  dispatcher contract drift had no local guard. This test fills the
  gap on the keycardai-a2a side; the keycardai-crewai integration test
  added in commit 4 covers the same chain with a CrewAIExecutor on top.

  a2a 45 passed (was 44; +1)
  crewai 27 passed
  agents 0 collected (empty package, expected)
  oauth/starlette/mcp/mcp-fastmcp/fastmcp green
  ruff clean

BREAKING:
  Wire shape changes; any caller depending on the old "message/send"
  method name or the lowercase "user" role on the outbound envelope
  needs to update. No production callers exist (closed alpha; the
  keycardai-crewai delegation path is the only consumer in this PR).
@github-actions
Copy link
Copy Markdown

📦 Release Preview

This analysis shows the expected release impact:

📈 Expected Version Changes

keycardai-agents: 0.2.0 → 0.3.0 (MINOR)
keycardai-a2a: 0.2.0 → 0.3.0 (MINOR)
keycardai-crewai: 0.0.0 → 0.1.0 (MINOR)

📋 Package Details

[
  {
    "package_name": "keycardai-agents",
    "package_dir": "packages/agents",
    "has_changes": true,
    "current_version": "0.2.0",
    "next_version": "0.3.0",
    "increment": "MINOR"
  },
  {
    "package_name": "keycardai-a2a",
    "package_dir": "packages/a2a",
    "has_changes": true,
    "current_version": "0.2.0",
    "next_version": "0.3.0",
    "increment": "MINOR"
  },
  {
    "package_name": "keycardai-crewai",
    "package_dir": "packages/crewai",
    "has_changes": true,
    "current_version": "0.0.0",
    "next_version": "0.1.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-agents:
## Unreleased


- refactor(keycardai-agents)!: empty out package, decomposition complete (ACC-231)
- Per the KEP "Decompose keycardai-agents", the final piece of code in
this package - the CrewAI-over-A2A integration at
keycardai.agents.integrations.crewai - moved to keycardai-crewai in the
preceding commit. This commit empties out the package.
- Deleted:
- packages/agents/src/keycardai/agents/integrations/ (whole directory)
- packages/agents/tests/integrations/ (whole directory)
- Reduced packages/agents/pyproject.toml:
- Dropped [project.optional-dependencies].crewai (the crewai>=0.86.0 extra)
- Dropped keycardai-a2a from core deps (no consumer left in this package)
- Dropped pydantic from core deps (same)
- Trimmed test/dev extras to the minimum needed for an empty package
- Reduced packages/agents/src/keycardai/agents/__init__.py to a
deprecation-only docstring with no re-exports.
- Replaced packages/agents/README.md with an upgrade-path table pointing
the three concerns at their new homes:
- A2A delegation -> keycardai-a2a (#105 / ACC-230)
- PKCE user-login -> keycardai-oauth (#101 / ACC-229)
- CrewAI-over-A2A -> keycardai-crewai (this PR / ACC-231)
- Hard cut, no transitional bridge or re-export shim. The Explore
inventory confirms zero runtime imports of
keycardai.agents.integrations.crewai outside packages/agents/ itself,
matching the precedent set by #105.
- ACC-232 will archive the packages/agents/ source directory in a
follow-up PR.
- BREAKING:
- from keycardai.agents.integrations.crewai import ... is gone; use
  from keycardai.crewai import ... .
- The keycardai-agents[crewai] extra no longer exists; install
  keycardai-crewai directly.

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


- fix(keycardai-agents): restore test suite (#100)
- Closes ACC-236. Surfaced by ACC-234 wait-and-see verification: keycardai-agents tests would not run on a fresh checkout.
- Three pre-existing breaks fixed:
- 1. a2a-sdk constraint pinned to <1.0. The unbounded >=0.3.22 resolved to 1.0.x today; a2a-sdk 1.0 moved A2AStarletteApplication out of a2a.server.apps.jsonrpc, which keycardai-agents/server/app.py imports. Pinning is the cheap fix because keycardai-agents is being decomposed and archived in ACC-229..232; the replacement keycardai-a2a package will be written against a2a-sdk 1.x natively.
- 2. tests/integrations/test_crewai_a2a.py imported from keycardai.agents.integrations.crewai_a2a, which does not exist. The module is keycardai.agents.integrations.crewai. Three import/patch references updated.
- 3. test_tool_run_with_task_and_inputs asserted "pr_number" was a top-level key in the task dict; the actual contract puts task_inputs under task["inputs"]. Assertion updated to match the implementation.
- Verified: 85/85 agents tests pass. mcp 560/560, starlette 49/49, oauth 208/208 unaffected.

## 0.1.1-keycardai-agents (2026-01-07)
Changelog for keycardai-a2a:
## Unreleased


- fix(keycardai-a2a)!: align DelegationClient with a2a-sdk 1.x JSONRPC dispatcher (ACC-231)
- DelegationClient.invoke_service was hardcoded to the 0.x JSONRPC method
"message/send", emitted a 0.x message envelope (lowercase role, no
messageId), omitted the A2A-Version header, and unwrapped a 0.x response
shape (result.parts). Against any real 1.x server the dispatcher
rejected the call before the executor ran, so the keycardai-crewai
delegation tools (the entire client-side path that runs through
DelegationClientSync.invoke_service) were dead-on-arrival.
- The bug survived because the existing test_a2a_client.py tests mock the
http client and the request body never sees a real dispatcher; they
asserted "method == message/send" against the stubbed wire shape, which
was internally consistent and externally wrong. Caught by the
keycardai-crewai integration test added in commit 4: that test sends a
real JSONRPC POST through Starlette TestClient against the real
a2a-sdk DefaultRequestHandler, so dispatcher contract drift fails fast.
- Changes:
- - _build_jsonrpc_message_send -> _build_jsonrpc_send_message: method
  name is now "SendMessage" (the 1.x CamelCase form), the message envelope
  carries a messageId (required by the dispatcher) and the canonical
  enum-string role "ROLE_USER".
- Both async and sync invoke_service set the A2A-Version: 1.0 header.
  The header constant comes from a2a.utils.constants so a future a2a-sdk
  rename follows our code automatically.
- _unwrap_jsonrpc_response unwraps result.message.parts[].text, the
  shape SendMessageResponse takes when the executor enqueues a Message.
  Tasks fall back to JSON-stringified for now; full Task lifecycle
  consumers should reach for a2a.client.create_client directly.
- Tests:
  test_a2a_client.py mocks updated to the 1.x response shape and now
  assert method == "SendMessage", role == "ROLE_USER", messageId
  presence, and A2A-Version header on the outbound POST.
-   Added test_jsonrpc_dispatch.py: a positive-path TestClient test that
  drives a real a2a-sdk DefaultRequestHandler with a stub
  AgentExecutor enqueuing a Message, then asserts the executor saw the
  user input and the access_token from KeycardServerCallContextBuilder.
  The auth-gate tests in test_agent_card_server.py only cover 401, so
  dispatcher contract drift had no local guard. This test fills the
  gap on the keycardai-a2a side; the keycardai-crewai integration test
  added in commit 4 covers the same chain with a CrewAIExecutor on top.
-   a2a 45 passed (was 44; +1)
  crewai 27 passed
  agents 0 collected (empty package, expected)
  oauth/starlette/mcp/mcp-fastmcp/fastmcp green
  ruff clean
- BREAKING:
  Wire shape changes; any caller depending on the old "message/send"
  method name or the lowercase "user" role on the outbound envelope
  needs to update. No production callers exist (closed alpha; the
  keycardai-crewai delegation path is the only consumer in this PR).

## 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

## 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

## 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-crewai:
## Unreleased


- test(keycardai-crewai): integration test driving CrewAIExecutor through DefaultRequestHandler over JSONRPC (ACC-231)
- The unit tests in test_crewai_a2a.py prove CrewAIExecutor.execute does
the right thing when called directly with mocked context and event_queue.
They do not prove that DefaultRequestHandler actually invokes execute
when a real JSONRPC SendMessage request lands on the mount. That gap is
the highest-value thing to close before this hits PyPI; the README quickstart
shape (DefaultRequestHandler(agent_executor=CrewAIExecutor(...), ...)) is
what users will copy, so a regression here is invisible until somebody
actually deploys the package.
- This module instantiates the headline composition the way the README
shows it, drives it with a real JSONRPC POST through Starlette is
TestClient, and asserts the crew result comes back.
- Three assertions:
  1. The user message text reaches crew.kickoff(inputs={"task": ...}).
     If the dispatcher does not invoke our execute method, this fails.
  2. The crew result string appears in the JSONRPC response body. Verifies
     the Message we enqueue is shaped correctly for SendMessageResponse.
  3. The KeycardUser access_token reaches the contextvar by the time
     crew.kickoff runs. End-to-end check of the full chain:
     KeycardUser -> request.scope["user"] -> KeycardServerCallContextBuilder
     -> ServerCallContext.state["access_token"] -> CrewAIExecutor.execute
     -> set_delegation_token -> asyncio.to_thread context inheritance
     -> crew.kickoff observes it.
- Auth uses a stub backend that always returns a fixed KeycardUser. Real
auth is exercised in keycardai-starlette is own tests; a real verifier
here would need a reachable JWKS endpoint, and the verifier-to-context
flow is already covered by the propagation tests in keycardai-a2a. The
isolation lets this test focus on the wire-up between the JSONRPC
dispatcher and our executor.
- Two non-obvious bits learned while wiring this up, both worth knowing:
- - a2a-sdk 1.x JSONRPC method names are CamelCase ("SendMessage"), not the
  0.x slash form ("message/send"). The keycardai-a2a tests at
  packages/a2a/tests/test_agent_card_server.py use "message/send" but
  only assert 401 from the auth gate, so the dispatcher never sees the
  body and the wrong method name was never caught there.
- The dispatcher requires an A2A-Version: 1.0 header on the request. Without
  it the server defaults to 0.3 and validate_version returns a JSONRPC error
  before the executor is ever called.
- Both worth following up on the keycardai-a2a side (the 401 tests should
also cover at least one positive path so this kind of dispatcher-shape
drift gets caught), but out of scope for this PR.
- Tests:
  crewai 27 passed (24 unit + 3 integration)
  a2a 44 passed
  agents 0 collected (empty package, expected)
  oauth/starlette/mcp/mcp-fastmcp/fastmcp green
  ruff clean
- refactor(keycardai-crewai)!: subclass a2a-sdk AgentExecutor, drop parallel surface (ACC-231)
- Addresses fresh-eyes review on PR #106. The previous commit moved
CrewAIExecutor verbatim, which carried the 0.x sync execute(task, inputs)
signature and forced the README quickstart to hand-write the exact
parallel-protocol bridge that #105 was reshaped to remove. Ships as the
0.1.0 PyPI surface, the README example becomes the canonical pattern
users copy, so the gap had to close before publish.
- CrewAIExecutor now subclasses a2a.server.agent_execution.AgentExecutor
with async execute(self, context, event_queue) and slots straight into
DefaultRequestHandler(agent_executor=...). The README quickstart drops
the hand-written async wrapper.
- execute() does four things in order:
  1. Read context.call_context.state["access_token"] (set by
     keycardai.a2a.KeycardServerCallContextBuilder), call
     set_delegation_token so synchronous CrewAI tools see it; warn if
     absent so misconfigured deployments are visible in logs.
  2. Build a fresh Crew via the factory.
  3. Run crew.kickoff() on a worker thread via asyncio.to_thread.
     crew.kickoff is synchronous and CPU/IO-bound for seconds at a time;
     calling it directly inside an async def starves uvicorn. The
     contextvar set in step 1 propagates into the worker via
     contextvars.copy_context (which asyncio.to_thread does for us), so
     ServiceDelegationTool._run can still read the token. The class
     docstring spells this out so a future reader does not swap to_thread
     for a raw ThreadPoolExecutor.
  4. Wrap the string result in an A2A Message (Role.ROLE_AGENT) and
     enqueue it.
- Drops the dead set_token_context flag and set_token_for_delegation
method on CrewAIExecutor: both existed only for the Keycard
AgentExecutor protocol that #105 deleted. Nothing in the new world
calls them.
- Drops the dead try/except around `from crewai import Crew`: crewai is
a hard dependency in pyproject.toml, not an extra, so the except branch
is unreachable.
- Trims the Topic :: Security classifier from pyproject.toml: copy-paste
from keycardai-a2a, but a CrewAI integration is not security software.
- Adds 8 new tests (TestCrewAIExecutor + TestSetDelegationToken):
  - subclass check (isinstance(executor, AgentExecutor))
  - execute runs crew.kickoff with the user input as task
  - execute enqueues a Message with the crew result
  - execute propagates access_token from RequestContext into the
    contextvar, observed from inside crew.kickoff via
    asyncio.to_thread context inheritance
  - execute warns when access_token is absent
  - execute does not block the event loop (probe inside kickoff
    confirms asyncio.get_running_loop() raises in the worker thread)
  - cancel returns None
  - set_delegation_token writes to the public contextvar
- Tests:
  crewai 24 passed (was 16; +8)
  a2a 44 passed
  agents 0 collected (empty package, expected)
  ruff clean
- feat(keycardai-crewai)!: new package split from keycardai-agents (ACC-231)
- Per the KEP "Decompose keycardai-agents", the CrewAI-over-A2A integration
moves out of keycardai-agents into a new keycardai-crewai package, sister
to keycardai-a2a (#105 / ACC-230).
- Public surface in keycardai.crewai:
- - CrewAIExecutor — adapter that runs a CrewAI Crew factory and returns
  the result as a string. Intended to be called from inside an a2a-sdk
  AgentExecutor.execute method.
- set_delegation_token(access_token) — stash the inbound bearer in a
  contextvar so synchronous CrewAI tool calls can pick it up at
  delegation time.
- get_a2a_tools(service_config, delegatable_services) — return a list
  of crewai.tools.BaseTool instances, one per delegatable service.
  Each tool calls the target service via keycardai-a2a is
  DelegationClientSync.invoke_service.
- create_a2a_tool_for_service(service_config, target_service_url) —
  single-tool variant for callers that already know the target URL.
- The body is the same code that lived at
keycardai.agents.integrations.crewai with three string-level changes:
- ImportError message updated to "pip install keycardai-crewai".
- Docstring usage examples updated to "from keycardai.crewai import ...".
- Module path in test mock targets ("patch(...ServiceDiscovery)")
  updated to "keycardai.crewai.ServiceDiscovery".
- Wrap rules (mirrored from keycardai-a2a):
1. Use CrewAI native Crew and BaseTool types directly. No parallel protocol.
2. Use keycardai-a2a native primitives (DelegationClientSync, ServiceDiscovery,
   AgentServiceConfig, KeycardServerCallContextBuilder) directly.
3. The bearer-token contextvar bridges from ServerCallContext.state["access_token"]
   (set by keycardai-a2a is KeycardServerCallContextBuilder) into the
   synchronous CrewAI tool execution path.
4. No new HTTP endpoints, no new wire formats. Customers compose
   CrewAIExecutor with keycardai-a2a primitives in their own Starlette
   or FastAPI app.
- The CrewAI-over-MCP-tools integration in keycardai-mcp is a separate
concern (different protocol, different package) and stays put.
- Followups:
- Empty out keycardai-agents (next commit on this branch).
- ACC-232 archives the keycardai-agents source directory.
- Refactor CrewAIExecutor to subclass a2a.server.agent_execution.AgentExecutor
  (the 1.x async base) so it can be passed straight to DefaultRequestHandler
  without an outer wrapper.
- Update the keycardai-crewai row in the ACC-216 feature parity matrix.
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.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.

@Larry-Osakwe Larry-Osakwe marked this pull request as ready for review April 30, 2026 15:04
@Larry-Osakwe
Copy link
Copy Markdown
Contributor Author

Closing without merging.

After review (see Slack thread), we decided not to ship keycardai-crewai without a concrete customer asking for it. Customers who bring their own crew can wire Keycard in directly with keycardai-a2a + keycardai-oauth + raw crewai (~15-20 lines for either on-behalf or self access). Shipping a Keycard-flavored wrapper would invent a dependency for a customer we don't have, and signal to future customers that they need it when they don't.

Doc coverage for the CrewAI integration (and the same template for LangChain, etc.) folds into the upcoming docs overhaul.

One independent bug surfaced while doing this work: DelegationClient.invoke_service was emitting the 0.x JSONRPC shape against a 1.x dispatcher. Cut into a focused fix at #107.

Larry-Osakwe added a commit that referenced this pull request May 1, 2026
Removes packages/agents/ entirely. The PKCE user-login client moved to
keycardai-oauth (#101); the A2A delegation surface moved to keycardai-a2a
(#105). The CrewAI integration that briefly remained had no users; the
extraction PR (#106) was closed without merging in favor of customers
wiring CrewAI to Keycard primitives directly.

Drops keycardai-agents from the root workspace sources and the
release.yml tag allowlist so accidental tags cannot trigger republish.
keycardai-agents on PyPI is frozen at 0.2.0 and is being yanked
separately.

Updates packages/a2a/README.md to drop the now-stale references to a
forthcoming keycardai-crewai package and the in-progress archival.

BREAKING: any "from keycardai.agents.*" import fails. No production
users. Closes ACC-232.
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.

1 participant