keycardai-a2a
Keycard auth primitives for a2a-sdk 1.x agent services. This package is glue, not a parallel server abstraction. Customers compose these primitives with a2a-sdk's standard route factories and request handler in their own Starlette / FastAPI app to get bearer token verification, OAuth metadata discovery, and OAuth 2.0 token exchange (RFC 8693) for downstream delegated calls.
What's in here
Server-side wiring:
KeycardServerCallContextBuilder: aServerCallContextBuildersubclass. Pass toa2a.server.routes.create_jsonrpc_routes. Propagates the verifiedKeycardUserontoServerCallContext.state.keycard_user(context): typed accessor executors call on theirRequestContextto get thatKeycardUserback (Nonewhen the request was unauthenticated), including theaccess_tokenfor delegated downstream calls.build_agent_card_from_config(config): produces a 1.x protobufAgentCard. Pass toa2a.server.routes.create_agent_card_routesanda2a.server.request_handlers.DefaultRequestHandler.
For the auth backend itself, use keycardai.starlette.KeycardAuthBackend(verifier, require_authentication=True) on the JSONRPC mount. The kwarg flips the default mixed-route behavior to "every path on this mount needs auth," which matches the JSONRPC dispatcher's lack of a per-route gate.
Outbound delegation:
DelegationClient,DelegationClientSync: server-to-server token exchange and JSONRPC invocation against another agent service.
Inbound discovery:
ServiceDiscovery: query a remote agent service's.well-known/agent-card.jsonwith caching.
Configuration:
AgentServiceConfig: service identity + Keycard credentials + agent card metadata.
Installation
pip install keycardai-a2a
This pulls in keycardai-oauth, keycardai-starlette, a2a-sdk[http-server]>=1.0.
Quick start
You already have an a2a-sdk server. Add the Keycard-protected A2A mount to your existing Starlette / FastAPI app:
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
from a2a.server.tasks import InMemoryTaskStore
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.routing import Mount
from keycardai.a2a import (
AgentServiceConfig,
KeycardServerCallContextBuilder,
build_agent_card_from_config,
)
from keycardai.oauth.server.credentials import ClientSecret
from keycardai.starlette import AuthProvider, KeycardAuthBackend, keycard_on_error
from keycardai.starlette.routers.metadata import (
well_known_authorization_server_route,
well_known_protected_resource_route,
)
config = AgentServiceConfig(
service_name="My Agent",
client_id="...",
client_secret="...",
identity_url="https://my-agent.example.com",
zone_id="your-zone-id",
capabilities=["chat"],
)
auth_provider = AuthProvider(
zone_url=config.auth_server_url,
server_name=config.service_name,
server_url=config.identity_url,
# Bind accepted tokens to this service; leaving audience unset
# disables the audience check entirely.
audience=config.identity_url,
application_credential=ClientSecret((config.client_id, config.client_secret)),
)
verifier = auth_provider.get_token_verifier()
agent_card = build_agent_card_from_config(config)
request_handler = DefaultRequestHandler(
agent_executor=YourExecutor(), # subclass of a2a.server.agent_execution.AgentExecutor
task_store=InMemoryTaskStore(),
agent_card=agent_card,
)
# Add these routes to your existing Starlette / FastAPI app:
your_app.routes.extend(create_agent_card_routes(agent_card=agent_card))
your_app.routes.append(well_known_protected_resource_route(
issuer=config.auth_server_url,
resource="/.well-known/oauth-protected-resource{resource_path:path}",
))
your_app.routes.append(well_known_authorization_server_route(
issuer=config.auth_server_url,
resource="/.well-known/oauth-authorization-server{resource_path:path}",
))
your_app.routes.append(Mount(
"/a2a",
routes=create_jsonrpc_routes(
request_handler=request_handler,
rpc_url="/jsonrpc",
context_builder=KeycardServerCallContextBuilder(),
),
middleware=[
Middleware(
AuthenticationMiddleware,
backend=KeycardAuthBackend(verifier, require_authentication=True),
on_error=keycard_on_error,
),
],
))
Inside your AgentExecutor.execute(self, context, event_queue), read the verified caller with keycard_user(context) and use its access_token as the subject token in keycardai-oauth's TokenExchangeRequest for downstream API calls:
from keycardai.a2a import keycard_user
async def execute(self, context, event_queue):
caller = keycard_user(context)
if caller is None:
raise PermissionError("unauthenticated")
subject_token = caller.access_token # also caller.client_id, caller.scopes, caller.zone_id
The builder still writes the bare token under the legacy state["access_token"] key for executors written against keycardai-a2a 0.4.x.
For a runnable greenfield example (no existing app), see examples/keycard_protected_server/.
Protocol version
A server composed this way speaks A2A protocol 1.0: SendMessage under an A2A-Version: 1.0 header. All four Keycard SDKs send that generation, so no compatibility flag is needed between them:
- Python:
keycardai-a2a0.3.0 and later - TypeScript:
@keycardai/a2a0.4.0 and later - Go:
github.com/keycardai/go-sdkv0.22.0 and later (a2a.DelegationClient) - Ruby:
keycardai-a2a0.2.0 and later
A 0.3-generation caller (message/send with no A2A-Version header) is rejected by the a2a-sdk 1.x dispatcher with -32601 MethodNotFound. Such callers should upgrade; if one cannot, pass a2a-sdk's own enable_v0_3_compat=True to create_jsonrpc_routes yourself. This package no longer enables or recommends it.
Relationship to other Keycard packages
keycardai-oauth: OAuth 2.0 primitives used for token exchange and PKCE.keycardai-starlette: providesAuthenticationMiddleware+KeycardAuthBackendand the OAuth metadata route helpers used here.keycardai-mcp: sister package for MCP server protection. Same auth shape, different protocol.
History
This package was extracted from the original keycardai-agents package (KEP: Decompose keycardai-agents). The PKCE user-login client moved to keycardai-oauth. The keycardai-agents package itself is archived.
Release files for keycardai-a2a 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| keycardai_a2a-0.5.0.tar.gz | 23.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keycardai_a2a-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 40.4 kB
Release files / keycardai_a2a-0.5.0.tar.gz
| Download URL | keycardai_a2a-0.5.0.tar.gz |
|---|---|
| Size | 23.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cfa495f2afce7c0cca89a4761850c312e50dbed2d3694866620922b360678934
|
|
BLAKE2b-256 checksum How to use checksums |
d38241a65c16e2f46b1de72f7652694fbd38909296ce043496fcd2038eb3bb9e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / keycardai_a2a-0.5.0-py3-none-any.whl
| Download URL | keycardai_a2a-0.5.0-py3-none-any.whl |
|---|---|
| Size | 16.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
516f66e4e7521ee66297c43b4a0e58fc67e60a157749da121009bff8f09bc7f0
|
|
BLAKE2b-256 checksum How to use checksums |
c3ec2a4ee520735199d42a15d4d1fcd883ddf2b5144f427b7a537a31cbdf0203
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|