Skip to main content

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: a ServerCallContextBuilder subclass. Pass to a2a.server.routes.create_jsonrpc_routes. Propagates the verified KeycardUser onto ServerCallContext.state.
  • keycard_user(context): typed accessor executors call on their RequestContext to get that KeycardUser back (None when the request was unauthenticated), including the access_token for delegated downstream calls.
  • build_agent_card_from_config(config): produces a 1.x protobuf AgentCard. Pass to a2a.server.routes.create_agent_card_routes and a2a.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.json with 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-a2a 0.3.0 and later
  • TypeScript: @keycardai/a2a 0.4.0 and later
  • Go: github.com/keycardai/go-sdk v0.22.0 and later (a2a.DelegationClient)
  • Ruby: keycardai-a2a 0.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: provides AuthenticationMiddleware + KeycardAuthBackend and 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.

Metadata

Release files for keycardai-a2a 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for keycardai-a2a 0.5.1
File Size Uploaded Provenance Actions
keycardai_a2a-0.5.1.tar.gz 24.7 kB Without provenance Details for keycardai_a2a-0.5.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for keycardai-a2a 0.5.1
File Interpreter ABI Platform Actions
keycardai_a2a-0.5.1-py3-none-any.whl Python 3 none any Details for keycardai_a2a-0.5.1-py3-none-any.whl

Total release size: 42.0 kB

Release files / keycardai_a2a-0.5.1.tar.gz

Download URL keycardai_a2a-0.5.1.tar.gz
Size 24.7 kB
Tags Source
SHA-256 checksum
How to use checksums
f23b7e9e42c989105226046a2939cf9fdf7c5306df778523ed87b227c1e98659
BLAKE2b-256 checksum
How to use checksums
9120f8abb3fff0b9fa903d45b78d2f5cddde112d5fa3dd3170d000fed9c85d8b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","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.1-py3-none-any.whl

Download URL keycardai_a2a-0.5.1-py3-none-any.whl
Size 17.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
eee8861ae201c0ebcff2be9d67bcfdb39845de7fb808419c75e6d1fd6431e98e
BLAKE2b-256 checksum
How to use checksums
962f6e943ab276ab113f377844e710e6933e10453c4c9024e275ab39c3b5c9a2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","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}

Security for keycardai-a2a 0.5.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page