dsh-any-agent 0.5.0 → 0.5.2

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.
@@ -10,8 +10,16 @@
10
10
 
11
11
  `on_approval` 可选 `auto-allow` / `auto-deny` / `ask`(`ask` 会把审批挂起,交给 `dsh_approval_decide` 裁决)。
12
12
 
13
+ **`auto-allow` 下 `workspace-write` 是软边界。** DSH 碰到沙箱挡住的操作(例如 Windows 上子进程管道被拦)会申请 `sandbox_permissions: danger-full-access`,`auto-allow` 会直接放行。默认值不变,但放行**不再是静默的**:每个回合结果都带 `auto_approvals_total`、`auto_escalations_total` 和最多 20 条脱敏后的 `auto_approvals` 摘要(工具、提权档、理由、命令预览);审核单的证据与清单会点名"本轮有 N 次自动放行,其中 M 次提权"。要逐次把关,就用 `on_approval=ask` 新建会话。
14
+
13
15
  **`read-only` 是强制边界,不只是默认值**:它固定使用 `auto-deny`;给只读会话传 `auto-allow` / `ask` 会在 `dsh_start` 被直接拒绝,审批层也会拒绝来自只读会话的任何权限请求。建会话/resume 后还会回读会话自己记录的权限档;只读档如果在 DSH 投影缓存刷盘窗口内仍读不到记录,会 **fail-closed 拒绝继续**,而不是只依赖审批层兜底。否则 profile 的 `defaultPreset` 加宽松审批策略可能放行写操作(对抗审计实测过)。
14
16
 
17
+ ## 🧷 输入边界
18
+
19
+ - **`conversation_id` / `run_id` 是路径安全的单段 id**:它们会参与哨兵、审核、元数据与墓碑的路径拼接;服务只接受字母/数字/`._-` 组成的单段(最长 200),带分隔符或 `..` 的值在 `join()` 之前就被拒绝。覆盖 `dsh_get` 的哨兵回读与 `dsh_review` 的 `<状态目录>/reviews/<会话>/<run>.review.json` 写入。
20
+ - **可写档位下 `cwd` 不能是文件系统根目录**:`workspace-write` 选 `/` 或 `C:\` 会让沙箱根变成整个磁盘;家目录/根目录守卫会先解析 symlink,所以指向根目录或家目录的软链也会被拒绝。`danger-full-access` 仍需显式开启。
21
+ - **`title_hint` 最长 200 字**(schema + 运行时):它会写进注册表与侧车,且每轮都会重复落盘。
22
+
15
23
  ## 🪟 会话在 DSH GUI 里的可见性
16
24
 
17
25
  两件事,别混淆:
@@ -50,10 +58,11 @@ node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话
50
58
  | `debug` | 更细的本服务事件;仍不打印 DSH 输出 |
51
59
  | `DSH_MCP_LOG_STDERR=1` | 额外转发 DSH 原始 stderr(**可能含思考内容**) |
52
60
 
53
- **本服务不把思考内容写进自己的落盘物**,只有一个**刻意留下**的例外:
61
+ **本服务不把思考内容写进自己的落盘物**;它只会落盘调用方离线收活/审核所需的正文,但从不落盘思考文本:
54
62
 
55
63
  - 不写日志(默认静默);状态文件与 `<状态目录>/meta/` 下的会话元数据侧车都只有元数据(id、cwd、权限、计数、标题提示),冒烟测试里有一条断言专门守注册表。
56
64
  - **异步完成哨兵需要把正文写进文件**(否则调用方无法离线收活),载荷还包含用量与(已按审批规则脱敏的)工具输入,但 `result.thinking` 会被**剥掉**,并始终给出布尔 `thinking_omitted`。测试里专门验证了:即使请求 `reasoning=full`,哨兵里也不出现任何思考文本——而同一 run 在内存(`dsh_get`)里仍能看到。设 `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` 可刻意打破这条。
57
- - 哨兵默认 7 天后由启动清理删除(`DSH_MCP_SENTINEL_TTL_MS`,`0` 表示永久保留)。
65
+ - **审核记录是第二个刻意落盘的产物**:`<状态目录>/reviews/<会话id>/<run_id>.review.json` 保存裁决、理由/findings、清单与证据摘要。理由、findings、任务预览、证据**以及每个清单项**都走同一套凭证脱敏(清单会拼接 `result.error`),且**不含思考正文**;但它可能引用代码或路径,同样按敏感数据对待。
66
+ - 哨兵**与审核记录**默认 7 天后由启动清理删除(`DSH_MCP_SENTINEL_TTL_MS`,`0` 表示永久保留)。
58
67
 
59
68
  > ⚠️ **DSH 自己会在 `~/.dsh/sessions` 写会话日志,而且其中包含完整 reasoning 文本**(即使 `reasoning=hide` 也照存)。那是 resume 的依据,不在本服务控制范围内;“思考不落盘”的绝对表述只适用于**本服务自己的**日志/哨兵/注册表。
package/docs/zh/tests.md CHANGED
@@ -15,22 +15,23 @@ node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目
15
15
 
16
16
  | 套件 | 检查数 | 覆盖 |
17
17
  |---|---|---|
18
- | `smoke` | 29 | 握手、工具表、建会话、配置、注册表纯净性 |
18
+ | `smoke` | 31 | 握手、工具表(15 个工具)、建会话、配置、注册表纯净性、原型链工具名拒绝 |
19
19
  | `boundary` | 46 | 协议边界(重复 initialize、脏行、未知方法)、参数校验(含 read-only + 宽松审批必须拒绝、未知 provider/model、失败创建不留下写锁登记)、未知 id、游标边界、生命周期幂等、unicode |
20
20
  | `integration` | 30 | 跨进程 resume 与记忆、中断、两种插话 |
21
- | `async` | 16 | fire-and-forget + 事后验收 |
21
+ | `async` | 21 | fire-and-forget + 事后验收,含收据/结果/哨兵里的审核单,以及一次真实 `dsh_review` 往返 |
22
22
  | `sentinel` | 36 | 完成哨兵:原子性、闩锁语义、多会话并发不串台、被取消也落地、**思考不进文件** |
23
23
  | `prune` | 11 | **哨兵留存**:清超期与崩溃残留、留新、清空壳目录、`TTL=0` 关闭清理、且绝不碰同目录的会话注册表(不花 token,每次必跑) |
24
24
  | `guards` | 40 | **环境变量守卫**:奇怪的数值一律压到安全侧 —— `IDLE_TTL_MS` 的 `-1` 当"永不回收"、非法值/空串回落默认 10 分钟、回收间隔过小压回默认(防空转紧循环)、**失联阈值下限 30 秒**(防别的实例"合法地"抢占并杀掉我们的回合);以及团队模式的默认值与优先级(`DSH_MCP_TEAM` > `settings.json` > 默认关)(不花 token,每次必跑) |
25
25
  | `sandbox-guard` | 21 | **测试沙箱自身**:「临时 HOME」如果落到真实用户目录、它的子目录、盘符根目录,或 OS 临时目录之外,一律报错拒绝,绝不被写入和删除。其余套件都通过 `test/sandbox-home.mjs` 拿目录、只销毁自己建的那个(不花 token,每次必跑) |
26
- | `approval-details` | 27 | **审批审查上下文**:待决审批的快照带命令、理由、cwd 与原始审批 ID —— 不只是一个裸 ID,调用方才能真正裁决;缺失的 `rawInput` 会按 ID 从先前的 `tool_call` 更新关联回来;凭据形状的字段与常见内联字面量在结构化上下文和展示标题里都会脱敏;关闭会话会取消待决审批。经真实 stdio MCP 端到端验证 |
26
+ | `approval-details` | 37 | **审批审查上下文**:待决审批的快照带命令、理由、cwd 与原始审批 ID —— 不只是一个裸 ID,调用方才能真正裁决;缺失的 `rawInput` 会按 ID 从先前的 `tool_call` 更新关联回来;凭据形状的字段与常见内联字面量在结构化上下文和展示标题里都会脱敏;关闭会话会取消待决审批。经真实 stdio MCP 端到端验证 |
27
27
  | `failure-terminal` | 11 | **失败路径的终态**:`ensureAlive` 失败时唤醒 `dsh_interject` 等待者、落 `status=error` 哨兵、错误对象挂 `runId`;审批没有对应 option 时 `effective=cancelled` 而不是假报 allow;`dsh_get` 能从哨兵文件回读(不花 token) |
28
28
  | `shared-host` | 25 | **共享进程宿主**:通知/审批按 `sessionId` 路由、未知会话审批一律取消、进程退出扇出所有挂载会话、最后一个会话 detach 才停进程;**默认开启且 `=0` 可关闭**(空串按未设置处理);`takeover` 杀共享进程前的连坐检查;以及 RSS 读取(不花 token) |
29
29
  | `shared-process` | 8(无 DSH 时 skip) | **共享进程真机验证**:同 `(cwd, permission)` 的两个会话落在同一个 PID、关一个不影响另一个、全部关闭后宿主清空且 PID 退出(需要本机 DSH,`--all` 时运行) |
30
- | `dispatch-tools` | 31 | **派发面**:`dsh_inbox` 跨会话汇总待决审批、`client_run_id` 幂等命中与淘汰、队列 list/update/promote/remove(remove 写 cancelled 哨兵)、`dsh_get(max_chars)` 只截断 text 且 structured 保留全文、审计只记元数据不记正文、`dsh_doctor` 关键检查项(不花 token) |
30
+ | `dispatch-tools` | 55(非 Windows 53) | **派发面**:`dsh_inbox` 跨会话汇总待决审批、`client_run_id` 幂等命中与淘汰、队列 list/update/promote/remove(remove 写 cancelled 哨兵)、`dsh_get(max_chars)` 同时压缩 text 与 structured(正文截断、工具输入只留 `input_chars`,`max_chars=0` 仍返回全文)、`dsh_doctor` 的 DSH 版本档位与 npx 缓存识别、PowerShell 哨兵等待命令的转义与真机执行(仅 Windows)、审计只记元数据不记正文、`dsh_doctor` 关键检查项(不花 token) |
31
31
  | `read-cursor` | 10 | **`dsh_read` 游标语义**:追加到已读条目里的文本必须再次返回(修订号游标,按 `seq` 替换全文);`limit` 钳到 1..500,`0`/负数不会空转或丢尾(不花 token) |
32
32
  | `line-decoder` | 4 | **UTF-8 行分帧**:MCP/ACP 的 chunk 边界不能把中文/emoji 截成 U+FFFD;覆盖逐字节、任意 chunk、CRLF 与 EOF 残留(不花 token) |
33
33
  | `sentinel-redaction` | 19 | **哨兵落盘卫生**:`result.thinking` 被剥掉、`thinking_omitted` 始终是布尔值、`answer`/`error`/`title`/`tools_used[].input` 都走与审批相同的凭据脱敏(不花 token) |
34
+ | `review` | 70 | **调用方审核**:默认 `advisory` 审核单带清单 + 证据 + `dsh_review` 入口;`dsh_review` 把裁决落盘并回写内存 run 与哨兵;`fail`/`partial` 必须给理由;未完成/未知 run 被拒绝;重启后仍可凭哨兵审核;队列与 `client_run_id` 重放收据也带审核单;审核记录与哨兵共享 TTL;理由、预览、证据**与清单项**都凭据脱敏;**路径穿越 id 在触盘前就被拒绝**(不花 token) |
34
35
  | `forget-tombstone` | 10 | **`forget` 是终态**:`release(forget=true)` 写墓碑,按 id 查询明确失败并给出恢复路径,列表不再展示;删除墓碑后回到常规“未知会话”路径(不花 token) |
35
36
  | `bootstrap-profile` | 24 | **首启 profile 在零依赖发布形态下可用**:内置 YAML 子集解析器复制路由、剥掉 `apiKey`/`headers`/字面密钥、绝不写 `defaultPreset`、幂等;用户手写文件逐字节不动(不花 token) |
36
37
  | `workspace-safety` | 23 | **数据丢失边界**:`isTempPath` 按路径分段(`tmpdir()+'Suffix'`、`/tmpfoo` 都不算临时目录)、坏 `workspace.json` 绝不覆盖、会话根不可读时绝不清空 `sessionIds`、自动清理只删测试造的会话(不花 token) |
@@ -42,7 +43,7 @@ node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目
42
43
  | `real-configs` | 84(7/9 配置时 66,本机 5/9 时 48) | **拿真家伙做往返**:把本机**真实**的客户端配置复制进临时目录(18 万字节、含几百个 `projects` 条目与大小写重复键的 `~/.claude.json`,238 行的 Codex TOML,Zed 的完整 `settings.json`),注册再撤销,要求每个文件**逐字节**还原;条目是否存在按**键**判断而不是裸子串;缺少 fixture 时明确打印 `skipped`,绝不假绿(不花 token) |
43
44
  | `commit-script` | 26 | 提交信息走 stdin,绝不落临时文件让 `git add -A` 卷进发布提交(不花 token) |
44
45
  | `roots` | 12 | 不传 `cwd` 时通过 MCP roots 取客户端当前项目;不支持 roots 的客户端得到可操作的报错(不花 token) |
45
- | `workspace-titles` | 15 | 家目录当工作区会被提前拦住;`title_hint` 与 DSH 自动标题分开返回(不花 token) |
46
+ | `workspace-titles` | 18 | 家目录**与文件系统根目录**当工作区都会在 DSH 启动前被拦住;`title_hint` 与 DSH 自动标题分开返回(不花 token) |
46
47
  | `package-check` | 14 | `files` 白名单、零运行时依赖、Node 下限、bin shebang(不花 token) |
47
48
  | `timeout` | 38 | **超时语义**:`wait=false` 不受 `timeout_ms` 影响、`wait=true` 到期只转后台(不取消/不丢结果、`busy` 不说谎)、`timeout_ms<=0` 一直等、resume 失败会作废进程而不永久卡死、空 prompt 给清晰错误 |
48
49
  | `lock` | 79 | **写锁与抢占**:四类持有者判定、`writeMarker` 不覆盖活着的持有者、登记文件损坏/目录缺失等边界;**端到端**两实例抢锁 → 清晰报错 → 拒绝 → `force` 夺锁;**崩溃自动释放锁**、**卡死持有者无 force 自动抢占**;以及"一个回合 = 一个哨兵"的不变量(含内联回合与空闲插话) |
@@ -56,13 +57,13 @@ node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目
56
57
  | `list` | 13 | **`dsh_list` 的开销**(实测驱动):含磁盘探测的默认调用约 1 秒(要 spawn 一个 DSH 进程),**连续调用命中缓存降到个位数毫秒**;`only_running=true` 与 `include_closed=false` **跳过探测**且语义等价(不把磁盘上的未打开会话算进来) |
57
58
  | `reasoning-check` | 8 | **思考可见性**:`reasoning=hide` 不返回思考正文但统计非零;`reasoning=full` 返回正文且渲染出思考段 |
58
59
 
59
- **`node test/run.mjs` 在本机 7/9 客户端配置时跑 937 项(九种齐全时 955 项)** —— smoke 29 + 离线套件 908 项(九种齐全时 926 项)。**0.5.0 上全部退出码 0**(2026-11-03,Windows / Node 24.21.0);都不需要 provider(没有 DSH 时 smoke 自动跳过建会话段)。
60
+ **`node test/run.mjs` 在本机 7/9 客户端配置时跑 1012 项(九种齐全时 1030 项)** —— smoke 31 + 离线套件 981 项(九种齐全时 999 项)。**0.5.0 加调用方审核后全部退出码 0**(2026-11-04,Windows / Node 22.19.0;本机 5/9 配置时实测 994 项);都不需要 provider(没有 DSH 时 smoke 自动跳过建会话段)。
60
61
 
61
62
  其中 **542 项属于客户端注册器套件**(`setup` 45、`removal-scope` 24、`config-safety` 407、`real-configs` 66;九种齐全时 560 项)—— 0.3 的活其实都在这里。
62
63
 
63
- 需要模型的套件(`smoke` 29、`boundary` 46、`integration` 30、`async` 16、`sentinel` 36、`timeout` 38、`lock` 79、`cycle` 36、`concurrency` 31、`capability` 22、`workspace-effect` 24、`acceptance` 36、`multi` 19、`permission` 11、`list` 13、`reasoning-check` 8)合计 474 项,**最近一次完整实测在 0.3.9** —— 它们要可用 provider,发版前请自己重跑一遍:`node test/run.mjs --all`。`acceptance` 在没有 IDA MCP(`mcp__ida__*`)时会明确 **skip**。
64
+ 需要模型的套件(`smoke` 31、`boundary` 46、`integration` 30、`async` 21、`sentinel` 36、`timeout` 38、`lock` 79、`cycle` 36、`concurrency` 31、`capability` 22、`workspace-effect` 24、`acceptance` 36、`multi` 19、`permission` 11、`list` 13、`reasoning-check` 8、`shared-process` 8)合计 489 项。多数最近一次完整实测在 0.3.9;与调用方审核相关的 `smoke`/`integration`/`async`/`sentinel`/`boundary` 已在本树重跑。它们要可用 provider,发版前请自己重跑一遍:`node test/run.mjs --all`。`acceptance` 在没有 IDA MCP(`mcp__ida__*`)时会明确 **skip**;`shared-process` 没有 DSH 时也会 skip。
64
65
 
65
- > 三个总数是刻意分开的:`node test/run.mjs`(937/955)、CI 离线 job(名义 908/926,不含 smoke;hosted runner 没有客户端 fixture 时 `real-configs` 会整体跳过)、需要模型的集合(名义 474)。数字取各套件自己打印的汇总,不在这里手抄;不要把一个数字套到另一个 runner 上。
66
+ > 三个总数是刻意分开的:`node test/run.mjs`(1012/1030)、CI 离线 job(名义 981/999,不含 smoke;hosted runner 没有客户端 fixture 时 `real-configs` 会整体跳过)、需要模型的集合(名义 489)。数字取各套件自己打印的汇总,不在这里手抄;不要把一个数字套到另一个 runner 上。
66
67
 
67
68
  ### 🧯 为什么会有 `sandbox-guard`
68
69
 
package/docs/zh/usage.md CHANGED
@@ -17,14 +17,20 @@ MCP 工具调用是**阻塞**的,所以 `dsh_send` **默认就不阻塞**(`w
17
17
  → [我] … | [轮次] 第 1 轮开始 | [工具] pwsh [in_progress]
18
18
 
19
19
  5. dsh_get(conversation_id, run_id) # 验收
20
- → status=done, elapsed=20.3s, 完整答复
20
+ → status=done, elapsed=20.3s, 完整答复,
21
+ review: { status: "pending", checklist: [...], submit: { tool: "dsh_review", ... } }
22
+
23
+ 6. dsh_review(conversation_id, run_id, verdict="pass", notes="已看 diff 并跑过测试")
24
+ → review: { status: "pass", reviewed_at: "...", review_file: "..." }
21
25
  ```
22
26
 
23
- `run_id` 是验收凭据。回合记录**只在内存、只留最近 20 条**;并发跑很多 run 时旧记录会被挤掉,`dsh_get` 就会失败——这正是下面"完成哨兵"自带结果的原因。
27
+ **结果返回 ≠ 已完成。** 每个结果都带一张 `review` 审核单:根据这一轮的情况生成核验清单(有没有改文件、回合是否正常结束、用了哪些工具),并附上证据摘要。调用方应该用自己的工具独立核验至少一项关键声明(读改动的文件、看 `git diff`、跑相关测试),再用 `dsh_review` 提交裁决。默认 `advisory` **不阻塞**下一次 `dsh_send`,它的作用是把"先审核"放进返回载荷本身,而不是依赖调用方记得。`fail` / `partial` 必须给 `notes` 或 `findings`。审核不通过**不会**自动回灌给 DSH——要不要再派修复回合由调用方自己决定。
28
+
29
+ `run_id` 是验收凭据。回合记录**只在内存、只留最近 20 条**;并发跑很多 run 时旧记录会被挤掉,`dsh_get` 就会失败——这正是下面"完成哨兵"自带结果(以及 `dsh_review` 仍能凭哨兵审核)的原因。
24
30
 
25
31
  ## 🔔 用法:跑完自动通知(不轮询)
26
32
 
27
- `wait=false` 除了 `run_id`,还会返回 **`sentinel_file`**(哨兵文件路径)以及 **`sentinel_file_posix`**(Bash/MSYS 形式,**直接用它,别无手工转换**——转错了等待任务会一直挂到超时)。回合结束时——**无论成功、失败还是被取消**——本服务都会往这个路径**原子写出**一个含最终 `status` 与 `result` 的小文件。于是你不用轮询、也不用傻等:**用你自己宿主(Claude/Codex)的后台任务等这个文件出现即可,文件一到就会唤醒你。**
33
+ `wait=false` 除了 `run_id`,还会返回 **`sentinel_file`**(哨兵文件路径)以及 **`sentinel_file_posix`**(Bash/MSYS 形式,**直接用它,别无手工转换**——转错了等待任务会一直挂到超时)。回合结束时——**无论成功、失败还是被取消**——本服务都会往这个路径**原子写出**一个含最终 `status`、`result` 与**审核单**(`result.review`)的小文件。于是你不用轮询、也不用傻等:**用你自己宿主(Claude/Codex)的后台任务等这个文件出现即可,文件一到就会唤醒你。**
28
34
 
29
35
  ```bash
30
36
  # sent="$sentinel_file_posix" ← 直接用工具返回里那个字段
@@ -37,7 +43,16 @@ done
37
43
  echo "DONE" # 后台任务退出 → 宿主唤醒你 → 读文件收活
38
44
  ```
39
45
 
40
- 文件出现后:**直接读它**即得 `status` 和 `result`(推荐——不受内存窗口限制、还扛服务重启),或用 `dsh_get(conversation_id, run_id)` 验收。
46
+ Windows 宿主上 Git Bash 不可用(或会挂住)时,用 PowerShell 等原生路径 `sentinel_file`。在 Windows 上,后台收据的 structured 里有拼好、已转义的 **`sentinel_wait_powershell`**,直接用它:
47
+
48
+ ```powershell
49
+ $f='C:\Users\you\.dsh\.dsh-any-agent\runs\<conversation_id>\<run_id>.json' # 路径里的 ' 写成 ''
50
+ $dl=(Get-Date).AddSeconds(2100)
51
+ while(-not (Test-Path -LiteralPath $f)){ if((Get-Date) -gt $dl){ 'TIMEOUT'; exit 1 }; Start-Sleep -Seconds 2 }
52
+ 'DONE'
53
+ ```
54
+
55
+ 文件出现后:**直接读它**即得 `status` 和 `result`(推荐——不受内存窗口限制、还扛服务重启),或用 `dsh_get(conversation_id, run_id)` 验收。一个回合用了很多工具时,完整结果(含每个工具的完整输入)可能超过客户端单次工具结果的上限;这时给 `dsh_get` 传 `max_chars`(如 8000):预算**同时**作用于 text 与 structured,正文截断、工具只留名字/状态与 `input_chars`,完整版本始终在哨兵文件里。
41
56
 
42
57
  设计要点(尤其**多会话并发**):
43
58
 
@@ -45,9 +60,10 @@ echo "DONE" # 后台任务退出 → 宿主唤醒你
45
60
  - **闩锁语义**:文件写一次并保留。即便 run 在你启动等待任务**之前**就已完成,`[ -f ]` 也立刻为真,不会漏。
46
61
  - **原子落地**:`tmp` + `rename`,文件一旦存在内容必然完整;每个 run 用独立 tmp,并发写互不干扰。
47
62
  - **无主动推送**:本服务在 MCP 协议上只应答请求、不发通知;"跑完通知"完全由这个哨兵文件 + 你的后台等待任务实现。
63
+ - **审核不随哨兵消失**:`result.review` 随结果一起写入,所以只读文件的调用方也能看到清单。之后 `dsh_review` 会**原子重写**这个审核块(`status` → `pass`/`partial`/`fail`,并写入 `notes`、`reviewed_at`、`review_file`),闩锁文件本身始终存在。审核记录在 `<状态目录>/reviews/` 下,和哨兵一样按 7 天 TTL 清理。
48
64
  - **★ 哨兵里没有思考内容**:载荷含正文、思考**统计**、用量与(已脱敏的)工具输入,但没有思考正文;`result.thinking` 会被剥掉,并始终给出布尔 `thinking_omitted`(剥离了为 `true`,本来没有为 `false`)——本服务自己的落盘物里不写思考。`dsh_get(run_id)` 在内存里仍能看到它(记录还在时)。设 `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` 可以故意打破这条保证。注意:**DSH 自己的会话日志仍含思考**,见“日志与隐私”。
49
65
 
50
- > 清理:哨兵内含完整正文,所以服务**启动时会清掉超过 7 天的哨兵**(用 `DSH_MCP_SENTINEL_TTL_MS` 调;`0` 关闭)。等待任务消费后仍建议自行 `rm`。
66
+ > 清理:哨兵与审核记录内含完整正文/理由,所以服务**启动时会清掉超过 7 天的哨兵与审核记录**(用 `DSH_MCP_SENTINEL_TTL_MS` 调;`0` 关闭)。等待任务消费后仍建议自行 `rm`。
51
67
 
52
68
  ## 🧵 用法:同时控制多个会话
53
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-any-agent",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Drive DeepSeek Harness (DSH) as a long-lived coding agent from any MCP client — Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Zed, Gemini CLI, opencode, VS Code. Resume-aware, interruptible, approval-capable, and it only ever touches the one config entry it owns.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/config.mjs CHANGED
@@ -128,6 +128,46 @@ export function resolveDshBin() {
128
128
  );
129
129
  }
130
130
 
131
+ /**
132
+ * 兼容性矩阵(docs/zh/compatibility.md)的机器可读版本,供 dsh_doctor 使用。
133
+ * 改矩阵时同步改这里:`[verifiedMin, verifiedBelow)` 是“已验证”区间。
134
+ * 用 `-0` 后缀是为了把同号的 rc 版本也算进去(semver 里 `0.1.7-rc.1 < 0.1.7`)。
135
+ */
136
+ export const DSH_COMPAT = Object.freeze({
137
+ partialMin: '0.1.5-0',
138
+ verifiedMin: '0.1.7-0',
139
+ verifiedBelow: '0.2.0-0',
140
+ });
141
+
142
+ /**
143
+ * 给一个 DSH 版本打兼容性档位:ok(已验证)/ warn(部分可用、未验证或读不到)。
144
+ * 只是体检提示,不拦启动 —— 未验证不等于不能用。
145
+ */
146
+ export function dshVersionStatus(version) {
147
+ if (!version) {
148
+ return { status: 'warn', level: 'unknown', note: '读不到 DSH 版本(package.json 缺失或不可读)' };
149
+ }
150
+ if (cmpVersion(version, DSH_COMPAT.partialMin) < 0) {
151
+ return { status: 'warn', level: 'unsupported', note: `DSH ${version} 低于已知可用的最低版本 0.1.5` };
152
+ }
153
+ if (cmpVersion(version, DSH_COMPAT.verifiedMin) < 0) {
154
+ return { status: 'warn', level: 'partial', note: `DSH ${version} 部分可用:ACP 基本路径可用,缺 team 包` };
155
+ }
156
+ if (cmpVersion(version, DSH_COMPAT.verifiedBelow) < 0) {
157
+ return { status: 'ok', level: 'verified', note: `DSH ${version} 在已验证区间(0.1.7 ~ 0.2.0 之前)` };
158
+ }
159
+ return {
160
+ status: 'warn',
161
+ level: 'unverified',
162
+ note: `DSH ${version} 未经本版本验证(兼容性矩阵只覆盖到 0.1.x),ACP session/* 面可能有变化`,
163
+ };
164
+ }
165
+
166
+ /** DSH 入口是不是落在 npx 缓存里(`_npx/<hash>/…`):那里随 `npm cache clean` 或 npx 自身淘汰随时消失。 */
167
+ export function isNpxCachePath(p) {
168
+ return /[\\/]_npx[\\/]/.test(String(p ?? ''));
169
+ }
170
+
131
171
  /**
132
172
  * DSH 主目录。**默认就是本机用户自己的 ~/.dsh** —— 本服务不拥有独立的 home,
133
173
  * 因此凭据(.credentials.yaml)、会话历史、工作区登记与 GUI 完全共用一份。
@@ -278,6 +318,35 @@ export function assertWritableDir(dir, label = '目录') {
278
318
  */
279
319
  export const RUNS_DIR = process.env.DSH_MCP_RUNS_DIR ?? join(dirname(STATE_FILE), 'runs');
280
320
 
321
+ /**
322
+ * 调用方审核记录的落盘目录(默认与状态文件同目录下的 reviews/)。
323
+ *
324
+ * 每个 run 一份 `<REVIEWS_DIR>/<conversation_id>/<run_id>.review.json`,
325
+ * 里面是调用方的裁决(pass/partial/fail)、理由、审核清单和证据摘要。
326
+ * 审核记录与哨兵分开存放:哨兵是"回合完成"的闩锁,审核是"调用方已验收"的独立事实,
327
+ * 后者可能在服务重启、run 记录被挤出内存之后才发生。
328
+ */
329
+ export const REVIEWS_DIR = process.env.DSH_MCP_REVIEWS_DIR ?? join(dirname(STATE_FILE), 'reviews');
330
+
331
+ /**
332
+ * 调用方审核策略:
333
+ * advisory(默认)—— 每个回合结果都附审核单(清单 + dsh_review 入口 + 证据),
334
+ * 但**不阻塞**后续 dsh_send;调用方仍可自己决定先做什么。
335
+ * off —— 不生成审核单;dsh_review 仍可对旧 run 显式提交。
336
+ *
337
+ * 默认开着的原因:返回值是"另一个 agent 的自述",调用方最容易犯的错就是直接转述。
338
+ * 审核单把"先核验"放进返回载荷本身,而不是依赖调用方记得要审核。
339
+ */
340
+ export const REVIEW_MODES = ['off', 'advisory'];
341
+ export const DEFAULT_REVIEW = (() => {
342
+ const raw = process.env.DSH_MCP_REVIEW;
343
+ if (raw === undefined || String(raw).trim() === '') return 'advisory';
344
+ const v = String(raw).trim().toLowerCase();
345
+ if (v === '0' || v === 'false' || v === 'off' || v === 'no') return 'off';
346
+ if (v === '1' || v === 'true' || v === 'on' || v === 'advisory' || v === 'yes') return 'advisory';
347
+ return 'advisory';
348
+ })();
349
+
281
350
  /**
282
351
  * 哨兵文件里是否带上**思考内容**。
283
352
  *