a2acode 0.4.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.
- a2acode-0.4.0/PKG-INFO +215 -0
- a2acode-0.4.0/README.md +194 -0
- a2acode-0.4.0/pyproject.toml +66 -0
- a2acode-0.4.0/src/a2acode/__init__.py +9 -0
- a2acode-0.4.0/src/a2acode/auth.py +87 -0
- a2acode-0.4.0/src/a2acode/backends/__init__.py +51 -0
- a2acode-0.4.0/src/a2acode/backends/acp.py +291 -0
- a2acode-0.4.0/src/a2acode/backends/base.py +96 -0
- a2acode-0.4.0/src/a2acode/backends/claude.py +125 -0
- a2acode-0.4.0/src/a2acode/backends/diff.py +79 -0
- a2acode-0.4.0/src/a2acode/backends/echo.py +47 -0
- a2acode-0.4.0/src/a2acode/backends/session.py +132 -0
- a2acode-0.4.0/src/a2acode/card.py +187 -0
- a2acode-0.4.0/src/a2acode/cli.py +295 -0
- a2acode-0.4.0/src/a2acode/executor.py +365 -0
- a2acode-0.4.0/src/a2acode/py.typed +0 -0
- a2acode-0.4.0/src/a2acode/server.py +88 -0
- a2acode-0.4.0/src/a2acode/tracing.py +41 -0
a2acode-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: a2acode
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Serve Claude Code and other ACP coding agents over the A2A protocol.
|
|
5
|
+
Keywords: a2a,acp,claude-code,agent,agent2agent
|
|
6
|
+
Author: kanywst
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Dist: a2a-sdk[http-server,signing]>=1.1,<2
|
|
9
|
+
Requires-Dist: agent-client-protocol>=0.10,<1
|
|
10
|
+
Requires-Dist: uvicorn>=0.49
|
|
11
|
+
Requires-Dist: httpx>=0.28
|
|
12
|
+
Requires-Dist: typer>=0.26
|
|
13
|
+
Requires-Dist: claude-agent-sdk>=0.2.101 ; extra == 'claude'
|
|
14
|
+
Requires-Dist: a2a-sdk[telemetry]>=1.1,<2 ; extra == 'telemetry'
|
|
15
|
+
Requires-Dist: opentelemetry-api>=1.33 ; extra == 'telemetry'
|
|
16
|
+
Requires-Python: >=3.13
|
|
17
|
+
Project-URL: Repository, https://github.com/kanywst/a2acode
|
|
18
|
+
Provides-Extra: claude
|
|
19
|
+
Provides-Extra: telemetry
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<img src="assets/mascot.png" alt="a2acode" width="150" align="right">
|
|
23
|
+
|
|
24
|
+
# a2acode
|
|
25
|
+
|
|
26
|
+
Serve a coding agent over the [A2A](https://a2aprotocol.ai/) protocol. Other agents call it over A2A; it drives a real coding-agent session in your project — Claude Code, or any agent that speaks Zed's [Agent Client Protocol](https://agentclientprotocol.com) (ACP): Gemini CLI, Codex, OpenHands, and more — and streams the work back as it happens.
|
|
27
|
+
|
|
28
|
+
[](https://github.com/kanywst/a2acode/actions/workflows/ci.yml)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
[](https://www.python.org/)
|
|
31
|
+
[](https://a2aprotocol.ai/)
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
Most adapters that put a coding agent behind A2A flatten everything to text in, text out. a2acode keeps the structure the agent produces: the tools it runs, the files it changes, what it costs, the approvals it needs, and how to continue on the next turn. It bridges two Linux Foundation interop standards — **ACP** (how editors and clients talk to coding agents) on the agent side, **A2A** (how agents delegate to each other) on the caller side — so any ACP agent becomes a peer any A2A orchestrator can call.
|
|
36
|
+
|
|
37
|
+
## How it maps to A2A
|
|
38
|
+
|
|
39
|
+
| The coding agent produces | A2A surface it lands on |
|
|
40
|
+
| ------------------------- | -------------------------------------------------- |
|
|
41
|
+
| Assistant text | A streamed artifact (`append` / `last_chunk`) |
|
|
42
|
+
| A tool call (Bash, Edit) | A `working` status update for the action |
|
|
43
|
+
| A file edit (diff) | A named artifact carrying the diff |
|
|
44
|
+
| A permission request | An `input-required` pause the caller answers |
|
|
45
|
+
| Run result | Cost, turns, and usage on the completion message |
|
|
46
|
+
| Session id | Mapped to the A2A `contextId` so follow-ups resume |
|
|
47
|
+
|
|
48
|
+
The mapping is all in `executor.py`. Backends only emit normalized events; they never touch the protocol.
|
|
49
|
+
|
|
50
|
+
## Where this fits
|
|
51
|
+
|
|
52
|
+
Anthropic now ships its own ways to run Claude Code beyond the terminal: Claude Code on the web, background agents, cloud-hosted Routines, and the Managed Agents API. These are the right choices when you want Anthropic to host the run and you live in their ecosystem, and they are typically tied to Anthropic infrastructure and a GitHub-centric flow.
|
|
53
|
+
|
|
54
|
+
a2acode solves a different problem: making any coding agent a first-class peer on a vendor-neutral [A2A](https://a2aprotocol.ai/) mesh. An orchestrator built on any framework discovers it through its agent card and delegates coding work the same way it would to any other A2A agent. The run happens on infrastructure you control, in a workspace you point it at. Reach for a2acode when:
|
|
55
|
+
|
|
56
|
+
- another agent (not a human at a prompt) is the caller, and it speaks A2A;
|
|
57
|
+
- you want the run on your own infrastructure and data boundary, not a vendor VM;
|
|
58
|
+
- you do not want to bet on one vendor's coding agent: ACP makes the backend a launch-command choice, so swapping Claude Code for Codex, Gemini CLI, or OpenHands does not touch the protocol surface your callers depend on.
|
|
59
|
+
|
|
60
|
+
ACP already standardizes the editor↔agent side and a dozen agents speak it; a2acode is the piece that exposes an ACP agent to *remote autonomous callers* over A2A, with permission round-trips and cost preserved as first-class protocol citizens — the part ACP leaves out because it assumes a human in an editor. The practical user is the platform team building that mesh, not the individual developer.
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
- Python 3.13+
|
|
65
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
66
|
+
- An ACP agent adapter for the `acp` backend, launched as a subprocess. The `claude` preset uses `npx @zed-industries/claude-agent-acp` (needs Node and a Claude credential); `gemini` uses the Gemini CLI; or point `--agent-command` at any ACP agent.
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
Install:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv sync
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `echo` backend needs no API key and no Claude install, so you can exercise the whole path offline first:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv run a2acode serve --backend echo &
|
|
80
|
+
# once the "Uvicorn running" line appears:
|
|
81
|
+
uv run a2acode call "fix the failing test"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
task 189b1c63-1a7b-4908-87c4-c8f3bba8f6b5
|
|
86
|
+
context 0b2a901e-2b6f-4c56-bba2-d0da546936e9
|
|
87
|
+
|
|
88
|
+
· Echo
|
|
89
|
+
fix the failing test
|
|
90
|
+
[completed] $0.0 · 1 turns
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then point it at a real project. The default backend is `acp`, fronting Claude Code through its ACP adapter:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv run a2acode serve --cwd /path/to/project # acp + claude by default
|
|
97
|
+
uv run a2acode call "add a /health endpoint" --url http://localhost:9100/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Swap the agent without touching anything else:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv run a2acode serve --agent gemini --cwd /path/to/project
|
|
104
|
+
uv run a2acode serve --agent-command "npx -y some-other-acp-agent"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Continue the same conversation by passing the `context` from a previous turn:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv run a2acode call "now add a test for it" --context <context-id>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Commands
|
|
114
|
+
|
|
115
|
+
| Command | Description |
|
|
116
|
+
| -------------------- | -------------------------------------------- |
|
|
117
|
+
| `a2acode serve` | Start the A2A server |
|
|
118
|
+
| `a2acode call TEXT` | Send a message and print the streamed events |
|
|
119
|
+
| `a2acode card` | Fetch and print the agent card |
|
|
120
|
+
|
|
121
|
+
The agent card is served at `/.well-known/agent-card.json` and advertises Claude Code's abilities as discrete skills (generation, refactor, debug, review, test, explain).
|
|
122
|
+
|
|
123
|
+
## Backends
|
|
124
|
+
|
|
125
|
+
A backend turns a prompt into a stream of normalized events. Three ship today:
|
|
126
|
+
|
|
127
|
+
- `acp` (default): drives any agent that speaks Zed's Agent Client Protocol as a subprocess. `--agent claude|gemini|codex` selects a launch preset; `--agent-command` drives any other ACP agent. This is the vendor-neutral path.
|
|
128
|
+
- `claude`: drives Claude Code directly through the Claude Agent SDK, no subprocess. Install with `uv sync --extra claude`. Use it when you want the SDK-native path (e.g. `--max-budget-usd`) rather than ACP.
|
|
129
|
+
- `echo`: no dependencies, mirrors the input. For wiring checks and tests.
|
|
130
|
+
|
|
131
|
+
The split keeps the A2A layer independent of how the agent is invoked: backends emit normalized events and never import `a2a.*`; the executor maps those events onto the protocol and never imports an agent SDK. Adding a backend never touches the server or the protocol mapping.
|
|
132
|
+
|
|
133
|
+
## Authentication
|
|
134
|
+
|
|
135
|
+
Each agent authenticates the way its own tooling does, inherited from the server's environment: the `acp` backend passes the environment through to the adapter subprocess (e.g. `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`), and the `claude` backend uses whatever the Claude CLI is configured with. When the server answers on behalf of other agents, a Claude credential has to be an Anthropic API key (or Bedrock / Vertex); Anthropic does not permit subscription credentials for third-party serving. The `claude` backend can cap per-run cost with `--max-budget-usd`.
|
|
136
|
+
|
|
137
|
+
## Signed agent cards
|
|
138
|
+
|
|
139
|
+
A caller that discovers this server only has the agent card to go on. Sign it so the caller can confirm the card came from you and was not swapped in transit:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv run a2acode serve --sign-key card-signing.pem --sign-kid my-key-1 --sign-alg ES256
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The card is then served with a JWS signature over its canonical form. `--sign-key` is a path to a file holding the key: a PEM private key for asymmetric algorithms (`ES256`, `RS256`), or a shared secret for `HS256`. `--sign-kid` is the key id a verifier uses to look up the matching public key. Unsigned is still the default.
|
|
146
|
+
|
|
147
|
+
## Caller authentication
|
|
148
|
+
|
|
149
|
+
A signed card proves who the server is; this proves the caller is allowed in. Require a bearer token and the server rejects any task request that does not carry it:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
uv run a2acode serve --auth-token-file caller-token.txt
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
When `--auth-token-file` is set, callers must send `Authorization: Bearer <token>`; a request without a valid token gets `401 Unauthorized`. The agent card stays public so a caller can still fetch it to discover the requirement, and the card advertises the bearer scheme in `securitySchemes`. Without the flag the server stays open, as before.
|
|
156
|
+
|
|
157
|
+
A2A keeps the credential at the HTTP layer, so this composes with whatever your gateway already does: terminate TLS, validate OAuth, or rate-limit in front, and let the server enforce the token behind it.
|
|
158
|
+
|
|
159
|
+
## Permissions
|
|
160
|
+
|
|
161
|
+
A tool that needs approval pauses the task in the A2A `input-required` state instead of being skipped. The caller answers with a follow-up message on the same task:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
uv run a2acode call "sudo reboot"
|
|
165
|
+
# ... [input-required] Permission requested for Bash: $ sudo reboot
|
|
166
|
+
# reply: a2acode call "allow" --task <id> --context <id>
|
|
167
|
+
uv run a2acode call "allow" --task <id> --context <id>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`allow` (or `yes`, `approve`, `ok`) approves; anything else denies. The agent session stays alive across the pause, so it resumes exactly where it stopped. Over ACP this is the agent's `session/request_permission` call answered from the A2A caller's reply; with the `claude` backend it routes through the Claude SDK's `can_use_tool`.
|
|
171
|
+
|
|
172
|
+
Whatever the agent decides needs approval becomes an `input-required` pause rather than being silently skipped or auto-approved; the caller, not the server, holds the decision. Read-only actions the agent already treats as safe still run without a prompt.
|
|
173
|
+
|
|
174
|
+
## Long-running tasks
|
|
175
|
+
|
|
176
|
+
The agent card advertises push notifications. A caller can register a webhook for a task and receive status and artifact updates by HTTP POST instead of holding a stream open, which helps when a run takes minutes. Streaming and polling (`tasks/get`) both work too.
|
|
177
|
+
|
|
178
|
+
## Observability
|
|
179
|
+
|
|
180
|
+
Debugging one agent is hard; debugging a chain of them without traces is worse. Because A2A runs over HTTP, it drops straight into OpenTelemetry: install the extra and the A2A SDK's instrumentation plus a per-task `a2acode.execute` span light up, with W3C trace context propagating across the call so client and server spans share one trace.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
uv sync --extra telemetry
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Tracing is off unless OpenTelemetry is installed, and you configure the exporter the standard way (e.g. `OTEL_EXPORTER_OTLP_ENDPOINT`, or run under `opentelemetry-instrument`). It works against an on-prem or air-gapped collector, so traces never have to leave your network.
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
uv sync --dev
|
|
192
|
+
uv run ruff check src tests
|
|
193
|
+
uv run ruff format src tests
|
|
194
|
+
uv run mypy
|
|
195
|
+
uv run pytest
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
CI runs these on Python 3.13 and 3.14, plus a Markdown lint, on every push and pull request.
|
|
199
|
+
|
|
200
|
+
## Releasing
|
|
201
|
+
|
|
202
|
+
Pushing a `v*` tag builds the package, creates a GitHub release with the artifacts, and publishes to PyPI via trusted publishing:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
git tag v0.1.0
|
|
206
|
+
git push origin v0.1.0
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Status
|
|
210
|
+
|
|
211
|
+
The mapping is complete end to end and verified against real Claude: text round trip, tool-progress updates, streaming artifacts, file diffs as artifacts, run metadata, session continuity, the permission-to-`input-required` round trip, and push notifications. The offline `echo` backend covers every path including permissions, so it can all be exercised without an API key.
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
Apache 2.0. See [LICENSE](LICENSE).
|
a2acode-0.4.0/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<img src="assets/mascot.png" alt="a2acode" width="150" align="right">
|
|
2
|
+
|
|
3
|
+
# a2acode
|
|
4
|
+
|
|
5
|
+
Serve a coding agent over the [A2A](https://a2aprotocol.ai/) protocol. Other agents call it over A2A; it drives a real coding-agent session in your project — Claude Code, or any agent that speaks Zed's [Agent Client Protocol](https://agentclientprotocol.com) (ACP): Gemini CLI, Codex, OpenHands, and more — and streams the work back as it happens.
|
|
6
|
+
|
|
7
|
+
[](https://github.com/kanywst/a2acode/actions/workflows/ci.yml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://www.python.org/)
|
|
10
|
+
[](https://a2aprotocol.ai/)
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
Most adapters that put a coding agent behind A2A flatten everything to text in, text out. a2acode keeps the structure the agent produces: the tools it runs, the files it changes, what it costs, the approvals it needs, and how to continue on the next turn. It bridges two Linux Foundation interop standards — **ACP** (how editors and clients talk to coding agents) on the agent side, **A2A** (how agents delegate to each other) on the caller side — so any ACP agent becomes a peer any A2A orchestrator can call.
|
|
15
|
+
|
|
16
|
+
## How it maps to A2A
|
|
17
|
+
|
|
18
|
+
| The coding agent produces | A2A surface it lands on |
|
|
19
|
+
| ------------------------- | -------------------------------------------------- |
|
|
20
|
+
| Assistant text | A streamed artifact (`append` / `last_chunk`) |
|
|
21
|
+
| A tool call (Bash, Edit) | A `working` status update for the action |
|
|
22
|
+
| A file edit (diff) | A named artifact carrying the diff |
|
|
23
|
+
| A permission request | An `input-required` pause the caller answers |
|
|
24
|
+
| Run result | Cost, turns, and usage on the completion message |
|
|
25
|
+
| Session id | Mapped to the A2A `contextId` so follow-ups resume |
|
|
26
|
+
|
|
27
|
+
The mapping is all in `executor.py`. Backends only emit normalized events; they never touch the protocol.
|
|
28
|
+
|
|
29
|
+
## Where this fits
|
|
30
|
+
|
|
31
|
+
Anthropic now ships its own ways to run Claude Code beyond the terminal: Claude Code on the web, background agents, cloud-hosted Routines, and the Managed Agents API. These are the right choices when you want Anthropic to host the run and you live in their ecosystem, and they are typically tied to Anthropic infrastructure and a GitHub-centric flow.
|
|
32
|
+
|
|
33
|
+
a2acode solves a different problem: making any coding agent a first-class peer on a vendor-neutral [A2A](https://a2aprotocol.ai/) mesh. An orchestrator built on any framework discovers it through its agent card and delegates coding work the same way it would to any other A2A agent. The run happens on infrastructure you control, in a workspace you point it at. Reach for a2acode when:
|
|
34
|
+
|
|
35
|
+
- another agent (not a human at a prompt) is the caller, and it speaks A2A;
|
|
36
|
+
- you want the run on your own infrastructure and data boundary, not a vendor VM;
|
|
37
|
+
- you do not want to bet on one vendor's coding agent: ACP makes the backend a launch-command choice, so swapping Claude Code for Codex, Gemini CLI, or OpenHands does not touch the protocol surface your callers depend on.
|
|
38
|
+
|
|
39
|
+
ACP already standardizes the editor↔agent side and a dozen agents speak it; a2acode is the piece that exposes an ACP agent to *remote autonomous callers* over A2A, with permission round-trips and cost preserved as first-class protocol citizens — the part ACP leaves out because it assumes a human in an editor. The practical user is the platform team building that mesh, not the individual developer.
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- Python 3.13+
|
|
44
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
45
|
+
- An ACP agent adapter for the `acp` backend, launched as a subprocess. The `claude` preset uses `npx @zed-industries/claude-agent-acp` (needs Node and a Claude credential); `gemini` uses the Gemini CLI; or point `--agent-command` at any ACP agent.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
Install:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv sync
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `echo` backend needs no API key and no Claude install, so you can exercise the whole path offline first:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv run a2acode serve --backend echo &
|
|
59
|
+
# once the "Uvicorn running" line appears:
|
|
60
|
+
uv run a2acode call "fix the failing test"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
task 189b1c63-1a7b-4908-87c4-c8f3bba8f6b5
|
|
65
|
+
context 0b2a901e-2b6f-4c56-bba2-d0da546936e9
|
|
66
|
+
|
|
67
|
+
· Echo
|
|
68
|
+
fix the failing test
|
|
69
|
+
[completed] $0.0 · 1 turns
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then point it at a real project. The default backend is `acp`, fronting Claude Code through its ACP adapter:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv run a2acode serve --cwd /path/to/project # acp + claude by default
|
|
76
|
+
uv run a2acode call "add a /health endpoint" --url http://localhost:9100/
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Swap the agent without touching anything else:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv run a2acode serve --agent gemini --cwd /path/to/project
|
|
83
|
+
uv run a2acode serve --agent-command "npx -y some-other-acp-agent"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Continue the same conversation by passing the `context` from a previous turn:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run a2acode call "now add a test for it" --context <context-id>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Commands
|
|
93
|
+
|
|
94
|
+
| Command | Description |
|
|
95
|
+
| -------------------- | -------------------------------------------- |
|
|
96
|
+
| `a2acode serve` | Start the A2A server |
|
|
97
|
+
| `a2acode call TEXT` | Send a message and print the streamed events |
|
|
98
|
+
| `a2acode card` | Fetch and print the agent card |
|
|
99
|
+
|
|
100
|
+
The agent card is served at `/.well-known/agent-card.json` and advertises Claude Code's abilities as discrete skills (generation, refactor, debug, review, test, explain).
|
|
101
|
+
|
|
102
|
+
## Backends
|
|
103
|
+
|
|
104
|
+
A backend turns a prompt into a stream of normalized events. Three ship today:
|
|
105
|
+
|
|
106
|
+
- `acp` (default): drives any agent that speaks Zed's Agent Client Protocol as a subprocess. `--agent claude|gemini|codex` selects a launch preset; `--agent-command` drives any other ACP agent. This is the vendor-neutral path.
|
|
107
|
+
- `claude`: drives Claude Code directly through the Claude Agent SDK, no subprocess. Install with `uv sync --extra claude`. Use it when you want the SDK-native path (e.g. `--max-budget-usd`) rather than ACP.
|
|
108
|
+
- `echo`: no dependencies, mirrors the input. For wiring checks and tests.
|
|
109
|
+
|
|
110
|
+
The split keeps the A2A layer independent of how the agent is invoked: backends emit normalized events and never import `a2a.*`; the executor maps those events onto the protocol and never imports an agent SDK. Adding a backend never touches the server or the protocol mapping.
|
|
111
|
+
|
|
112
|
+
## Authentication
|
|
113
|
+
|
|
114
|
+
Each agent authenticates the way its own tooling does, inherited from the server's environment: the `acp` backend passes the environment through to the adapter subprocess (e.g. `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`), and the `claude` backend uses whatever the Claude CLI is configured with. When the server answers on behalf of other agents, a Claude credential has to be an Anthropic API key (or Bedrock / Vertex); Anthropic does not permit subscription credentials for third-party serving. The `claude` backend can cap per-run cost with `--max-budget-usd`.
|
|
115
|
+
|
|
116
|
+
## Signed agent cards
|
|
117
|
+
|
|
118
|
+
A caller that discovers this server only has the agent card to go on. Sign it so the caller can confirm the card came from you and was not swapped in transit:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uv run a2acode serve --sign-key card-signing.pem --sign-kid my-key-1 --sign-alg ES256
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The card is then served with a JWS signature over its canonical form. `--sign-key` is a path to a file holding the key: a PEM private key for asymmetric algorithms (`ES256`, `RS256`), or a shared secret for `HS256`. `--sign-kid` is the key id a verifier uses to look up the matching public key. Unsigned is still the default.
|
|
125
|
+
|
|
126
|
+
## Caller authentication
|
|
127
|
+
|
|
128
|
+
A signed card proves who the server is; this proves the caller is allowed in. Require a bearer token and the server rejects any task request that does not carry it:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uv run a2acode serve --auth-token-file caller-token.txt
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
When `--auth-token-file` is set, callers must send `Authorization: Bearer <token>`; a request without a valid token gets `401 Unauthorized`. The agent card stays public so a caller can still fetch it to discover the requirement, and the card advertises the bearer scheme in `securitySchemes`. Without the flag the server stays open, as before.
|
|
135
|
+
|
|
136
|
+
A2A keeps the credential at the HTTP layer, so this composes with whatever your gateway already does: terminate TLS, validate OAuth, or rate-limit in front, and let the server enforce the token behind it.
|
|
137
|
+
|
|
138
|
+
## Permissions
|
|
139
|
+
|
|
140
|
+
A tool that needs approval pauses the task in the A2A `input-required` state instead of being skipped. The caller answers with a follow-up message on the same task:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
uv run a2acode call "sudo reboot"
|
|
144
|
+
# ... [input-required] Permission requested for Bash: $ sudo reboot
|
|
145
|
+
# reply: a2acode call "allow" --task <id> --context <id>
|
|
146
|
+
uv run a2acode call "allow" --task <id> --context <id>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`allow` (or `yes`, `approve`, `ok`) approves; anything else denies. The agent session stays alive across the pause, so it resumes exactly where it stopped. Over ACP this is the agent's `session/request_permission` call answered from the A2A caller's reply; with the `claude` backend it routes through the Claude SDK's `can_use_tool`.
|
|
150
|
+
|
|
151
|
+
Whatever the agent decides needs approval becomes an `input-required` pause rather than being silently skipped or auto-approved; the caller, not the server, holds the decision. Read-only actions the agent already treats as safe still run without a prompt.
|
|
152
|
+
|
|
153
|
+
## Long-running tasks
|
|
154
|
+
|
|
155
|
+
The agent card advertises push notifications. A caller can register a webhook for a task and receive status and artifact updates by HTTP POST instead of holding a stream open, which helps when a run takes minutes. Streaming and polling (`tasks/get`) both work too.
|
|
156
|
+
|
|
157
|
+
## Observability
|
|
158
|
+
|
|
159
|
+
Debugging one agent is hard; debugging a chain of them without traces is worse. Because A2A runs over HTTP, it drops straight into OpenTelemetry: install the extra and the A2A SDK's instrumentation plus a per-task `a2acode.execute` span light up, with W3C trace context propagating across the call so client and server spans share one trace.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
uv sync --extra telemetry
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Tracing is off unless OpenTelemetry is installed, and you configure the exporter the standard way (e.g. `OTEL_EXPORTER_OTLP_ENDPOINT`, or run under `opentelemetry-instrument`). It works against an on-prem or air-gapped collector, so traces never have to leave your network.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
uv sync --dev
|
|
171
|
+
uv run ruff check src tests
|
|
172
|
+
uv run ruff format src tests
|
|
173
|
+
uv run mypy
|
|
174
|
+
uv run pytest
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
CI runs these on Python 3.13 and 3.14, plus a Markdown lint, on every push and pull request.
|
|
178
|
+
|
|
179
|
+
## Releasing
|
|
180
|
+
|
|
181
|
+
Pushing a `v*` tag builds the package, creates a GitHub release with the artifacts, and publishes to PyPI via trusted publishing:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git tag v0.1.0
|
|
185
|
+
git push origin v0.1.0
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Status
|
|
189
|
+
|
|
190
|
+
The mapping is complete end to end and verified against real Claude: text round trip, tool-progress updates, streaming artifacts, file diffs as artifacts, run metadata, session continuity, the permission-to-`input-required` round trip, and push notifications. The offline `echo` backend covers every path including permissions, so it can all be exercised without an API key.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
Apache 2.0. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "a2acode"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "Serve Claude Code and other ACP coding agents over the A2A protocol."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
authors = [{ name = "kanywst" }]
|
|
9
|
+
keywords = ["a2a", "acp", "claude-code", "agent", "agent2agent"]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"a2a-sdk[http-server,signing]>=1.1,<2",
|
|
12
|
+
"agent-client-protocol>=0.10,<1",
|
|
13
|
+
"uvicorn>=0.49",
|
|
14
|
+
"httpx>=0.28",
|
|
15
|
+
"typer>=0.26",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
# The Claude SDK backend (backends/claude.py): drives Claude Code directly
|
|
20
|
+
# through the Claude Agent SDK instead of over ACP. The ACP backend fronts
|
|
21
|
+
# Claude Code too, so this is only needed for the SDK-native path.
|
|
22
|
+
claude = ["claude-agent-sdk>=0.2.101"]
|
|
23
|
+
# Distributed tracing. Pulls the A2A SDK's OpenTelemetry instrumentation plus
|
|
24
|
+
# the API a2acode's own spans import directly (declared rather than relied on
|
|
25
|
+
# transitively through the SDK). Install with `a2acode[telemetry]`.
|
|
26
|
+
telemetry = ["a2a-sdk[telemetry]>=1.1,<2", "opentelemetry-api>=1.33"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
a2acode = "a2acode.cli:app"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Repository = "https://github.com/kanywst/a2acode"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"ruff>=0.15",
|
|
37
|
+
"pytest>=9",
|
|
38
|
+
"pytest-asyncio>=1.4",
|
|
39
|
+
"mypy>=2.1",
|
|
40
|
+
# So the test suite can exercise the tracing path with a real exporter.
|
|
41
|
+
"opentelemetry-sdk>=1.33",
|
|
42
|
+
# The Claude SDK backend is an optional extra; pull it in for dev so its
|
|
43
|
+
# tests (test_claude_backend.py) run.
|
|
44
|
+
"claude-agent-sdk>=0.2.101",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
49
|
+
build-backend = "uv_build"
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 88
|
|
53
|
+
target-version = "py313"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
asyncio_mode = "auto"
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
files = ["src"]
|
|
64
|
+
python_version = "3.13"
|
|
65
|
+
ignore_missing_imports = true
|
|
66
|
+
check_untyped_defs = true
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Run Claude Code as an A2A protocol agent server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .card import build_card
|
|
6
|
+
from .executor import ClaudeCodeExecutor
|
|
7
|
+
from .server import build_app
|
|
8
|
+
|
|
9
|
+
__all__ = ["build_app", "build_card", "ClaudeCodeExecutor"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Caller authentication.
|
|
2
|
+
|
|
3
|
+
A server that answers on behalf of other agents should be able to require a
|
|
4
|
+
credential. This is a pure-ASGI middleware (not ``BaseHTTPMiddleware``) so it
|
|
5
|
+
passes the request straight through to the inner app when authorized, leaving
|
|
6
|
+
streaming and server-sent events untouched; it only short-circuits with a 401
|
|
7
|
+
when a token is missing or wrong.
|
|
8
|
+
|
|
9
|
+
The agent card stays public: a caller fetches it to learn the auth scheme
|
|
10
|
+
*before* it has a credential, so discovery paths under ``/.well-known/`` are
|
|
11
|
+
exempt while the task endpoints are protected.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import hmac
|
|
18
|
+
from collections.abc import Awaitable, Callable
|
|
19
|
+
|
|
20
|
+
Receive = Callable[[], Awaitable[dict]]
|
|
21
|
+
Send = Callable[[dict], Awaitable[None]]
|
|
22
|
+
ASGIApp = Callable[[dict, Receive, Send], Awaitable[None]]
|
|
23
|
+
|
|
24
|
+
_PUBLIC_PREFIXES = ("/.well-known/",)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BearerAuthMiddleware:
|
|
28
|
+
"""Require ``Authorization: Bearer <token>`` on non-discovery requests."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
app: ASGIApp,
|
|
33
|
+
*,
|
|
34
|
+
token: str,
|
|
35
|
+
public_prefixes: tuple[str, ...] = _PUBLIC_PREFIXES,
|
|
36
|
+
) -> None:
|
|
37
|
+
if not token.strip():
|
|
38
|
+
raise ValueError("auth token must not be empty")
|
|
39
|
+
self.app = app
|
|
40
|
+
# Compare SHA-256 digests rather than the tokens themselves: the
|
|
41
|
+
# constant-time compare is then always over a fixed 32 bytes, so it
|
|
42
|
+
# cannot leak the token length, and the raw secret is not kept around.
|
|
43
|
+
self._token_digest = hashlib.sha256(token.encode("utf-8")).digest()
|
|
44
|
+
self._public = public_prefixes
|
|
45
|
+
|
|
46
|
+
async def __call__(self, scope: dict, receive: Receive, send: Send) -> None:
|
|
47
|
+
if scope["type"] != "http" or self._is_public(scope.get("path", "")):
|
|
48
|
+
await self.app(scope, receive, send)
|
|
49
|
+
return
|
|
50
|
+
if self._authorized(scope):
|
|
51
|
+
await self.app(scope, receive, send)
|
|
52
|
+
return
|
|
53
|
+
await self._reject(send)
|
|
54
|
+
|
|
55
|
+
def _is_public(self, path: str) -> bool:
|
|
56
|
+
return any(path.startswith(p) for p in self._public)
|
|
57
|
+
|
|
58
|
+
def _authorized(self, scope: dict) -> bool:
|
|
59
|
+
# Scan the headers list for the one we need instead of materializing a
|
|
60
|
+
# dict on every request.
|
|
61
|
+
raw = b""
|
|
62
|
+
for key, value in scope.get("headers") or []:
|
|
63
|
+
if key == b"authorization":
|
|
64
|
+
raw = value
|
|
65
|
+
break
|
|
66
|
+
# split(None, 1) tolerates extra whitespace between scheme and token.
|
|
67
|
+
parts = raw.split(None, 1)
|
|
68
|
+
if len(parts) != 2 or parts[0].lower() != b"bearer":
|
|
69
|
+
return False
|
|
70
|
+
presented = hashlib.sha256(parts[1].strip()).digest()
|
|
71
|
+
return hmac.compare_digest(presented, self._token_digest)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
async def _reject(send: Send) -> None:
|
|
75
|
+
body = b'{"error": "unauthorized"}'
|
|
76
|
+
await send(
|
|
77
|
+
{
|
|
78
|
+
"type": "http.response.start",
|
|
79
|
+
"status": 401,
|
|
80
|
+
"headers": [
|
|
81
|
+
(b"content-type", b"application/json"),
|
|
82
|
+
(b"www-authenticate", b"Bearer"),
|
|
83
|
+
(b"content-length", str(len(body)).encode()),
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
await send({"type": "http.response.body", "body": body})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Backends drive Claude Code and emit normalized events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import (
|
|
6
|
+
Backend,
|
|
7
|
+
BackendEvent,
|
|
8
|
+
FileChange,
|
|
9
|
+
PermissionDecision,
|
|
10
|
+
PermissionRequest,
|
|
11
|
+
Result,
|
|
12
|
+
RunRequest,
|
|
13
|
+
TextDelta,
|
|
14
|
+
ToolUse,
|
|
15
|
+
)
|
|
16
|
+
from .echo import EchoBackend
|
|
17
|
+
from .session import BackendSession
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Backend",
|
|
21
|
+
"BackendEvent",
|
|
22
|
+
"BackendSession",
|
|
23
|
+
"FileChange",
|
|
24
|
+
"PermissionDecision",
|
|
25
|
+
"PermissionRequest",
|
|
26
|
+
"Result",
|
|
27
|
+
"RunRequest",
|
|
28
|
+
"TextDelta",
|
|
29
|
+
"ToolUse",
|
|
30
|
+
"EchoBackend",
|
|
31
|
+
"make_backend",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_backend(name: str, **kwargs) -> Backend:
|
|
36
|
+
"""Construct a backend by name.
|
|
37
|
+
|
|
38
|
+
``acp`` and ``claude`` are imported lazily so the echo backend works without
|
|
39
|
+
their runtime dependencies (the ACP SDK / the Claude Agent SDK) present.
|
|
40
|
+
"""
|
|
41
|
+
if name == "echo":
|
|
42
|
+
return EchoBackend()
|
|
43
|
+
if name == "acp":
|
|
44
|
+
from .acp import ACPBackend
|
|
45
|
+
|
|
46
|
+
return ACPBackend(**kwargs)
|
|
47
|
+
if name == "claude":
|
|
48
|
+
from .claude import ClaudeBackend
|
|
49
|
+
|
|
50
|
+
return ClaudeBackend(**kwargs)
|
|
51
|
+
raise ValueError(f"unknown backend: {name!r} (expected 'acp', 'claude', or 'echo')")
|