dsh-a2a-server 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/README.zh-CN.md +316 -0
- package/cordis.patch.yml +30 -0
- package/lib/config.d.ts +116 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +110 -0
- package/lib/config.js.map +1 -0
- package/lib/contexts.d.ts +84 -0
- package/lib/contexts.d.ts.map +1 -0
- package/lib/contexts.js +103 -0
- package/lib/contexts.js.map +1 -0
- package/lib/index.d.ts +33 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +505 -0
- package/lib/index.js.map +1 -0
- package/lib/projection.d.ts +131 -0
- package/lib/projection.d.ts.map +1 -0
- package/lib/projection.js +98 -0
- package/lib/projection.js.map +1 -0
- package/lib/protocol/brand.d.ts +32 -0
- package/lib/protocol/brand.d.ts.map +1 -0
- package/lib/protocol/brand.js +25 -0
- package/lib/protocol/brand.js.map +1 -0
- package/lib/protocol/card.d.ts +43 -0
- package/lib/protocol/card.d.ts.map +1 -0
- package/lib/protocol/card.js +58 -0
- package/lib/protocol/card.js.map +1 -0
- package/lib/protocol/index.d.ts +23 -0
- package/lib/protocol/index.d.ts.map +1 -0
- package/lib/protocol/index.js +20 -0
- package/lib/protocol/index.js.map +1 -0
- package/lib/protocol/jsonrpc.d.ts +93 -0
- package/lib/protocol/jsonrpc.d.ts.map +1 -0
- package/lib/protocol/jsonrpc.js +81 -0
- package/lib/protocol/jsonrpc.js.map +1 -0
- package/lib/protocol/normalize.d.ts +72 -0
- package/lib/protocol/normalize.d.ts.map +1 -0
- package/lib/protocol/normalize.js +278 -0
- package/lib/protocol/normalize.js.map +1 -0
- package/lib/protocol/sse.d.ts +20 -0
- package/lib/protocol/sse.d.ts.map +1 -0
- package/lib/protocol/sse.js +23 -0
- package/lib/protocol/sse.js.map +1 -0
- package/lib/protocol/wire.d.ts +164 -0
- package/lib/protocol/wire.d.ts.map +1 -0
- package/lib/protocol/wire.js +26 -0
- package/lib/protocol/wire.js.map +1 -0
- package/lib/router.d.ts +56 -0
- package/lib/router.d.ts.map +1 -0
- package/lib/router.js +432 -0
- package/lib/router.js.map +1 -0
- package/lib/security.d.ts +89 -0
- package/lib/security.d.ts.map +1 -0
- package/lib/security.js +171 -0
- package/lib/security.js.map +1 -0
- package/lib/tasks.d.ts +75 -0
- package/lib/tasks.d.ts.map +1 -0
- package/lib/tasks.js +84 -0
- package/lib/tasks.js.map +1 -0
- package/lib/types.d.ts +71 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +17 -0
- package/lib/types.js.map +1 -0
- package/package.json +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Juhua
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
<h1 align="center">dsh-a2a-server</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Inbound A2A protocol server for DeepSeek Harness.</strong><br>
|
|
5
|
+
Publish an Agent Card, accept tasks from any compliant peer.<br>
|
|
6
|
+
Everything is a plugin — this is one.
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="README.md"><b>English</b></a> · <a href="README.zh-CN.md">简体中文</a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center"><sub>A community plugin, not an official DeepSeek product.</sub></p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT License"></a>
|
|
17
|
+
<img src="https://img.shields.io/badge/A2A-v0.3.0%20JSON--RPC-4D6BFE?style=flat" alt="A2A v0.3.0 JSON-RPC binding">
|
|
18
|
+
<img src="https://img.shields.io/badge/DSH-0.1.0--rc.6-4493F8?style=flat" alt="Built against DSH 0.1.0-rc.6">
|
|
19
|
+
<img src="https://img.shields.io/badge/tests-129-2EA44F?style=flat" alt="129 tests">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
`dsh-a2a-server` makes a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
23
|
+
agent reachable over the [A2A (Agent2Agent)](https://a2a-protocol.org) protocol. It
|
|
24
|
+
serves an Agent Card at a well-known URI and implements the **v0.3.0 JSON-RPC
|
|
25
|
+
binding**, so any compliant peer that knows the deployment's URL can discover the
|
|
26
|
+
agent and submit tasks to it.
|
|
27
|
+
|
|
28
|
+
The plugin is **inbound only**: it never connects to another agent. There is no
|
|
29
|
+
client, no peer directory, and no A2A subagent provider. It is a transport
|
|
30
|
+
adapter over `ctx.agents`, not a capability seam.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
dsh plugin --profile web add ./path/to/dsh-a2a-server
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`dsh plugin` forwards to pnpm inside the profile directory and appends this
|
|
39
|
+
bundle to `dsh.profile.bundles`, because the package declares `dsh.bundle`.
|
|
40
|
+
|
|
41
|
+
| Requirement | Supplied by |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `ctx.agents` | `dsh-base` |
|
|
44
|
+
| `ctx.credentials` | `dsh-base` |
|
|
45
|
+
| `ctx.webServer` | **`dsh-web-app`** |
|
|
46
|
+
| `ctx.sessionProjections` (optional) | composition; enables `tasks/get` after settlement |
|
|
47
|
+
|
|
48
|
+
The plugin stays PENDING until the three required services exist. `ctx.webServer`
|
|
49
|
+
ships in `dsh-web-app` rather than `dsh-base`, so a `headless` profile needs
|
|
50
|
+
`@deepseek-ai/dsh-host-webserver` mounted before this bundle serves anything.
|
|
51
|
+
`dsh --profile <name> --dump-config` prints the composed rows.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
The bundled example composition runs a real model and a listening server.
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pnpm install
|
|
59
|
+
A2A_PEER_ALICE=demo123 A2A_PORT=9922 pnpm serve
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
$env:A2A_PEER_ALICE = "demo123"
|
|
64
|
+
$env:A2A_PORT = "9922"
|
|
65
|
+
pnpm serve
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The model credential is resolved through `ctx.credentials`, so an existing
|
|
69
|
+
`DEEPSEEK_API_KEY` in the harness home, either `.env` layer, or the process
|
|
70
|
+
environment is picked up unchanged. A missing credential fails the boot.
|
|
71
|
+
|
|
72
|
+
Fetch the card and submit a task:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
curl -s http://127.0.0.1:9922/.well-known/agent-card.json
|
|
76
|
+
|
|
77
|
+
curl -s http://127.0.0.1:9922/a2a \
|
|
78
|
+
-H "authorization: Bearer demo123" \
|
|
79
|
+
-H 'content-type: application/json' \
|
|
80
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{
|
|
81
|
+
"message":{"kind":"message","messageId":"m1","role":"user",
|
|
82
|
+
"parts":[{"kind":"text","text":"hello"}]}}}'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Variable | Default | Meaning |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `A2A_PEER_ALICE` | — | Bearer token for the demo peer; required |
|
|
88
|
+
| `A2A_PORT` | `9900` | Listening port |
|
|
89
|
+
| `A2A_SEND_MODE` | `block` | `block` or `immediate` |
|
|
90
|
+
| `A2A_WORKSPACE_ROOT` | temp dir | Parent of the per-peer working directories |
|
|
91
|
+
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | Model id requested from the adapter |
|
|
92
|
+
|
|
93
|
+
A one-shot probe runs 48 checks against a live server and exits non-zero on any
|
|
94
|
+
mismatch:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
pnpm probe # defaults to :9922 / demo123
|
|
98
|
+
node example/probe.mjs http://127.0.0.1:9922 demo123
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Interface
|
|
102
|
+
|
|
103
|
+
| Route | Method | Auth |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `/.well-known/agent-card.json` | GET | Public by default |
|
|
106
|
+
| `/.well-known/agent.json` | GET | Public by default |
|
|
107
|
+
| `{basePath}` (default `/a2a`) | POST | Bearer required |
|
|
108
|
+
|
|
109
|
+
| JSON-RPC method | v1.0 alias | Status |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `message/send` | `SendMessage` | Blocking negotiated per request |
|
|
112
|
+
| `message/stream` | `SendStreamingMessage` | SSE |
|
|
113
|
+
| `tasks/get` | `GetTask` | Idempotent; answers after settlement |
|
|
114
|
+
| `tasks/cancel` | `CancelTask` | Cancels the running turn |
|
|
115
|
+
| `tasks/resubscribe` | `SubscribeToTask` | Live stream, or one terminal frame |
|
|
116
|
+
| `tasks/pushNotificationConfig/*` | `*TaskPushNotificationConfig` | `-32003` |
|
|
117
|
+
| `tasks/list` | `ListTasks` | `-32601` |
|
|
118
|
+
| `agent/getAuthenticatedExtendedCard` | `GetExtendedAgentCard` | `-32601` |
|
|
119
|
+
|
|
120
|
+
Both dialects are accepted. v0.3 (`message/send`, `"working"`, `kind`-tagged
|
|
121
|
+
parts) is the mainline; the v1.0 spellings (`SendMessage`, `TASK_STATE_WORKING`,
|
|
122
|
+
member-presence parts) are normalized inbound and rendered back in whichever
|
|
123
|
+
dialect the request used.
|
|
124
|
+
|
|
125
|
+
Inbound messages may carry text, file, and data parts. File and data parts are
|
|
126
|
+
rendered into the model's context as bracketed references. Replies are text.
|
|
127
|
+
|
|
128
|
+
Authentication, rate limiting, and the trust gate answer with HTTP `401`, `429`,
|
|
129
|
+
and `403`; the body remains a valid JSON-RPC error envelope. A task belonging to
|
|
130
|
+
another peer answers exactly as an absent one.
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
- id: a2a-server
|
|
136
|
+
name: dsh-a2a-server
|
|
137
|
+
config:
|
|
138
|
+
basePath: /a2a
|
|
139
|
+
publicUrl: https://agents.example.com/a2a # advertised on the card
|
|
140
|
+
protocolVersion: 0.3.0
|
|
141
|
+
provider: deepseek-official
|
|
142
|
+
model: deepseek-v4-flash
|
|
143
|
+
|
|
144
|
+
card:
|
|
145
|
+
name: dsh-harness
|
|
146
|
+
description: Reads code, runs commands, reports findings.
|
|
147
|
+
public: true
|
|
148
|
+
skills:
|
|
149
|
+
- id: general
|
|
150
|
+
name: general
|
|
151
|
+
description: General-purpose task execution.
|
|
152
|
+
tags: [coding, research]
|
|
153
|
+
provider:
|
|
154
|
+
organization: Example Inc.
|
|
155
|
+
url: https://example.com
|
|
156
|
+
|
|
157
|
+
peers:
|
|
158
|
+
alice: { tokenEnv: A2A_PEER_ALICE }
|
|
159
|
+
bob: { tokenEnv: A2A_PEER_BOB }
|
|
160
|
+
trustedPeers: [alice]
|
|
161
|
+
rateLimitPerMinute: 60
|
|
162
|
+
maxContextTurns: 5
|
|
163
|
+
|
|
164
|
+
sendMode: block
|
|
165
|
+
blockTimeoutMs: 60000
|
|
166
|
+
contextIdleTtlMs: 1800000
|
|
167
|
+
maxResidentContexts: 64
|
|
168
|
+
|
|
169
|
+
isolation:
|
|
170
|
+
workspaceMode: per-peer
|
|
171
|
+
workspaceRoot: /srv/dsh/a2a
|
|
172
|
+
peerWorkspaces:
|
|
173
|
+
alice: /srv/project
|
|
174
|
+
|
|
175
|
+
push:
|
|
176
|
+
enabled: false
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
| Key | Default | Meaning |
|
|
180
|
+
| --- | --- | --- |
|
|
181
|
+
| `basePath` | `/a2a` | JSON-RPC route |
|
|
182
|
+
| `publicUrl` | derived from `Host` | Routable URL published on the card |
|
|
183
|
+
| `protocolVersion` | `0.3.0` | Version advertised on the card |
|
|
184
|
+
| `provider` · `model` | — | Model route for every agent this server creates |
|
|
185
|
+
| `card.public` | `true` | Serve the card without a credential |
|
|
186
|
+
| `card.skills` | `[]` | Declared skills; falls back to one `general` entry |
|
|
187
|
+
| `peers` | `{}` | Identity → credential **reference name** |
|
|
188
|
+
| `trustedPeers` | all authenticated | Allow-list of identities that may run tasks |
|
|
189
|
+
| `rateLimitPerMinute` | `60` | Sliding window per identity |
|
|
190
|
+
| `maxContextTurns` | `5` | Messages accepted per context before `rejected` |
|
|
191
|
+
| `sendMode` | `block` | Default when the client states no preference |
|
|
192
|
+
| `blockTimeoutMs` | `60000` | After which a blocking request is declined |
|
|
193
|
+
| `contextIdleTtlMs` | `1800000` | Idle time before a context's agent is released |
|
|
194
|
+
| `maxResidentContexts` | `64` | Ceiling on resident contexts |
|
|
195
|
+
| `isolation.workspaceMode` | `per-peer` | `per-peer` or `shared` |
|
|
196
|
+
| `isolation.workspaceRoot` | — | Required; parent directory or shared cwd |
|
|
197
|
+
| `isolation.peerWorkspaces` | `{}` | Per-identity working-directory override |
|
|
198
|
+
| `push.enabled` | `false` | Reserved; see [Boundaries](#boundaries) |
|
|
199
|
+
|
|
200
|
+
Configuration is refused at load when `isolation.workspaceRoot` is absent, a peer
|
|
201
|
+
name is not `[A-Za-z0-9][A-Za-z0-9_-]*`, a `tokenEnv` is not a POSIX identifier,
|
|
202
|
+
`trustedPeers` or `peerWorkspaces` names an undeclared peer, or `basePath` does
|
|
203
|
+
not start with `/`.
|
|
204
|
+
|
|
205
|
+
### Credentials
|
|
206
|
+
|
|
207
|
+
Configuration carries credential **references**, never values:
|
|
208
|
+
|
|
209
|
+
```yaml
|
|
210
|
+
peers:
|
|
211
|
+
alice: { tokenEnv: A2A_PEER_ALICE }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
# ~/.dsh/.credentials.yaml
|
|
216
|
+
A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`ctx.credentials` resolves the reference per request across four layers — the
|
|
220
|
+
process environment, the managed document, `<cwd>/.env`, and `$DSH_HOME/.env` —
|
|
221
|
+
so rotating a token takes effect on the next request without a restart. Peer
|
|
222
|
+
identity comes from the presented credential only; nothing in a request body can
|
|
223
|
+
assert it. There is no shared bearer token: isolation is built on distinct
|
|
224
|
+
identities.
|
|
225
|
+
|
|
226
|
+
## Isolation
|
|
227
|
+
|
|
228
|
+
| Layer | Guarantee | Mechanism |
|
|
229
|
+
| --- | --- | --- |
|
|
230
|
+
| Model context | One peer's conversation cannot enter another's model request | Separate `contextId` → separate Session → separate log |
|
|
231
|
+
| Protocol access | A peer cannot read, continue, or cancel another's context or task | Ownership by authenticated identity |
|
|
232
|
+
| Tooling | A peer's agent cannot use tools to read another peer's session | `workspaceMode: per-peer` |
|
|
233
|
+
|
|
234
|
+
`per-peer` (the default) derives each identity's `cwd` from `workspaceRoot`.
|
|
235
|
+
`shared` places every peer in one directory, which suits peers collaborating on a
|
|
236
|
+
single repository; under it, files written by one peer are readable by another.
|
|
237
|
+
|
|
238
|
+
## Boundaries
|
|
239
|
+
|
|
240
|
+
- Inbound only. No outbound client, peer directory, or A2A subagent provider.
|
|
241
|
+
- JSONRPC binding only. gRPC and HTTP+JSON are not served, and the card says so.
|
|
242
|
+
- Push notifications are not implemented. `push.enabled` selects which error the
|
|
243
|
+
push methods return; the card advertises `pushNotifications: false`.
|
|
244
|
+
- No extended Agent Card, `stateTransitionHistory`, protocol extensions, or card
|
|
245
|
+
signatures.
|
|
246
|
+
- Streaming emits committed assistant messages. Per-chunk streaming is not
|
|
247
|
+
implemented.
|
|
248
|
+
- No orphan-task watchdog: a task wedged non-terminal stays that way.
|
|
249
|
+
- Cross-session tool denial is not implemented; isolation rests on
|
|
250
|
+
`workspaceMode`.
|
|
251
|
+
- A token ceiling settles a task as `completed`; the real turn ending travels in
|
|
252
|
+
`Task.metadata.dsh.stopReason`, which A2A's state enum cannot express.
|
|
253
|
+
- Task state survives settlement but not a process restart: session persistence
|
|
254
|
+
is not composed, so the projection has no log to cold-fold after a reboot.
|
|
255
|
+
- `ctx.webServer` provides no TLS. Any non-loopback exposure belongs behind a
|
|
256
|
+
reverse proxy.
|
|
257
|
+
- A configuration change restarts the plugin and cancels in-flight tasks.
|
|
258
|
+
|
|
259
|
+
## Architecture
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
src/
|
|
263
|
+
├── protocol/ dependency-free library: no Cordis, no HTTP, no harness
|
|
264
|
+
│ ├── wire.ts the A2A vocabulary, normalized to v0.3 spelling
|
|
265
|
+
│ ├── normalize.ts v0.3 <-> v1.0 dialect translation, both directions
|
|
266
|
+
│ ├── jsonrpc.ts framing and the A2A error codes
|
|
267
|
+
│ ├── card.ts Agent Card construction
|
|
268
|
+
│ └── sse.ts SSE frame encoding
|
|
269
|
+
├── index.ts the Cordis plugin: wiring, agent ownership, teardown
|
|
270
|
+
├── router.ts HTTP + JSON-RPC dispatch, free of Cordis so it unit-tests
|
|
271
|
+
├── contexts.ts contextId -> Activation registry and residency policy
|
|
272
|
+
├── tasks.ts task slots and the three-stage turn correlation
|
|
273
|
+
├── projection.ts the a2aTask fold over the session log
|
|
274
|
+
├── security.ts authentication, rate limiting, defanging, redaction
|
|
275
|
+
├── config.ts schema plus the cross-field checks that fail at load
|
|
276
|
+
└── types.ts declaration merges into SessionEventMap / MessageSourceMap
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**A task is an interval, not a turn.** One submitted message may span several
|
|
280
|
+
turns if tools queue more work, so settlement uses three hooks:
|
|
281
|
+
`agent/inbox/claimed` binds the message to a turn, `turn/end` records that turn's
|
|
282
|
+
ending, and `agent.whenIdle()` settles once the whole agent is quiet.
|
|
283
|
+
|
|
284
|
+
**Task state lives in the session log.** Lifecycle transitions are `a2a/task`
|
|
285
|
+
events; a projection unit folds them into the read model. The terminal edge
|
|
286
|
+
carries the committed output, so the fold serves the answer without reaching
|
|
287
|
+
back into message history.
|
|
288
|
+
|
|
289
|
+
**Residency is explicit.** HTTP has no connection lifetime, so each `contextId`
|
|
290
|
+
maps to an Activation evicted when idle, leaving the durable Session behind.
|
|
291
|
+
|
|
292
|
+
## Development
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
pnpm install
|
|
296
|
+
pnpm typecheck
|
|
297
|
+
pnpm test # 129 tests across 9 files
|
|
298
|
+
pnpm serve # a listening server
|
|
299
|
+
pnpm probe # 48 checks against a running server
|
|
300
|
+
pnpm build # emit lib/
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The end-to-end suites boot a real Cordis composition with a real agent loop and
|
|
304
|
+
drive it over HTTP, using a deterministic stub adapter so assertions do not
|
|
305
|
+
depend on model output.
|
|
306
|
+
|
|
307
|
+
## Relationship to the official project
|
|
308
|
+
|
|
309
|
+
Built against [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness).
|
|
310
|
+
|
|
311
|
+
The official project supplies the agent runtime, the plugin system, and the
|
|
312
|
+
capability seams this plugin consumes. This project supplies:
|
|
313
|
+
|
|
314
|
+
- The A2A v0.3.0 JSON-RPC binding, served inbound
|
|
315
|
+
- Agent Card construction and dialect normalization
|
|
316
|
+
- Mapping between A2A tasks and harness turns
|
|
317
|
+
- Per-peer authentication, isolation, and workspace policy
|
|
318
|
+
|
|
319
|
+
The harness is pre-release and does not promise compatibility across renames or
|
|
320
|
+
repackaging, so peer dependencies are pinned exactly to `0.1.0-rc.6`.
|
|
321
|
+
|
|
322
|
+
## Community
|
|
323
|
+
|
|
324
|
+
Scan to join the WeChat group **A2A 产品应用和探索** — discussion of A2A in
|
|
325
|
+
practice, this plugin included.
|
|
326
|
+
|
|
327
|
+
<p align="center">
|
|
328
|
+
<img src="assets/community-wechat.jpg" alt="WeChat group QR code" width="280">
|
|
329
|
+
</p>
|
|
330
|
+
|
|
331
|
+
If the code has expired, open an issue and we will refresh it.
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
<h1 align="center">dsh-a2a-server</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>为 DeepSeek Harness 提供入站 A2A 协议服务。</strong><br>
|
|
5
|
+
发布 Agent Card,接收任何合规 peer 提交的任务。<br>
|
|
6
|
+
万物皆「插件」,它就是其中之一。
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="README.md">English</a> · <a href="README.zh-CN.md"><b>简体中文</b></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center"><sub>社区维护的插件,并非 DeepSeek 官方产品。</sub></p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT License"></a>
|
|
17
|
+
<img src="https://img.shields.io/badge/A2A-v0.3.0%20JSON--RPC-4D6BFE?style=flat" alt="A2A v0.3.0 JSON-RPC 绑定">
|
|
18
|
+
<img src="https://img.shields.io/badge/DSH-0.1.0--rc.6-4493F8?style=flat" alt="基于 DSH 0.1.0-rc.6 构建">
|
|
19
|
+
<img src="https://img.shields.io/badge/tests-129-2EA44F?style=flat" alt="129 项测试">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
`dsh-a2a-server` 让 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
23
|
+
上的 agent 可以通过 [A2A(Agent2Agent)](https://a2a-protocol.org)协议被访问。它在
|
|
24
|
+
well-known 路径发布 Agent Card,并实现 **v0.3.0 JSON-RPC 绑定**——任何知道本部署
|
|
25
|
+
URL 的合规 peer 都能发现这个 agent 并向它提交任务。
|
|
26
|
+
|
|
27
|
+
本插件**只做入站**:从不主动连接其他 agent,没有 client、没有 peer 目录、没有 A2A
|
|
28
|
+
subagent provider。它是 `ctx.agents` 之上的传输适配层,不是能力接缝。
|
|
29
|
+
|
|
30
|
+
## 安装
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
dsh plugin --profile web add ./path/to/dsh-a2a-server
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`dsh plugin` 会在 profile 目录里转发给 pnpm,并把这个 bundle 追加进
|
|
37
|
+
`dsh.profile.bundles`——因为包中声明了 `dsh.bundle`。
|
|
38
|
+
|
|
39
|
+
| 依赖 | 由谁提供 |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `ctx.agents` | `dsh-base` |
|
|
42
|
+
| `ctx.credentials` | `dsh-base` |
|
|
43
|
+
| `ctx.webServer` | **`dsh-web-app`** |
|
|
44
|
+
| `ctx.sessionProjections`(可选) | 组合层;启用后 `tasks/get` 在任务结算后仍可应答 |
|
|
45
|
+
|
|
46
|
+
三个必需服务齐备之前,插件保持 PENDING。`ctx.webServer` 由 `dsh-web-app` 提供而不在
|
|
47
|
+
`dsh-base` 中,因此 `headless` profile 需要先挂载 `@deepseek-ai/dsh-host-webserver`。
|
|
48
|
+
`dsh --profile <name> --dump-config` 可以打印组合出来的配置行。
|
|
49
|
+
|
|
50
|
+
## 快速开始
|
|
51
|
+
|
|
52
|
+
仓库自带的示例组合会运行真实模型并启动一个监听服务。
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pnpm install
|
|
56
|
+
A2A_PEER_ALICE=demo123 A2A_PORT=9922 pnpm serve
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
$env:A2A_PEER_ALICE = "demo123"
|
|
61
|
+
$env:A2A_PORT = "9922"
|
|
62
|
+
pnpm serve
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
模型凭据经 `ctx.credentials` 解析,因此 harness home、任一 `.env` 层或进程环境中已有的
|
|
66
|
+
`DEEPSEEK_API_KEY` 都会被直接采用。缺少凭据时启动失败。
|
|
67
|
+
|
|
68
|
+
获取 Card 并提交任务:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
curl -s http://127.0.0.1:9922/.well-known/agent-card.json
|
|
72
|
+
|
|
73
|
+
curl -s http://127.0.0.1:9922/a2a \
|
|
74
|
+
-H "authorization: Bearer demo123" \
|
|
75
|
+
-H 'content-type: application/json' \
|
|
76
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{
|
|
77
|
+
"message":{"kind":"message","messageId":"m1","role":"user",
|
|
78
|
+
"parts":[{"kind":"text","text":"hello"}]}}}'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| 变量 | 默认值 | 含义 |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `A2A_PEER_ALICE` | — | 示例 peer 的 bearer token,必填 |
|
|
84
|
+
| `A2A_PORT` | `9900` | 监听端口 |
|
|
85
|
+
| `A2A_SEND_MODE` | `block` | `block` 或 `immediate` |
|
|
86
|
+
| `A2A_WORKSPACE_ROOT` | 临时目录 | per-peer 工作目录的父目录 |
|
|
87
|
+
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | 向适配器请求的模型 id |
|
|
88
|
+
|
|
89
|
+
一次性探针会对运行中的服务执行 48 项检查,任何一项不符即以非零码退出:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
pnpm probe # 默认 :9922 / demo123
|
|
93
|
+
node example/probe.mjs http://127.0.0.1:9922 demo123
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 对外接口
|
|
97
|
+
|
|
98
|
+
| 路由 | 方法 | 认证 |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `/.well-known/agent-card.json` | GET | 默认公开 |
|
|
101
|
+
| `/.well-known/agent.json` | GET | 默认公开 |
|
|
102
|
+
| `{basePath}`(默认 `/a2a`) | POST | 必须携带 Bearer |
|
|
103
|
+
|
|
104
|
+
| JSON-RPC 方法 | v1.0 别名 | 状态 |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `message/send` | `SendMessage` | 是否阻塞逐请求协商 |
|
|
107
|
+
| `message/stream` | `SendStreamingMessage` | SSE |
|
|
108
|
+
| `tasks/get` | `GetTask` | 幂等;结算后仍可应答 |
|
|
109
|
+
| `tasks/cancel` | `CancelTask` | 取消正在执行的 turn |
|
|
110
|
+
| `tasks/resubscribe` | `SubscribeToTask` | 续订活任务,或给一帧终态 |
|
|
111
|
+
| `tasks/pushNotificationConfig/*` | `*TaskPushNotificationConfig` | `-32003` |
|
|
112
|
+
| `tasks/list` | `ListTasks` | `-32601` |
|
|
113
|
+
| `agent/getAuthenticatedExtendedCard` | `GetExtendedAgentCard` | `-32601` |
|
|
114
|
+
|
|
115
|
+
两种方言都接受。v0.3(`message/send`、`"working"`、带 `kind` 的 part)是主线;v1.0
|
|
116
|
+
拼写(`SendMessage`、`TASK_STATE_WORKING`、成员存在式 part)在入站时被归一化,并按请求
|
|
117
|
+
所用的方言渲染回去。
|
|
118
|
+
|
|
119
|
+
入站消息可携带 text、file、data 三类 part。file 与 data 会以方括号引用的形式进入模型
|
|
120
|
+
上下文。回复为纯文本。
|
|
121
|
+
|
|
122
|
+
认证、限流与信任门分别以 HTTP `401`、`429`、`403` 应答,响应体仍是合法的 JSON-RPC 错误
|
|
123
|
+
信封。属于其他 peer 的任务,应答方式与不存在的任务完全一致。
|
|
124
|
+
|
|
125
|
+
## 配置
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
- id: a2a-server
|
|
129
|
+
name: dsh-a2a-server
|
|
130
|
+
config:
|
|
131
|
+
basePath: /a2a
|
|
132
|
+
publicUrl: https://agents.example.com/a2a # 写入 Card 的对外地址
|
|
133
|
+
protocolVersion: 0.3.0
|
|
134
|
+
provider: deepseek-official
|
|
135
|
+
model: deepseek-v4-flash
|
|
136
|
+
|
|
137
|
+
card:
|
|
138
|
+
name: dsh-harness
|
|
139
|
+
description: 读代码、执行命令、给出结论。
|
|
140
|
+
public: true
|
|
141
|
+
skills:
|
|
142
|
+
- id: general
|
|
143
|
+
name: general
|
|
144
|
+
description: 通用任务执行。
|
|
145
|
+
tags: [coding, research]
|
|
146
|
+
provider:
|
|
147
|
+
organization: Example Inc.
|
|
148
|
+
url: https://example.com
|
|
149
|
+
|
|
150
|
+
peers:
|
|
151
|
+
alice: { tokenEnv: A2A_PEER_ALICE }
|
|
152
|
+
bob: { tokenEnv: A2A_PEER_BOB }
|
|
153
|
+
trustedPeers: [alice]
|
|
154
|
+
rateLimitPerMinute: 60
|
|
155
|
+
maxContextTurns: 5
|
|
156
|
+
|
|
157
|
+
sendMode: block
|
|
158
|
+
blockTimeoutMs: 60000
|
|
159
|
+
contextIdleTtlMs: 1800000
|
|
160
|
+
maxResidentContexts: 64
|
|
161
|
+
|
|
162
|
+
isolation:
|
|
163
|
+
workspaceMode: per-peer
|
|
164
|
+
workspaceRoot: /srv/dsh/a2a
|
|
165
|
+
peerWorkspaces:
|
|
166
|
+
alice: /srv/project
|
|
167
|
+
|
|
168
|
+
push:
|
|
169
|
+
enabled: false
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
| 配置项 | 默认值 | 含义 |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| `basePath` | `/a2a` | JSON-RPC 路由 |
|
|
175
|
+
| `publicUrl` | 由 `Host` 推导 | 写入 Card 的可路由地址 |
|
|
176
|
+
| `protocolVersion` | `0.3.0` | Card 上声明的协议版本 |
|
|
177
|
+
| `provider` · `model` | — | 本服务创建的每个 agent 使用的模型路由 |
|
|
178
|
+
| `card.public` | `true` | 无需凭据即可获取 Card |
|
|
179
|
+
| `card.skills` | `[]` | 声明的 skills;为空时回退到一条 `general` |
|
|
180
|
+
| `peers` | `{}` | 身份 → 凭据**引用名** |
|
|
181
|
+
| `trustedPeers` | 全部已认证身份 | 允许执行任务的身份白名单 |
|
|
182
|
+
| `rateLimitPerMinute` | `60` | 按身份的滑动窗口 |
|
|
183
|
+
| `maxContextTurns` | `5` | 单个 context 接受的消息数上限,超出后 `rejected` |
|
|
184
|
+
| `sendMode` | `block` | 客户端未表态时的默认行为 |
|
|
185
|
+
| `blockTimeoutMs` | `60000` | 超过后拒绝继续阻塞 |
|
|
186
|
+
| `contextIdleTtlMs` | `1800000` | context 的 agent 被释放前的空闲时长 |
|
|
187
|
+
| `maxResidentContexts` | `64` | 常驻 context 数量上限 |
|
|
188
|
+
| `isolation.workspaceMode` | `per-peer` | `per-peer` 或 `shared` |
|
|
189
|
+
| `isolation.workspaceRoot` | — | 必填;父目录或共享 cwd |
|
|
190
|
+
| `isolation.peerWorkspaces` | `{}` | 按身份覆盖工作目录 |
|
|
191
|
+
| `push.enabled` | `false` | 保留字段,见[边界](#边界) |
|
|
192
|
+
|
|
193
|
+
以下情况在加载期即被拒绝:缺少 `isolation.workspaceRoot`;peer 名不匹配
|
|
194
|
+
`[A-Za-z0-9][A-Za-z0-9_-]*`;`tokenEnv` 不是 POSIX 标识符;`trustedPeers` 或
|
|
195
|
+
`peerWorkspaces` 引用了未声明的 peer;`basePath` 不以 `/` 开头。
|
|
196
|
+
|
|
197
|
+
### 凭据
|
|
198
|
+
|
|
199
|
+
配置中携带的是凭据**引用名**,而非凭据值:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
peers:
|
|
203
|
+
alice: { tokenEnv: A2A_PEER_ALICE }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
# ~/.dsh/.credentials.yaml
|
|
208
|
+
A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`ctx.credentials` 在每次请求时跨四层解析该引用——进程环境、托管文档、`<cwd>/.env`、
|
|
212
|
+
`$DSH_HOME/.env`——因此轮换 token 在下一次请求即生效,无需重启。peer 身份只来自所出示
|
|
213
|
+
的凭据,请求体中的任何内容都无法声明身份。本插件不提供共享 bearer token:隔离建立在
|
|
214
|
+
互不相同的身份之上。
|
|
215
|
+
|
|
216
|
+
## 隔离
|
|
217
|
+
|
|
218
|
+
| 层 | 保证 | 机制 |
|
|
219
|
+
| --- | --- | --- |
|
|
220
|
+
| 模型上下文 | 一个 peer 的对话不会进入另一个 peer 的模型请求 | 不同 `contextId` → 不同 Session → 不同 log |
|
|
221
|
+
| 协议访问 | peer 无法读取、续接或取消他人的 context 与任务 | 按认证身份判定归属 |
|
|
222
|
+
| 工具层 | peer 的 agent 无法借工具读取他人的 session | `workspaceMode: per-peer` |
|
|
223
|
+
|
|
224
|
+
`per-peer`(默认)依据 `workspaceRoot` 为每个身份派生独立 `cwd`。`shared` 则把所有 peer
|
|
225
|
+
放进同一个目录,适用于协作维护同一个仓库的场景;此模式下一个 peer 写入的文件对其他 peer
|
|
226
|
+
可读。
|
|
227
|
+
|
|
228
|
+
## 边界
|
|
229
|
+
|
|
230
|
+
- 只做入站。没有出站 client、peer 目录或 A2A subagent provider。
|
|
231
|
+
- 只提供 JSONRPC 绑定。不提供 gRPC 与 HTTP+JSON,Card 上如实声明。
|
|
232
|
+
- 未实现推送通知。`push.enabled` 仅决定推送方法返回哪个错误码,Card 上声明
|
|
233
|
+
`pushNotifications: false`。
|
|
234
|
+
- 不支持扩展 Agent Card、`stateTransitionHistory`、协议扩展与 Card 签名。
|
|
235
|
+
- 流式只推送已提交的 assistant 消息,未实现逐 chunk 推送。
|
|
236
|
+
- 无孤儿任务看门狗:卡在非终态的任务会一直保持该状态。
|
|
237
|
+
- 未实现跨会话工具拒绝;隔离依赖 `workspaceMode`。
|
|
238
|
+
- 触及 token 上限的任务结算为 `completed`,真实的 turn 结束原因放在
|
|
239
|
+
`Task.metadata.dsh.stopReason`——A2A 的状态枚举无法表达它。
|
|
240
|
+
- 任务状态可以跨结算存活,但无法跨进程重启:未组合 session 持久化,重启后 projection
|
|
241
|
+
没有日志可供冷折叠。
|
|
242
|
+
- `ctx.webServer` 不提供 TLS。任何非回环地址的暴露都应置于反向代理之后。
|
|
243
|
+
- 修改配置会重启插件并取消进行中的任务。
|
|
244
|
+
|
|
245
|
+
## 架构
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
src/
|
|
249
|
+
├── protocol/ 零依赖库:不碰 Cordis、不碰 HTTP、不碰 harness
|
|
250
|
+
│ ├── wire.ts A2A 词汇表,统一归一到 v0.3 拼写
|
|
251
|
+
│ ├── normalize.ts v0.3 <-> v1.0 方言双向翻译
|
|
252
|
+
│ ├── jsonrpc.ts 信封框架与 A2A 错误码
|
|
253
|
+
│ ├── card.ts Agent Card 构造
|
|
254
|
+
│ └── sse.ts SSE 帧编码
|
|
255
|
+
├── index.ts Cordis 插件本体:接线、agent 归属、拆卸
|
|
256
|
+
├── router.ts HTTP + JSON-RPC 分发;不依赖 Cordis,因此可单测
|
|
257
|
+
├── contexts.ts contextId -> Activation 注册表与驻留策略
|
|
258
|
+
├── tasks.ts 任务槽位与三段式 turn 关联
|
|
259
|
+
├── projection.ts 在 session log 上折叠出的 a2aTask 读模型
|
|
260
|
+
├── security.ts 认证、限流、注入去势、外发脱敏
|
|
261
|
+
├── config.ts schema,以及加载期就会拒绝的跨字段校验
|
|
262
|
+
└── types.ts 向 SessionEventMap / MessageSourceMap 的声明合并
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**task 是一个区间,不是一个 turn。** 一条提交进来的消息,如果工具排出了更多工作,可能
|
|
266
|
+
横跨好几个 turn,因此结算使用三个钩子:`agent/inbox/claimed` 把消息绑到某个 turn,
|
|
267
|
+
`turn/end` 记录该 turn 的结束原因,`agent.whenIdle()` 在整个 agent 安静后才结算。
|
|
268
|
+
|
|
269
|
+
**任务状态活在 session log 里。** 生命周期迁移是 `a2a/task` 事件,由 projection unit
|
|
270
|
+
折叠成读模型。终态那条边携带已提交的输出,因此折叠结果可以直接给出答案,无需回头翻消息
|
|
271
|
+
历史。
|
|
272
|
+
|
|
273
|
+
**驻留是显式管理的。** HTTP 没有连接生命周期,因此每个 `contextId` 对应一个 Activation,
|
|
274
|
+
空闲后被驱逐,持久化的 Session 留在原地。
|
|
275
|
+
|
|
276
|
+
## 开发
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
pnpm install
|
|
280
|
+
pnpm typecheck
|
|
281
|
+
pnpm test # 9 个文件共 129 项测试
|
|
282
|
+
pnpm serve # 启动监听服务
|
|
283
|
+
pnpm probe # 对运行中的服务执行 48 项检查
|
|
284
|
+
pnpm build # 产出 lib/
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
端到端测试会启动真实的 Cordis 组合与真实的 agent loop,并通过 HTTP 驱动它;模型使用
|
|
288
|
+
确定性的 stub 适配器,使断言不依赖模型输出。
|
|
289
|
+
|
|
290
|
+
## 与官方项目的关系
|
|
291
|
+
|
|
292
|
+
本项目基于 [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) 构建。
|
|
293
|
+
|
|
294
|
+
官方项目提供 agent 运行时、插件系统,以及本插件所消费的各个能力接缝。本项目提供:
|
|
295
|
+
|
|
296
|
+
- 入站的 A2A v0.3.0 JSON-RPC 绑定
|
|
297
|
+
- Agent Card 构造与方言归一化
|
|
298
|
+
- A2A task 与 harness turn 之间的映射
|
|
299
|
+
- 按 peer 的认证、隔离与工作目录策略
|
|
300
|
+
|
|
301
|
+
harness 处于 pre-release 阶段,不承诺跨重命名或重新打包的兼容性,因此 peer 依赖精确锁定
|
|
302
|
+
在 `0.1.0-rc.6`。
|
|
303
|
+
|
|
304
|
+
## 社区交流
|
|
305
|
+
|
|
306
|
+
扫码加入微信群 **A2A 产品应用和探索** —— 交流 A2A 的实际应用,也包括这个插件。
|
|
307
|
+
|
|
308
|
+
<p align="center">
|
|
309
|
+
<img src="assets/community-wechat.jpg" alt="微信群二维码" width="280">
|
|
310
|
+
</p>
|
|
311
|
+
|
|
312
|
+
若二维码已过期,欢迎提 issue,我们会更新。
|
|
313
|
+
|
|
314
|
+
## 许可
|
|
315
|
+
|
|
316
|
+
MIT
|