dsh-a2a-server 0.2.0 → 0.3.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 (55) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/CHANGELOG.zh-CN.md +28 -0
  3. package/README.md +99 -26
  4. package/README.zh-CN.md +67 -27
  5. package/cordis.patch.yml +10 -6
  6. package/lib/briefing.d.ts +39 -0
  7. package/lib/briefing.d.ts.map +1 -0
  8. package/lib/briefing.js +108 -0
  9. package/lib/briefing.js.map +1 -0
  10. package/lib/config.d.ts +54 -15
  11. package/lib/config.d.ts.map +1 -1
  12. package/lib/config.js +70 -5
  13. package/lib/config.js.map +1 -1
  14. package/lib/contexts.d.ts +9 -4
  15. package/lib/contexts.d.ts.map +1 -1
  16. package/lib/contexts.js +7 -13
  17. package/lib/contexts.js.map +1 -1
  18. package/lib/index.d.ts +1 -31
  19. package/lib/index.d.ts.map +1 -1
  20. package/lib/index.js +418 -439
  21. package/lib/index.js.map +1 -1
  22. package/lib/management.d.ts +5 -0
  23. package/lib/management.d.ts.map +1 -0
  24. package/lib/management.js +74 -0
  25. package/lib/management.js.map +1 -0
  26. package/lib/projection.d.ts +46 -3
  27. package/lib/projection.d.ts.map +1 -1
  28. package/lib/projection.js +19 -8
  29. package/lib/projection.js.map +1 -1
  30. package/lib/router.d.ts +45 -0
  31. package/lib/router.d.ts.map +1 -1
  32. package/lib/router.js +207 -65
  33. package/lib/router.js.map +1 -1
  34. package/lib/security.d.ts +4 -2
  35. package/lib/security.d.ts.map +1 -1
  36. package/lib/security.js +4 -2
  37. package/lib/security.js.map +1 -1
  38. package/lib/state.d.ts +37 -0
  39. package/lib/state.d.ts.map +1 -0
  40. package/lib/state.js +146 -0
  41. package/lib/state.js.map +1 -0
  42. package/lib/tasks.d.ts +4 -0
  43. package/lib/tasks.d.ts.map +1 -1
  44. package/lib/tasks.js +18 -0
  45. package/lib/tasks.js.map +1 -1
  46. package/lib/types.d.ts +9 -0
  47. package/lib/types.d.ts.map +1 -1
  48. package/lib/types.js.map +1 -1
  49. package/lib/version.d.ts +1 -1
  50. package/lib/version.js +1 -1
  51. package/lib/workspace-read.d.ts +9 -0
  52. package/lib/workspace-read.d.ts.map +1 -0
  53. package/lib/workspace-read.js +152 -0
  54. package/lib/workspace-read.js.map +1 -0
  55. package/package.json +39 -30
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ [English](CHANGELOG.md) · [简体中文](CHANGELOG.zh-CN.md)
4
+
5
+ ## 0.3.0
6
+
7
+ Requires DeepSeek Harness `>=0.2.1-rc.2 <0.3.0` and speaks A2A 1.0 JSON-RPC.
8
+
9
+ - Restrict visitors to chat and `read_file` within the owner's selected data directory. Block path escapes, links, private plugin state and other tools; separate public prompt context from the owner's runtime context.
10
+ - Move owner RPC and monitoring to authenticated local IPC. Public HTTP accepts visitor credentials only. An empty `trustedPeers` list denies all visitors.
11
+ - Bound concurrent tasks, daily admission, execution time, model calls, outputs and stream buffers. Prevent concurrent tasks in one context from mixing answers; clean up disconnected SSE subscriptions.
12
+ - Persist task results and idle conversation history across eviction and restart within the retention window. Fail interrupted work without replaying it.
13
+ - Add the optional Portal-configured automatic briefing loop: default-on hourly updates, a persistent switch, restart-safe timing, and direct publication to `.hop/handoff.md`.
14
+ - Declare required runtime packages and verify imports without development dependencies. Preserve the guard against malformed Unicode from upstream streaming decoders.
15
+
16
+ Validation: 217 tests in 15 files, TypeScript checking, build and declared-runtime package import check. Models are deterministic test adapters; real-model and cross-network deployment verification remain separate.
17
+
18
+ ### Upgrade
19
+
20
+ Stop the agent, update dsh if needed, then install into the existing profile:
21
+
22
+ ```sh
23
+ dsh plugin --profile YOUR_PROFILE add dsh-a2a-server@0.3.0
24
+ ```
25
+
26
+ Replace `YOUR_PROFILE` with the saved profile name and restart the agent. Upgrade the companion [Talk2MyAgent Portal](https://github.com/huangjuhua-aigc/Talk2MyAgent) for the private management endpoint and automatic briefing settings. Do not recreate an existing profile with `--from-default-profile` during an upgrade.
27
+
28
+ Old HTTP owner/monitoring integrations must migrate to local IPC. Review visitor admission and the selected data directory before exposing the service. [Migration details](https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/docs/public-access-hardening.en.md).
@@ -0,0 +1,28 @@
1
+ # 更新日志
2
+
3
+ [English](CHANGELOG.md) · [简体中文](CHANGELOG.zh-CN.md)
4
+
5
+ ## 0.3.0
6
+
7
+ 要求 DeepSeek Harness `>=0.2.1-rc.2 <0.3.0`,使用 A2A 1.0 JSON-RPC。
8
+
9
+ - 访客可以聊天,并通过 `read_file` 读取主人指定的资料目录。拦截路径越界、链接、插件私有状态及其他工具;公共模型提示与主人运行时上下文分离。
10
+ - 主人 RPC 和监控迁到需要认证的本机 IPC;公网 HTTP 仅接受访客凭据。空白名单 `trustedPeers: []` 拒绝全部访客。
11
+ - 限制并发、每日任务数、执行时限、模型调用、输出和流缓冲;同一 context 的并发请求不会串答,SSE 断开后释放订阅。
12
+ - 在保留期内持久化任务结果与空闲对话,支持驱逐和重启后的恢复。重启中断的任务标记失败,不自动重放。
13
+ - 增加由 Portal 配置的自动简报循环:默认每小时更新、一个持久化开关、重启去重,完成后直接写入 `.hop/handoff.md`。
14
+ - 补齐运行依赖声明,验证脱离开发依赖后可导入。保留上游流式解码异常的 Unicode 防护。
15
+
16
+ 验证:15 个文件共 217 项测试、类型检查、构建和仅使用声明运行依赖的包导入检查。测试使用确定性模型;真实模型与跨网络部署需要单独验收。
17
+
18
+ ### 升级
19
+
20
+ 停止 agent,必要时升级 dsh,然后更新原有 profile 中的插件:
21
+
22
+ ```sh
23
+ dsh plugin --profile YOUR_PROFILE add dsh-a2a-server@0.3.0
24
+ ```
25
+
26
+ 将 `YOUR_PROFILE` 替换为已保存的 profile 名称,安装后重新启动。配套 [Talk2MyAgent Portal](https://github.com/huangjuhua-aigc/Talk2MyAgent) 也需要更新,以使用私有管理入口和自动简报开关。升级已有 profile 时不要再次使用 `--from-default-profile`。
27
+
28
+ 原来的 HTTP 主人/监控调用必须迁移至本机 IPC。部署前检查访客许可与资料目录,详见[迁移指南](https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/docs/public-access-hardening.md)。
package/README.md CHANGED
@@ -16,8 +16,8 @@
16
16
  <a href="https://www.npmjs.com/package/dsh-a2a-server"><img src="https://img.shields.io/npm/v/dsh-a2a-server?style=flat&label=npm&color=CB3837" alt="npm version"></a>
17
17
  <a href="https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT License"></a>
18
18
  <img src="https://img.shields.io/badge/A2A-v1.0%20JSON--RPC-4D6BFE?style=flat" alt="A2A v1.0 JSON-RPC binding">
19
- <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">
20
- <img src="https://img.shields.io/badge/tests-167-2EA44F?style=flat" alt="167 tests">
19
+ <img src="https://img.shields.io/badge/DSH-0.2.1--rc.2-4493F8?style=flat" alt="Built against DSH 0.2.1-rc.2">
20
+ <img src="https://img.shields.io/badge/tests-217-2EA44F?style=flat" alt="217 tests">
21
21
  </p>
22
22
 
23
23
  `dsh-a2a-server` makes a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
@@ -30,14 +30,21 @@ The plugin is **inbound only**: it never connects to another agent. There is no
30
30
  client, no peer directory, and no A2A subagent provider. It is a transport
31
31
  adapter over `ctx.agents`, not a capability seam.
32
32
 
33
+ For a local configuration UI and cross-network access, use the companion
34
+ [Talk2MyAgent setup guide](https://github.com/huangjuhua-aigc/Talk2MyAgent/blob/main/README.en.md).
35
+ See the [deployment and migration guide](https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/docs/public-access-hardening.en.md)
36
+ for visitor permissions, automatic briefings, private owner access and Tailscale.
37
+
33
38
  ## Install
34
39
 
35
40
  ```sh
36
- dsh plugin --profile web add dsh-a2a-server
41
+ dsh plugin --profile web add dsh-a2a-server@0.3.0
37
42
  ```
38
43
 
39
44
  `dsh plugin` forwards to pnpm inside the profile directory and appends this
40
45
  bundle to `dsh.profile.bundles`, because the package declares `dsh.bundle`.
46
+ Since DSH v0.2.1-alpha.1, `--profile` must come before any pnpm arguments —
47
+ the forms above already do.
41
48
 
42
49
  To work from a checkout instead, point it at the directory:
43
50
 
@@ -50,13 +57,28 @@ dsh plugin --profile web add ./path/to/dsh-a2a-server
50
57
  | `ctx.agents` | `dsh-base` |
51
58
  | `ctx.credentials` | `dsh-base` |
52
59
  | `ctx.webServer` | **`dsh-web-app`** |
53
- | `ctx.sessionProjections` (optional) | composition; enables `GetTask` after settlement |
60
+ | `ctx.tools` and `ctx.systemPrompt` | required visitor tool and prompt isolation services |
61
+ | `ctx.sessionProjections` (optional) | composition; exposes task lifecycle projections |
54
62
 
55
- The plugin stays PENDING until the three required services exist. `ctx.webServer`
63
+ The plugin stays PENDING until all required services exist. `ctx.webServer`
56
64
  ships in `dsh-web-app` rather than `dsh-base`, so a `headless` profile needs
57
65
  `@deepseek-ai/dsh-host-webserver` mounted before this bundle serves anything.
58
66
  `dsh --profile <name> --dump-config` prints the composed rows.
59
67
 
68
+ ### Upgrading from 0.2.x
69
+
70
+ Version 0.3.0 requires DSH `>=0.2.1-rc.2 <0.3.0`. Stop the agent, install
71
+ `dsh-a2a-server@0.3.0` into its existing profile, then restart it. Owner RPC and
72
+ monitoring now use authenticated local IPC; update the companion Portal as well.
73
+ Visitors retain chat and scoped workspace reads, while other tools are denied.
74
+ An empty `trustedPeers: []` denies all visitors. Task results persist across
75
+ restart within the configured retention window.
76
+
77
+ The current [Talk2MyAgent Portal](https://github.com/huangjuhua-aigc/Talk2MyAgent)
78
+ configures default-on hourly briefings with one persistent switch. The plugin
79
+ alone does not enable automatic briefing without its `briefing` configuration.
80
+ See the [0.3.0 release notes](https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/CHANGELOG.md).
81
+
60
82
  ## Quick start
61
83
 
62
84
  The bundled example composition runs a real model and a listening server.
@@ -103,7 +125,7 @@ curl -s http://127.0.0.1:9922/a2a \
103
125
  | `A2A_PEERS` | `alice` | `name[:token]` list; a token is required for each |
104
126
  | `A2A_PORT` | `9900` | Listening port |
105
127
  | `A2A_SEND_MODE` | `block` | `block` or `immediate` |
106
- | `A2A_WORKSPACE_ROOT` | temp dir | Parent of the per-peer working directories |
128
+ | `A2A_WORKSPACE_ROOT` | temp dir | Authorized visitor data root and parent of per-peer working directories |
107
129
  | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | Model id requested from the adapter |
108
130
 
109
131
  A one-shot probe runs 67 checks against a live server and exits non-zero on any
@@ -121,13 +143,14 @@ node example/probe.mjs http://127.0.0.1:9922 demo123
121
143
  | `/.well-known/agent-card.json` | GET | Public by default |
122
144
  | `/.well-known/agent.json` | GET | Public by default |
123
145
  | `{basePath}` (default `/a2a`) | POST | Bearer required |
146
+ | `{basePath}/admin/contexts[/<id>]` | GET | Local IPC only + admin token; never registered on public HTTP |
124
147
 
125
148
  | JSON-RPC method | Status |
126
149
  | --- | --- |
127
150
  | `SendMessage` | Waiting negotiated per request |
128
151
  | `SendStreamingMessage` | SSE |
129
152
  | `GetTask` | Idempotent; answers after settlement |
130
- | `ListTasks` | Cursor-paginated, filterable, resident contexts only |
153
+ | `ListTasks` | Cursor-paginated, filterable, live and retained tasks |
131
154
  | `CancelTask` | Cancels the running turn |
132
155
  | `SubscribeToTask` | Live stream; `-32004` on a terminal task |
133
156
  | `GetExtendedAgentCard` | Extra skills for an authenticated peer; `-32007` when unconfigured |
@@ -179,17 +202,17 @@ another peer answers exactly as an absent one.
179
202
 
180
203
  card:
181
204
  name: dsh-harness
182
- description: Reads code, runs commands, reports findings.
205
+ description: Answers questions using code and text in the authorized directory.
183
206
  public: true
184
207
  skills:
185
208
  - id: general
186
209
  name: general
187
- description: General-purpose task execution.
210
+ description: Chat and scoped file reading.
188
211
  tags: [coding, research]
189
212
  extendedSkills: # authenticated peers only
190
213
  - id: internal
191
214
  name: internal diagnostics
192
- description: Reports harness internals.
215
+ description: Explains project documentation from the authorized directory.
193
216
  tags: [internal]
194
217
  provider:
195
218
  organization: Example Inc.
@@ -226,16 +249,31 @@ another peer answers exactly as an absent one.
226
249
  | `card.skills` | `[]` | Declared skills; falls back to one `general` entry |
227
250
  | `card.extendedSkills` | `[]` | Skills revealed only through `GetExtendedAgentCard` |
228
251
  | `peers` | `{}` | Identity → credential **reference name** |
229
- | `trustedPeers` | all authenticated | Allow-list of identities that may run tasks |
252
+ | `trustedPeers` | all authenticated | Access allow-list for conversation and scoped file reads; `[]` denies everyone |
230
253
  | `rateLimitPerMinute` | `60` | Sliding window per identity |
231
254
  | `maxContextTurns` | `5` | Messages accepted per context before `rejected` |
232
255
  | `sendMode` | `block` | Default when the client sends no `returnImmediately` |
233
- | `blockTimeoutMs` | `60000` | After which a blocking request is declined |
256
+ | `blockTimeoutMs` | `60000` | Maximum blocking wait; returns the current task on timeout so the client can poll |
234
257
  | `contextIdleTtlMs` | `1800000` | Idle time before a context's agent is released |
235
258
  | `maxResidentContexts` | `64` | Ceiling on resident contexts |
259
+ | `maxConcurrentTasks` / `maxConcurrentTasksPerPeer` | `8` / `2` | Atomic task admission; excess requests receive 429 |
260
+ | `maxTasksPerPeerPerDay` | `200` | Persisted task allowance per identity per UTC day |
261
+ | `taskTimeoutMs` | `120000` | Cancels execution when its deadline expires |
262
+ | `maxModelCallsPerTask` / `maxOutputTokens` | `8` / `4096` | Model request and per-request output limits |
263
+ | `maxInputChars` / `maxOutputChars` | `32000` / `64000` | Input/output text limits |
264
+ | `streamHeartbeatMs` / `maxStreamsPerTask` | `15000` / `4` | Heartbeat interval and subscription cap |
265
+ | `maxStreamBufferBytes` | `262144` | Disconnect a stream whose buffered frame exceeds the cap |
266
+ | `storage.directory` | `~/.dsh-a2a/<deployment-key>` | Private checkpoints; one process per directory |
267
+ | `storage.retentionMs` / `storage.maxContexts` | `86400000` / `256` | Retention after last activity, and maximum stored contexts |
268
+ | `storage.maxContextBytes` | `4000000` | Maximum checkpoint bytes per context |
236
269
  | `isolation.workspaceMode` | `per-peer` | `per-peer` or `shared` |
237
- | `isolation.workspaceRoot` | — | Required; parent directory or shared cwd |
238
- | `isolation.peerWorkspaces` | `{}` | Per-identity working-directory override |
270
+ | `isolation.workspaceRoot` | — | Required; authorized shared read-only data root and parent directory or shared cwd |
271
+ | `isolation.peerWorkspaces` | `{}` | Per-identity cwd and authorized read-root override |
272
+ | `preamble.files` | `[".hop/handoff.md"]` | Host briefing files (**agent-level**: resolved against the workspace root and shared by every visitor; absolute paths pass through), merged into each context's first message; absent files are skipped |
273
+ | `briefing.prompt` | — | Optional single-update instruction executed as the bounded local owner at startup and hourly; configured by Portal, requires owner administration |
274
+ | `briefing.settingsFile` | — | Absolute path to a JSON `{ "enabled": boolean }` preference; absent file means enabled, observed every 5 seconds. Disabling preserves the current briefing and lets active work finish; restart timing is persisted |
275
+ | `admin.tokenEnv` | — | Credential reference for local owner and monitoring requests; rejected by public RPC |
276
+ | `admin.socketPath` | deployment-specific | Windows named pipe or private Unix socket for owner RPC and monitoring |
239
277
  | `push.enabled` | `false` | Reserved; see [Boundaries](#boundaries) |
240
278
 
241
279
  Configuration is refused at load when `isolation.workspaceRoot` is absent, a peer
@@ -254,7 +292,8 @@ peers:
254
292
 
255
293
  ```yaml
256
294
  # ~/.dsh/.credentials.yaml
257
- A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>
295
+ refs:
296
+ A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>
258
297
  ```
259
298
 
260
299
  `ctx.credentials` resolves the reference per request across four layers — the
@@ -270,11 +309,29 @@ identities.
270
309
  | --- | --- | --- |
271
310
  | Model context | One peer's conversation cannot enter another's model request | Separate `contextId` → separate Session → separate log |
272
311
  | Protocol access | A peer cannot read, continue, or cancel another's context or task | Ownership by authenticated identity |
273
- | Tooling | A peer's agent cannot use tools to read another peer's session | `workspaceMode: per-peer` |
312
+ | Tooling | Only the scoped `read_file` is available; no writes, shell, network or cross-session tools | Inherited-tool masking, scoped reader, and execution guard |
313
+ | Prompt context | Public agents have a dedicated system prompt and suppress optional host runtime context | Scoped complete prompt and context suppression |
274
314
 
275
315
  `per-peer` (the default) derives each identity's `cwd` from `workspaceRoot`.
276
- `shared` places every peer in one directory, which suits peers collaborating on a
277
- single repository; under it, files written by one peer are readable by another.
316
+ `shared` places every peer in one directory. These are working-directory choices,
317
+ not filesystem sandboxes. `read_file` reads from the owner-selected
318
+ `workspaceRoot`; a `peerWorkspaces` override restricts that peer to the overridden
319
+ directory. Relative tool paths resolve from this **data root**, not the per-peer
320
+ cwd. These files are available to admitted visitors, so choose the data root
321
+ accordingly. The A2A checkpoint directory is always excluded.
322
+
323
+ `read_file({"path":"."})` lists the root's entries. A file path reads UTF-8 text;
324
+ `offset` (1-based) and `limit` page lines or directory entries. Reads are bounded
325
+ to 2 MiB files, 32,000 returned characters and 500 lines/entries per call; directory
326
+ scans cover at most 1,000 entries. Binary/office-document extraction is not yet
327
+ implemented. Canonical path checks refuse escapes; symlinks, junctions, hard
328
+ links and Windows alternate data streams are rejected. No shell is invoked.
329
+ `preamble.files` continues to provide an optional opening briefing.
330
+
331
+ Owner RPC uses the same `/a2a` path on **local IPC**, authenticated with the admin
332
+ token. It retains the standard caretaker preset. The public HTTP listener accepts
333
+ only visitor credentials and never mounts the monitoring API. See the
334
+ [migration and deployment notes](https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/docs/public-access-hardening.en.md).
278
335
 
279
336
  ## Boundaries
280
337
 
@@ -285,13 +342,20 @@ single repository; under it, files written by one peer are readable by another.
285
342
  - No `stateTransitionHistory`, protocol extensions, or card signatures.
286
343
  - Streaming emits committed assistant messages. Per-chunk streaming is not
287
344
  implemented.
288
- - No orphan-task watchdog: a task wedged non-terminal stays that way.
289
- - Cross-session tool denial is not implemented; isolation rests on
290
- `workspaceMode`.
345
+ - Execution deadlines cancel the agent. Admission remains occupied until the
346
+ underlying adapter finishes cancellation; an adapter ignoring AbortSignal
347
+ needs process-level supervision. These limits are not a currency budget.
348
+ - Only rooted `read_file` is enabled for visitors. Other tools require separate
349
+ capability work; changing the sandbox mode does not grant those tools. This
350
+ application-level file reader is not an OS sandbox against a malicious local
351
+ administrator mutating the filesystem.
291
352
  - A token ceiling settles a task as `completed`; the real turn ending travels in
292
353
  `Task.metadata.dsh.stopReason`, which A2A's state enum cannot express.
293
- - Task state survives settlement but not a process restart: session persistence
294
- is not composed, so the projection has no log to cold-fold after a reboot.
354
+ - Task results and idle conversation history survive eviction/restart within
355
+ the retention window. Interrupted tasks become failed and are never replayed.
356
+ Cold restoration creates a new harness session behind the stable contextId.
357
+ - One task may run per context; concurrent submissions receive 409. Message IDs
358
+ are not deduplication keys: proxies must not blindly retry submissions.
295
359
  - `ctx.webServer` provides no TLS. Any non-loopback exposure belongs behind a
296
360
  reverse proxy.
297
361
  - A configuration change restarts the plugin and cancels in-flight tasks.
@@ -312,6 +376,10 @@ src/
312
376
  ├── tasks.ts task slots and the three-stage turn correlation
313
377
  ├── projection.ts the a2aTask fold over the session log
314
378
  ├── security.ts authentication, rate limiting, defanging, redaction
379
+ ├── workspace-read.ts bounded read_file within the authorized data root
380
+ ├── management.ts private owner RPC and monitoring over local IPC
381
+ ├── state.ts durable task and conversation checkpoints
382
+ ├── briefing.ts hourly briefing updates and persistent scheduling
315
383
  ├── config.ts schema plus the cross-field checks that fail at load
316
384
  └── types.ts declaration merges into SessionEventMap / MessageSourceMap
317
385
  ```
@@ -327,17 +395,19 @@ carries the committed output, so the fold serves the answer without reaching
327
395
  back into message history.
328
396
 
329
397
  **Residency is explicit.** HTTP has no connection lifetime, so each `contextId`
330
- maps to an Activation evicted when idle, leaving the durable Session behind.
398
+ maps to an Activation evicted when idle. Private checkpoints retain task results
399
+ and conversation history; cold restoration creates a new harness session.
331
400
 
332
401
  ## Development
333
402
 
334
403
  ```sh
335
404
  pnpm install
336
405
  pnpm typecheck
337
- pnpm test # 167 tests across 10 files
406
+ pnpm test # protocol, lifecycle, security and workspace-read regressions
338
407
  pnpm serve # a listening server
339
408
  pnpm probe # 67 checks against a running server
340
409
  pnpm build # emit lib/
410
+ pnpm test:package # verify imports with only declared runtime packages
341
411
  ```
342
412
 
343
413
  The end-to-end suites boot a real Cordis composition with a real agent loop and
@@ -357,7 +427,10 @@ capability seams this plugin consumes. This project supplies:
357
427
  - Per-peer authentication, isolation, and workspace policy
358
428
 
359
429
  The harness is pre-release and does not promise compatibility across renames or
360
- repackaging, so peer dependencies are pinned exactly to `0.1.0-rc.6`.
430
+ repackaging, so compatibility is declared through `engines.dsh`
431
+ (`>=0.2.1-rc.2 <0.3.0`) plus range-bounded `@deepseek-ai/dsh-*` peer
432
+ dependencies: an install preflight refuses any combination the plugin was not
433
+ built against, and the range admits every 0.2.x patch that follows.
361
434
 
362
435
  ## Community
363
436
 
package/README.zh-CN.md CHANGED
@@ -16,8 +16,8 @@
16
16
  <a href="https://www.npmjs.com/package/dsh-a2a-server"><img src="https://img.shields.io/npm/v/dsh-a2a-server?style=flat&label=npm&color=CB3837" alt="npm 版本"></a>
17
17
  <a href="https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT License"></a>
18
18
  <img src="https://img.shields.io/badge/A2A-v1.0%20JSON--RPC-4D6BFE?style=flat" alt="A2A v1.0 JSON-RPC 绑定">
19
- <img src="https://img.shields.io/badge/DSH-0.1.0--rc.6-4493F8?style=flat" alt="基于 DSH 0.1.0-rc.6 构建">
20
- <img src="https://img.shields.io/badge/tests-167-2EA44F?style=flat" alt="167 项测试">
19
+ <img src="https://img.shields.io/badge/DSH-0.2.1--rc.2-4493F8?style=flat" alt="基于 DSH 0.2.1-rc.2 构建">
20
+ <img src="https://img.shields.io/badge/tests-217-2EA44F?style=flat" alt="217 项测试">
21
21
  </p>
22
22
 
23
23
  `dsh-a2a-server` 让 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
@@ -30,12 +30,16 @@ subagent provider。它是 `ctx.agents` 之上的传输适配层,不是能力
30
30
 
31
31
  ## 安装
32
32
 
33
+ 需要本机配置界面或跨网络访问时,先看配套 [Talk2MyAgent 使用说明](https://github.com/huangjuhua-aigc/Talk2MyAgent)。
34
+ 访客读取权限、自动简报、主人 IPC 与 Tailscale 部署详见[部署与迁移指南](https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/docs/public-access-hardening.md)。
35
+
33
36
  ```sh
34
- dsh plugin --profile web add dsh-a2a-server
37
+ dsh plugin --profile web add dsh-a2a-server@0.3.0
35
38
  ```
36
39
 
37
40
  `dsh plugin` 会在 profile 目录里转发给 pnpm,并把这个 bundle 追加进
38
- `dsh.profile.bundles`——因为包中声明了 `dsh.bundle`。
41
+ `dsh.profile.bundles`——因为包中声明了 `dsh.bundle`。自 DSH v0.2.1-alpha.1 起,
42
+ `--profile` 必须放在所有 pnpm 参数之前——上面的写法已经是正确的。
39
43
 
40
44
  若想直接使用本地检出,把路径指过去即可:
41
45
 
@@ -48,12 +52,25 @@ dsh plugin --profile web add ./path/to/dsh-a2a-server
48
52
  | `ctx.agents` | `dsh-base` |
49
53
  | `ctx.credentials` | `dsh-base` |
50
54
  | `ctx.webServer` | **`dsh-web-app`** |
51
- | `ctx.sessionProjections`(可选) | 组合层;启用后 `GetTask` 在任务结算后仍可应答 |
55
+ | `ctx.tools` 和 `ctx.systemPrompt` | 必需的访客工具及提示词隔离服务 |
56
+ | `ctx.sessionProjections`(可选) | 组合层;提供任务生命周期投影 |
52
57
 
53
- 三个必需服务齐备之前,插件保持 PENDING。`ctx.webServer` 由 `dsh-web-app` 提供而不在
58
+ 必需服务齐备之前,插件保持 PENDING。`ctx.webServer` 由 `dsh-web-app` 提供而不在
54
59
  `dsh-base` 中,因此 `headless` profile 需要先挂载 `@deepseek-ai/dsh-host-webserver`。
55
60
  `dsh --profile <name> --dump-config` 可以打印组合出来的配置行。
56
61
 
62
+ ### 从 0.2.x 升级
63
+
64
+ 0.3.0 适配 DSH `>=0.2.1-rc.2 <0.3.0`。先停止 agent,将
65
+ `dsh-a2a-server@0.3.0` 安装到原有 profile,再重新启动;配套 Portal 也需更新。
66
+ 主人 RPC 和监控已改为需要认证的本机 IPC,公网 HTTP 不接受主人凭据。
67
+ 访客可聊天并读取授权目录,其他工具关闭;空白名单 `trustedPeers: []` 拒绝所有访客。
68
+ 任务结果与空闲会话在保留期内可跨驱逐和重启恢复。
69
+
70
+ 新版 [Talk2MyAgent Portal](https://github.com/huangjuhua-aigc/Talk2MyAgent) 默认开启每小时简报更新,
71
+ 用一个开关控制,完成后直接写入 `.hop/handoff.md`。单独安装协议插件而未配置 `briefing`
72
+ 不会自动执行简报任务。详见 [0.3.0 发布说明](https://github.com/huangjuhua-aigc/dsh-a2a/blob/main/CHANGELOG.zh-CN.md)。
73
+
57
74
  ## 快速开始
58
75
 
59
76
  仓库自带的示例组合会运行真实模型并启动一个监听服务。
@@ -99,7 +116,7 @@ curl -s http://127.0.0.1:9922/a2a \
99
116
  | `A2A_PEERS` | `alice` | `名字[:token]` 列表;每个 peer 都必须有 token |
100
117
  | `A2A_PORT` | `9900` | 监听端口 |
101
118
  | `A2A_SEND_MODE` | `block` | `block` 或 `immediate` |
102
- | `A2A_WORKSPACE_ROOT` | 临时目录 | per-peer 工作目录的父目录 |
119
+ | `A2A_WORKSPACE_ROOT` | 临时目录 | 访客授权资料目录,同时作为 per-peer 工作目录的父目录 |
103
120
  | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | 向适配器请求的模型 id |
104
121
 
105
122
  一次性探针会对运行中的服务执行 67 项检查,任何一项不符即以非零码退出:
@@ -116,13 +133,14 @@ node example/probe.mjs http://127.0.0.1:9922 demo123
116
133
  | `/.well-known/agent-card.json` | GET | 默认公开 |
117
134
  | `/.well-known/agent.json` | GET | 默认公开 |
118
135
  | `{basePath}`(默认 `/a2a`) | POST | 必须携带 Bearer |
136
+ | `{basePath}/admin/contexts[/<id>]` | GET | 仅本机 IPC + admin token,公网 HTTP 不挂载 |
119
137
 
120
138
  | JSON-RPC 方法 | 状态 |
121
139
  | --- | --- |
122
140
  | `SendMessage` | 是否等待逐请求协商 |
123
141
  | `SendStreamingMessage` | SSE |
124
142
  | `GetTask` | 幂等;结算后仍可应答 |
125
- | `ListTasks` | 游标分页、可过滤;仅覆盖常驻的 context |
143
+ | `ListTasks` | 游标分页、可过滤;包括保留期内的持久化任务 |
126
144
  | `CancelTask` | 取消正在执行的 turn |
127
145
  | `SubscribeToTask` | 续订活任务;终态任务返回 `-32004` |
128
146
  | `GetExtendedAgentCard` | 已认证 peer 可见的额外 skill;未配置时 `-32007` |
@@ -171,12 +189,12 @@ part 是被**删除**而不是做了别名:本服务写出的每一个回包
171
189
 
172
190
  card:
173
191
  name: dsh-harness
174
- description: 读代码、执行命令、给出结论。
192
+ description: 读取授权目录中的代码和资料,回答访客问题。
175
193
  public: true
176
194
  skills:
177
195
  - id: general
178
196
  name: general
179
- description: 通用任务执行。
197
+ description: 对话与授权目录内的文件读取。
180
198
  tags: [coding, research]
181
199
  provider:
182
200
  organization: Example Inc.
@@ -213,16 +231,21 @@ part 是被**删除**而不是做了别名:本服务写出的每一个回包
213
231
  | `card.skills` | `[]` | 声明的 skills;为空时回退到一条 `general` |
214
232
  | `card.extendedSkills` | `[]` | 只对已认证 peer 经 `GetExtendedAgentCard` 公开的 skills |
215
233
  | `peers` | `{}` | 身份 → 凭据**引用名** |
216
- | `trustedPeers` | 全部已认证身份 | 允许执行任务的身份白名单 |
234
+ | `trustedPeers` | 全部已认证身份 | 对话与受限文件读取白名单;`[]` 拒绝所有访客 |
217
235
  | `rateLimitPerMinute` | `60` | 按身份的滑动窗口 |
218
236
  | `maxContextTurns` | `5` | 单个 context 接受的消息数上限,超出后 `rejected` |
219
237
  | `sendMode` | `block` | 客户端未表态时的默认行为 |
220
- | `blockTimeoutMs` | `60000` | 超过后拒绝继续阻塞 |
238
+ | `blockTimeoutMs` | `60000` | 最长阻塞等待;到期返回任务当前状态,客户端可继续查询 |
221
239
  | `contextIdleTtlMs` | `1800000` | context 的 agent 被释放前的空闲时长 |
222
240
  | `maxResidentContexts` | `64` | 常驻 context 数量上限 |
223
241
  | `isolation.workspaceMode` | `per-peer` | `per-peer` 或 `shared` |
224
- | `isolation.workspaceRoot` | — | 必填;父目录或共享 cwd |
225
- | `isolation.peerWorkspaces` | `{}` | 按身份覆盖工作目录 |
242
+ | `isolation.workspaceRoot` | — | 必填;授权资料目录,同时作为访客 cwd 的父目录或共享 cwd |
243
+ | `isolation.peerWorkspaces` | `{}` | 按身份覆盖工作目录和授权读取范围 |
244
+ | `preamble.files` | `[".hop/handoff.md"]` | 主人背景简报文件(**agent 级**:相对工作区根目录解析,全体访客共用一份;绝对路径直通),合并进每个 context 的首条访客消息;文件不存在则跳过 |
245
+ | `briefing.prompt` | — | 可选的单次简报更新指令,由 Portal 配置,通过有执行预算的主人 agent 每小时运行 |
246
+ | `briefing.settingsFile` | — | 绝对路径;读取 `{ "enabled": boolean }` 设置,缺省开启,每 5 秒检查 |
247
+ | `admin.tokenEnv` | — | 本机主人 RPC 和监控的凭据引用,公网 RPC 拒绝此凭据 |
248
+ | `admin.socketPath` | 按部署生成 | Windows 命名管道或私有 Unix socket |
226
249
  | `push.enabled` | `false` | 保留字段,见[边界](#边界) |
227
250
 
228
251
  以下情况在加载期即被拒绝:缺少 `isolation.workspaceRoot`;peer 名不匹配
@@ -240,7 +263,8 @@ peers:
240
263
 
241
264
  ```yaml
242
265
  # ~/.dsh/.credentials.yaml
243
- A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>
266
+ refs:
267
+ A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>
244
268
  ```
245
269
 
246
270
  `ctx.credentials` 在每次请求时跨四层解析该引用——进程环境、托管文档、`<cwd>/.env`、
@@ -254,11 +278,18 @@ A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>
254
278
  | --- | --- | --- |
255
279
  | 模型上下文 | 一个 peer 的对话不会进入另一个 peer 的模型请求 | 不同 `contextId` → 不同 Session → 不同 log |
256
280
  | 协议访问 | peer 无法读取、续接或取消他人的 context 与任务 | 按认证身份判定归属 |
257
- | 工具层 | peer 的 agent 无法借工具读取他人的 session | `workspaceMode: per-peer` |
281
+ | 工具层 | 仅提供受控 `read_file`,不开放写入、命令、联网或跨会话工具 | 屏蔽继承工具、专用读取工具、执行校验 |
282
+ | 提示词 | 访客不继承可选的主人个性化提示与运行时上下文 | 独立的公共系统提示词 |
283
+
284
+ `per-peer`(默认)依据 `workspaceRoot` 为每个身份派生独立 `cwd`;`shared` 使用同一个目录。
285
+ 这只是工作目录策略,不是文件系统沙箱。`read_file` 的读取范围默认是用户选择的
286
+ `workspaceRoot`,有 `peerWorkspaces` 覆盖时限制为该访客的专属目录。工具相对路径从
287
+ 这个资料目录解析;插件私有状态目录始终排除。
258
288
 
259
- `per-peer`(默认)依据 `workspaceRoot` 为每个身份派生独立 `cwd`。`shared` 则把所有 peer
260
- 放进同一个目录,适用于协作维护同一个仓库的场景;此模式下一个 peer 写入的文件对其他 peer
261
- 可读。
289
+ `read_file({"path":"."})` 列出资料目录;文件支持 UTF-8 文本,按 `offset`/`limit`
290
+ 分页。单文件上限 2 MiB,单次最多 32000 字符、500 行/项,目录扫描最多 1000 项。
291
+ 拒绝越界、符号链接/junction、硬链接和 Windows ADS。主人维护能力使用独立本机 IPC,
292
+ 保留可信管理工具,不能从公网入口调用。资源配额的完整配置表见英文 README。
262
293
 
263
294
  ## 边界
264
295
 
@@ -268,12 +299,14 @@ A2A_PEER_ALICE: <32-byte-hex-from-openssl-rand>
268
299
  `pushNotifications: false`。
269
300
  - 不支持 `stateTransitionHistory`、协议扩展与 Card 签名。
270
301
  - 流式只推送已提交的 assistant 消息,未实现逐 chunk 推送。
271
- - 无孤儿任务看门狗:卡在非终态的任务会一直保持该状态。
272
- - 未实现跨会话工具拒绝;隔离依赖 `workspaceMode`。
302
+ - 有并发准入、任务期限及模型调用预算;超时后等待底层适配器完成取消才释放执行名额。
303
+ 不响应取消的适配器仍需进程级监管,这些额度不是按货币计费的预算。
304
+ - 访客仅能使用有目录边界的 `read_file`;改变沙箱模式不会自动开放其他工具。
305
+ 该工具边界不替代针对恶意本机管理员的操作系统隔离。
273
306
  - 触及 token 上限的任务结算为 `completed`,真实的 turn 结束原因放在
274
307
  `Task.metadata.dsh.stopReason`——A2A 的状态枚举无法表达它。
275
- - 任务状态可以跨结算存活,但无法跨进程重启:未组合 session 持久化,重启后 projection
276
- 没有日志可供冷折叠。
308
+ - 任务结果与空闲会话在保留期内可跨进程重启恢复;中断的任务标记失败,不重放。
309
+ - 每个 context 同时只允许一个任务,并发提交返回 409;messageId 不是提交去重键。
277
310
  - `ctx.webServer` 不提供 TLS。任何非回环地址的暴露都应置于反向代理之后。
278
311
  - 修改配置会重启插件并取消进行中的任务。
279
312
 
@@ -293,6 +326,10 @@ src/
293
326
  ├── tasks.ts 任务槽位与三段式 turn 关联
294
327
  ├── projection.ts 在 session log 上折叠出的 a2aTask 读模型
295
328
  ├── security.ts 认证、限流、注入去势、外发脱敏
329
+ ├── workspace-read.ts 授权目录内受限的 read_file
330
+ ├── management.ts 本机 IPC 主人 RPC 与监控
331
+ ├── state.ts 持久化任务及对话 checkpoint
332
+ ├── briefing.ts 每小时简报更新与持久化调度
296
333
  ├── config.ts schema,以及加载期就会拒绝的跨字段校验
297
334
  └── types.ts 向 SessionEventMap / MessageSourceMap 的声明合并
298
335
  ```
@@ -306,17 +343,18 @@ src/
306
343
  历史。
307
344
 
308
345
  **驻留是显式管理的。** HTTP 没有连接生命周期,因此每个 `contextId` 对应一个 Activation,
309
- 空闲后被驱逐,持久化的 Session 留在原地。
346
+ 空闲后被驱逐。私有 checkpoint 保留任务结果与对话历史,冷恢复时创建新的 harness session。
310
347
 
311
348
  ## 开发
312
349
 
313
350
  ```sh
314
351
  pnpm install
315
352
  pnpm typecheck
316
- pnpm test # 10 个文件共 167 项测试
353
+ pnpm test # 15 个文件共 217 项测试
317
354
  pnpm serve # 启动监听服务
318
355
  pnpm probe # 对运行中的服务执行 67 项检查
319
356
  pnpm build # 产出 lib/
357
+ pnpm test:package # 仅声明运行依赖时验证构建产物导入
320
358
  ```
321
359
 
322
360
  端到端测试会启动真实的 Cordis 组合与真实的 agent loop,并通过 HTTP 驱动它;模型使用
@@ -333,8 +371,10 @@ pnpm build # 产出 lib/
333
371
  - A2A task 与 harness turn 之间的映射
334
372
  - 按 peer 的认证、隔离与工作目录策略
335
373
 
336
- harness 处于 pre-release 阶段,不承诺跨重命名或重新打包的兼容性,因此 peer 依赖精确锁定
337
- 在 `0.1.0-rc.6`。
374
+ harness 处于 pre-release 阶段,不承诺跨重命名或重新打包的兼容性,因此兼容性通过
375
+ `engines.dsh`(`>=0.2.1-rc.2 <0.3.0`)加上按 range 声明的 `@deepseek-ai/dsh-*`
376
+ peer 依赖来表达:安装预检会拒绝任何插件未曾适配过的组合,而这个 range 也放行后续
377
+ 所有的 0.2.x 补丁版本。
338
378
 
339
379
  ## 社区交流
340
380
 
package/cordis.patch.yml CHANGED
@@ -7,12 +7,15 @@
7
7
  # REQUIRES ctx.webServer, which ships in `dsh-web-app`, NOT in `dsh-base`. On a
8
8
  # profile without it this row loads but stays PENDING (Cordis holds a plugin
9
9
  # whose injected services are absent), so nothing serves and nothing errors.
10
+ #
11
+ # NOTE: keep this file plain YAML — the installer's package validation parses
12
+ # it with a strict loader that rejects `!!js` expressions, even though the
13
+ # runtime overlay loader accepts them.
10
14
  - insert:
11
15
  - id: a2a-server
12
16
  name: dsh-a2a-server
13
17
  config:
14
18
  basePath: /a2a
15
- protocolVersion: '0.3.0'
16
19
  card:
17
20
  name: dsh-harness
18
21
  description: A DeepSeek Harness agent reachable over A2A.
@@ -20,11 +23,12 @@
20
23
  skills: []
21
24
  peers: {}
22
25
  isolation:
23
- # Default posture is isolation: each peer gets its own working
24
- # directory, which also makes the cwd-equality authority in
25
- # cross-session tooling separate peers for free. A deployment whose
26
- # peers collaborate on one repository must say so explicitly.
26
+ # Each peer gets its own working directory. This is not a filesystem
27
+ # sandbox: read_file separately restricts reads to the selected root,
28
+ # or to a peerWorkspaces override. Other visitor tools are disabled.
29
+ # This placeholder is overridden by any real deployment layer (the
30
+ # console's overlay sets it per agent); supply an absolute path.
27
31
  workspaceMode: per-peer
28
- workspaceRoot: !!js `${process.env.DSH_HOME ?? process.env.HOME}/a2a-workspaces`
32
+ workspaceRoot: /srv/dsh/a2a
29
33
  push:
30
34
  enabled: false
@@ -0,0 +1,39 @@
1
+ export declare const BRIEFING_INTERVAL_MS: number;
2
+ interface LegacyBriefingScheduler {
3
+ catalog(): Promise<Array<{
4
+ id: string;
5
+ sessionId: string;
6
+ title: string;
7
+ prompt: string;
8
+ }>>;
9
+ delete(request: {
10
+ id: string;
11
+ sessionId: string;
12
+ }): Promise<unknown>;
13
+ }
14
+ /** Retire only the old Portal-generated job for this exact project. */
15
+ export declare function retireLegacyBriefing(scheduler: LegacyBriefingScheduler | undefined, root: string): Promise<void>;
16
+ export declare class AutomaticBriefing {
17
+ private readonly options;
18
+ private timer;
19
+ private closed;
20
+ private running;
21
+ private enabled;
22
+ private nextRunAt;
23
+ private readonly stateFile;
24
+ constructor(options: {
25
+ settingsFile: string;
26
+ stateDirectory: string;
27
+ run: () => Promise<void>;
28
+ warn: (message: string) => void;
29
+ now?: () => number;
30
+ });
31
+ private save;
32
+ private readEnabled;
33
+ /** Polling also observes an off/on switch without restarting the agent. */
34
+ tick(): Promise<void>;
35
+ start(): void;
36
+ stop(): void;
37
+ }
38
+ export {};
39
+ //# sourceMappingURL=briefing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"briefing.d.ts","sourceRoot":"","sources":["../src/briefing.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,oBAAoB,QAAiB,CAAA;AAGlD,UAAU,uBAAuB;IAC/B,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;IAC3F,MAAM,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACrE;AAED,uEAAuE;AACvE,wBAAsB,oBAAoB,CAAC,SAAS,EAAE,uBAAuB,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtH;AAED,qBAAa,iBAAiB;IAQhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAO;IACtB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;gBAEL,OAAO,EAAE;QACpC,YAAY,EAAE,MAAM,CAAA;QACpB,cAAc,EAAE,MAAM,CAAA;QACtB,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;QACxB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;QAC/B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KACnB;IAWD,OAAO,CAAC,IAAI;IAQZ,OAAO,CAAC,WAAW;IAWnB,2EAA2E;IACrE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,KAAK,IAAI,IAAI;IAOb,IAAI,IAAI,IAAI;CAIb"}