dsh-any-agent 0.3.7 → 0.3.9

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/CHANGELOG.md CHANGED
@@ -7,7 +7,92 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- Post-0.3.7 hardening.
10
+ Post-0.3.9 hardening.
11
+
12
+ ## [0.3.9] — 2026-11-02
13
+
14
+ ### Security / robustness
15
+
16
+ - **UTF-8 stream framing is lossless.** MCP and ACP now use a shared
17
+ `LineDecoder` backed by `StringDecoder`, so a multi-byte Chinese/emoji
18
+ character split across stream chunks is no longer silently replaced by
19
+ U+FFFD. Covers both input prompts and DSH output.
20
+ - **Concurrent adoption of the same disk session is deduplicated.** `Hub.get`
21
+ now shares one in-flight `findSessionHeader`/adoption promise per id and
22
+ rechecks the registry, preventing two Conversation objects (and the
23
+ unreachable child/write-lock leak that followed).
24
+ - **`read-only` runtime verification fails closed.** It waits up to ~10s
25
+ (covering DSH's projection-cache flush window); if no recorded permission is
26
+ available, the call is refused instead of relying on an approval layer that
27
+ a danger sandbox may never invoke.
28
+ - **`spawned_at` is reset when the child PID changes.** PID-reuse protection no
29
+ longer rejects takeover of a legitimate restarted child.
30
+ - **`forget` tombstones survive unreadable session headers.** Header read is
31
+ three-state (found / missing / error); only a genuinely missing session file
32
+ prunes a tombstone.
33
+ - **`DSH_MCP_SENTINEL_INCLUDE_REASONING=1` no longer disables redaction.**
34
+ Thinking is kept, but answer/error/title/tool inputs are still sanitized.
35
+ - **`close`/`releaseAll`/`shutdown` keep lock registrations until the child is
36
+ confirmed dead.** If stop times out, the registration is retained so
37
+ takeover can still attribute the orphan; `forget` is refused while the child
38
+ is alive.
39
+ - setup preserves the target file's mode (e.g. 0600) and rejects an invalid
40
+ `--permission` with exit code 2.
41
+ - `restore --latest` excludes its own `pre-restore-*` safety copies, so running
42
+ it twice no longer undoes the first restore.
43
+
44
+ ### Behavior
45
+
46
+ - `workspace_changed` compares git-status entry snapshots instead of object
47
+ identity; output now says "workspace state changed" rather than claiming a
48
+ per-turn file count.
49
+ - `dsh_takeover` prefers a live `known_child_pid` when the marker points at a
50
+ dead child.
51
+
52
+ ### Website
53
+
54
+ - Mobile navigation keeps the language switch visible; the footer also carries
55
+ a language link.
56
+ - `hero.jpg` and `favicon.svg` are content-hashed; all `/assets/*` can stay
57
+ immutable.
58
+ - Light-theme muted/faint/accent colors were darkened to meet WCAG AA on the
59
+ surfaces they are used on.
60
+
61
+ ### Docs / tests
62
+
63
+ - README/Chinese README/USAGE-MIGRATION: workspace-write approval semantics,
64
+ setup's `workspace-write` default, Zed `context_servers`, English npm publish
65
+ step, current route/limit links.
66
+ - New `line-decoder` suite (4), expanded sentinel redaction (19), tombstone
67
+ pruning (10), permission adoption (17), state/lock safety (26), sandbox guard
68
+ (21), setup (45), config-safety (407). Offline total 843 / 879 with nine
69
+ client fixtures; website 29.
70
+
71
+ ## [0.3.8] — 2026-11-02
72
+
73
+ ### Changed
74
+
75
+ - `npm test` now works without a local DSH: smoke still checks handshake and
76
+ the tool table, then skips session-dependent sections instead of failing, so
77
+ all offline suites run. `npm run test:all` is the explicit full-suite entry.
78
+ - Added the `model-suites` workflow for a self-hosted runner with DSH and
79
+ provider credentials; hosted runners print an explicit skip.
80
+ - Added an English `USAGE-MIGRATION.en.md` and linked it from the English
81
+ README and `/en/` install section.
82
+
83
+ ### Testing
84
+
85
+ - Removed the remaining always-true `check(..., true)` assertions; cleanup
86
+ checks now assert actual release/forget results or timeout state.
87
+ - `prune` runs with an isolated `DSH_HOME` so it cannot touch real tombstones.
88
+ - Added 8 static accessibility tests for both locales (28 website tests total
89
+ counting the existing site/release/demo suites).
90
+ - `readHeaderAsync` now uses `StringDecoder` for multi-byte UTF-8 boundaries.
91
+
92
+ ### Website
93
+
94
+ - `robots.txt` now mirrors the complete Cloudflare Managed crawler list, not
95
+ just the four most common bots.
11
96
 
12
97
  ## [0.3.7] — 2026-11-02
13
98
 
@@ -670,7 +755,9 @@ First public release, published to npm as [`dsh-any-agent`](https://www.npmjs.co
670
755
  profiles, sessions and client configs. The product name changed; the on-disk
671
756
  contract did not. See the README FAQ.
672
757
 
673
- [Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.7...HEAD
758
+ [Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.9...HEAD
759
+ [0.3.9]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.8...v0.3.9
760
+ [0.3.8]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.7...v0.3.8
674
761
  [0.3.7]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.6...v0.3.7
675
762
  [0.3.6]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.5...v0.3.6
676
763
  [0.3.5]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.4...v0.3.5
package/DESIGN.md CHANGED
@@ -2,7 +2,15 @@
2
2
 
3
3
  > 目标:做一个 MCP Server,让 **Codex / Claude Code** 能调用 **DeepSeek Harness (DSH)** 当"外包工程师"——写代码、搭脚手架、跑任务,并且 DSH 自己还能挂它自己的 MCP。
4
4
  >
5
- > 本文只谈**功能与接口**,不写实现代码;其中 M2/M3 工具(`dsh_rename` 等)是分期计划,不是当前 0.3.7 的缺失功能。技术判断最初基于 DSH 0.1.7-rc.2,当前验证基线是 **DSH 0.2.1-rc.5**(见 §2 证据)。
5
+ > 本文只谈**功能与接口**,不写实现代码;其中 M2/M3 工具(`dsh_rename` 等)是分期计划,不是当前 0.3.9 的缺失功能。技术判断最初基于 DSH 0.1.7-rc.2,当前验证基线是 **DSH 0.2.1-rc.5**(见 §2 证据)。
6
+
7
+ ## 当前实现状态(0.3.9)
8
+
9
+ 当前对外暴露 11 个工具,均已实现并有回归:
10
+
11
+ `dsh_start`、`dsh_send`、`dsh_list`、`dsh_read`、`dsh_get`、`dsh_interject`、`dsh_interrupt`、`dsh_release`、`dsh_takeover`、`dsh_approval_decide`、`dsh_status`
12
+
13
+ 本文后续出现的 `dsh_resume`、`dsh_rename`、`dsh_delete`、`dsh_transcript`、`dsh_diff`、`dsh_doctor` 等属于 M2/M3 路线图,**不是当前版本的缺失**。当前版本的能力边界以 README 的 Known limitations 与 USAGE-NOTES 为准。
6
14
 
7
15
  ---
8
16
 
@@ -431,7 +439,7 @@ Profile = `~/.dsh/profiles/<name>/`,由 `package.json` 的 `dsh.profile.bundle
431
439
  `dsh_start` / `dsh_send` / `dsh_list` / `dsh_get` / `dsh_release` + 专用 profile 脚手架(provider/model/权限/标题 LLM)+ 思考默认隐藏 + Windows 拉起方式 + 进程池与空闲回收。
432
440
 
433
441
  **M2(把它变成好用的"外包工程师")**
434
- `dsh_diff`、计划模式、`dsh_read` 游标增量、progress/log 通知、审批**报告**(G3,注意不可应答)、脱敏、`dsh_search`(需先开 `session-query-sqlite` 的 `openAt`,默认 `never` 时全文搜索直接报 `SESSION_QUERY_SEARCH_DISABLED`)、resources/prompts。
442
+ `dsh_diff`、计划模式、`dsh_read` 游标增量、progress/log 通知、审批**可应答**(G3 已由 `dsh_approval_decide` 实现)、脱敏、`dsh_search`(需先开 `session-query-sqlite` 的 `openAt`,默认 `never` 时全文搜索直接报 `SESSION_QUERY_SEARCH_DISABLED`)、resources/prompts。
435
443
 
436
444
  **M3(能力面与体验)**
437
445
  MCP 服务器增删查(F2)、插件与技能管理、`dsh_doctor`、分叉/回滚、可选 TUI 伴生进程。
@@ -443,9 +451,9 @@ MCP 服务器增删查(F2)、插件与技能管理、`dsh_doctor`、分叉/
443
451
  | # | 问题 | 我的建议 |
444
452
  |---|---|---|
445
453
  | Q1 | 会话空间:与用户 GUI 共用 `~/.dsh`(历史统一、但会抢写锁)还是独立 `DSH_HOME`(隔离、但要另配凭据)? | 默认共用,提供 `--dsh-home` 覆盖;MCP 起的会话打标记便于过滤 |
446
- | Q2 | 空闲回收 TTL 默认值?(太长会占锁让用户打不开,太短会反复重启丢上下文) | 5 分钟,且 `dsh_release` 可显式交还 |
454
+ | Q2 | 空闲回收 TTL 默认值?(太长会占锁让用户打不开,太短会反复重启丢上下文) | ✅ **已定:默认 `0` = 永不回收**(`dsh_release` 可显式交还;显式设非零值才回收) |
447
455
  | Q3 | **中断怎么做?** SDK 协议没有 cancel,且杀进程 = 永久失去该会话(约束 6) | 强烈倾向 ②:写个极小 DSH 插件补 `session/interrupt`。① 已排除——headless 无法接管 web 建的会话 |
448
- | Q4 | 默认权限档给哪一档? | ✅ **已定:默认 `danger-full-access`,其余可选。** 成因几经变化:SDK 协议没有审批通道时中间档等于废档(越界操作静默失败);**改用 ACP 后 `session/request_permission` 让中间档复活**,所以现在是"默认最省事 + 需要时可收窄",而不是被迫只留一档。`read-only` 留给分析不信任样本的场景。注意 `approval.policy: never` 的语义是"每次询问都确定性拒绝"(不是自动放行),它只在 `danger-full-access` 下无副作用——因为沙箱根本不产生询问 |
456
+ | Q4 | 默认权限档给哪一档? | ✅ **已定:服务端裸默认 `danger-full-access`;`setup` 写入 `workspace-write`;其余可选。** 成因几经变化:SDK 协议没有审批通道时中间档等于废档(越界操作静默失败);**改用 ACP 后 `session/request_permission` 让中间档复活**,所以现在是"默认最省事 + 需要时可收窄",而不是被迫只留一档。`read-only` 留给分析不信任样本的场景。注意 `approval.policy: never` 的语义是"每次询问都确定性拒绝"(不是自动放行),它只在 `danger-full-access` 下无副作用——因为沙箱根本不产生询问 |
449
457
  | Q5 | 会话 id 是否直接采用 DSH 原生 `session-<uuid>`? | ✅ 已定:采用。实测语义 = 全新 id 落成磁盘会话 id(磁盘/标题/GUI 全对齐);已存在的 id 直接报错。故**每个会话只在首次 prompt 生成一次 id,之后仅在本进程内复用** |
450
458
  | Q6 | 首版面向 Codex 还是 Claude Code?(两者 MCP 能力不同:progress/log 通知、resources、prompts 支持度有差异) | 先保 Codex 的保守子集(tools 即可),Claude 的增强能力渐进加 |
451
459
  | Q7 | 要不要把 `session-title-llm` 打开(每个会话多一次小 LLM 调用换更好的标题)? | 打开,标题质量对"列表里找对话"影响很大 |
@@ -460,7 +468,7 @@ MCP 服务器增删查(F2)、插件与技能管理、`dsh_doctor`、分叉/
460
468
  | 位置 | 内容 |
461
469
  |---|---|
462
470
  | [`src/`](src) | 实现:ACP 客户端、会话池与注册表、工具层、MCP 服务端、工作区登记 |
463
- | [`test/`](test) | 可复跑的测试套件(冒烟 / 边界 / 集成 / 异步 / 并发 / 能力 / 工作区有效性 / 验收),合计 229 项检查 |
471
+ | [`test/`](test) | 可复跑的测试套件;当前基线见 README(0.3.9:离线 843 项、官网 29 项,模型套件另计)。本文件是历史设计稿,不再重复抄写计数 |
464
472
  | [`README.md`](README.md) / [`README.zh.md`](README.zh.md) | 使用文档(英文 / 中文) |
465
473
  | [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml) | DSH profile 补丁层模板(含两个必踩的坑的说明) |
466
474
 
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  </div>
17
17
 
18
- 🧪 815 checks green on 0.3.7 on this machine (851 when all nine client fixtures exist); 519 of them are in the client-registration suites (555 with all fixtures) · 🔌 MCP over stdio · 📦 Zero dependencies · 📜 MIT
18
+ 🧪 843 checks green on 0.3.9 on this machine (879 when all nine client fixtures exist); 524 of them are in the client-registration suites (560 with all fixtures) · 🔌 MCP over stdio · 📦 Zero dependencies · 📜 MIT
19
19
 
20
20
  An MCP server that drives DeepSeek Harness as a **long-lived coding agent**. Hand it a goal, let it write code and run scripts, steer it mid-flight, and collect the result when you are ready.
21
21
 
@@ -33,7 +33,7 @@ dsh-any-agent-setup --yes
33
33
 
34
34
  The second command previews the changes; `--yes` applies them. Reconnect your client's MCP server, then ask your agent to start a DSH conversation in your project, dispatch a task, and collect the result. The [dispatch workflow](#-workflow-dispatch-then-collect-later) shows the tool sequence.
35
35
 
36
- > **Before your first task:** read the [Usage Notes](USAGE-NOTES.en.md), especially the model-config and external-MCP permission boundaries. External MCP tools bypass DSH's sandbox. For a fresh machine, follow the [migration guide](USAGE-MIGRATION.md).
36
+ > **Before your first task:** read the [Usage Notes](USAGE-NOTES.en.md), especially the model-config and external-MCP permission boundaries. External MCP tools bypass DSH's sandbox. For a fresh machine, follow the [migration guide](USAGE-MIGRATION.en.md).
37
37
 
38
38
  <details>
39
39
  <summary>Full contents</summary>
@@ -214,7 +214,7 @@ What changed since 0.1.x: JSON configs used to be **re-serialised whole** (value
214
214
 
215
215
  Codex's `config.toml` gets the same treatment line-wise: your other `[tables]` and their comments, the top-level keys (`model`, `approval_policy`, `sandbox_mode`, …) and CRLF endings come back exactly as they were — `--remove` deletes only the two `[mcp_servers.dsh-any-agent]` sections and leaves the rest intact.
216
216
 
217
- The coverage for each of those claims is in [`test/`](test/) (`test/config-safety.mjs` asserts them one by one, 406 checks).
217
+ The coverage for each of those claims is in [`test/`](test/) (`test/config-safety.mjs` asserts them one by one, 407 checks).
218
218
 
219
219
  **Checked against a real home, read-only.** Running `setup --json` (plan mode) against an actual developer profile reported all nine clients and left every existing config untouched — identical size, mtime and SHA-256, no `.bak-dsh-*` created, no new file created:
220
220
 
@@ -359,7 +359,7 @@ tool_timeout_sec = 120.0
359
359
 
360
360
  For a local checkout, use `"command": "node"` and `"args": ["/absolute/path/to/server/bin/dsh-any-agent.mjs"]`.
361
361
 
362
- > Cursor, Windsurf, Zed, Cline, Continue and most other clients use this same `mcpServers` shape — if it can launch a stdio MCP server, it works.
362
+ > Cursor, Windsurf, Cline, Continue and most other clients use this same `mcpServers` shape — if it can launch a stdio MCP server, it works. Zed is the exception: it uses `context_servers` (shown above).
363
363
 
364
364
  </details>
365
365
 
@@ -432,7 +432,7 @@ Design notes (especially under **concurrent multi-session** use):
432
432
  - **Latch semantics**: the file is written once and kept. Even if the run finishes *before* you arm the waiter, `[ -f ]` is immediately true — you never miss it.
433
433
  - **Atomic write**: `tmp` + `rename`; if the file exists its contents are complete. Each run uses its own tmp, so concurrent writes never clobber.
434
434
  - **No server push**: the service only answers requests over MCP (it sends no notifications). "Completion notification" is entirely the sentinel file plus your background waiter.
435
- - **★ No reasoning in the sentinel**: the payload carries the answer, thinking *statistics*, usage and (credential-redacted) tool inputs, but never reasoning text — `result.thinking` is stripped and replaced by a boolean `thinking_omitted` (`true` when reasoning was removed, `false` when there was none), because this server's rule is that reasoning never reaches its own disk artifacts. `dsh_get(run_id)` still returns it from memory while the record lives. `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` breaks that guarantee deliberately. Note that **DSH's own session log does contain reasoning** — see [Logging and privacy](#️-logging-and-privacy).
435
+ - **★ No reasoning in the sentinel**: the payload carries the answer, thinking *statistics*, usage and (credential-redacted) tool inputs, but never reasoning text — `result.thinking` is stripped and replaced by a boolean `thinking_omitted` (`true` when reasoning was removed, `false` when there was none), because this server's rule is that reasoning never reaches its own disk artifacts. `dsh_get(run_id)` still returns it from memory while the record lives. `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` breaks that guarantee deliberately. Note that **DSH's own session log does contain reasoning** — see [Logging and privacy](#-logging-and-privacy).
436
436
 
437
437
  > Cleanup: sentinels contain full answers, so the service **prunes sentinels older than 7 days at startup** (tune with `DSH_MCP_SENTINEL_TTL_MS`; `0` disables). Your waiter should still `rm` the file after consuming it.
438
438
 
@@ -461,13 +461,13 @@ DSH separates reasoning from answer text at the model layer, and ACP exposes the
461
461
 
462
462
  | Tier | Approval default | Use for |
463
463
  |---|---|---|
464
- | `danger-full-access` (default) | `auto-allow` | Zero friction |
465
- | `workspace-write` | `auto-allow` | Out-of-workspace operations surface as pending approvals |
464
+ | `danger-full-access` (raw server default) | `auto-allow` | Zero friction; `setup` writes `workspace-write` by default |
465
+ | `workspace-write` | `auto-allow` | Writes are limited to the workspace by DSH's sandbox; out-of-workspace requests are auto-allowed unless you pass `on_approval=ask` |
466
466
  | `read-only` | `auto-deny` | Analyzing untrusted samples (malware, unknown dumps) |
467
467
 
468
468
  `on_approval` can be `auto-allow`, `auto-deny`, or `ask` (which exposes a pending approval for `dsh_approval_decide` to adjudicate).
469
469
 
470
- **`read-only` is enforced, not merely a default**: it always runs with `auto-deny`; passing `auto-allow` or `ask` for a read-only conversation is rejected at `dsh_start`, and the approval layer refuses any permission request from a read-only conversation. Without that, DSH can ask for a `sandbox_permissions` escalation and a permissive approval policy would let the write through (found by the adversarial audit).
470
+ **`read-only` is enforced, not merely a default**: it always runs with `auto-deny`; passing `auto-allow` or `ask` for a read-only conversation is rejected at `dsh_start`, and the approval layer refuses any permission request from a read-only conversation. After create/resume the server reads back the session's recorded tier; for read-only it **fails closed** if the projection cache has no permission record within the DSH flush window, rather than relying on the approval layer alone. Without that, a profile `defaultPreset` plus a permissive approval policy could let a write through (found by the adversarial audit).
471
471
 
472
472
  ## 🪟 Workspace visibility in the DSH GUI
473
473
 
@@ -530,12 +530,12 @@ Silent by default — **not one byte is written**.
530
530
  | `DSH_MCP_FORGOTTEN_DIR` | `$DSH_HOME/.dsh-any-agent-forgotten` | Tombstones written by `dsh_release(forget=true)` |
531
531
  | `DSH_MCP_RUNS_DIR` | `<state file dir>/runs` | Root for async completion sentinels (`<this>/<conversation_id>/<run_id>.json`) |
532
532
  | `DSH_MCP_LOCKS_DIR` | `<state file dir>/locks` | Holder registrations for the write lock. ⚠️ **All instances must agree on this value**, or they cannot see each other's registrations and will misread each other as an unidentifiable holder (a GUI) and refuse to preempt |
533
- | `DSH_MCP_LOCK_STALE_MS` | `90000` | How long without a heartbeat before a holder counts as lost (and becomes auto-preemptible by `dsh_takeover`) |
533
+ | `DSH_MCP_LOCK_STALE_MS` | `90000` (3× `REAP_INTERVAL_MS`, floor 30 s) | How long without a heartbeat before a holder counts as lost (and becomes auto-preemptible by `dsh_takeover`); changing the reap interval changes the derived default |
534
534
  | `DSH_MCP_IDLE_TTL_MS` | `0` (never reap) | How long a conversation may sit idle before its DSH process is reaped. ★★ **`0` means never reap (the default)**: the server keeps holding the write lock, so **the GUI cannot take it** — opening the conversation there is read-only and still works. Cost: one resident DSH process per conversation (**measured 300 MB and 476 MB in two cold starts**). Set a millisecond value (e.g. `300000`) to save memory and accept that the lock can be taken |
535
535
  | `DSH_MCP_MAX_LIVE` | `8` (≈2.5–4 GB worst case; see note 8) | ★ **How many conversation processes to keep alive at once** (`0` = unlimited). Going over the cap reaps the **least recently used** one (a running turn is never reaped) — "**bounded memory, locks kept where it matters**". Use `4` when memory is tight, `0` on a big machine. To free idle conversations at once: `dsh_release(all=true)` (busy turns are skipped unless `force=true`) |
536
536
  | `DSH_MCP_LIST_PROBE_TTL_MS` | `10000` | Cache lifetime for `dsh_list`'s on-disk probe (the probe spawns a DSH process and takes ~1 s) |
537
537
  | `DSH_MCP_SENTINEL_TTL_MS` | `604800000` (7 days) | Startup pruning age for sentinels; `0` disables pruning |
538
- | `DSH_MCP_SENTINEL_INCLUDE_REASONING` | unset | `1` writes `result.thinking` into the sentinel (**breaks the never-on-disk guarantee**) |
538
+ | `DSH_MCP_SENTINEL_INCLUDE_REASONING` | unset | `1` writes `result.thinking` into the sentinel (**breaks the never-on-disk guarantee**); answer/error/title/tool inputs are still credential-redacted |
539
539
  | `DSH_MCP_PERMISSION` | `danger-full-access` | Default tier for `dsh_start` |
540
540
  | `DSH_MCP_REASONING_EFFORT` | `max` | Default reasoning effort |
541
541
  | `DSH_MCP_PROMPT_TIMEOUT_MS` | `0` (no timeout) | Wait bound for **`wait=true` only**; on expiry the turn merely moves to the background |
@@ -550,11 +550,15 @@ Silent by default — **not one byte is written**.
550
550
  ## 🧪 Tests
551
551
 
552
552
  ```bash
553
- node test/run.mjs # all offline suites (no LLM calls; needs DSH for smoke)
554
- node test/run.mjs --all # offline + model-backed suites
553
+ npm test # offline suites; smoke skips DSH sections if no DSH
554
+ npm run test:all # offline + model-backed suites (needs DSH/provider)
555
555
  node test/cleanup.mjs # suite teardown on its own (test-created dirs only; --dry-run to preview)
556
556
  ```
557
557
 
558
+ Model-backed suites are not part of the hosted CI matrix (they need a real DSH
559
+ install and provider credentials). Run them locally, or dispatch the
560
+ `model-suites` workflow with a self-hosted runner that has DSH configured.
561
+
558
562
  The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`, which removes sessions and workspace registrations whose cwd is **a test-created directory** (`tmpdir()/dsh-any-agent-*` or `Desktop/dsh-any-agent-test-*`). Real project sessions — including projects under `%TEMP%` — and your live state directory are never touched. To remove `Desktop/dsh-any-agent-test-*` artifacts explicitly, run `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`.
559
563
 
560
564
  | Suite | Checks | Covers |
@@ -566,18 +570,19 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
566
570
  | `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
567
571
  | `prune` | 11 | **sentinel retention**: prunes over-age and crash-leftover files, keeps fresh ones, removes empty shells, `TTL=0` disables pruning, and never touches the conversation registry beside it (no tokens, runs every time) |
568
572
  | `guards` | 31 | **environment-variable guards**: odd numbers always land on the safe side — `IDLE_TTL_MS` values like `-1` or garbage mean "never reap" (never give the write lock away), a too-small reap interval falls back to the default (no busy loop), and the **lost-holder threshold is floored at 30 s** (so another instance cannot "legitimately" preempt and kill our turn) (no tokens, runs every time) |
569
- | `sandbox-guard` | 18 | **the test sandbox itself**: a "throwaway HOME" that resolves to the real user profile, to the filesystem root, or to anywhere outside the OS temp dir is refused with an error instead of being written to and deleted. Every other suite builds its HOME through `test/sandbox-home.mjs` and can only destroy what it created there (no tokens, runs every time) |
573
+ | `sandbox-guard` | 21 | **the test sandbox itself**: a "throwaway HOME" that resolves to the real user profile, to the filesystem root, or to anywhere outside the OS temp dir is refused with an error instead of being written to and deleted. Every other suite builds its HOME through `test/sandbox-home.mjs` and can only destroy what it created there (no tokens, runs every time) |
570
574
  | `approval-details` | 27 | **approval review context**: the pending-approval snapshot carries the command, reason, cwd and original approval ID — not just a bare ID — so a caller can actually adjudicate; missing `rawInput` is correlated from the earlier `tool_call` update by ID; credential-shaped fields and common inline literals are redacted; closing a conversation cancels pending approvals. Verified end-to-end over real stdio MCP |
575
+ | `line-decoder` | 4 | **UTF-8 line framing**: multi-byte characters split across MCP/ACP stream chunks are preserved (no U+FFFD), including CRLF and EOF-without-newline (no tokens) |
571
576
  | `read-cursor` | 10 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`), and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
572
- | `sentinel-redaction` | 15 | **sentinel disk hygiene**: `result.thinking` is stripped, `thinking_omitted` is always a boolean, and `answer`/`error`/`title`/`tools_used[].input` all pass the same credential redaction as approvals (no tokens) |
573
- | `forget-tombstone` | 7 | **`forget` is terminal**: `release(forget=true)` writes a tombstone, by-id lookups fail with a recovery path, lists exclude it, and deleting the tombstone restores the normal unknown-session path (no tokens) |
577
+ | `sentinel-redaction` | 19 | **sentinel disk hygiene**: `result.thinking` is stripped, `thinking_omitted` is always a boolean, and `answer`/`error`/`title`/`tools_used[].input` all pass the same credential redaction as approvals (no tokens) |
578
+ | `forget-tombstone` | 10 | **`forget` is terminal**: `release(forget=true)` writes a tombstone, by-id lookups fail with a recovery path, lists exclude it, and deleting the tombstone restores the normal unknown-session path (no tokens) |
574
579
  | `bootstrap-profile` | 24 | **first-start profile works in the published zero-dependency form**: copies routing with the built-in YAML subset parser, strips `apiKey`/`headers`/literal secrets, never writes `defaultPreset`, is idempotent, and leaves a user-maintained profile byte-identical (no tokens) |
575
580
  | `workspace-safety` | 23 | **data-loss boundaries**: `isTempPath` is segment-aware (`tmpdir()+'Suffix'` and `/tmpfoo` are *not* temp), a corrupt `workspace.json` is never overwritten, an unreadable session root never wipes `sessionIds`, and automatic cleanup deletes only test-created sessions (no tokens) |
576
- | `permission-adopt` | 13 | **no silent privilege escalation**: adopting an external session with no permission record fails closed to `read-only`; a recorded `read-only` cannot be promoted by the sidecar; the runtime read-back rejects a recorded/requested mismatch (no tokens) |
577
- | `state-safety` | 21 | **state/sentinel/lock storage**: defaults live under `$DSH_HOME/.dsh-any-agent`, an unwritable directory fails startup loudly, sentinel write failures return `null` + `sentinel_error`, and bad numeric env values land on the safe side (no tokens) |
578
- | `setup` | 41 | **the registration CLI**: preview writes nothing, `--remove` takes out only its own entry, an absent client is skipped rather than failed, the Codex TOML block round-trips, and the command recorded in a config is actually spawned and must answer `initialize` (no tokens). It never invokes `claude mcp get` — that CLI rewrites `~/.claude.json` |
581
+ | `permission-adopt` | 17 | **no silent privilege escalation**: adopting an external session with no permission record fails closed to `read-only`; a recorded `read-only` cannot be promoted by the sidecar; the runtime read-back rejects a recorded/requested mismatch (no tokens) |
582
+ | `state-safety` | 26 | **state/sentinel/lock storage**: defaults live under `$DSH_HOME/.dsh-any-agent`, an unwritable directory fails startup loudly, sentinel write failures return `null` + `sentinel_error`, and bad numeric env values land on the safe side (no tokens) |
583
+ | `setup` | 45 | **the registration CLI**: preview writes nothing, `--remove` takes out only its own entry, an absent client is skipped rather than failed, the Codex TOML block round-trips, and the command recorded in a config is actually spawned and must answer `initialize` (no tokens). It never invokes `claude mcp get` — that CLI rewrites `~/.claude.json` |
579
584
  | `removal-scope` | 24 | **unregistering touches only its own entry**: configs holding unrelated MCP servers, top-level Codex settings and sibling Zed settings are registered and then removed, asserting every other value is byte-identical; removing something never written is a no-op with no write (no tokens) |
580
- | `config-safety` | 406 | **the property the 0.3 line is about**, per client: preview leaves files byte-identical with no `.bak`; register-then-remove restores every other entry byte-for-byte, keeps key order and drops no top-level key; 4-space / 2-space indentation, single-line JSON, missing trailing newline, CRLF and a UTF-8 BOM all survive a write; malformed JSON and read-only files are refused without damage; the second `--yes` is a no-op with no new backup; every `.bak-dsh-<stamp>` appears once and equals the pre-write bytes; a `claude` on PATH is never invoked (no tokens) |
585
+ | `config-safety` | 407 | **the property the 0.3 line is about**, per client: preview leaves files byte-identical with no `.bak`; register-then-remove restores every other entry byte-for-byte, keeps key order and drops no top-level key; 4-space / 2-space indentation, single-line JSON, missing trailing newline, CRLF and a UTF-8 BOM all survive a write; malformed JSON and read-only files are refused without damage; the second `--yes` is a no-op with no new backup; every `.bak-dsh-<stamp>` appears once and equals the pre-write bytes; a `claude` on PATH is never invoked (no tokens) |
581
586
  | `real-configs` | 84 (66 with 7/9, 48 with 5/9 fixtures) | **round-trip on the real thing**: copies this machine's actual client configs (an 182693-byte `~/.claude.json` holding hundreds of `projects` entries and case-colliding keys, a 238-line Codex TOML, Zed's full `settings.json`) into a temp dir, registers, then removes, and requires every file back **byte for byte**; entry-presence is checked by key (not a bare substring), and missing fixtures are reported as skipped rather than green (no tokens) |
582
587
  | `commit-script` | 26 | commit messages go through stdin, never a temp file that `git add -A` could capture (no tokens) |
583
588
  | `roots` | 12 | `dsh_start` without `cwd` asks the client for MCP roots; a client without roots gets an actionable error (no tokens) |
@@ -595,13 +600,13 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
595
600
  | `list` | 13 | **the cost of `dsh_list`** (measured): a default call with the on-disk probe takes ~1 s (it spawns a DSH process) while **repeated calls hit a cache and drop to single-digit milliseconds**; `only_running=true` and `include_closed=false` **skip the probe** with equivalent semantics (unopened on-disk sessions are simply excluded) |
596
601
  | `reasoning-check` | 8 | **reasoning visibility**: `reasoning=hide` returns no thinking body but non-zero stats; `reasoning=full` returns the text and the rendered section |
597
602
 
598
- **`node test/run.mjs` runs 815 checks on a machine with five client fixtures (851 with all nine)** — smoke 29 + 786 offline checks (822 with nine). Every suite exited 0 on 0.3.7; none needs a provider (smoke needs a DSH install for its session-creation section).
603
+ **`node test/run.mjs` runs 843 checks on a machine with five client fixtures (879 with all nine)** — smoke 29 + 814 offline checks (850 with nine). Every suite exited 0 on 0.3.9; none needs a provider (smoke skips its session-creation sections when DSH is absent).
599
604
 
600
- That is **519 assertions in the client-registration suites alone** (`setup` 41, `removal-scope` 24, `config-safety` 406, `real-configs` 48; 555 with all nine fixtures), which is where the 0.3 work actually is.
605
+ That is **524 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 48; 560 with all nine fixtures), which is where the 0.3 work actually is.
601
606
 
602
- The model-backed suites (`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) total 474 and were last measured in full on 0.3.7 — they need a working provider, so re-run them yourself before trusting a release: `node test/run.mjs --all`. `acceptance` now **skips explicitly** when the DSH profile has no IDA MCP (`mcp__ida__*`).
607
+ The model-backed suites (`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) total 474 and were last measured in full on 0.3.9 — they need a working provider, so re-run them yourself before trusting a release: `node test/run.mjs --all`. `acceptance` now **skips explicitly** when the DSH profile has no IDA MCP (`mcp__ida__*`).
603
608
 
604
- > Three different totals are intentional: `node test/run.mjs` (815/851), the CI offline job (786/822, no smoke), and the model-backed set (474 nominal). Counts are the numbers each suite prints itself; don't use one number for a different runner.
609
+ > Three different totals are intentional: `node test/run.mjs` (843/879), the CI offline job (nominal 814/850, no smoke; hosted runners without client fixtures skip real-configs' 48 checks), and the model-backed set (474 nominal). Counts are the numbers each suite prints itself; don't use one number for a different runner.
605
610
 
606
611
  ### 🧯 Why `sandbox-guard` exists
607
612
 
@@ -665,7 +670,7 @@ DSH conversations carry a **cross-process write lock**: **it never expires while
665
670
  5. **A conversation that dies before its first successful turn may never have materialized on disk** — resume then fails with a clear error. Safe after the first message.
666
671
  6. **No renaming** — DSH's title subsystem has no external rename API (`SessionTitleService.rename` requires a live in-process session). Titles are auto-generated from the first message.
667
672
  7. **`session/list` returns only `{sessionId, cwd}` and excludes already-open sessions** — titles are filled in by this server from DSH's projection cache.
668
- 8. **"Never reap" costs memory (measured 300–480 MB per conversation)** — that is what buys "the GUI can never take the write lock". Two fresh-start measurements of one active conversation gave 476 MB and 295 MB of added resident memory (the DSH child plus this server); expect the higher figures on a cold process. Two ways to spend less: ① **`DSH_MCP_MAX_LIVE` (default 8** — over the cap, the least recently used process is reaped → bounded memory, locks still held where it matters); ② set `DSH_MCP_IDLE_TTL_MS` to a millisecond value (at the cost of the lock being takeable). To drop to zero right after a batch of work: **`dsh_release(all=true)`** ✓ (releases idle conversations; busy turns are skipped and keep running — pass `force=true` to interrupt them too. Everything resumes on demand).
673
+ 8. **"Never reap" costs memory (measured 300–480 MB per conversation)** — within the `MAX_LIVE` window that is what buys "the GUI cannot take the write lock". Two fresh-start measurements of one active conversation gave 476 MB and 295 MB of added resident memory (the DSH child plus this server); expect the higher figures on a cold process. Two ways to spend less: ① **`DSH_MCP_MAX_LIVE` (default 8** — over the cap, the least recently used process is reaped → bounded memory, locks still held where it matters); ② set `DSH_MCP_IDLE_TTL_MS` to a millisecond value (at the cost of the lock being takeable). To drop to zero right after a batch of work: **`dsh_release(all=true)`** ✓ (releases idle conversations; busy turns are skipped and keep running — pass `force=true` to interrupt them too. Everything resumes on demand).
669
674
 
670
675
  ## ❓ Why is the profile still called `dsh-mcp`?
671
676
 
@@ -701,6 +706,13 @@ npm pack --dry-run
701
706
  # 4. Tag and push (wait for CI before publishing).
702
707
  git add -A && git commit -m "chore: release x.y.z"
703
708
  git tag vx.y.z && git push origin main --tags
709
+
710
+ # 5. Publish to npm and verify.
711
+ npm publish --access public
712
+ npm view dsh-any-agent version
713
+ # A 404 right after publish is usually propagation, not failure: npm returns
714
+ # 202 and the registry can lag briefly. Confirm the local npm log shows the
715
+ # version before retrying.
704
716
  ```
705
717
 
706
718
  ## Star History
package/README.zh.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  </div>
17
17
 
18
- 🧪 0.3.7 本机 815 项检查全绿(九种客户端配置齐全时 851 项;其中 519 项属于客户端注册器套件,齐全时 555 项) · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
18
+ 🧪 0.3.9 本机 843 项检查全绿(九种客户端配置齐全时 879 项;其中 524 项属于客户端注册器套件,齐全时 560 项) · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
19
19
 
20
20
  一个 MCP 服务端,把 DeepSeek Harness 当作**长驻的编码 agent** 来驱动。开个会话、扔个目标,它自己写代码、跑脚本、派子代理;你随时能看、能插话、能回头验收。
21
21
 
@@ -227,7 +227,7 @@ Zed 33 -> 33 个键 只有 context_servers 多了个子项
227
227
 
228
228
  Codex 的 `config.toml` 同理,是按行处理的:其它 `[表]` 及其注释、顶层键(`model`、`approval_policy`、`sandbox_mode` 等)、CRLF 行尾都原样保留 —— `--remove` 只删 `[mcp_servers.dsh-any-agent]` 那两段,其余一律不动。
229
229
 
230
- 这些断言的详细覆盖见 [`test/`](test/)(`test/config-safety.mjs` 共 406 项,逐条断言上面每一条)。
230
+ 这些断言的详细覆盖见 [`test/`](test/)(`test/config-safety.mjs` 共 407 项,逐条断言上面每一条)。
231
231
 
232
232
  **在真实 home 上做过只读验证。** 用 `setup --json`(纯计划模式)跑一遍开发机上的真实配置:9 个客户端全部报出,而现存的每份配置都**原封未动** —— size、mtime、SHA-256 全同,没有新增任何 `.bak-dsh-*`,也没有新建任何文件:
233
233
 
@@ -376,7 +376,7 @@ tool_timeout_sec = 120.0
376
376
 
377
377
  本地目录安装时改用 `"command": "node"` 和 `"args": ["/绝对路径/server/bin/dsh-any-agent.mjs"]`。
378
378
 
379
- > Cursor、Windsurf、Zed、Cline、Continue 等大部分客户端都吃同一套 `mcpServers` 结构——只要能拉起 stdio MCP 服务,就能用。
379
+ > Cursor、Windsurf、Cline、Continue 等大部分客户端都吃同一套 `mcpServers` 结构——只要能拉起 stdio MCP 服务,就能用。Zed 是例外:它用 `context_servers`(见上方示例)。
380
380
 
381
381
  </details>
382
382
 
@@ -476,13 +476,13 @@ DSH 在模型层就把思考与正文分开,ACP 又把它们做成**两条独
476
476
 
477
477
  | 档位 | 审批默认 | 用途 |
478
478
  |---|---|---|
479
- | `danger-full-access`(默认) | `auto-allow` | 零摩擦 |
480
- | `workspace-write` | `auto-allow` | 越界操作会冒泡为待决审批 |
479
+ | `danger-full-access`(服务端裸默认) | `auto-allow` | 零摩擦;`setup` 默认写入 `workspace-write` |
480
+ | `workspace-write` | `auto-allow` | DSH 沙箱限制可写范围;越界请求默认放行,传 `on_approval=ask` 才挂起审批 |
481
481
  | `read-only` | `auto-deny` | 分析不信任样本(恶意软件、来路不明 dump) |
482
482
 
483
483
  `on_approval` 可选 `auto-allow` / `auto-deny` / `ask`(`ask` 会把审批挂起,交给 `dsh_approval_decide` 裁决)。
484
484
 
485
- **`read-only` 是强制边界,不只是默认值**:它固定使用 `auto-deny`;给只读会话传 `auto-allow` / `ask` 会在 `dsh_start` 被直接拒绝,审批层也会拒绝来自只读会话的任何权限请求。否则 DSH 可以请求 `sandbox_permissions` 提权,而宽松的审批策略会把写操作放行(对抗审计实测过)。
485
+ **`read-only` 是强制边界,不只是默认值**:它固定使用 `auto-deny`;给只读会话传 `auto-allow` / `ask` 会在 `dsh_start` 被直接拒绝,审批层也会拒绝来自只读会话的任何权限请求。建会话/resume 后还会回读会话自己记录的权限档;只读档如果在 DSH 投影缓存刷盘窗口内仍读不到记录,会 **fail-closed 拒绝继续**,而不是只依赖审批层兜底。否则 profile 的 `defaultPreset` 加宽松审批策略可能放行写操作(对抗审计实测过)。
486
486
 
487
487
  ## 🪟 会话在 DSH GUI 里的可见性
488
488
 
@@ -545,12 +545,12 @@ node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话
545
545
  | `DSH_MCP_APPROVAL` | `auto-allow`(非只读档) | 默认审批策略。设成 `ask` 才能让越界请求挂起给 `dsh_approval_decide`,而不是自动放行 |
546
546
  | `DSH_MCP_APPROVAL_TIMEOUT_MS` | `300000` | 待决 `ask` 审批的等待上限;超时按拒绝处理 |
547
547
  | `DSH_MCP_FORGOTTEN_DIR` | `$DSH_HOME/.dsh-any-agent-forgotten` | `dsh_release(forget=true)` 写的墓碑目录 |
548
- | `DSH_MCP_LOCK_STALE_MS` | `90000` | 持锁登记多久没续心跳就算"持有者失联"(此时可被 `dsh_takeover` 自动抢占) |
548
+ | `DSH_MCP_LOCK_STALE_MS` | `90000`(3× `REAP_INTERVAL_MS`,下限 30 秒) | 持锁登记多久没续心跳就算"持有者失联"(此时可被 `dsh_takeover` 自动抢占);改回收间隔会改变推导出的默认值 |
549
549
  | `DSH_MCP_IDLE_TTL_MS` | `0`(永不回收) | 会话空闲多久回收其 DSH 进程。★★ **`0` = 永不回收(默认)**:本服务一直握着写锁,**GUI 抢不走**(你在 GUI 里点开是**只读**,照样能看 ✓)。代价:每会话常驻一个 DSH 进程(**两次冷启动实测 476MB / 295MB**)。设成毫秒数(如 `300000`)可省内存,但要接受"锁可能被 GUI 抢走" |
550
550
  | `DSH_MCP_MAX_LIVE` | `8`(最坏约 2.5–4GB,见第 8 条) | ★ **最多同时保活几个会话进程**(`0` = 不限)。超标时回收**最久没用过**的(**在跑的回合绝不回收**)—— "**内存有界,锁尽量在手**"。内存紧用 `4`,机器大可以 `0`。用完想立刻放掉空闲会话:`dsh_release(all=true)`(busy 回合默认跳过,`force=true` 才中断) |
551
551
  | `DSH_MCP_LIST_PROBE_TTL_MS` | `10000` | `dsh_list` 磁盘探测结果的缓存时长(探测要 spawn 一个 DSH 进程,实测约 1 秒) |
552
552
  | `DSH_MCP_SENTINEL_TTL_MS` | `604800000`(7 天) | 启动时清理超过此时长的哨兵;`0` 关闭清理 |
553
- | `DSH_MCP_SENTINEL_INCLUDE_REASONING` | 未设置 | `1` 会把 `result.thinking` 一并写进哨兵(**打破"思考不落盘"的保证**) |
553
+ | `DSH_MCP_SENTINEL_INCLUDE_REASONING` | 未设置 | `1` 会把 `result.thinking` 一并写进哨兵(**打破"思考不落盘"的保证**);answer/error/title/工具输入仍会脱敏 |
554
554
  | `DSH_MCP_PERMISSION` | `danger-full-access` | `dsh_start` 未指定时的权限档 |
555
555
  | `DSH_MCP_REASONING_EFFORT` | `max` | 默认思考深度 |
556
556
  | `DSH_MCP_PROMPT_TIMEOUT_MS` | `0`(不设超时) | **仅 `wait=true` 时**的等待上限;到期只转后台,不取消回合 |
@@ -565,11 +565,14 @@ node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话
565
565
  ## 🧪 测试
566
566
 
567
567
  ```bash
568
- node test/run.mjs # 全部离线套件(不消耗 token;smoke 段需要装 DSH)
569
- node test/run.mjs --all # 离线 + 需要模型的套件
568
+ npm test # 全部离线套件;没有 DSH 时 smoke 自动跳过会话段
569
+ npm run test:all # 离线 + 需要模型的套件(需要 DSH/provider)
570
570
  node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目录;--dry-run 可预演)
571
571
  ```
572
572
 
573
+ 模型套件不在 GitHub 托管 CI 里(需要真实 DSH、provider 与凭据)。在本机跑,
574
+ 或用 `model-suites` workflow 指定一台装好 DSH 的 self-hosted runner。
575
+
573
576
  测试套件**会自己收尾**:`run.mjs` 结束时必定执行 `cleanup.mjs`,它只清理 cwd 在**测试自己造的目录**(`tmpdir()/dsh-any-agent-*` 或桌面 `dsh-any-agent-test-*`)里的会话与工作区登记。**用户在 `%TEMP%` 里自己的真实项目、以及你的状态目录都不会被碰**。要连桌面测试目录一起清,可显式跑 `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`。
574
577
 
575
578
  | 套件 | 检查数 | 覆盖 |
@@ -581,18 +584,19 @@ node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目
581
584
  | `sentinel` | 36 | 完成哨兵:原子性、闩锁语义、多会话并发不串台、被取消也落地、**思考不进文件** |
582
585
  | `prune` | 11 | **哨兵留存**:清超期与崩溃残留、留新、清空壳目录、`TTL=0` 关闭清理、且绝不碰同目录的会话注册表(不花 token,每次必跑) |
583
586
  | `guards` | 31 | **环境变量守卫**:奇怪的数值一律压到安全侧 —— `IDLE_TTL_MS` 的 `-1`/非法值都当"永不回收"(绝不主动让出写锁)、回收间隔过小压回默认(防空转紧循环)、**失联阈值下限 30 秒**(防别的实例"合法地"抢占并杀掉我们的回合)(不花 token,每次必跑) |
584
- | `sandbox-guard` | 18 | **测试沙箱自身**:「临时 HOME」如果落到真实用户目录、它的子目录、盘符根目录,或 OS 临时目录之外,一律报错拒绝,绝不被写入和删除。其余套件都通过 `test/sandbox-home.mjs` 拿目录、只销毁自己建的那个(不花 token,每次必跑) |
587
+ | `sandbox-guard` | 21 | **测试沙箱自身**:「临时 HOME」如果落到真实用户目录、它的子目录、盘符根目录,或 OS 临时目录之外,一律报错拒绝,绝不被写入和删除。其余套件都通过 `test/sandbox-home.mjs` 拿目录、只销毁自己建的那个(不花 token,每次必跑) |
585
588
  | `approval-details` | 27 | **审批审查上下文**:待决审批的快照带命令、理由、cwd 与原始审批 ID —— 不只是一个裸 ID,调用方才能真正裁决;缺失的 `rawInput` 会按 ID 从先前的 `tool_call` 更新关联回来;凭据形状的字段与常见内联字面量在结构化上下文和展示标题里都会脱敏;关闭会话会取消待决审批。经真实 stdio MCP 端到端验证 |
586
589
  | `read-cursor` | 10 | **`dsh_read` 游标语义**:追加到已读条目里的文本必须再次返回(修订号游标,按 `seq` 替换全文);`limit` 钳到 1..500,`0`/负数不会空转或丢尾(不花 token) |
587
- | `sentinel-redaction` | 15 | **哨兵落盘卫生**:`result.thinking` 被剥掉、`thinking_omitted` 始终是布尔值、`answer`/`error`/`title`/`tools_used[].input` 都走与审批相同的凭据脱敏(不花 token) |
588
- | `forget-tombstone` | 7 | **`forget` 是终态**:`release(forget=true)` 写墓碑,按 id 查询明确失败并给出恢复路径,列表不再展示;删除墓碑后回到常规“未知会话”路径(不花 token) |
590
+ | `line-decoder` | 4 | **UTF-8 行分帧**:MCP/ACP 的 chunk 边界不能把中文/emoji 截成 U+FFFD;覆盖逐字节、任意 chunk、CRLF 与 EOF 残留(不花 token) |
591
+ | `sentinel-redaction` | 19 | **哨兵落盘卫生**:`result.thinking` 被剥掉、`thinking_omitted` 始终是布尔值、`answer`/`error`/`title`/`tools_used[].input` 都走与审批相同的凭据脱敏(不花 token) |
592
+ | `forget-tombstone` | 10 | **`forget` 是终态**:`release(forget=true)` 写墓碑,按 id 查询明确失败并给出恢复路径,列表不再展示;删除墓碑后回到常规“未知会话”路径(不花 token) |
589
593
  | `bootstrap-profile` | 24 | **首启 profile 在零依赖发布形态下可用**:内置 YAML 子集解析器复制路由、剥掉 `apiKey`/`headers`/字面密钥、绝不写 `defaultPreset`、幂等;用户手写文件逐字节不动(不花 token) |
590
594
  | `workspace-safety` | 23 | **数据丢失边界**:`isTempPath` 按路径分段(`tmpdir()+'Suffix'`、`/tmpfoo` 都不算临时目录)、坏 `workspace.json` 绝不覆盖、会话根不可读时绝不清空 `sessionIds`、自动清理只删测试造的会话(不花 token) |
591
- | `permission-adopt` | 13 | **不无声提权**:接管没有权限记录的外部会话时 fail-closed 到 `read-only`;记录为 `read-only` 的会话不能被侧车提升;运行期回读记录与请求不一致会失败(不花 token) |
592
- | `state-safety` | 21 | **状态/哨兵/锁目录**:默认在 `$DSH_HOME/.dsh-any-agent`、不可写时启动明确失败、哨兵写失败返回 `null` + `sentinel_error`、非法数值落安全侧(不花 token) |
593
- | `setup` | 41 | **注册 CLI**:预览不写盘、`--remove` 只摘自己那条、没装客户端算跳过不算失败、Codex TOML 段往返、以及把写进配置里的命令真的拉起一次并要求应答 `initialize`(不花 token)。**不会调用 `claude mcp get`** —— 那个 CLI 会整体回写 `~/.claude.json` |
595
+ | `permission-adopt` | 17 | **不无声提权**:接管没有权限记录的外部会话时 fail-closed 到 `read-only`;记录为 `read-only` 的会话不能被侧车提升;运行期回读记录与请求不一致会失败(不花 token) |
596
+ | `state-safety` | 26 | **状态/哨兵/锁目录**:默认在 `$DSH_HOME/.dsh-any-agent`、不可写时启动明确失败、哨兵写失败返回 `null` + `sentinel_error`、非法数值落安全侧(不花 token) |
597
+ | `setup` | 45 | **注册 CLI**:预览不写盘、`--remove` 只摘自己那条、没装客户端算跳过不算失败、Codex TOML 段往返、以及把写进配置里的命令真的拉起一次并要求应答 `initialize`(不花 token)。**不会调用 `claude mcp get`** —— 那个 CLI 会整体回写 `~/.claude.json` |
594
598
  | `removal-scope` | 24 | **注销只动自己那条**:给一份装着别的 MCP 服务器、顶层 Codex 设置和 Zed 兄弟设置的配置,注册再注销,断言其余每个值逐字节不变;删一个从没写过的东西 = 无写入、无报错(不花 token) |
595
- | `config-safety` | 406 | **0.3 这条线的核心性质**,逐客户端验证:预览后文件逐字节不变且不产生 `.bak`;注册再注销后其余条目逐字节还原、键顺序不变、顶层键不丢;4 空格 / 2 空格缩进、单行 JSON、无末尾换行、CRLF、UTF-8 BOM 都能在写入后保留;坏 JSON 与只读文件被拒绝且原文件无损;第二次 `--yes` 空跑不新增备份;每个 `.bak-dsh-<时间戳>` 只出现一次且内容等于写入前字节;PATH 上有 `claude` 也绝不调用(不花 token) |
599
+ | `config-safety` | 407 | **0.3 这条线的核心性质**,逐客户端验证:预览后文件逐字节不变且不产生 `.bak`;注册再注销后其余条目逐字节还原、键顺序不变、顶层键不丢;4 空格 / 2 空格缩进、单行 JSON、无末尾换行、CRLF、UTF-8 BOM 都能在写入后保留;坏 JSON 与只读文件被拒绝且原文件无损;第二次 `--yes` 空跑不新增备份;每个 `.bak-dsh-<时间戳>` 只出现一次且内容等于写入前字节;PATH 上有 `claude` 也绝不调用(不花 token) |
596
600
  | `real-configs` | 84(7/9 配置时 66,本机 5/9 时 48) | **拿真家伙做往返**:把本机**真实**的客户端配置复制进临时目录(18 万字节、含几百个 `projects` 条目与大小写重复键的 `~/.claude.json`,238 行的 Codex TOML,Zed 的完整 `settings.json`),注册再撤销,要求每个文件**逐字节**还原;条目是否存在按**键**判断而不是裸子串;缺少 fixture 时明确打印 `skipped`,绝不假绿(不花 token) |
597
601
  | `commit-script` | 26 | 提交信息走 stdin,绝不落临时文件让 `git add -A` 卷进发布提交(不花 token) |
598
602
  | `roots` | 12 | 不传 `cwd` 时通过 MCP roots 取客户端当前项目;不支持 roots 的客户端得到可操作的报错(不花 token) |
@@ -610,13 +614,13 @@ node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目
610
614
  | `list` | 13 | **`dsh_list` 的开销**(实测驱动):含磁盘探测的默认调用约 1 秒(要 spawn 一个 DSH 进程),**连续调用命中缓存降到个位数毫秒**;`only_running=true` 与 `include_closed=false` **跳过探测**且语义等价(不把磁盘上的未打开会话算进来) |
611
615
  | `reasoning-check` | 8 | **思考可见性**:`reasoning=hide` 不返回思考正文但统计非零;`reasoning=full` 返回正文且渲染出思考段 |
612
616
 
613
- **`node test/run.mjs` 在本机 5/9 客户端配置时跑 815 项(九种齐全时 851 项)** —— smoke 29 + 离线套件 786 项(九种齐全时 822 项)。0.3.7 上全部退出码 0;都不需要 provider(smoke 的建会话段需要装 DSH)。
617
+ **`node test/run.mjs` 在本机 5/9 客户端配置时跑 843 项(九种齐全时 879 项)** —— smoke 29 + 离线套件 814 项(九种齐全时 850 项)。0.3.9 上全部退出码 0;都不需要 provider(没有 DSH 时 smoke 自动跳过建会话段)。
614
618
 
615
- 其中 **519 项属于客户端注册器套件**(`setup` 41、`removal-scope` 24、`config-safety` 406、`real-configs` 48;九种齐全时 555 项)—— 0.3 的活其实都在这里。
619
+ 其中 **524 项属于客户端注册器套件**(`setup` 45、`removal-scope` 24、`config-safety` 407、`real-configs` 48;九种齐全时 560 项)—— 0.3 的活其实都在这里。
616
620
 
617
- 需要模型的套件(`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.7** —— 它们要可用 provider,发版前请自己重跑一遍:`node test/run.mjs --all`。`acceptance` 在没有 IDA MCP(`mcp__ida__*`)时会明确 **skip**。
621
+ 需要模型的套件(`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**。
618
622
 
619
- > 三个总数是刻意分开的:`node test/run.mjs`(815/851)、CI 离线 job(786/822,不含 smoke)、需要模型的集合(名义 474)。数字取各套件自己打印的汇总,不在这里手抄;不要把一个数字套到另一个 runner 上。
623
+ > 三个总数是刻意分开的:`node test/run.mjs`(843/879)、CI 离线 job(名义 814/850,不含 smoke;hosted runner 没有客户端 fixture 时 real-configs 的 48 项会跳过)、需要模型的集合(名义 474)。数字取各套件自己打印的汇总,不在这里手抄;不要把一个数字套到另一个 runner 上。
620
624
 
621
625
  ### 🧯 为什么会有 `sandbox-guard`
622
626
 
@@ -681,7 +685,7 @@ DSH 的会话有**跨进程写锁**:**持有者活着就永不过期,也没
681
685
  5. **首次成功回合之前就死掉的会话可能尚未落盘** —— 此时 resume 会报清晰错误。发出第一条消息后就安全了。
682
686
  6. **不能改名** —— DSH 的标题子系统没有对外改名接口(`SessionTitleService.rename` 需要进程内的活会话)。标题一律由首条消息自动生成。
683
687
  7. **`session/list` 只返回 `{sessionId, cwd}`,且排除已打开的会话** —— 对话名由本服务从 DSH 投影缓存补全。
684
- 8. **默认"永不回收"要花内存(实测 300–480MB/会话)** —— 换来的是"GUI 抢不走写锁"。两次冷启动实测单个活跃会话新增常驻内存分别为 476MB 和 295MB(含 DSH 子进程与本服务),冷进程首次偏高。两种省内存办法:① **`DSH_MCP_MAX_LIVE`(默认 8**,超标就回收最久没用过的 → 内存有界 ✓ 锁仍尽量在手 ✓);② 把 `DSH_MCP_IDLE_TTL_MS` 设成毫秒数(代价是锁可能被 GUI 拿走)。一批活干完想立刻归零:**`dsh_release(all=true)`** ✓(默认只释放空闲会话,**正在跑的回合会跳过、继续跑**;要连它们一起停掉传 `force=true`。之后照样 resume)。
688
+ 8. **默认"永不回收"要花内存(实测 300–480MB/会话)** —— 在 `MAX_LIVE` 窗口内,换来的是"GUI 抢不走写锁"。两次冷启动实测单个活跃会话新增常驻内存分别为 476MB 和 295MB(含 DSH 子进程与本服务),冷进程首次偏高。两种省内存办法:① **`DSH_MCP_MAX_LIVE`(默认 8**,超标就回收最久没用过的 → 内存有界 ✓ 锁仍尽量在手 ✓);② 把 `DSH_MCP_IDLE_TTL_MS` 设成毫秒数(代价是锁可能被 GUI 拿走)。一批活干完想立刻归零:**`dsh_release(all=true)`** ✓(默认只释放空闲会话,**正在跑的回合会跳过、继续跑**;要连它们一起停掉传 `force=true`。之后照样 resume)。
685
689
 
686
690
  ## ❓ 为什么 profile 还叫 `dsh-mcp`?
687
691