@hanphone/dsh-a2a 0.1.0 → 0.2.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.
Files changed (44) hide show
  1. package/README.md +98 -47
  2. package/README.zh.md +43 -42
  3. package/lib/client.js +49 -1
  4. package/lib/index.js +272 -17
  5. package/lib/tsconfig.client.tsbuildinfo +1 -1
  6. package/lib/tsconfig.tsbuildinfo +1 -1
  7. package/lib/types/api.d.ts +13 -0
  8. package/lib/types/api.d.ts.map +1 -1
  9. package/lib/types/api.js +5 -0
  10. package/lib/types/api.js.map +1 -1
  11. package/lib/types/client/index.d.ts +5 -0
  12. package/lib/types/client/index.d.ts.map +1 -1
  13. package/lib/types/index.d.ts.map +1 -1
  14. package/lib/types/index.js +74 -10
  15. package/lib/types/index.js.map +1 -1
  16. package/lib/types/server/a2a-server.d.ts +3 -1
  17. package/lib/types/server/a2a-server.d.ts.map +1 -1
  18. package/lib/types/server/a2a-server.js +5 -1
  19. package/lib/types/server/a2a-server.js.map +1 -1
  20. package/lib/types/server/identity.d.ts +38 -0
  21. package/lib/types/server/identity.d.ts.map +1 -0
  22. package/lib/types/server/identity.js +89 -0
  23. package/lib/types/server/identity.js.map +1 -0
  24. package/lib/types/server/inbound-registry.d.ts +60 -0
  25. package/lib/types/server/inbound-registry.d.ts.map +1 -0
  26. package/lib/types/server/inbound-registry.js +86 -0
  27. package/lib/types/server/inbound-registry.js.map +1 -0
  28. package/lib/types/server/store.d.ts +2 -0
  29. package/lib/types/server/store.d.ts.map +1 -1
  30. package/lib/types/server/store.js +4 -0
  31. package/lib/types/server/store.js.map +1 -1
  32. package/lib/types/service.d.ts +19 -0
  33. package/lib/types/service.d.ts.map +1 -1
  34. package/lib/types/service.js +12 -0
  35. package/lib/types/service.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/api.ts +27 -2
  38. package/src/client/index.ts +99 -2
  39. package/src/index.ts +76 -10
  40. package/src/server/a2a-server.ts +7 -2
  41. package/src/server/identity.ts +109 -0
  42. package/src/server/inbound-registry.ts +129 -0
  43. package/src/server/store.ts +5 -0
  44. package/src/service.ts +23 -0
package/README.md CHANGED
@@ -1,36 +1,52 @@
1
1
  # dsh-a2a
2
2
 
3
- Agent2Agent (A2A) Protocol v1.0 dual-end plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
3
+ Agent2Agent (A2A) Protocol v1.0 dual-end plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — **English** · [中文](README.zh.md)
4
4
 
5
- > **English** | [中文](README.zh.md)
5
+ `@hanphone/dsh-a2a` is an independent, open-source A2A plugin that turns a
6
+ DeepSeek Harness profile into a first-class agent-to-agent citizen: an inbound
7
+ server with a durable governed task store and a live-derived AgentCard, an
8
+ outbound client whose remote skills appear as model tools, and a GUI
9
+ dashboard that manages both halves without touching any file.
6
10
 
7
- `@hanphone/dsh-a2a` turns a DeepSeek Harness profile into a first-class A2A agent:
8
-
9
- - **Inbound server** — AgentCard derived from the live tool registry, JSON-RPC + SSE, durable task store, pluggable session/subagent executors, and a policy gate (`a2a/inbound-task`) with audit.
10
- - **Outbound client** — a persisted multi-agent AgentCard registry, remote skills mapped to model tools (`a2a__<name>__<skill>`), sync calls with per-agent timeout.
11
- - **GUI dashboard** — an **A2A 连接** settings page in the Harness Web UI: toggle the inbound server, manage outbound agents, view and cancel tasks — no config files required.
12
-
13
- Design decisions are recorded in [docs/architecture.md](docs/architecture.md). Current scope is P0 of that document.
11
+ Architecture and design decisions: [docs/architecture.md](docs/architecture.md).
14
12
 
15
13
  ## Features
16
14
 
17
- - **A2A v1.0 protocol surface** — `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `GetExtendedAgentCard`, `SubscribeToTask` over JSON-RPC; SSE streaming with catch-up frames.
18
- - **Dynamic AgentCard** — skills derived from the live `ctx.tools` registry (explicit id list, loud failure on missing referents) plus a built-in `chat` skill so a fresh install is immediately exercisable.
19
- - **Durable task store** — tasks live in the `a2a` storage domain (JSON backend by default, SQLite per deployment choice); task ids are server-generated and survive restarts.
20
- - **Executors** — `session` (one DSH session per `contextId`) and `subagent` (delegates to `ctx.subagents`, streams tool-call artifacts back) as built-in implementations.
21
- - **Governed inbound** every inbound task passes through the `a2a/inbound-task` waterfall, so policy plugins can veto or audit before execution.
22
- - **Auth by environment variable** inbound bearer token is referenced by env-var name (`authTokenEnv`), never stored in config as plaintext.
23
- - **Install-and-use** — both halves are enabled by default after `dsh plugin add`; no manual patch required to start.
15
+ - **A2A v1.0 protocol surface** — `SendMessage`, `SendStreamingMessage`,
16
+ `GetTask`, `ListTasks`, `CancelTask`, `GetExtendedAgentCard`,
17
+ `SubscribeToTask` over JSON-RPC; SSE streaming with catch-up frames.
18
+ - **Dynamic AgentCard** — skills derived from the live `ctx.tools` registry
19
+ (explicit id list, loud failure on missing referents) plus a built-in
20
+ `chat` skill; a fresh install answers end-to-end immediately.
21
+ - **Durable task store** — tasks live in the `a2a` storage domain (JSON
22
+ backend by default, SQLite per deployment choice); server-generated ids
23
+ survive restarts.
24
+ - **Executors** — `session` (one DSH session per `contextId`) and `subagent`
25
+ (delegates to `ctx.subagents`, streams tool-call artifacts back).
26
+ - **Governed inbound** — every inbound task passes through the
27
+ `a2a/inbound-task` waterfall so policy plugins can veto or audit.
28
+ - **Inbound connection monitoring** — the dashboard shows which remote peers
29
+ are talking to this DSH (source, first/last seen, task counts, active
30
+ streams) and can close a peer.
31
+ - **Runtime service identity** — edit the AgentCard's name/description/version
32
+ from the dashboard; the card rebuilds immediately and the identity persists
33
+ across restarts.
34
+ - **First-run onboarding** — a fresh install shows a pre-filled identity form
35
+ ("service identity") so publishing the service is one click.
36
+ - **Auth by environment variable** — inbound bearer token is referenced by
37
+ env-var name (`authTokenEnv`), never stored in config as plaintext.
38
+ - **Install-and-use** — both halves are enabled by default after
39
+ `dsh plugin add`; no manual patch required to start.
24
40
 
25
41
  ## Installation
26
42
 
27
- ### From npm (published)
43
+ ### From npm
28
44
 
29
45
  ```sh
30
46
  dsh plugin --profile web add @hanphone/dsh-a2a
31
47
  ```
32
48
 
33
- This works for any profile name (`web`, custom profiles, etc.):
49
+ Any profile name works (`web`, custom profiles, headless etc.):
34
50
 
35
51
  ```sh
36
52
  dsh plugin --profile <name> add @hanphone/dsh-a2a
@@ -42,16 +58,20 @@ dsh plugin --profile <name> add @hanphone/dsh-a2a
42
58
  cd dsh-a2a
43
59
  pnpm build
44
60
  npm pack
45
- dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.1.0.tgz
61
+ dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.2.0.tgz
46
62
  ```
47
63
 
48
64
  ## Quick start
49
65
 
50
66
  1. **Install** — `dsh plugin --profile web add @hanphone/dsh-a2a`.
51
- 2. **Restart the GUI** — the browser half is scanned at host startup, so restart `pnpm dsh web` (or your profile launcher) once after installing.
52
- 3. **Open Settings A2A 连接** you will see the inbound server status, the outbound agent list, and the task list.
67
+ 2. **Restart the GUI** — the browser half is scanned at host startup, so
68
+ restart once after installing (`pnpm dsh web` or your profile launcher).
69
+ 3. **Open Settings → A2A 连接** — the dashboard shows the inbound server
70
+ status, the service identity (with first-run onboarding pre-filled), the
71
+ outbound agent list, inbound peer connections, and the task list.
53
72
 
54
- The inbound server listens on the profile's webServer (default `http://127.0.0.1:3080`):
73
+ The inbound server listens on the profile's webServer (default
74
+ `http://127.0.0.1:3080`):
55
75
 
56
76
  ```sh
57
77
  curl http://127.0.0.1:3080/.well-known/agent-card.json
@@ -67,17 +87,26 @@ curl -X POST http://127.0.0.1:3080/a2a \
67
87
 
68
88
  ## GUI dashboard
69
89
 
70
- The browser half registers an **A2A 连接** page under Settings. From it you can, without touching any file:
90
+ The browser half registers an **A2A 连接** page under Settings. From it you
91
+ can, without touching any file:
71
92
 
72
- - toggle the inbound server (`server.enable` / `server.disable`),
73
- - list, add, enable/disable, refresh, and remove outbound agents,
93
+ - toggle the inbound server;
94
+ - view and edit the service identity (name/description/version) — the
95
+ AgentCard rebuilds immediately and the change persists;
96
+ - list, add, enable/disable, refresh, and remove outbound agents;
97
+ - see which inbound peers are connected (source, activity, tasks) and close
98
+ one;
74
99
  - view and cancel inbound tasks.
75
100
 
76
- All dashboard traffic goes through the **loopback-only** `/a2a/api` route on the profile's webServer remote peers can never drive it.
101
+ All dashboard traffic goes through the **loopback-only** `/a2a/api` route —
102
+ remote peers can never drive it.
77
103
 
78
104
  ## Configuration
79
105
 
80
- The dashboard covers the day-to-day operations. Values the dashboard does not edit (name/description, baseUrl, `authTokenEnv`, skills, executors, toolPrefix) are configured through the profile's user patch layer (`$DSH_HOME/profiles/<name>/cordis.patch.yml`) — the reserve path:
106
+ The dashboard covers day-to-day operations. Values the dashboard does not
107
+ edit (baseUrl, `authTokenEnv`, skills, executors, toolPrefix) are configured
108
+ through the profile's user patch layer (`$DSH_HOME/profiles/<name>/cordis.patch.yml`)
109
+ — the reserve path:
81
110
 
82
111
  ```yaml
83
112
  - id: a2a
@@ -103,11 +132,17 @@ The dashboard covers the day-to-day operations. Values the dashboard does not ed
103
132
 
104
133
  ### Required host services
105
134
 
106
- Base-backed profiles mount them all: `webServer` (`@deepseek-ai/dsh-host-webserver`), the storage stack (`@deepseek-ai/dsh-storage` + `@deepseek-ai/dsh-storage-domain`), the tools registry (`@deepseek-ai/dsh-tools`), and an agent loop (`@deepseek-ai/dsh-agent` + `@deepseek-ai/dsh-agent-loop`; the subagent executor additionally needs `@deepseek-ai/dsh-subagent`).
135
+ Base-backed profiles mount them all: `webServer` (`@deepseek-ai/dsh-host-webserver`),
136
+ the storage stack (`@deepseek-ai/dsh-storage` + `@deepseek-ai/dsh-storage-domain`),
137
+ the tools registry (`@deepseek-ai/dsh-tools`), and an agent loop
138
+ (`@deepseek-ai/dsh-agent` + `@deepseek-ai/dsh-agent-loop`; the subagent
139
+ executor additionally needs `@deepseek-ai/dsh-subagent`).
107
140
 
108
141
  ### Storage backend
109
142
 
110
- The task store lives in the `a2a` storage domain. The base composition routes storage through the `json` backend; to use SQLite, route the domain and add the backend in the same patch layer:
143
+ The task store lives in the `a2a` storage domain. The base composition routes
144
+ storage through the `json` backend; to use SQLite, route the domain and add
145
+ the backend in the same patch layer:
111
146
 
112
147
  ```yaml
113
148
  - id: storage-domain
@@ -137,7 +172,9 @@ The task store lives in the `a2a` storage domain. The base composition routes st
137
172
  timeoutMs: 60000
138
173
  ```
139
174
 
140
- Each enabled remote agent's skills become model tools named `a2a__<name>__<skill>` (normalized, collision-hashed). The registry persists across restarts.
175
+ Each enabled remote agent's skills become model tools named
176
+ `a2a__<name>__<skill>` (normalized, collision-hashed). The registry persists
177
+ across restarts.
141
178
 
142
179
  ## CLI
143
180
 
@@ -151,9 +188,15 @@ a2a status | enable | disable | card | agents |
151
188
 
152
189
  ## How it works
153
190
 
154
- - **Inbound** — `POST /a2a` (JSON-RPC) and `GET /.well-known/agent-card.json`; the AgentCard is derived from the live tool registry. Tasks flow through `a2a/inbound-task` → executor → task store, with SSE frames streamed to subscribers.
155
- - **Outbound** a persisted `agents` table in the `a2a` domain; `A2AClient` discovers an AgentCard, and each skill registers as a tool.
156
- - **Dashboard** the browser half (React, `settings.section`) reads/writes the loopback-only `/a2a/api` route.
191
+ - **Inbound** — `POST /a2a` (JSON-RPC) and `GET /.well-known/agent-card.json`;
192
+ the AgentCard derives from the live tool registry and carries the persisted
193
+ service identity. Tasks flow through `a2a/inbound-task` executor task
194
+ store, with SSE frames streamed to subscribers.
195
+ - **Outbound** — a persisted `agents` table in the `a2a` domain; `A2AClient`
196
+ discovers an AgentCard, and each skill registers as a tool.
197
+ - **Dashboard** — the browser half (React, `settings.section`) reads/writes
198
+ the loopback-only `/a2a/api` route; the host half feeds it snapshots of the
199
+ server, tasks, agents, inbound peers, and the service identity.
157
200
 
158
201
  See [docs/architecture.md](docs/architecture.md) for the full design.
159
202
 
@@ -161,19 +204,21 @@ See [docs/architecture.md](docs/architecture.md) for the full design.
161
204
 
162
205
  ```
163
206
  src/
164
- api.ts # loopback dashboard API (/a2a/api)
165
- index.ts # Cordis plugin entry (apply)
166
- protocol.ts # A2A v1.0 protocol constants + types
167
- jsonrpc.ts # JSON-RPC framing
168
- server/ # inbound half: store, card, a2a-server, routes, executors
169
- outbound/ # outbound half: A2AClient, registry, tools
170
- client/ # browser half: settings dashboard (React)
171
- service.ts # ctx.a2a service facade
172
- commands.ts # /a2a chat command
207
+ api.ts # loopback dashboard API (/a2a/api)
208
+ index.ts # Cordis plugin entry (apply)
209
+ protocol.ts # A2A v1.0 protocol constants + types
210
+ jsonrpc.ts # JSON-RPC framing
211
+ server/ # inbound half: store, card, a2a-server, routes,
212
+ # executors, inbound-registry, identity
213
+ outbound/ # outbound half: A2AClient, registry, tools
214
+ client/ # browser half: settings dashboard (React)
215
+ service.ts # ctx.a2a service facade
216
+ commands.ts # /a2a chat command
173
217
  tests/
174
- unit/ # protocol, framing, card, store, registry, server, client, api
175
- composition/ # apply() on a real Cordis Context with stub host services
176
- cordis.patch.yml # bundle patch (mounts the plugin, enabled by default)
218
+ unit/ # protocol, framing, card, store, registry, server,
219
+ # client, api, inbound-registry, identity
220
+ composition/ # apply() on a real Cordis Context with stub host services
221
+ cordis.patch.yml # bundle patch (mounts the plugin, enabled by default)
177
222
  ```
178
223
 
179
224
  ## Development
@@ -187,9 +232,15 @@ pnpm test # vitest run (unit + composition suites)
187
232
  pnpm build # tsc + tsdown → lib/index.js (host) + lib/client.js (browser)
188
233
  ```
189
234
 
190
- ## Known limitations
235
+ ## Acknowledgements
191
236
 
192
- OAuth 2.0 / per-client credentials, gRPC binding, push notifications, `INPUT_REQUIRED` ↔ approval, and passive outbound result injection are listed as P1 or explicitly-not-doing in [docs/architecture.md](docs/architecture.md). They are documented gaps, not a roadmap.
237
+ This plugin was inspired by and developed alongside
238
+ [ryubyte/dsh-a2a](https://github.com/ryubyte/dsh-a2a), an earlier A2A plugin
239
+ for DeepSeek Harness. That project's design — dual-end scope, AgentCard
240
+ derivation from the tool registry, and the settings-dashboard pattern — set
241
+ the direction this implementation follows. Our protocol layer, task store,
242
+ and executor seam are independent implementations; the GUI management model
243
+ owes a direct debt to ryubyte's connection dashboard.
193
244
 
194
245
  ## License
195
246
 
package/README.zh.md CHANGED
@@ -1,36 +1,33 @@
1
1
  # dsh-a2a
2
2
 
3
- Agent2Agent(A2A)v1.0 双端插件,用于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
3
+ Agent2Agent(A2A)v1.0 双端插件,用于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — [English](README.md) · **中文**
4
4
 
5
- > [English](README.md) | **中文**
5
+ `@hanphone/dsh-a2a` 是一个独立开源的 A2A 插件,把 DeepSeek Harness profile 变成 A2A 一等公民:带持久化、受治理任务存储与实时派生 AgentCard 的入站服务端;远程技能映射为模型工具的出站客户端;以及无需改任何文件即可管理两端的 GUI 面板。
6
6
 
7
- `@hanphone/dsh-a2a` 把 DeepSeek Harness profile 变成 A2A 一等公民:
8
-
9
- - **入站服务端** —— 从实时工具注册表派生的 AgentCard、JSON-RPC + SSE、持久化任务存储、可插拔的会话/子代理执行器,以及带审计的策略门禁(`a2a/inbound-task`)。
10
- - **出站客户端** —— 持久化的多 Agent 注册表,远程技能映射为模型工具(`a2a__<name>__<skill>`),带每 agent 超时的同步调用。
11
- - **GUI 面板** —— Harness Web UI 设置中的 **A2A 连接** 页:开关入站服务端、管理出站 agent、查看与取消任务——无需改任何配置文件。
12
-
13
- 设计决策见 [docs/architecture.md](docs/architecture.md)。当前范围为该文档中的 P0。
7
+ 架构与设计决策:[docs/architecture.md](docs/architecture.md)。
14
8
 
15
9
  ## 功能
16
10
 
17
- - **A2A v1.0 协议面** —— JSON-RPC 上的 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`GetExtendedAgentCard`、`SubscribeToTask`;SSE 流式带补发帧。
18
- - **动态 AgentCard** —— 技能从实时 `ctx.tools` 注册表派生(显式 id 清单,缺失引用大声失败),外加内置 `chat` 技能,装完即可直接验证。
19
- - **持久化任务存储** —— 任务存于 `a2a` 存储域(默认 json 后端,可按部署切 SQLite);任务 id 服务端生成且跨重启存活。
20
- - **执行器** —— `session`(每个 `contextId` 一个 DSH 会话)与 `subagent`(委托 `ctx.subagents`,把工具调用过程流式回传)两种内置实现。
21
- - **受治理的入站** —— 每个入站任务都经过 `a2a/inbound-task` waterfall,策略插件可在执行前否决或审计。
22
- - **环境变量鉴权** —— 入站 Bearer token 只以环境变量名(`authTokenEnv`)引用,不以明文落配置。
23
- - **装完即用** —— `dsh plugin add` 后两端默认启用,无需手动 patch。
11
+ - **A2A v1.0 协议面** JSON-RPC 上的 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`GetExtendedAgentCard`、`SubscribeToTask`;SSE 流式带补发帧。
12
+ - **动态 AgentCard** 技能从实时 `ctx.tools` 注册表派生(显式 id 清单,缺失引用大声失败),外加内置 `chat` 技能;全新安装即可端到端验证。
13
+ - **持久化任务存储** 任务存于 `a2a` 存储域(默认 json 后端,可按部署切 SQLite);服务端生成 id,跨重启存活。
14
+ - **执行器** `session`(每个 `contextId` 一个 DSH 会话)与 `subagent`(委托 `ctx.subagents`,把工具调用过程流式回传)。
15
+ - **受治理入站** 每个入站任务都经过 `a2a/inbound-task` waterfall,策略插件可否决或审计。
16
+ - **入站连接监控** 面板展示谁在调用本 DSH(来源、首/末次、任务数、活跃流),可关闭某个对端。
17
+ - **运行时服务身份** 在面板中编辑 AgentCard name/description/version;卡片立即重建,身份跨重启持久化。
18
+ - **引导式首次配置** — 全新安装显示预填身份表单("服务身份"),一步发布服务。
19
+ - **环境变量鉴权** — 入站 Bearer token 只以环境变量名(`authTokenEnv`)引用,不以明文落配置。
20
+ - **装完即用** — `dsh plugin add` 后两端默认启用,无需手动 patch。
24
21
 
25
22
  ## 安装
26
23
 
27
- ### 从 npm 发布版安装
24
+ ### 从 npm
28
25
 
29
26
  ```sh
30
27
  dsh plugin --profile web add @hanphone/dsh-a2a
31
28
  ```
32
29
 
33
- 任意 profile 名均可:
30
+ 任意 profile 名均可(`web`、自定义 profile、headless 等):
34
31
 
35
32
  ```sh
36
33
  dsh plugin --profile <name> add @hanphone/dsh-a2a
@@ -42,14 +39,14 @@ dsh plugin --profile <name> add @hanphone/dsh-a2a
42
39
  cd dsh-a2a
43
40
  pnpm build
44
41
  npm pack
45
- dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.1.0.tgz
42
+ dsh plugin --profile <name> add <path-to>/hanphone-dsh-a2a-0.2.0.tgz
46
43
  ```
47
44
 
48
45
  ## 快速开始
49
46
 
50
- 1. **安装** —— `dsh plugin --profile web add @hanphone/dsh-a2a`。
51
- 2. **重启 GUI** —— 浏览器端插件表在 host 启动时扫描,装完请重启一次 `pnpm dsh web`(或对应 profile 启动命令)。
52
- 3. **打开 设置 → A2A 连接** —— 会看到入站服务端状态、出站 agent 列表与任务列表。
47
+ 1. **安装** `dsh plugin --profile web add @hanphone/dsh-a2a`。
48
+ 2. **重启 GUI** 浏览器端插件表在 host 启动时扫描,装完请重启一次(`pnpm dsh web` 或对应 profile 启动命令)。
49
+ 3. **打开 设置 → A2A 连接** 面板展示入站服务端状态、服务身份(全新安装预填引导表单)、出站 agent 列表、入站对端连接、任务列表。
53
50
 
54
51
  入站服务端监听在 profile 的 webServer 上(默认 `http://127.0.0.1:3080`):
55
52
 
@@ -69,15 +66,17 @@ curl -X POST http://127.0.0.1:3080/a2a \
69
66
 
70
67
  浏览器端在设置中注册 **A2A 连接** 页。无需改文件即可:
71
68
 
72
- - 开关入站服务端(`server.enable` / `server.disable`),
73
- - 列出、添加、启用/停用、刷新、删除出站 agent,
69
+ - 开关入站服务端;
70
+ - 查看与编辑服务身份(name/description/version)——AgentCard 立即重建,变更持久化;
71
+ - 列出、添加、启用/停用、刷新、删除出站 agent;
72
+ - 查看入站对端连接(来源、活动、任务)并关闭某个对端;
74
73
  - 查看与取消入站任务。
75
74
 
76
75
  所有面板流量都走 profile webServer 上的**仅回环** `/a2a/api` 路由——远程对端永远无法驱动它。
77
76
 
78
77
  ## 配置
79
78
 
80
- 面板覆盖日常操作。面板不编辑的项(name/description、baseUrl、`authTokenEnv`、skills、executors、toolPrefix)通过 profile 用户 patch 层(`$DSH_HOME/profiles/<name>/cordis.patch.yml`)配置——保留方式:
79
+ 面板覆盖日常操作。面板不编辑的项(baseUrl、`authTokenEnv`、skills、executors、toolPrefix)通过 profile 用户 patch 层(`$DSH_HOME/profiles/<name>/cordis.patch.yml`)配置——保留方式:
81
80
 
82
81
  ```yaml
83
82
  - id: a2a
@@ -151,9 +150,9 @@ a2a status | enable | disable | card | agents |
151
150
 
152
151
  ## 工作原理
153
152
 
154
- - **入站** —— `POST /a2a`(JSON-RPC)与 `GET /.well-known/agent-card.json`;AgentCard 从实时工具注册表派生。任务流经 `a2a/inbound-task` → 执行器 → 任务存储,SSE 帧推送给订阅者。
155
- - **出站** —— `a2a` 域中的持久化 `agents` 表;`A2AClient` 发现 AgentCard,每个技能注册为一个工具。
156
- - **面板** —— 浏览器端(React,`settings.section`)经仅回环 `/a2a/api` 路由读写。
153
+ - **入站** `POST /a2a`(JSON-RPC)与 `GET /.well-known/agent-card.json`;AgentCard 从实时工具注册表派生并携带持久化服务身份。任务流经 `a2a/inbound-task` → 执行器 → 任务存储,SSE 帧推送给订阅者。
154
+ - **出站** `a2a` 域中的持久化 `agents` 表;`A2AClient` 发现 AgentCard,每个技能注册为一个工具。
155
+ - **面板** 浏览器端(React,`settings.section`)经仅回环 `/a2a/api` 路由读写;host 端喂给它 server/tasks/agents/入站对端/服务身份的快照。
157
156
 
158
157
  完整设计见 [docs/architecture.md](docs/architecture.md)。
159
158
 
@@ -161,19 +160,21 @@ a2a status | enable | disable | card | agents |
161
160
 
162
161
  ```
163
162
  src/
164
- api.ts # 回环面板 API (/a2a/api)
165
- index.ts # Cordis 插件入口 (apply)
166
- protocol.ts # A2A v1.0 协议常量与类型
167
- jsonrpc.ts # JSON-RPC 帧
168
- server/ # 入站半区:store、card、a2a-server、routes、executors
169
- outbound/ # 出站半区:A2AClient、registry、tools
170
- client/ # 浏览器半区:设置面板 (React)
171
- service.ts # ctx.a2a 服务 facade
172
- commands.ts # /a2a 聊天命令
163
+ api.ts # 回环面板 API (/a2a/api)
164
+ index.ts # Cordis 插件入口 (apply)
165
+ protocol.ts # A2A v1.0 协议常量与类型
166
+ jsonrpc.ts # JSON-RPC 帧
167
+ server/ # 入站半区:store、card、a2a-server、routes、
168
+ # executorsinbound-registry、identity
169
+ outbound/ # 出站半区:A2AClient、registry、tools
170
+ client/ # 浏览器半区:设置面板 (React)
171
+ service.ts # ctx.a2a 服务 facade
172
+ commands.ts # /a2a 聊天命令
173
173
  tests/
174
- unit/ # protocol、framing、card、store、registry、server、client、api
175
- composition/ # 在真实 Cordis Context 上以 stub 宿主服务跑 apply()
176
- cordis.patch.yml # bundle patch(挂载插件,默认启用)
174
+ unit/ # protocol、framing、card、store、registry、server、
175
+ # client、api、inbound-registry、identity
176
+ composition/ # 在真实 Cordis Context 上以 stub 宿主服务跑 apply()
177
+ cordis.patch.yml # bundle patch(挂载插件,默认启用)
177
178
  ```
178
179
 
179
180
  ## 开发
@@ -186,9 +187,9 @@ pnpm test # vitest run(单元 + 组合套件)
186
187
  pnpm build # tsc + tsdown → lib/index.js(host)+ lib/client.js(浏览器)
187
188
  ```
188
189
 
189
- ## 已知限制
190
+ ## 致谢
190
191
 
191
- OAuth 2.0 / 每客户端凭据、gRPC 绑定、push notifications、`INPUT_REQUIRED` 审批、被动出站结果注入,在 [docs/architecture.md](docs/architecture.md) 中列为 P1 或明确不做。它们是已记录的空缺,不是路线图。
192
+ 本项目受 [ryubyte/dsh-a2a](https://github.com/ryubyte/dsh-a2a) 启发并与其并行开发——那是 DeepSeek Harness 上更早的 A2A 插件。其设计——双端范围、从工具注册表派生 AgentCard、设置面板模式——为本文实现指明了方向。我们的协议层、任务存储与执行器接缝均为独立实现;GUI 管理模型则直接承袭了 ryubyte 的连接面板。
192
193
 
193
194
  ## 许可证
194
195
 
package/lib/client.js CHANGED
@@ -65,6 +65,10 @@ window.__ModuleLoader__.load({
65
65
  const [agentName, setAgentName] = (0, react.useState)("");
66
66
  const [agentUrl, setAgentUrl] = (0, react.useState)("");
67
67
  const [busy, setBusy] = (0, react.useState)(false);
68
+ const [editing, setEditing] = (0, react.useState)(false);
69
+ const [idName, setIdName] = (0, react.useState)("");
70
+ const [idDescription, setIdDescription] = (0, react.useState)("");
71
+ const [idVersion, setIdVersion] = (0, react.useState)("");
68
72
  (0, react.useEffect)(() => {
69
73
  let alive = true;
70
74
  const tick = async () => {
@@ -73,6 +77,11 @@ window.__ModuleLoader__.load({
73
77
  if (alive) {
74
78
  setSnap(next);
75
79
  setError(void 0);
80
+ if (!editing) {
81
+ setIdName(next.server.name ?? "");
82
+ setIdDescription(next.server.description ?? "");
83
+ setIdVersion(next.server.version ?? "");
84
+ }
76
85
  }
77
86
  } catch (err) {
78
87
  if (alive) setError(err.message);
@@ -99,12 +108,51 @@ window.__ModuleLoader__.load({
99
108
  };
100
109
  const server = snap?.server;
101
110
  const agents = snap?.agents ?? [];
111
+ const inbounds = snap?.inbounds ?? [];
102
112
  const tasks = snap?.tasks ?? [];
103
113
  return (0, react.createElement)("div", { className: "dsh-a2a-panel" }, error !== void 0 ? (0, react.createElement)("div", { className: "dsh-a2a-error" }, error) : null, (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, "入站 Server"), server === void 0 ? (0, react.createElement)("div", { className: "muted" }, "加载中…") : (0, react.createElement)("div", { className: "dsh-a2a-row" }, (0, react.createElement)("span", { className: `dsh-a2a-state ${server.enabled ? "ok" : "bad"}` }, server.enabled ? "已启用" : "已停用"), (0, react.createElement)("button", {
104
114
  className: server.enabled ? "" : "primary",
105
115
  disabled: busy,
106
116
  onClick: () => void control({ action: server.enabled ? "server.disable" : "server.enable" })
107
- }, server.enabled ? "停用" : "启用")), server !== void 0 && server.enabled ? (0, react.createElement)("div", { className: "dsh-a2a-row muted" }, `AgentCard: ${server.cardUrl ?? "(n/a)"} · 技能: ${server.skills.join(", ") || "(chat)"}`) : null), (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, `出站 Agents(${agents.length})`), agents.length === 0 ? (0, react.createElement)("div", { className: "muted" }, "未配置远程 agent。") : (0, react.createElement)("table", { className: "dsh-a2a-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, "名称"), (0, react.createElement)("th", null, "状态"), (0, react.createElement)("th", null, "工具"), (0, react.createElement)("th", null, "操作"))), (0, react.createElement)("tbody", null, ...agents.map((a) => (0, react.createElement)("tr", { key: a.id }, (0, react.createElement)("td", null, (0, react.createElement)("div", null, a.name), (0, react.createElement)("div", { className: "muted" }, a.agentCardUrl), a.lastError !== void 0 ? (0, react.createElement)("div", { className: "dsh-a2a-error" }, a.lastError) : null), (0, react.createElement)("td", null, (0, react.createElement)("span", { className: `dsh-a2a-state ${a.state === "connected" ? "ok" : "bad"}` }, a.state)), (0, react.createElement)("td", null, `${a.toolCount} / ${a.skillCount}`), (0, react.createElement)("td", null, (0, react.createElement)("button", {
117
+ }, server.enabled ? "停用" : "启用")), server !== void 0 && server.enabled ? (0, react.createElement)("div", { className: "dsh-a2a-row muted" }, `AgentCard: ${server.cardUrl ?? "(n/a)"} · 技能: ${server.skills.join(", ") || "(chat)"}`) : null), (0, react.createElement)("div", { className: "dsh-a2a-card" }, server === void 0 || server.configured === false ? (0, react.createElement)("h3", null, "服务身份(首次配置)") : (0, react.createElement)("h3", null, "服务身份"), (0, react.createElement)("form", {
118
+ className: "dsh-a2a-form",
119
+ onSubmit: (ev) => {
120
+ ev.preventDefault();
121
+ control({
122
+ action: "identity.update",
123
+ name: idName,
124
+ description: idDescription,
125
+ version: idVersion
126
+ }).then(() => {
127
+ setEditing(false);
128
+ });
129
+ }
130
+ }, (0, react.createElement)("input", {
131
+ value: idVersion,
132
+ placeholder: "版本",
133
+ onChange: (e) => setIdVersion(e.target.value),
134
+ style: { width: "60px" }
135
+ }), (0, react.createElement)("input", {
136
+ value: idName,
137
+ placeholder: "服务名称",
138
+ onChange: (e) => setIdName(e.target.value),
139
+ style: { width: "160px" }
140
+ }), (0, react.createElement)("input", {
141
+ value: idDescription,
142
+ placeholder: "服务描述",
143
+ onChange: (e) => setIdDescription(e.target.value),
144
+ style: { width: "260px" }
145
+ }), (0, react.createElement)("button", {
146
+ type: "submit",
147
+ className: "primary",
148
+ disabled: busy
149
+ }, "保存身份")), (0, react.createElement)("div", { className: "dsh-a2a-row muted" }, "名称、描述、版本即刻重建 AgentCard;技能始终从工具注册表派生。")), (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, `入站连接(${inbounds.length})`), inbounds.length === 0 ? (0, react.createElement)("div", { className: "muted" }, "当前无远程对端正在调用本服务。") : (0, react.createElement)("table", { className: "dsh-a2a-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, "来源"), (0, react.createElement)("th", null, "任务"), (0, react.createElement)("th", null, "流式"), (0, react.createElement)("th", null, "首次 / 最近"), (0, react.createElement)("th", null, "操作"))), (0, react.createElement)("tbody", null, ...inbounds.map((p) => (0, react.createElement)("tr", { key: p.id }, (0, react.createElement)("td", null, p.source ?? p.label), (0, react.createElement)("td", null, `${p.taskCount}(活跃 ${p.activeTaskIds.length})`), (0, react.createElement)("td", null, p.streaming ? "●" : "—"), (0, react.createElement)("td", null, (0, react.createElement)("span", { className: "muted" }, `${p.firstSeen ?? ""} / ${p.lastSeen ?? ""}`)), (0, react.createElement)("td", null, (0, react.createElement)("button", {
150
+ disabled: busy,
151
+ onClick: () => void control({
152
+ action: "inbound.close",
153
+ id: p.id
154
+ })
155
+ }, "关闭"))))))), (0, react.createElement)("div", { className: "dsh-a2a-card" }, (0, react.createElement)("h3", null, `出站 Agents(${agents.length})`), agents.length === 0 ? (0, react.createElement)("div", { className: "muted" }, "未配置远程 agent。") : (0, react.createElement)("table", { className: "dsh-a2a-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, "名称"), (0, react.createElement)("th", null, "状态"), (0, react.createElement)("th", null, "工具"), (0, react.createElement)("th", null, "操作"))), (0, react.createElement)("tbody", null, ...agents.map((a) => (0, react.createElement)("tr", { key: a.id }, (0, react.createElement)("td", null, (0, react.createElement)("div", null, a.name), (0, react.createElement)("div", { className: "muted" }, a.agentCardUrl), a.lastError !== void 0 ? (0, react.createElement)("div", { className: "dsh-a2a-error" }, a.lastError) : null), (0, react.createElement)("td", null, (0, react.createElement)("span", { className: `dsh-a2a-state ${a.state === "connected" ? "ok" : "bad"}` }, a.state)), (0, react.createElement)("td", null, `${a.toolCount} / ${a.skillCount}`), (0, react.createElement)("td", null, (0, react.createElement)("button", {
108
156
  disabled: busy,
109
157
  onClick: () => void control({
110
158
  action: a.enabled ? "agent.disable" : "agent.enable",