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.
- keycardai_a2a-0.2.0/.gitignore +193 -0
- keycardai_a2a-0.2.0/CHANGELOG.md +183 -0
- keycardai_a2a-0.2.0/PKG-INFO +162 -0
- keycardai_a2a-0.2.0/README.md +122 -0
- keycardai_a2a-0.2.0/examples/a2a_jsonrpc_usage/README.md +66 -0
- keycardai_a2a-0.2.0/examples/a2a_jsonrpc_usage/main.py +115 -0
- keycardai_a2a-0.2.0/examples/a2a_jsonrpc_usage/pyproject.toml +16 -0
- keycardai_a2a-0.2.0/examples/keycard_protected_server/README.md +37 -0
- keycardai_a2a-0.2.0/examples/keycard_protected_server/main.py +159 -0
- keycardai_a2a-0.2.0/examples/keycard_protected_server/pyproject.toml +16 -0
- keycardai_a2a-0.2.0/pyproject.toml +107 -0
- keycardai_a2a-0.2.0/src/keycardai/__init__.py +2 -0
- keycardai_a2a-0.2.0/src/keycardai/a2a/__init__.py +50 -0
- keycardai_a2a-0.2.0/src/keycardai/a2a/client/__init__.py +7 -0
- keycardai_a2a-0.2.0/src/keycardai/a2a/client/discovery.py +261 -0
- keycardai_a2a-0.2.0/src/keycardai/a2a/config.py +89 -0
- keycardai_a2a-0.2.0/src/keycardai/a2a/server/__init__.py +38 -0
- keycardai_a2a-0.2.0/src/keycardai/a2a/server/app.py +96 -0
- keycardai_a2a-0.2.0/src/keycardai/a2a/server/delegation.py +586 -0
|
@@ -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.
|