keycardai-a2a 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,193 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be added to the global gitignore or merged into this project gitignore. For a PyCharm
158
+ # project, uncomment below!
159
+ #.idea/
160
+
161
+ # VS Code
162
+ .vscode/
163
+
164
+ # OS generated files
165
+ .DS_Store
166
+ .DS_Store?
167
+ ._*
168
+ .Spotlight-V100
169
+ .Trashes
170
+ ehthumbs.db
171
+ Thumbs.db
172
+
173
+ # Backup files
174
+ *~
175
+ *.bak
176
+ *.backup
177
+ *.old
178
+ *.orig
179
+
180
+ # Temporary files
181
+ *.tmp
182
+ *.temp
183
+
184
+ # IDE files
185
+ *.swp
186
+ *.swo
187
+ *~
188
+
189
+ # Local development
190
+ .local/
191
+
192
+ # Claude Code
193
+ CLAUDE.md
@@ -0,0 +1,183 @@
1
+ ## 0.2.0-keycardai-a2a (2026-04-29)
2
+
3
+
4
+ - feat(keycardai-a2a): new package split from keycardai-agents (ACC-230) (#105)
5
+ - * feat(keycardai-a2a)!: new package split from keycardai-agents (ACC-230)
6
+ - Per the KEP "Decompose keycardai-agents", the A2A delegation surface moves
7
+ out of keycardai-agents into a new keycardai-a2a package, structurally
8
+ analogous to keycardai-mcp. Symbols available at the new namespace:
9
+ - - AgentServer, create_agent_card_server, serve_agent
10
+ - DelegationClient, DelegationClientSync
11
+ - AgentExecutor, SimpleExecutor, LambdaExecutor
12
+ - KeycardToA2AExecutorBridge
13
+ - ServiceDiscovery
14
+ - AgentServiceConfig
15
+ - The bearer middleware in server/app.py also migrates from the deprecated
16
+ BearerAuthMiddleware to the canonical KeycardAuthBackend +
17
+ AuthenticationMiddleware pattern from keycardai-starlette. The
18
+ keycardai-mcp dependency drops from this code path.
19
+ - Hard cut, no transitional bridge: ACC-232 confirms no known production
20
+ users of keycardai.agents.* paths.
21
+ - The PKCE user-login client (AgentClient) is dropped entirely. Its
22
+ capability already lives in keycardai-oauth as
23
+ keycardai.oauth.pkce.authenticate (ACC-229 / #101). The duplicate in
24
+ keycardai-agents is removed.
25
+ - What stays in keycardai-agents: the CrewAI integration only, with its
26
+ imports repointed at keycardai.a2a. ACC-231 will move it to a dedicated
27
+ keycardai-crewai; ACC-232 will archive the now-stub source directory.
28
+ - BREAKING:
29
+ - from keycardai.agents import AgentServer, DelegationClient, ...
30
+ becomes from keycardai.a2a import ... .
31
+ - from keycardai.agents.client import AgentClient is gone; use
32
+ keycardai.oauth.pkce.authenticate.
33
+ - keycardai-agents 0.3.0 ships with the dependency set reduced to
34
+ keycardai-a2a + pydantic, mirroring its now-CrewAI-only scope.
35
+ - * fix(keycardai-a2a): apply migration edits to moved files (ACC-230)
36
+ - The first commit on this branch did the git mv's but staged the new
37
+ files only; the Edit-tool modifications to the moved files (import
38
+ rewrites, server/app.py bearer-wiring migration to KeycardAuthBackend +
39
+ AuthenticationMiddleware, example pyproject swap from keycardai-agents
40
+ to keycardai-a2a, conftest/tests import repoints, agents/__init__.py
41
+ trim, agents/pyproject dep set, crewai integration repoint, top-level
42
+ workspace sources, justfile test recipe, uv.lock refresh) all sat
43
+ uncommitted. CI rejected the prior commit because the example pyproject
44
+ still claimed keycardai-agents at packages/a2a/, conflicting with the
45
+ real keycardai-agents at packages/agents/ in the workspace graph.
46
+ - This commit lands the actual migration content. Tests pass locally:
47
+ keycardai-a2a 60/60, keycardai-agents 16/16, no regression in oauth /
48
+ starlette / mcp / mcp-fastmcp / fastmcp; ruff workspace check clean.
49
+ - * refactor(keycardai-a2a)!: wrap a2a-sdk 1.x, drop parallel surface (ACC-230)
50
+ - Aligns keycardai-a2a with the wrap-do-not-reinvent pattern used in
51
+ keycardai-mcp and keycardai-starlette: customers implement a2a-sdk native
52
+ async AgentExecutor directly; this package contributes only Keycard auth
53
+ wiring, OAuth metadata discovery, and convenience composition.
54
+ - Drops the parallel-protocol surface inherited from the keycardai-agents
55
+ move:
56
+ - AgentExecutor protocol (sync execute(task, inputs)) and SimpleExecutor /
57
+ LambdaExecutor implementations
58
+ - KeycardToA2AExecutorBridge (the sync->async adapter that existed only to
59
+ bridge our protocol to a2a-sdk)
60
+ - Custom POST /invoke endpoint with bespoke InvokeRequest / InvokeResponse
61
+ Pydantic models alongside the standard A2A JSONRPC interface
62
+ - AgentServiceConfig.invoke_url (replaced by jsonrpc_url) and
63
+ AgentServiceConfig.to_agent_card() (the 0.x dict-shape constructor)
64
+ - Migrates from a2a-sdk 0.x to 1.x natively:
65
+ - pyproject pin a2a-sdk[http-server]>=1.0
66
+ - Server composition uses route factories (create_jsonrpc_routes,
67
+ create_agent_card_routes) instead of the gone A2AStarletteApplication
68
+ - Request handler is DefaultRequestHandlerV2 (alias DefaultRequestHandler)
69
+ - AgentCard built from 1.x protobuf schema (supported_interfaces,
70
+ AgentCapabilities streaming/push_notifications/extended_agent_card)
71
+ - Example main.py uses a2a-sdk 1.x Client via create_client + A2ACardResolver
72
+ - Adds a KeycardServerCallContextBuilder that subclasses a2a-sdk default
73
+ builder and stashes the verified KeycardUser plus access_token into
74
+ ServerCallContext.state so AgentExecutor implementations can read the
75
+ bearer token from context.call_context.state["access_token"] for
76
+ downstream delegated token exchange.
77
+ - Tests:
78
+ - a2a 44/44 pass
79
+ - agents 16/16 pass with crewai extra
80
+ - ruff clean
81
+ - Note: the high-level @auth.grant decorator parity with keycardai-mcp is
82
+ not yet shipped here. Customers use DelegationClient (already in this
83
+ package) for explicit server-to-server delegation. The decorator port is
84
+ a follow-up.
85
+ - * fix(keycardai-a2a): address review findings on PR #105
86
+ - Three blockers caught in fresh-eyes review:
87
+ - 1. release.yml tag-trigger list was hardcoded; *-keycardai-a2a was missing,
88
+ so the post-merge auto-bump would push the tag but the publish workflow
89
+ would never trigger. Trusted Publisher being registered would have been
90
+ moot.
91
+ - 2. DelegationClient.invoke_service hardcoded service_url + /invoke. The
92
+ wrap-aligned server only exposes /a2a/jsonrpc; calling invoke_service
93
+ against any 1.x server returned 404. The CrewAI delegation tool runs
94
+ through this code path. Both async and sync variants now build a
95
+ message/send JSONRPC envelope, POST it to /a2a/jsonrpc, and unwrap the
96
+ result to preserve the legacy {result, delegation_chain} shape so the
97
+ CrewAI integration keeps working unchanged.
98
+ - 3. discover_service in both DelegationClient and ServiceDiscovery validated
99
+ the 0.x card shape (required_fields = [name, endpoints, auth]). The 1.x
100
+ protobuf-derived JSON has none of endpoints / auth. Discovery against
101
+ any 1.x server raised ValueError. Validation now requires only "name";
102
+ transport / auth specifics live under supportedInterfaces and the OAuth
103
+ metadata routes.
104
+ - Plus four important findings:
105
+ - 4. Test mocks across conftest.py, test_a2a_client.py, test_discovery.py,
106
+ and test_crewai_a2a.py used the old shape (endpoints/auth keys). Tests
107
+ passed because the validator wrongly accepted them. Mocks now use the
108
+ 1.x JSON shape (supportedInterfaces, capabilities object, skills with
109
+ id/name).
110
+ - 5. A2AServiceClient and A2AServiceClientSync backward-compat aliases at
111
+ the bottom of delegation.py contradicted the "hard cut, no transitional
112
+ bridge" stance in the PR description. Removed.
113
+ - 6. TestJsonRpcAuthGate.test_jsonrpc_requires_authorization asserted
114
+ status_code in (400, 401). 400 means the JSONRPC dispatcher saw the
115
+ request and bailed on the body shape, not that the auth gate caught
116
+ it. Pinned to == 401 with a WWW-Authenticate header check so the gate
117
+ contract is enforced.
118
+ - 7. Zero coverage existed for _KeycardServerCallContextBuilder propagating
119
+ the verified KeycardUser plus access_token into ServerCallContext.state.
120
+ Added two unit tests that build the context directly: one with a
121
+ KeycardUser asserting state["access_token"] is set, one with an
122
+ UnauthenticatedUser asserting state["access_token"] is absent (so an
123
+ executor reading it sees None rather than a stale token).
124
+ - Tests:
125
+ - a2a 47/47 (was 44; +3 new wrap-coverage tests)
126
+ - agents 16/16 with crewai extra
127
+ - ruff clean
128
+ - * refactor(keycardai-a2a)!: ship primitives, not a server abstraction (ACC-230)
129
+ - Per Kamil's review on PR #105: AgentServer / create_agent_card_server /
130
+ serve_agent presupposed customers want a fresh Starlette app dedicated to
131
+ the agent service. The wrap-don't-reinvent stance, taken seriously,
132
+ says: customers already have an a2a-sdk app in their head; we ship
133
+ primitives that slot Keycard auth into THAT, not a parallel server.
134
+ - Public surface change:
135
+ - Dropped:
136
+ AgentServer, create_agent_card_server, serve_agent
137
+ Promoted to public (renamed off the underscore prefix):
138
+ EagerKeycardAuthBackend
139
+ KeycardServerCallContextBuilder
140
+ build_agent_card_from_config
141
+ - AgentServiceConfig trimmed: dropped agent_executor (DefaultRequestHandler
142
+ takes its own), port and host (uvicorn's job), status_url (no /status
143
+ in the primitives layer).
144
+ - The composed-server flow moves to a runnable example at
145
+ packages/a2a/examples/keycard_protected_server/. README quickstart
146
+ rewritten to show primitive composition into an existing app; greenfield
147
+ users follow the example.
148
+ - Tests:
149
+ a2a 44/44 (was 47; net -3 from dropping the /status endpoint tests
150
+ and the port-validation test)
151
+ agents 16/16 with crewai extra
152
+ ruff clean
153
+ - This change is breaking, but the package is 0.1.0-pre-publish so no
154
+ customer is on these names yet.
155
+ - * fix(keycardai-a2a): ruff import-organization auto-fix
156
+ - * refactor(keycardai-starlette,keycardai-a2a): collapse EagerKeycardAuthBackend into KeycardAuthBackend kwarg (ACC-230)
157
+ - Per Kamil's second review observation on PR #105: with keycardai-a2a
158
+ now depending on keycardai-starlette, the question of WHERE these
159
+ primitives live matters. EagerKeycardAuthBackend was a 5-line subclass
160
+ that flipped one branch of KeycardAuthBackend.authenticate to raise on
161
+ missing Authorization, with no a2a-sdk specifics. The behavior is a
162
+ policy choice ("this mount requires auth"), not a different kind of
163
+ backend.
164
+ - Collapsed to a kwarg on the existing class:
165
+ - KeycardAuthBackend(verifier) # default,
166
+ # mixed-route
167
+ KeycardAuthBackend(verifier, require_authentication=True) # all-paths-protected
168
+ - The OAuth metadata bypass (RFC 9728 §2 / RFC 8414 §3) takes precedence
169
+ over the kwarg: even with require_authentication=True, requests to
170
+ /.well-known/oauth-* and /.well-known/jwks.json still pass through
171
+ anonymously per spec. New parametrized test asserts this.
172
+ - Net effect:
173
+ - One class instead of two; existing KeycardAuthBackend(verifier) callers
174
+ unchanged.
175
+ - keycardai-a2a no longer ships EagerKeycardAuthBackend; the kwarg is
176
+ used directly in tests, the example, and the README quickstart.
177
+ - Migration story is zero churn for existing users; new behavior is
178
+ opt-in via the kwarg.
179
+ - Tests:
180
+ starlette 40 passed (+2 new tests for the kwarg semantics)
181
+ a2a 44 passed
182
+ agents 16 passed
183
+ ruff clean
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: keycardai-a2a
3
+ Version: 0.2.0
4
+ Summary: A2A delegation SDK for Keycard: agent-to-agent token exchange, agent service hosting, and service discovery.
5
+ Project-URL: Homepage, https://github.com/keycardai/python-sdk
6
+ Project-URL: Repository, https://github.com/keycardai/python-sdk
7
+ Project-URL: Documentation, https://docs.keycardai.com
8
+ Project-URL: Issues, https://github.com/keycardai/python-sdk/issues
9
+ Author-email: Keycard <support@keycard.ai>
10
+ License: MIT
11
+ Keywords: a2a,agent-to-agent,agents,ai,authentication,delegation,keycard,oauth,token-exchange
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Security
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: a2a-sdk[http-server]>=1.0
26
+ Requires-Dist: httpx>=0.27.2
27
+ Requires-Dist: keycardai-oauth>=0.11.0
28
+ Requires-Dist: keycardai-starlette>=0.3.0
29
+ Requires-Dist: pydantic>=2.11.7
30
+ Requires-Dist: uvicorn[standard]>=0.32.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: mypy>=1.14.1; extra == 'dev'
33
+ Requires-Dist: ruff>=0.8.6; extra == 'dev'
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest-asyncio>=1.1.0; extra == 'test'
36
+ Requires-Dist: pytest-cov>=6.2.1; extra == 'test'
37
+ Requires-Dist: pytest-timeout>=2.3.1; extra == 'test'
38
+ Requires-Dist: pytest>=8.4.1; extra == 'test'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # keycardai-a2a
42
+
43
+ Keycard auth primitives for [a2a-sdk](https://github.com/a2aproject/A2A) 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.
44
+
45
+ > **Preview.** This package is pre-1.0. APIs may change between minor versions.
46
+
47
+ ## What's in here
48
+
49
+ Server-side wiring:
50
+
51
+ - **`KeycardServerCallContextBuilder`**: a `ServerCallContextBuilder` subclass. Pass to `a2a.server.routes.create_jsonrpc_routes`. Propagates the verified bearer token onto `ServerCallContext.state["access_token"]` so executors can read it for delegated downstream calls.
52
+ - **`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`.
53
+
54
+ 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.
55
+
56
+ Outbound delegation:
57
+
58
+ - **`DelegationClient`**, **`DelegationClientSync`**: server-to-server token exchange and JSONRPC invocation against another agent service.
59
+
60
+ Inbound discovery:
61
+
62
+ - **`ServiceDiscovery`**: query a remote agent service's `.well-known/agent-card.json` with caching.
63
+
64
+ Configuration:
65
+
66
+ - **`AgentServiceConfig`**: service identity + Keycard credentials + agent card metadata.
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ pip install keycardai-a2a
72
+ ```
73
+
74
+ This pulls in `keycardai-oauth`, `keycardai-starlette`, `a2a-sdk[http-server]>=1.0`.
75
+
76
+ ## Quick start
77
+
78
+ You already have an `a2a-sdk` server. Add the Keycard-protected A2A mount to your existing Starlette / FastAPI app:
79
+
80
+ ```python
81
+ from a2a.server.request_handlers import DefaultRequestHandler
82
+ from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
83
+ from a2a.server.tasks import InMemoryTaskStore
84
+ from starlette.middleware import Middleware
85
+ from starlette.middleware.authentication import AuthenticationMiddleware
86
+ from starlette.routing import Mount
87
+
88
+ from keycardai.a2a import (
89
+ AgentServiceConfig,
90
+ KeycardServerCallContextBuilder,
91
+ build_agent_card_from_config,
92
+ )
93
+ from keycardai.oauth.server.credentials import ClientSecret
94
+ from keycardai.starlette import AuthProvider, KeycardAuthBackend, keycard_on_error
95
+ from keycardai.starlette.routers.metadata import (
96
+ well_known_authorization_server_route,
97
+ well_known_protected_resource_route,
98
+ )
99
+
100
+ config = AgentServiceConfig(
101
+ service_name="My Agent",
102
+ client_id="...",
103
+ client_secret="...",
104
+ identity_url="https://my-agent.example.com",
105
+ zone_id="your-zone-id",
106
+ capabilities=["chat"],
107
+ )
108
+ auth_provider = AuthProvider(
109
+ zone_url=config.auth_server_url,
110
+ server_name=config.service_name,
111
+ server_url=config.identity_url,
112
+ application_credential=ClientSecret((config.client_id, config.client_secret)),
113
+ )
114
+ verifier = auth_provider.get_token_verifier()
115
+
116
+ agent_card = build_agent_card_from_config(config)
117
+ request_handler = DefaultRequestHandler(
118
+ agent_executor=YourExecutor(), # subclass of a2a.server.agent_execution.AgentExecutor
119
+ task_store=InMemoryTaskStore(),
120
+ agent_card=agent_card,
121
+ )
122
+
123
+ # Add these routes to your existing Starlette / FastAPI app:
124
+ your_app.routes.extend(create_agent_card_routes(agent_card=agent_card))
125
+ your_app.routes.append(well_known_protected_resource_route(
126
+ issuer=config.auth_server_url,
127
+ resource="/.well-known/oauth-protected-resource{resource_path:path}",
128
+ ))
129
+ your_app.routes.append(well_known_authorization_server_route(
130
+ issuer=config.auth_server_url,
131
+ resource="/.well-known/oauth-authorization-server{resource_path:path}",
132
+ ))
133
+ your_app.routes.append(Mount(
134
+ "/a2a",
135
+ routes=create_jsonrpc_routes(
136
+ request_handler=request_handler,
137
+ rpc_url="/jsonrpc",
138
+ context_builder=KeycardServerCallContextBuilder(),
139
+ ),
140
+ middleware=[
141
+ Middleware(
142
+ AuthenticationMiddleware,
143
+ backend=KeycardAuthBackend(verifier, require_authentication=True),
144
+ on_error=keycard_on_error,
145
+ ),
146
+ ],
147
+ ))
148
+ ```
149
+
150
+ Inside your `AgentExecutor.execute(self, context, event_queue)`, read the bearer token via `context.call_context.state["access_token"]` and use it as the subject token in `keycardai-oauth`'s `TokenExchangeRequest` for downstream API calls.
151
+
152
+ For a runnable greenfield example (no existing app), see `examples/keycard_protected_server/`.
153
+
154
+ ## Relationship to other Keycard packages
155
+
156
+ - **`keycardai-oauth`**: OAuth 2.0 primitives used for token exchange and PKCE.
157
+ - **`keycardai-starlette`**: provides `AuthenticationMiddleware` + `KeycardAuthBackend` and the OAuth metadata route helpers used here.
158
+ - **`keycardai-mcp`**: sister package for MCP server protection. Same auth shape, different protocol.
159
+
160
+ ## History
161
+
162
+ This package was extracted from the original `keycardai-agents` package (KEP: Decompose keycardai-agents). The PKCE user-login client moved to `keycardai-oauth`; the CrewAI integration moves to a forthcoming `keycardai-crewai`; the `keycardai-agents` source directory is being archived.
@@ -0,0 +1,122 @@
1
+ # keycardai-a2a
2
+
3
+ Keycard auth primitives for [a2a-sdk](https://github.com/a2aproject/A2A) 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.
4
+
5
+ > **Preview.** This package is pre-1.0. APIs may change between minor versions.
6
+
7
+ ## What's in here
8
+
9
+ Server-side wiring:
10
+
11
+ - **`KeycardServerCallContextBuilder`**: a `ServerCallContextBuilder` subclass. Pass to `a2a.server.routes.create_jsonrpc_routes`. Propagates the verified bearer token onto `ServerCallContext.state["access_token"]` so executors can read it for delegated downstream calls.
12
+ - **`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`.
13
+
14
+ 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.
15
+
16
+ Outbound delegation:
17
+
18
+ - **`DelegationClient`**, **`DelegationClientSync`**: server-to-server token exchange and JSONRPC invocation against another agent service.
19
+
20
+ Inbound discovery:
21
+
22
+ - **`ServiceDiscovery`**: query a remote agent service's `.well-known/agent-card.json` with caching.
23
+
24
+ Configuration:
25
+
26
+ - **`AgentServiceConfig`**: service identity + Keycard credentials + agent card metadata.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install keycardai-a2a
32
+ ```
33
+
34
+ This pulls in `keycardai-oauth`, `keycardai-starlette`, `a2a-sdk[http-server]>=1.0`.
35
+
36
+ ## Quick start
37
+
38
+ You already have an `a2a-sdk` server. Add the Keycard-protected A2A mount to your existing Starlette / FastAPI app:
39
+
40
+ ```python
41
+ from a2a.server.request_handlers import DefaultRequestHandler
42
+ from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
43
+ from a2a.server.tasks import InMemoryTaskStore
44
+ from starlette.middleware import Middleware
45
+ from starlette.middleware.authentication import AuthenticationMiddleware
46
+ from starlette.routing import Mount
47
+
48
+ from keycardai.a2a import (
49
+ AgentServiceConfig,
50
+ KeycardServerCallContextBuilder,
51
+ build_agent_card_from_config,
52
+ )
53
+ from keycardai.oauth.server.credentials import ClientSecret
54
+ from keycardai.starlette import AuthProvider, KeycardAuthBackend, keycard_on_error
55
+ from keycardai.starlette.routers.metadata import (
56
+ well_known_authorization_server_route,
57
+ well_known_protected_resource_route,
58
+ )
59
+
60
+ config = AgentServiceConfig(
61
+ service_name="My Agent",
62
+ client_id="...",
63
+ client_secret="...",
64
+ identity_url="https://my-agent.example.com",
65
+ zone_id="your-zone-id",
66
+ capabilities=["chat"],
67
+ )
68
+ auth_provider = AuthProvider(
69
+ zone_url=config.auth_server_url,
70
+ server_name=config.service_name,
71
+ server_url=config.identity_url,
72
+ application_credential=ClientSecret((config.client_id, config.client_secret)),
73
+ )
74
+ verifier = auth_provider.get_token_verifier()
75
+
76
+ agent_card = build_agent_card_from_config(config)
77
+ request_handler = DefaultRequestHandler(
78
+ agent_executor=YourExecutor(), # subclass of a2a.server.agent_execution.AgentExecutor
79
+ task_store=InMemoryTaskStore(),
80
+ agent_card=agent_card,
81
+ )
82
+
83
+ # Add these routes to your existing Starlette / FastAPI app:
84
+ your_app.routes.extend(create_agent_card_routes(agent_card=agent_card))
85
+ your_app.routes.append(well_known_protected_resource_route(
86
+ issuer=config.auth_server_url,
87
+ resource="/.well-known/oauth-protected-resource{resource_path:path}",
88
+ ))
89
+ your_app.routes.append(well_known_authorization_server_route(
90
+ issuer=config.auth_server_url,
91
+ resource="/.well-known/oauth-authorization-server{resource_path:path}",
92
+ ))
93
+ your_app.routes.append(Mount(
94
+ "/a2a",
95
+ routes=create_jsonrpc_routes(
96
+ request_handler=request_handler,
97
+ rpc_url="/jsonrpc",
98
+ context_builder=KeycardServerCallContextBuilder(),
99
+ ),
100
+ middleware=[
101
+ Middleware(
102
+ AuthenticationMiddleware,
103
+ backend=KeycardAuthBackend(verifier, require_authentication=True),
104
+ on_error=keycard_on_error,
105
+ ),
106
+ ],
107
+ ))
108
+ ```
109
+
110
+ Inside your `AgentExecutor.execute(self, context, event_queue)`, read the bearer token via `context.call_context.state["access_token"]` and use it as the subject token in `keycardai-oauth`'s `TokenExchangeRequest` for downstream API calls.
111
+
112
+ For a runnable greenfield example (no existing app), see `examples/keycard_protected_server/`.
113
+
114
+ ## Relationship to other Keycard packages
115
+
116
+ - **`keycardai-oauth`**: OAuth 2.0 primitives used for token exchange and PKCE.
117
+ - **`keycardai-starlette`**: provides `AuthenticationMiddleware` + `KeycardAuthBackend` and the OAuth metadata route helpers used here.
118
+ - **`keycardai-mcp`**: sister package for MCP server protection. Same auth shape, different protocol.
119
+
120
+ ## History
121
+
122
+ This package was extracted from the original `keycardai-agents` package (KEP: Decompose keycardai-agents). The PKCE user-login client moved to `keycardai-oauth`; the CrewAI integration moves to a forthcoming `keycardai-crewai`; the `keycardai-agents` source directory is being archived.