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.
- package/CHANGELOG.md +109 -0
- package/DESIGN.md +10 -1
- package/README.md +5 -3
- package/README.zh.md +5 -3
- package/USAGE-NOTES.en.md +6 -5
- package/USAGE-NOTES.md +6 -5
- package/bin/dsh-any-agent.mjs +7 -2
- package/docs/en/architecture.md +3 -3
- package/docs/en/compatibility.md +2 -1
- package/docs/en/reference.md +3 -1
- package/docs/en/security.md +11 -2
- package/docs/en/tests.md +9 -8
- package/docs/en/usage.md +21 -5
- package/docs/zh/architecture.md +3 -3
- package/docs/zh/compatibility.md +2 -1
- package/docs/zh/limits.md +1 -1
- package/docs/zh/reference.md +3 -1
- package/docs/zh/security.md +11 -2
- package/docs/zh/tests.md +9 -8
- package/docs/zh/usage.md +21 -5
- package/package.json +1 -1
- package/src/config.mjs +69 -0
- package/src/hub.mjs +713 -185
- package/src/ids.mjs +21 -0
- package/src/mcp.mjs +18 -7
- package/src/redact.mjs +34 -0
- package/src/review.mjs +210 -0
- package/src/tools.mjs +254 -16
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,115 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.2] — 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`workspace_changed` no longer misses real edits.** The turn snapshot now
|
|
15
|
+
fingerprints every `git status` entry (status line + per-path size/mtime,
|
|
16
|
+
with untracked directories expanded recursively), not just the first 40
|
|
17
|
+
status lines. A file that was already dirty and changed again, or any change
|
|
18
|
+
past the 40th entry, is detected; `changed_files` and `entries` keep their
|
|
19
|
+
old meaning (`entries` is still display-truncated to 40).
|
|
20
|
+
- **`gitStatus` is asynchronous.** Turn start/end snapshots now use `execFile`
|
|
21
|
+
instead of `spawnSync`, so a large repository's `git status` (up to 8 s) no
|
|
22
|
+
longer blocks the shared MCP event loop, ACP messages, approvals or other
|
|
23
|
+
conversations. `busy` is still set before any await; turn finalization keeps
|
|
24
|
+
releasing `busy`, waking interject waiters and scheduling the drain before
|
|
25
|
+
waiting for git, and only writes the sentinel after the result (including
|
|
26
|
+
`workspace_changed` and the review ticket) is complete.
|
|
27
|
+
- `--no-optional-locks` is passed as a **global** git option (before the
|
|
28
|
+
subcommand); previously `git status ... --no-optional-locks` exited 129, so
|
|
29
|
+
every real snapshot was `null` and `workspace_changed` stayed false.
|
|
30
|
+
- **`dsh_get(max_chars)` now bounds the whole response.** The budget used to
|
|
31
|
+
apply only to the rendered text while `structured.run.result` kept every
|
|
32
|
+
tool's full input, so a 77-tool turn came back as ~75K characters and was
|
|
33
|
+
rejected by Claude Code's per-call limit even with `max_chars=8000`. With
|
|
34
|
+
`max_chars > 0`, structured is compacted too: the answer is truncated and
|
|
35
|
+
`tools_used[]` keeps `id/title/name/status/kind` plus `input_chars`
|
|
36
|
+
(`run.result_compacted: true`). `max_chars=0` (default) is unchanged; the
|
|
37
|
+
full result is always in the sentinel file.
|
|
38
|
+
- **`dsh_doctor` flags unverified DSH versions and fragile entry points.** A new
|
|
39
|
+
`dsh_version` check grades the DSH version against the compatibility matrix
|
|
40
|
+
(`verified` 0.1.7 ≤ v < 0.2.0 → ok; `partial`, `unsupported`, `unverified`
|
|
41
|
+
such as 0.2.x, or unreadable → warn). `dsh_bin` warns when DSH resolves from
|
|
42
|
+
an npx cache (`_npx/<hash>/`), which disappears on cache cleanup.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **Auto-approved approvals are visible.** Under `on_approval=auto-allow`, every
|
|
47
|
+
turn result now carries `auto_approvals_total`, `auto_escalations_total`
|
|
48
|
+
(requests with `sandbox_permissions`, e.g. `danger-full-access`) and up to 20
|
|
49
|
+
redacted `auto_approvals` summaries (tool, escalation, justification, command
|
|
50
|
+
preview). The review ticket's evidence and checklist call them out, so a
|
|
51
|
+
`workspace-write` session that silently escalated no longer looks like it
|
|
52
|
+
stayed in bounds. Defaults are unchanged; use `on_approval=ask` to gate each.
|
|
53
|
+
- **PowerShell sentinel waiter.** On Windows the background receipt includes a
|
|
54
|
+
ready-made, quote-escaped `sentinel_wait_powershell` command (also in the
|
|
55
|
+
text), and the server instructions document it, for hosts where Git Bash is
|
|
56
|
+
missing or hangs.
|
|
57
|
+
|
|
58
|
+
### Website
|
|
59
|
+
|
|
60
|
+
- Playground: step backward, stateful connection-line status, sentinel receipt
|
|
61
|
+
pulse, and a "try interject" cross-scenario action on dispatch frames.
|
|
62
|
+
- Code snippets now use one-pass syntax highlighting (quoted keys, values,
|
|
63
|
+
comments and arrows) without nested spans; the Cursor config tab and copy
|
|
64
|
+
success icon were added; `/en/` HTML is explicitly revalidated.
|
|
65
|
+
- Navigation now surfaces the FAQ; English and Chinese nav order match.
|
|
66
|
+
|
|
67
|
+
### Documentation
|
|
68
|
+
|
|
69
|
+
- Fix the profile-creation command in `USAGE-NOTES.md` / `.en.md`: it needs
|
|
70
|
+
`dsh --profile dsh-mcp --from-default-profile acp`, and 0.5.0+ normally
|
|
71
|
+
bootstraps the profile automatically.
|
|
72
|
+
- Correct the team-mode profile default in `docs/zh/limits.md` and the
|
|
73
|
+
architecture diagrams: `dsh-mcp` is default; `dsh-team` is used only when
|
|
74
|
+
team mode is on.
|
|
75
|
+
- Clarify the website README's Node.js floor (website build ≥ 22; the MCP
|
|
76
|
+
server itself requires ≥ 22.15).
|
|
77
|
+
|
|
78
|
+
## [0.5.1] — 2026-11-04
|
|
79
|
+
|
|
80
|
+
### Security
|
|
81
|
+
|
|
82
|
+
- **Path-safe ids before any state-file join.** `conversation_id` and `run_id`
|
|
83
|
+
are now validated as single path segments before building sentinel, review,
|
|
84
|
+
metadata or tombstone paths. Values containing `/`, `\`, `:`, `..` or other
|
|
85
|
+
separators are rejected instead of being passed to `path.join`; this closes
|
|
86
|
+
a traversal in `dsh_get`'s sentinel read-back and in `dsh_review`'s
|
|
87
|
+
`<state>/reviews/<conv>/<run>.review.json` write.
|
|
88
|
+
- **Filesystem-root `cwd` is rejected for writable tiers.** Choosing `/` or
|
|
89
|
+
`C:\` with `workspace-write` made the sandbox root the whole disk; symlinked
|
|
90
|
+
paths are resolved before the home/root guard, so a symlink to the root or
|
|
91
|
+
home cannot bypass it.
|
|
92
|
+
- **`title_hint` is capped at 200 characters** (schema + runtime). It is
|
|
93
|
+
persisted in the registry and sidecar on every turn; an unbounded value was
|
|
94
|
+
a stable disk/memory amplifier.
|
|
95
|
+
- **`dsh_doctor` checks `reviews/` writability**, alongside `runs/` and
|
|
96
|
+
`locks/`.
|
|
97
|
+
- **MCP tool dispatch only accepts own handler properties**, so names such as
|
|
98
|
+
`constructor`, `toString` or `__proto__` are reported as unknown tools
|
|
99
|
+
instead of reaching the prototype chain.
|
|
100
|
+
|
|
101
|
+
### Features
|
|
102
|
+
|
|
103
|
+
- **Caller review (`dsh_review`).** Every completed turn now carries a
|
|
104
|
+
`review` ticket in its result, `structuredContent`, `dsh_get` output and
|
|
105
|
+
async sentinel: a checklist derived from the turn (workspace changed? normal
|
|
106
|
+
stop? which tools ran?), the evidence needed to check it, and a `dsh_review`
|
|
107
|
+
entry. The calling agent can record `pass` / `partial` / `fail` with notes
|
|
108
|
+
and findings; the verdict is written atomically to
|
|
109
|
+
`<state dir>/reviews/<conversation_id>/<run_id>.review.json` and written back
|
|
110
|
+
into the in-memory run and the sentinel (so an async caller no longer sees a
|
|
111
|
+
stale `pending`). `fail`/`partial` require a reason; running/unknown runs are
|
|
112
|
+
refused. Default `advisory`: the ticket is loud but **does not block** the
|
|
113
|
+
next `dsh_send`, and a failed review is not fed back to DSH automatically.
|
|
114
|
+
Set `DSH_MCP_REVIEW=off` (or pass `review: "off"`) to disable tickets.
|
|
115
|
+
Review records share the sentinel's 7-day TTL and credential redaction,
|
|
116
|
+
including checklist items interpolated from `result.error` (Codex review
|
|
117
|
+
finding on #2).
|
|
118
|
+
|
|
10
119
|
## [0.5.0] — 2026-11-03
|
|
11
120
|
|
|
12
121
|
### Process model
|
package/DESIGN.md
CHANGED
|
@@ -238,6 +238,7 @@ Profile = `~/.dsh/profiles/<name>/`,由 `package.json` 的 `dsh.profile.bundle
|
|
|
238
238
|
| B8 | 上下文注入 | `contentBlocks` 支持文本与图片;支持 DSH 的 `@路径` 引用语法把文件带进会话 |
|
|
239
239
|
| B9 | 异步完成哨兵 | `wait=false` 的 run 终止(done/error/cancelled)时,往 `<RUNS_DIR>/<conversation_id>/<run_id>.json` 原子写出含 `status`+`result` 的文件;调用方(Claude/Codex)用后台任务 `until [ -f ]` 等它出现即被唤醒,无需轮询、也不占轮次 |
|
|
240
240
|
| B10 | 写锁抢占 | 写锁是跨进程内核信号量(持有者活着不过期、**无夺锁 API**),所以"抢"= 杀掉持有者。本服务登记自己拉起的子进程(`<状态目录>/locks/<会话id>.json`:pid + token + 心跳),据此把持有者分成四类,只对**判定为本服务残留的孤儿**自动动手;无法识别的一律不杀(见下) |
|
|
241
|
+
| B11 | 调用方审核 | 每个完成的回合在 `result.review` 带一张审核单(核验清单 + 证据摘要 + `dsh_review` 入口);调用方独立核验后提交 `pass`/`partial`/`fail`,裁决落盘 `<状态目录>/reviews/<会话id>/<run_id>.review.json`,并回写内存 run 与哨兵。默认 advisory:**不阻塞**后续派活,也不自动回灌给 DSH |
|
|
241
242
|
|
|
242
243
|
**B9 设计要点(并发正确性)**:本服务在 MCP 协议上只应答请求、**不主动推通知**,"跑完通知"由哨兵文件 + 调用方后台等待任务共同实现。
|
|
243
244
|
- **按会话分目录**:`run_id` 序号是每会话独立计数的,不同会话可能同毫秒生成同名 `run_id`;`<conversation_id>/` 前缀使其各写各的、不串台。会话内 `dsh_send` 忙时直接拒绝、序号单调,故会话内唯一。
|
|
@@ -249,6 +250,14 @@ Profile = `~/.dsh/profiles/<name>/`,由 `package.json` 的 `dsh.profile.bundle
|
|
|
249
250
|
- **给调用方的便利**:返回值同时给出 `sentinel_file_posix`(`D:\a\b` → `/d/a/b`)。手工转换 Windows 路径是等待任务白等到超时最常见的原因。
|
|
250
251
|
- **落点**:`tools.dsh_send` 的 `wait=false` 分支在 `.then`/`.catch` 各调一次 `hub.writeRunSentinel()`(覆盖所有终止态);`createRun` 起跑前 `clearRunSentinel()` 防御历史残留。
|
|
251
252
|
|
|
253
|
+
**B11 设计要点(调用方审核)**
|
|
254
|
+
- **为什么放进返回载荷**:返回值是"另一个 agent 的自述"。调用方最常见的失败模式是直接转述"已完成/已修好"。审核要求必须和结果出现在同一条消息里(`content.text`、`structuredContent`、哨兵 `result.review`),才可能改变调用方的下一步动作;单独一个"记得审核"的约定会被忽略。
|
|
255
|
+
- **审核单形状**:`{review_id, mode, status, run_id, prompt_preview, checklist, evidence, submit:{tool:"dsh_review", arguments}, instructions}`。清单随回合形状变化(有没有改工作区、有没有错误、用了哪些工具);`evidence` 只放核对所需的最小事实(stop_reason、error、changed_files、工具名)。`workspace_changed` 必须基于**全量**快照指纹(逐路径 size/mtime,未跟踪目录递归),只比前 40 条状态行会把"同一状态行但内容变了"误报成没落地,审核清单也就失去了证据价值。
|
|
256
|
+
- **默认 advisory,不阻塞**:这是刻意的产品选择。强制门禁会打断"审核失败 → 派修复回合"的自然流程,也会破坏既有调用方;advisory 只把审核请求显性化。`review=off` 可按会话/单次派活关闭;即使 off,`dsh_review` 仍可对旧 run 显式补审核。
|
|
257
|
+
- **裁决落盘与回写**:`reviews/<会话id>/<run_id>.review.json` 原子写入(tmp + rename,唯一 tmp 名),含裁决、理由/findings、清单与证据。`submitReview` 依次从内存 run → 哨兵 → 已有审核记录取事实,所以服务重启、run 被挤出内存后仍可审核。提交后 **原子重写哨兵里的 `result.review`**,避免异步调用方下次读文件看到与事实相反的 `pending`。
|
|
258
|
+
- **隐私**:审核单不含思考正文;理由、findings、任务预览、证据**与每个 checklist 项**在落盘前都走 `redact.sanitizeApprovalContext`(与审批上下文同一套规则)——checklist 会拼接 `result.error`,漏掉它就会成为第二个明文泄漏点。审核记录与哨兵共用 `DSH_MCP_SENTINEL_TTL_MS` 的 7 天清理。
|
|
259
|
+
- **失败语义**:fail/partial 必须有 notes 或 findings;对 `running`/`queued` 的 run 提交会被拒绝;写盘失败直接抛错(审核本身就是这次操作,静默失败比报错更糟)。
|
|
260
|
+
|
|
252
261
|
**B10 设计要点(写锁与抢占)**
|
|
253
262
|
- **为什么需要**:DSH 的 `SessionWriteLease` 是 Windows 命名内核信号量,**持有者活着就永不过期**,且**没有 API 能从活进程手里夺走** —— 唯一手段是杀掉持有者进程。既然要杀,就必须先判定那是谁。
|
|
254
263
|
- **登记**:一个会话一个文件 `<状态目录>/locks/<会话id>.json`,内容 `{mcp_pid, mcp_token, child_pid, cwd, spawned_at, heartbeat_at}`。`mcp_token` 防 PID 复用;`heartbeat_at` 由 reaper 周期刷新。
|
|
@@ -296,7 +305,7 @@ Profile = `~/.dsh/profiles/<name>/`,由 `package.json` 的 `dsh.profile.bundle
|
|
|
296
305
|
|
|
297
306
|
| # | 功能 | 说明 |
|
|
298
307
|
|---|---|---|
|
|
299
|
-
| E1 |
|
|
308
|
+
| E1 | **返回工作区变更** | 回合结束后返回 `workspace_changed`(本回合前后 `git status` 的**全量**快照指纹:状态行 + 逐路径 size/mtime,未跟踪目录递归展开)与 `diff_stat`(当前脏文件摘要)。快照走异步子进程,不阻塞共享 MCP 事件循环。**让调用方 agent 能审阅 DSH 的产出再决定是否接受**——这是"外包工程师"模式的关键 |
|
|
300
309
|
| E2 | 计划模式 | 只出计划不落地(`dsh-plan-mode`),调用方批准后再执行 |
|
|
301
310
|
| E3 | 审查模式 | 对既有改动做对抗式审查 |
|
|
302
311
|
| E4 | 接受/回滚 | 会话级 checkpoint(DSH 有 `dsh-session-checkpoint-policy`)配合 git 做回滚 |
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
🧪 0.
|
|
18
|
+
🧪 0.5.2 offline suite green, including the workspace-change detection fix and visible auto-approvals · 🔌 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
|
|
|
@@ -48,6 +48,7 @@ DSH (DeepSeek Harness) can do all of that — it has file/shell tools, skills, s
|
|
|
48
48
|
| 🤫 **Silent by default** | Not one byte on stderr, so nothing pollutes your host's logs. stdout carries the protocol and nothing else. |
|
|
49
49
|
| 🧩 **Capabilities compose** | DSH can mount its own MCP servers (IDA Pro, browsers, your internal tooling), so this is a bridge to a whole toolbox. |
|
|
50
50
|
| 🛡️ **Read-only that really is read-only** | `read-only` sandbox plus auto-denied approvals, for working on untrusted samples. |
|
|
51
|
+
| 🔎 **Caller review, not blind relay** | Every result carries a `review` ticket (checklist + evidence + `dsh_review` entry). The caller verifies with its own tools, then records `pass` / `partial` / `fail`. Advisory by default — it never blocks the next dispatch, and a failed review is not silently fed back to DSH. |
|
|
51
52
|
|
|
52
53
|
## 🧭 Positioning: a dispatcher, not a capability aggregator
|
|
53
54
|
|
|
@@ -76,7 +77,7 @@ multi-role work" line; simple tasks are not forced into a team. Turn it off with
|
|
|
76
77
|
profile disables the legacy `subagent` tools and replaces them with nine team
|
|
77
78
|
tools; teammates share one workspace.
|
|
78
79
|
|
|
79
|
-
**Only dispatch semantics become tools**: start, send, read, get, interject,
|
|
80
|
+
**Only dispatch semantics become tools**: start, send, read, get, review, interject,
|
|
80
81
|
interrupt, approve, queue, inbox, status, release, takeover, doctor. No file I/O, no
|
|
81
82
|
shell, no editor, no task board.
|
|
82
83
|
|
|
@@ -119,7 +120,8 @@ The second command previews the changes; `--yes` applies them. Reconnect your cl
|
|
|
119
120
|
| `dsh_send` | Hand it a task. **Defaults to `wait=false`: returns a `run_id` + `sentinel_file` immediately**, the turn runs in the background and the sentinel file notifies you when it's done (recommended). `wait=true` blocks until the turn ends; hitting `timeout_ms` (default `0` = wait forever) **only downgrades to background — the turn is never cancelled and the result is never lost** |
|
|
120
121
|
| `dsh_list` | All conversations with live state: `running` / `idle` / `detached`, elapsed time, current tool, output so far. `only_running=true` for active ones |
|
|
121
122
|
| `dsh_read` | **Cursor-based incremental read** of what a *running* conversation is producing right now. The cursor is a **revision number**: an entry whose text grew since your last read reappears — match it by `seq` and replace its text, don't append again |
|
|
122
|
-
| `dsh_get` | Conversation details; with `run_id`, the **final result of that dispatch** (the "collect the result later" entry point)
|
|
123
|
+
| `dsh_get` | Conversation details; with `run_id`, the **final result of that dispatch** (the "collect the result later" entry point) **plus its review ticket**. Run records live in memory (last 20) — use the sentinel file after a restart |
|
|
124
|
+
| `dsh_review` | **Caller review.** Submit `pass` / `partial` / `fail` for a completed run, with notes and optional findings. The verdict is written to `<state dir>/reviews/<conversation_id>/<run_id>.review.json` and back into the in-memory run and sentinel. Advisory by default: it never blocks the next `dsh_send` |
|
|
123
125
|
| `dsh_interject` | Interject while it's busy: `interject` = stop and redirect, `queue` = wait for the current turn, then speak. **This round writes a sentinel too**, and the reply carries its `sentinel_file`. On an idle conversation it behaves like `wait=true` and **blocks until the turn ends** |
|
|
124
126
|
| `dsh_takeover` | **Preempt the write lock.** The only way out when another DSH process holds it (DSH has no steal API — preempting means killing the holder). It classifies the holder first: our own orphaned child → taken automatically; another live instance → needs `force`; **GUI/unknown → never killed**, reported instead |
|
|
125
127
|
| `dsh_interrupt` | Stop the current turn (the conversation stays healthy) |
|
package/README.zh.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
🧪 0.
|
|
18
|
+
🧪 0.5.2 本机离线套件全绿,含工作区变化检测修复、自动放行审批可见 · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
|
|
19
19
|
|
|
20
20
|
一个 MCP 服务端,把 DeepSeek Harness 当作**长驻的编码 agent** 来驱动。开个会话、扔个目标,它自己写代码、跑脚本、派子代理;你随时能看、能插话、能回头验收。
|
|
21
21
|
|
|
@@ -48,6 +48,7 @@ DSH(DeepSeek Harness)本身**全都能做** —— 它有文件/shell 工具
|
|
|
48
48
|
| 🤫 **默认完全静默** | 一个字节都不输出,不污染宿主日志;stdout 只走协议。 |
|
|
49
49
|
| 🧩 **能力可叠加** | DSH 自己还能挂别的 MCP(IDA Pro、浏览器、内部工具)——这是一座通往整个工具箱的桥。 |
|
|
50
50
|
| 🛡️ **只读档真能只读** | `read-only` 沙箱 + 审批一律拒绝,适合分析不信任样本。 |
|
|
51
|
+
| 🔎 **让调用方审核,而不是盲转述** | 每个结果都带 `review` 审核单(清单 + 证据 + `dsh_review` 入口)。调用方用自己的工具核验后记录 `pass` / `partial` / `fail`。默认 advisory:不阻塞下一次派活,审核不通过也不会被悄悄回灌给 DSH。 |
|
|
51
52
|
|
|
52
53
|
## 🧭 定位:本服务只负责"派活",能力复用 DSH
|
|
53
54
|
|
|
@@ -71,7 +72,7 @@ profile 切到 `dsh-team`(含实验性 Agent Teams),`dsh_send` 会提示"
|
|
|
71
72
|
关闭:`dsh-any-agent team off`;单次关闭:`dsh_send(team=false)`。
|
|
72
73
|
注意 team profile 会关闭旧的 `subagent` 工具,换成 9 个团队工具;成员共享一个工作区。
|
|
73
74
|
|
|
74
|
-
**只有"派发语义"
|
|
75
|
+
**只有"派发语义"本身需要的才做成工具**:建会话、派活、读进度、验收、审核、插话、中断、审批、
|
|
75
76
|
队列、收件箱、状态、释放、抢占、体检。文件读写 / shell / 编辑器 / 任务板都不做。
|
|
76
77
|
|
|
77
78
|
**什么时候需要外层再加东西**:只有当你需要"保证"而不是"行为"时——
|
|
@@ -114,7 +115,8 @@ dsh-any-agent-setup --yes
|
|
|
114
115
|
| `dsh_send` | 派任务。**默认 `wait=false`:立刻返回 `run_id` + `sentinel_file`**,回合在后台跑,跑完由哨兵文件通知你(推荐)。`wait=true` 才原地阻塞到回合结束;到期(`timeout_ms`,默认 0 = 一直等)**只会转后台,不取消回合、不丢结果** |
|
|
115
116
|
| `dsh_list` | 所有会话及运行状态:`running` / `idle` / `detached`,已跑时长、当前工具、本轮输出。`only_running=true` 只看活动会话 |
|
|
116
117
|
| `dsh_read` | **游标式增量读**——看正在跑的会话此刻吐出了什么。cursor 是**修订号**:文本被追加过的条目会再次返回,按 `seq` 替换全文(不要重复累加) |
|
|
117
|
-
| `dsh_get` | 会话详情;带 `run_id`
|
|
118
|
+
| `dsh_get` | 会话详情;带 `run_id` 时返回**那一次派活的完整结果 + 审核单**("回头验收"的入口)。run 记录只在内存(最近 20 条),服务重启后请读哨兵文件 |
|
|
119
|
+
| `dsh_review` | **调用方审核**。对已完成的 run 提交 `pass` / `partial` / `fail`,可带理由与 findings。裁决写入 `<状态目录>/reviews/<conversation_id>/<run_id>.review.json`,并回写内存 run 与哨兵。默认 advisory:**不阻塞**下一次 `dsh_send` |
|
|
118
120
|
| `dsh_interject` | 会话在忙时插话:`interject` 打断并改口,`queue` 等本轮跑完接着说。**这一轮同样会写哨兵**,返回里给 `sentinel_file`。会话空闲时等价于 `wait=true`,**会阻塞到回合结束** |
|
|
119
121
|
| `dsh_takeover` | **抢占写锁**。锁被别的 DSH 进程占着时的唯一出路(DSH 没有夺锁 API,抢 = 杀掉持有者)。先判定持有者:本服务残留的孤儿 → 自动抢;另一个活着的实例 → 需 `force`;**GUI/未知 → 绝不杀**,只报告 |
|
|
120
122
|
| `dsh_interrupt` | 打断当前回合(会话不会损坏) |
|
package/USAGE-NOTES.en.md
CHANGED
|
@@ -8,7 +8,7 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
|
|
|
8
8
|
|
|
9
9
|
## 1. Setup
|
|
10
10
|
|
|
11
|
-
**1.1 The profile must exist first.** Otherwise every turn fails with `no adapter registered for provider "xxx"`. This server ships no provider config; it drives `~/.dsh/profiles/dsh-mcp/`. Create it with `dsh dsh-mcp --from-default-profile acp` and fill in your own provider per [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml).
|
|
11
|
+
**1.1 The profile must exist first.** Otherwise every turn fails with `no adapter registered for provider "xxx"`. This server ships no provider config; it drives `~/.dsh/profiles/dsh-mcp/`. Create it with `dsh --profile dsh-mcp --from-default-profile acp` and fill in your own provider per [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml). On 0.5.0+ the server bootstraps this profile on first start, so the manual command is normally only needed when `DSH_MCP_BOOTSTRAP=0` or the profile is broken.
|
|
12
12
|
|
|
13
13
|
**1.2 ★ The most common trap: ACP's model does NOT come from `agent-default-model`.** You configure your provider there, yet turns fail with `no API key for provider route "deepseek-official"`. ACP reads the `dsh-acp` plugin's own `config.provider` / `config.model`, which the `acp` bundle hard-codes. You must override the `acp` row:
|
|
14
14
|
|
|
@@ -130,7 +130,8 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
|
|
|
130
130
|
1. **Give goals, not commands.** DSH is an agent: say "add tests for this module and make them pass", not "run pytest". It picks tools, plans steps, and spawns subagents on its own.
|
|
131
131
|
2. **One conversation per task** — mixing unrelated work pollutes history and costs more.
|
|
132
132
|
3. **Split long work into dispatch + collect**: `wait=false` → do something else → `dsh_get(run_id)`.
|
|
133
|
-
4. **Review output via workspace changes** — `workspace_changed`
|
|
134
|
-
5. **
|
|
135
|
-
6. **
|
|
136
|
-
7. **
|
|
133
|
+
4. **Review output via workspace changes** — `workspace_changed` compares a **full** `git status` snapshot fingerprint taken before and after the turn (status line plus each dirty/untracked path's size and mtime; untracked directories are expanded recursively), so an already-dirty file that changes again, or a change past the 40th entry, is still detected. `diff_stat` lists the current dirty files (it is not a per-turn diff).
|
|
134
|
+
5. **Review the result, don't just relay it** — every result carries a `review` ticket. Check at least one claim with your own tools (read the changed file, inspect `git diff`, run the relevant test), then record `pass` / `partial` / `fail` with `dsh_review`. Default `advisory` does not block the next send; `fail`/`partial` need a reason, and a failed review is not fed back to DSH automatically.
|
|
135
|
+
6. **Ask for a plan first** if you want to approve before execution (DSH has plan mode).
|
|
136
|
+
7. **Three cost knobs**: lower `reasoning_effort`, keep `reasoning=hide`, release and start fresh.
|
|
137
|
+
8. **Don't paste large code blocks into the prompt** — tell it which file to read instead; DSH has its own file tools.
|
package/USAGE-NOTES.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
### 1.1 profile 必须先建,否则连不上模型
|
|
12
12
|
- **现象**:所有回合都失败,报 `no adapter registered for provider "xxx"`。
|
|
13
13
|
- **原因**:本服务不自带任何 provider 配置,它驱动的是 `~/.dsh/profiles/dsh-mcp/` 这个 profile。
|
|
14
|
-
- **对策**:`dsh dsh-mcp --from-default-profile acp`,然后照 [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml) 填上你自己的 provider。
|
|
14
|
+
- **对策**:`dsh --profile dsh-mcp --from-default-profile acp`,然后照 [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml) 填上你自己的 provider。0.5.0+ 首次启动会自动 bootstrap 这个 profile,通常无需手工执行;只有在 `DSH_MCP_BOOTSTRAP=0` 或 profile 损坏时才需要。
|
|
15
15
|
|
|
16
16
|
### 1.2 ★ 最常见的坑:ACP 的模型不走 `agent-default-model`
|
|
17
17
|
- **现象**:明明在 `agent-default-model` 里配了自己的 provider,会话却报 `no API key for provider route "deepseek-official"`。
|
|
@@ -271,7 +271,8 @@
|
|
|
271
271
|
1. **给它目标,不要给命令**。DSH 是 agent:说"把这个模块的测试补上并跑通",别说"执行 pytest"。它自己会选工具、分步骤、必要时派子代理。
|
|
272
272
|
2. **一个会话一件事**。别往一个会话里塞互不相关的任务,历史会互相污染且越来越贵。
|
|
273
273
|
3. **长任务拆成"派活 + 验收"**:`wait=false` → 干别的 → `dsh_get(run_id)`。
|
|
274
|
-
4. **要审查产出就用工作区变更**:`workspace_changed`
|
|
275
|
-
5.
|
|
276
|
-
6.
|
|
277
|
-
7.
|
|
274
|
+
4. **要审查产出就用工作区变更**:`workspace_changed` 比较的是本回合前后 `git status` 的**全量**快照指纹(状态行 + 每个脏/未跟踪路径的 size/mtime;未跟踪目录会递归展开),所以"回合前已经脏、回合内又被改内容"或"改动落在第 41 条之后"都能识别。`diff_stat` 列出当前仍处于改动状态的文件(不是本回合的精确 diff)。
|
|
275
|
+
5. **审核结果,别直接转述**:每个结果都带 `review` 审核单。用自己的工具核验至少一项关键声明(读改动的文件、看 `git diff`、跑相关测试),再用 `dsh_review` 记录 `pass` / `partial` / `fail`。默认 `advisory` 不阻塞下一次派活;`fail`/`partial` 必须给理由,且审核不通过不会自动回灌给 DSH。
|
|
276
|
+
6. **要它先想清楚再动手**:让它先给方案、你确认后再执行(DSH 有计划模式)。
|
|
277
|
+
7. **省钱的三个旋钮**:`reasoning_effort` 调低、`reasoning` 保持 `hide`、及时 `dsh_release` 并开新会话。
|
|
278
|
+
8. **别在提示词里重复贴大段代码**——告诉它去看哪个文件更划算(DSH 自己有文件工具)。
|
package/bin/dsh-any-agent.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* DSH_MCP_STATE 会话注册表路径(默认 $DSH_HOME/.dsh-any-agent/conversations.json)
|
|
9
9
|
* DSH_MCP_PERMISSION 新建会话的默认权限档(默认 danger-full-access)
|
|
10
10
|
* DSH_MCP_APPROVAL 默认审批策略(非只读档默认 auto-allow;ask = 挂起待裁决)
|
|
11
|
+
* DSH_MCP_REVIEW 调用方审核策略(默认 advisory;off 关闭自动审核单)
|
|
12
|
+
* DSH_MCP_REVIEWS_DIR 审核记录目录(默认 <状态文件同目录>/reviews)
|
|
11
13
|
* DSH_MCP_IDLE_TTL_MS 空闲多久回收会话进程(默认 0 = 永不回收)
|
|
12
14
|
* DSH_MCP_PROMPT_TIMEOUT_MS wait=true 时的等待上限(默认 0 = 不设超时)
|
|
13
15
|
* DSH_MCP_LOCK_STALE_MS 写锁登记多久没心跳算"持有者失联"(默认 90 秒)
|
|
@@ -46,6 +48,7 @@ if (argv.includes('--help') || argv.includes('-h')) {
|
|
|
46
48
|
' DSH_MCP_PROFILE DSH profile to drive (default dsh-mcp)\n' +
|
|
47
49
|
' DSH_MCP_PERMISSION danger-full-access | workspace-write | read-only\n' +
|
|
48
50
|
' DSH_MCP_APPROVAL auto-allow | auto-deny | ask\n' +
|
|
51
|
+
' DSH_MCP_REVIEW advisory (default) | off — caller review ticket on results\n' +
|
|
49
52
|
' DSH_MCP_LOG silent (default) | info | debug\n\n' +
|
|
50
53
|
'Docs: https://github.com/xmtlzzz/dsh-any-agent\n',
|
|
51
54
|
);
|
|
@@ -122,9 +125,11 @@ await hub.load();
|
|
|
122
125
|
hub.startReaper();
|
|
123
126
|
// 启动时清一次过期哨兵(哨兵内含正文,长期不清理会无声堆积)
|
|
124
127
|
const prunedSentinels = hub.pruneSentinels();
|
|
125
|
-
if (prunedSentinels.pruned) {
|
|
128
|
+
if (prunedSentinels.pruned || prunedSentinels.reviews_pruned) {
|
|
126
129
|
log.info(
|
|
127
|
-
`[dsh-any-agent] 启动清理:删除 ${prunedSentinels.pruned}
|
|
130
|
+
`[dsh-any-agent] 启动清理:删除 ${prunedSentinels.pruned} 个过期哨兵、` +
|
|
131
|
+
`${prunedSentinels.reviews_pruned ?? 0} 条过期审核记录、` +
|
|
132
|
+
`${prunedSentinels.dirs_removed + (prunedSentinels.review_dirs_removed ?? 0)} 个空目录`,
|
|
128
133
|
);
|
|
129
134
|
}
|
|
130
135
|
// 写锁登记清理:删掉"MCP 已死 + 子进程也死了"的记录;**保留**子进程还活着的那些 ——
|
package/docs/en/architecture.md
CHANGED
|
@@ -24,9 +24,9 @@ DSH ships two programmable entry points. The `sdk` profile looks simpler, but it
|
|
|
24
24
|
│ MCP over stdio │ newline-delimited JSON-RPC (ACP)
|
|
25
25
|
▼ ▼
|
|
26
26
|
┌─────────────┐ ┌──────────────────────────┐
|
|
27
|
-
│dsh-any-agent│───────────▶│ dsh --profile dsh-
|
|
28
|
-
└─────────────┘ │
|
|
29
|
-
|
|
27
|
+
│dsh-any-agent│───────────▶│ dsh --profile dsh-mcp │ sessions with the same
|
|
28
|
+
└─────────────┘ │ (dsh-team when team on) │ (cwd, permission) share
|
|
29
|
+
└────────────┬──────────────┘ one process (default since 0.5.0)
|
|
30
30
|
│
|
|
31
31
|
┌────────────┴─────────────┐
|
|
32
32
|
│ DSH's own tools │ files, shell, search,
|
package/docs/en/compatibility.md
CHANGED
|
@@ -36,7 +36,8 @@ dsh-any-agent team status # team switch and profile
|
|
|
36
36
|
|
|
37
37
|
In an MCP client, call `dsh_doctor` and look at:
|
|
38
38
|
|
|
39
|
-
- `dsh_bin`: DSH entry point and version;
|
|
39
|
+
- `dsh_bin`: DSH entry point and version; **warn** when it resolves from an npx cache (`_npx/<hash>/`), which disappears on cache cleanup — install DSH globally or pin `DSH_BIN`;
|
|
40
|
+
- `dsh_version`: the version graded against the table above — `verified` is ok; `partial` / `unsupported` / `unverified` (e.g. 0.2.x) / unreadable are **warn**;
|
|
40
41
|
- `profile`: exists, drift, route source;
|
|
41
42
|
- `team`: `on` / `off` / `requested but unavailable` (version or profile lacks the team package);
|
|
42
43
|
- `config`: effective permission, idle reaping, shared process, and so on.
|
package/docs/en/reference.md
CHANGED
|
@@ -25,8 +25,10 @@
|
|
|
25
25
|
| `DSH_MCP_SHARED_PROCESS` | `1` (on) | **On by default**: sessions with the same `(cwd, permission)` share one ACP process. Large memory and cold-start win (measured: second `session/new` ≈300 ms vs ≈4 s standalone), and `dsh_doctor` de-dupes RSS by PID. Trade-off: a process crash takes down the whole group (error sentinels are written; each session resumes on the next call), so `dsh_takeover` requires `force=true` to kill such a process. Set `0` for one process per conversation |
|
|
26
26
|
| `DSH_MCP_AUDIT_FILE` | empty (off) | Metadata audit JSONL path. Records events, conversation/run ids, decisions and counts; **never** prompts, answers, thinking or tool input. Rotates at 10 MB |
|
|
27
27
|
| `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) |
|
|
28
|
-
| `DSH_MCP_SENTINEL_TTL_MS` | `604800000` (7 days) | Startup pruning age for sentinels
|
|
28
|
+
| `DSH_MCP_SENTINEL_TTL_MS` | `604800000` (7 days) | Startup pruning age for sentinels **and review records**; `0` disables pruning |
|
|
29
29
|
| `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 |
|
|
30
|
+
| `DSH_MCP_REVIEW` | `advisory` | Caller-review policy. `advisory` (default) attaches a review ticket (checklist + evidence + `dsh_review` entry) to every result and sentinel **without blocking later sends**; `off` disables automatic tickets. Per-conversation/per-send overrides use the `review` argument |
|
|
31
|
+
| `DSH_MCP_REVIEWS_DIR` | `<state file dir>/reviews` | Where review verdicts are written (`<this>/<conversation_id>/<run_id>.review.json`). Must be writable, or startup fails loudly |
|
|
30
32
|
| `DSH_MCP_PERMISSION` | `workspace-write` | Default tier for `dsh_start`. `danger-full-access` additionally requires `DSH_MCP_ALLOW_FULL_ACCESS=1` |
|
|
31
33
|
| `DSH_MCP_ALLOW_FULL_ACCESS` | `0` | Whether `permission=danger-full-access` is allowed. Denied by default: it hands the caller whole-machine write access |
|
|
32
34
|
| `DSH_MCP_REASONING_EFFORT` | `max` | Default reasoning effort |
|
package/docs/en/security.md
CHANGED
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
`on_approval` can be `auto-allow`, `auto-deny`, or `ask` (which exposes a pending approval for `dsh_approval_decide` to adjudicate).
|
|
12
12
|
|
|
13
|
+
**Under `auto-allow`, `workspace-write` is a soft boundary.** When DSH's sandbox blocks something (e.g. child-process pipes on Windows), DSH asks for `sandbox_permissions: danger-full-access` and `auto-allow` grants it. The default is unchanged, but the grant is **no longer silent**: every turn result carries `auto_approvals_total`, `auto_escalations_total` and up to 20 redacted `auto_approvals` summaries (tool, escalation tier, justification, command preview), and the review ticket's evidence and checklist call out "N auto-approved this turn, M of them escalations". To gate each one, start a conversation with `on_approval=ask`.
|
|
14
|
+
|
|
13
15
|
**`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).
|
|
14
16
|
|
|
17
|
+
## 🧷 Input boundaries
|
|
18
|
+
|
|
19
|
+
- **`conversation_id` / `run_id` are path-safe ids.** They are used in sentinel, review, metadata and tombstone paths; the server accepts only a single segment (letters/digits/`._-`, max 200) and rejects separators or `..` before any `join()`. This covers `dsh_get`'s sentinel read-back and `dsh_review`'s `<state>/reviews/<conv>/<run>.review.json` write.
|
|
20
|
+
- **`cwd` cannot be the filesystem root for writable tiers.** `workspace-write` at `/` or `C:\` would make the sandbox root the whole disk; the home/root guard resolves symlinks first, so a symlink to the root or home is rejected too. `danger-full-access` remains an explicit opt-in.
|
|
21
|
+
- **`title_hint` is capped at 200 characters** (schema + runtime), because it is persisted in the registry and sidecar on every turn.
|
|
22
|
+
|
|
15
23
|
## 🪟 Workspace visibility in the DSH GUI
|
|
16
24
|
|
|
17
25
|
Two different things — don't confuse them:
|
|
@@ -50,10 +58,11 @@ Silent by default — **not one byte is written**.
|
|
|
50
58
|
| `debug` | more of our own events; still no DSH output |
|
|
51
59
|
| `DSH_MCP_LOG_STDERR=1` | additionally forward DSH's raw stderr (**may contain reasoning**) |
|
|
52
60
|
|
|
53
|
-
**This server never writes reasoning to disk** —
|
|
61
|
+
**This server never writes reasoning to disk** — it does persist the content a caller needs to collect or review a result offline, but never reasoning text:
|
|
54
62
|
|
|
55
63
|
- Not in logs (silent by default). The state file and the per-conversation metadata sidecars under `<state dir>/meta/` hold metadata only (id, cwd, permissions, counters, title hint) — a smoke-test assertion guards the registry.
|
|
56
64
|
- **The async completion sentinel persists the answer, usage and tool inputs** (it must, so a caller can collect offline) but **strips `result.thinking`** and always reports `thinking_omitted` (true when reasoning was removed, false when there was none). Tool inputs are passed through the same credential redaction as approvals, but the sentinel still contains commands and file contents — treat it as sensitive. A test asserts that even when `reasoning=full` is requested, no reasoning text appears in the sentinel — while `dsh_get(run_id)` still returns it from memory. Set `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` to break this on purpose.
|
|
57
|
-
-
|
|
65
|
+
- **Review records are the second intentional on-disk artifact.** `<state dir>/reviews/<conversation_id>/<run_id>.review.json` holds the verdict, the reviewer's `notes`/`findings`, the checklist and an evidence summary. `notes`/`findings`, the task preview, the evidence **and each checklist item** pass through the same credential redaction (the checklist can interpolate `result.error`); reasoning is never included. Treat review records as sensitive too (they can quote code or paths).
|
|
66
|
+
- Sentinels **and review records** are pruned after 7 days by default (`DSH_MCP_SENTINEL_TTL_MS`; `0` keeps them forever).
|
|
58
67
|
|
|
59
68
|
> ⚠️ **DSH writes its own session log under `~/.dsh/sessions` and that log contains the full reasoning text** (even with `reasoning=hide`). It is what makes resume possible and is outside this server's control. The absolute "reasoning never reaches disk" claim applies to *this server's* artifacts only.
|
package/docs/en/tests.md
CHANGED
|
@@ -16,22 +16,23 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
16
16
|
|
|
17
17
|
| Suite | Checks | Covers |
|
|
18
18
|
|---|---|---|
|
|
19
|
-
| `smoke` |
|
|
19
|
+
| `smoke` | 31 | handshake, tool table (15 tools), conversation creation, config, registry purity, prototype-name tool rejection |
|
|
20
20
|
| `boundary` | 46 | protocol edges (double initialize, malformed lines, unknown method), argument validation (including read-only + permissive approval rejection, unknown provider/model, and no stale write-lock marker on a failed create), unknown ids, cursor edges, lifecycle idempotence, unicode |
|
|
21
21
|
| `integration` | 30 | resume-with-memory across processes, interrupt, both interject modes |
|
|
22
|
-
| `async` |
|
|
22
|
+
| `async` | 21 | fire-and-forget + later collection, including the review ticket in the receipt/result/sentinel and a real `dsh_review` round trip |
|
|
23
23
|
| `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
|
|
24
24
|
| `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) |
|
|
25
25
|
| `guards` | 40 | **environment-variable guards**: odd numbers always land on the safe side — `IDLE_TTL_MS=-1` means "never reap", garbage/empty falls back to the 10-minute default, 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); plus the team-mode default and precedence (`DSH_MCP_TEAM` > `settings.json` > off) (no tokens, runs every time) |
|
|
26
26
|
| `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) |
|
|
27
|
-
| `approval-details` |
|
|
27
|
+
| `approval-details` | 37 | **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 |
|
|
28
28
|
| `failure-terminal` | 11 | **terminal state on failure**: an `ensureAlive` failure wakes `dsh_interject` waiters, writes an `error` sentinel, and attaches `runId` to the error; an approval with no matching option reports `effective=cancelled` instead of a fake allow; `dsh_get` reads a run back from its sentinel (no tokens) |
|
|
29
29
|
| `shared-host` | 25 | **shared process host**: notifications and permission requests route by `sessionId`, unknown-session approvals are cancelled, a process exit fans out to every attached conversation, the child stops only when the last conversation detaches; **on by default and switchable off with `=0`** (an empty value counts as unset); the pre-takeover collision check for a shared process; RSS helper (no tokens) |
|
|
30
30
|
| `shared-process` | 8 (skips without DSH) | **real-DSH shared process**: two conversations with the same `(cwd, permission)` share one PID, closing one leaves the other alive, closing both clears the host and exits the PID (needs a local DSH; runs with `--all`) |
|
|
31
|
-
| `dispatch-tools` |
|
|
31
|
+
| `dispatch-tools` | 55 (53 off Windows) | **dispatch surface**: `dsh_inbox` aggregates pending approvals across conversations, `client_run_id` idempotency hits and evicts with its run, queue list/update/promote/remove (remove writes a cancelled sentinel), `dsh_get(max_chars)` compacts both text and structured (answer truncated, tool inputs reduced to `input_chars`; `max_chars=0` still returns everything), `dsh_doctor` DSH-version grading and npx-cache detection, PowerShell sentinel-waiter escaping plus a real run (Windows only), audit records metadata only, `dsh_doctor` essential checks (no tokens) |
|
|
32
32
|
| `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) |
|
|
33
33
|
| `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) |
|
|
34
34
|
| `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) |
|
|
35
|
+
| `review` | 70 | **caller review**: default `advisory` tickets carry a checklist + evidence + `dsh_review` entry; `dsh_review` writes the verdict to disk and back into the run and sentinel; `fail`/`partial` require notes; running/unknown runs are refused; review still works after a restart (sentinel-backed); queue and `client_run_id` replay receipts carry the ticket; review records share the sentinel TTL; notes, previews, evidence **and checklist items** are credential-redacted; **path-traversal ids are rejected before any file I/O** (no tokens) |
|
|
35
36
|
| `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) |
|
|
36
37
|
| `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) |
|
|
37
38
|
| `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) |
|
|
@@ -43,7 +44,7 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
43
44
|
| `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) |
|
|
44
45
|
| `commit-script` | 26 | commit messages go through stdin, never a temp file that `git add -A` could capture (no tokens) |
|
|
45
46
|
| `roots` | 12 | `dsh_start` without `cwd` asks the client for MCP roots; a client without roots gets an actionable error (no tokens) |
|
|
46
|
-
| `workspace-titles` |
|
|
47
|
+
| `workspace-titles` | 18 | home-as-workspace **and filesystem-root cwd** are rejected before DSH starts; `title_hint` is returned separately from DSH's automatic title (no tokens) |
|
|
47
48
|
| `package-check` | 14 | `files` whitelist, zero runtime dependencies, Node floor, and bin shebangs (no tokens) |
|
|
48
49
|
| `timeout` | 38 | **timeout semantics**: `wait=false` is unaffected by `timeout_ms`; a `wait=true` expiry merely downgrades to background (turn not cancelled, result not lost, `busy` never lies); `timeout_ms<=0` waits forever; a failed resume invalidates the process instead of wedging the conversation; an empty prompt yields a clear error |
|
|
49
50
|
| `lock` | 79 | **write lock and preemption**: four holder classifications, `writeMarker` never overwriting a live holder, malformed/missing registration edge cases; **end-to-end** with two real MCP instances fighting over one conversation → clear error → refusal → `force` takeover; **a crash releases the lock automatically**, **a wedged holder is preempted without `force`**; plus the "one turn, one sentinel" invariant (inline turns and idle interjects included) |
|
|
@@ -57,13 +58,13 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
57
58
|
| `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) |
|
|
58
59
|
| `reasoning-check` | 8 | **reasoning visibility**: `reasoning=hide` returns no thinking body but non-zero stats; `reasoning=full` returns the text and the rendered section |
|
|
59
60
|
|
|
60
|
-
**`node test/run.mjs` runs
|
|
61
|
+
**`node test/run.mjs` runs 1012 checks on a machine with seven client fixtures (1030 with all nine)** — smoke 31 + 981 offline checks (999 with nine). Every suite exited 0 on the 0.5.0 tree plus caller review (2026-11-04, Windows / Node 22.19.0; the local five-fixture run reports 994); none needs a provider (smoke skips its session-creation sections when DSH is absent).
|
|
61
62
|
|
|
62
63
|
That is **542 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 66; 560 with all nine fixtures), which is where the 0.3 work actually is.
|
|
63
64
|
|
|
64
|
-
The model-backed suites (`smoke`
|
|
65
|
+
The model-backed suites (`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) total 489. Most were last measured in full on 0.3.9; the suites touched by caller review (`smoke`, `integration`, `async`, `sentinel`, `boundary`) were re-run on this tree. 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__*`); `shared-process` also skips without DSH.
|
|
65
66
|
|
|
66
|
-
> Three different totals are intentional: `node test/run.mjs` (
|
|
67
|
+
> Three different totals are intentional: `node test/run.mjs` (1012/1030), the CI offline job (nominal 981/999, no smoke; hosted runners without client fixtures skip `real-configs` entirely), and the model-backed set (489 nominal). Counts are the numbers each suite prints itself; don't use one number for a different runner.
|
|
67
68
|
|
|
68
69
|
### 🧯 Why `sandbox-guard` exists
|
|
69
70
|
|
package/docs/en/usage.md
CHANGED
|
@@ -17,14 +17,20 @@ MCP tool calls **block**, so `dsh_send` **does not block by default** (`wait=fal
|
|
|
17
17
|
→ [me] … | [turn] turn 1 started | [tool] pwsh [in_progress]
|
|
18
18
|
|
|
19
19
|
5. dsh_get(conversation_id, run_id) # collect the result
|
|
20
|
-
→ status=done, elapsed=20.3s, full answer
|
|
20
|
+
→ status=done, elapsed=20.3s, full answer,
|
|
21
|
+
review: { status: "pending", checklist: [...], submit: { tool: "dsh_review", ... } }
|
|
22
|
+
|
|
23
|
+
6. dsh_review(conversation_id, run_id, verdict="pass", notes="checked the diff and ran the tests")
|
|
24
|
+
→ review: { status: "pass", reviewed_at: "...", review_file: "..." }
|
|
21
25
|
```
|
|
22
26
|
|
|
23
|
-
`
|
|
27
|
+
**A returned result is not "done" until the caller has reviewed it.** Every result carries a `review` ticket: a checklist derived from the turn (did it change files? did it end normally? which tools ran?) plus the evidence needed to check it. Use your own tools to verify at least one claim — read the changed files, inspect `git diff`, run the relevant test — then submit a verdict with `dsh_review`. The default `advisory` mode does **not** block the next `dsh_send`; it only makes the review request part of the returned payload instead of something the caller has to remember. `fail` / `partial` require `notes` (or `findings`). A failed review is **not** fed back to DSH automatically — dispatch a fix with `dsh_send` when you decide to.
|
|
28
|
+
|
|
29
|
+
`run_id` is the receipt. Run records are **memory-only, last 20 kept**; under many concurrent runs old ones get evicted and `dsh_get` will fail — which is exactly why the completion sentinel below carries the result (and why `dsh_review` can still read the run from the sentinel).
|
|
24
30
|
|
|
25
31
|
## 🔔 Workflow: get notified on completion (no polling)
|
|
26
32
|
|
|
27
|
-
Besides `run_id`, `wait=false` returns a **`sentinel_file`** path plus **`sentinel_file_posix`** (the Bash/MSYS form — use it directly; hand-converting Windows paths is the easiest way to make your waiter hang until timeout). When the turn ends — **whether it succeeds, fails, or is cancelled** — the service **atomically writes** a small file there containing the final `status` and `result
|
|
33
|
+
Besides `run_id`, `wait=false` returns a **`sentinel_file`** path plus **`sentinel_file_posix`** (the Bash/MSYS form — use it directly; hand-converting Windows paths is the easiest way to make your waiter hang until timeout). When the turn ends — **whether it succeeds, fails, or is cancelled** — the service **atomically writes** a small file there containing the final `status`, `result` and the **review ticket** (`result.review`). So you neither poll nor block: **wait for that file with a background task in your own host (Claude/Codex); the moment it appears, you're woken up.**
|
|
28
34
|
|
|
29
35
|
```bash
|
|
30
36
|
# sent="$sentinel_file_posix" ← copy it straight from the tool result
|
|
@@ -37,7 +43,16 @@ done
|
|
|
37
43
|
echo "DONE" # task exits → host wakes you → read the file to collect
|
|
38
44
|
```
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
On a Windows host where Git Bash is unavailable (or hangs), wait on the native `sentinel_file` with PowerShell. On Windows the background receipt's structured content carries a ready-made, escaped **`sentinel_wait_powershell`** — use it directly:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
$f='C:\Users\you\.dsh\.dsh-any-agent\runs\<conversation_id>\<run_id>.json' # write ' as '' inside the path
|
|
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
|
+
Once the file exists: **read it** for `status` and `result` (preferred — immune to the in-memory window and survives restarts), or use `dsh_get(conversation_id, run_id)`. When a turn used many tools, the full result (every tool's complete input included) can exceed the client's per-call tool-result limit; pass `max_chars` (e.g. 8000) to `dsh_get`: the budget now applies to **both** text and structured — the answer is truncated and each tool keeps only its name/status plus `input_chars`. The full version is always in the sentinel file.
|
|
41
56
|
|
|
42
57
|
Design notes (especially under **concurrent multi-session** use):
|
|
43
58
|
|
|
@@ -45,9 +60,10 @@ Design notes (especially under **concurrent multi-session** use):
|
|
|
45
60
|
- **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.
|
|
46
61
|
- **Atomic write**: `tmp` + `rename`; if the file exists its contents are complete. Each run uses its own tmp, so concurrent writes never clobber.
|
|
47
62
|
- **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.
|
|
63
|
+
- **Review survives the sentinel**: `result.review` is written with the result, so a caller that only reads the file still sees the checklist. `dsh_review` then **atomically rewrites** that review block (`status` → `pass`/`partial`/`fail`, plus `notes`, `reviewed_at`, `review_file`); the latch itself never disappears. Review records live under `<state dir>/reviews/` and are pruned on the same 7-day TTL as sentinels.
|
|
48
64
|
- **★ 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](security.md#-logging-and-privacy).
|
|
49
65
|
|
|
50
|
-
> Cleanup: sentinels contain full answers, so the service **prunes
|
|
66
|
+
> Cleanup: sentinels and review records contain full answers/notes, so the service **prunes them once they are 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.
|
|
51
67
|
|
|
52
68
|
## 🧵 Workflow: many conversations at once
|
|
53
69
|
|
package/docs/zh/architecture.md
CHANGED
|
@@ -24,9 +24,9 @@ DSH 自带两个可编程入口。`sdk` profile 看着更简单,但它的 JSON
|
|
|
24
24
|
│ MCP over stdio │ 换行分隔 JSON-RPC(ACP)
|
|
25
25
|
▼ ▼
|
|
26
26
|
┌─────────────┐ ┌──────────────────────────┐
|
|
27
|
-
│dsh-any-agent│─────────▶│ dsh --profile dsh-
|
|
28
|
-
└─────────────┘ │
|
|
29
|
-
└────────────┬─────────────┘ (0.5.0
|
|
27
|
+
│dsh-any-agent│─────────▶│ dsh --profile dsh-mcp │ 同 (cwd, 权限) 的会话
|
|
28
|
+
└─────────────┘ │ (team on → dsh-team) │ 共用一个长驻进程
|
|
29
|
+
└────────────┬─────────────┘ (0.5.0 起默认共享进程)
|
|
30
30
|
│
|
|
31
31
|
┌────────────┴─────────────┐
|
|
32
32
|
│ DSH 自带工具 │ 文件/shell/搜索/
|
package/docs/zh/compatibility.md
CHANGED
|
@@ -34,7 +34,8 @@ dsh-any-agent team status # 团队开关与 profile
|
|
|
34
34
|
|
|
35
35
|
在 MCP 客户端里调用 `dsh_doctor`,重点看:
|
|
36
36
|
|
|
37
|
-
- `dsh_bin`:DSH
|
|
37
|
+
- `dsh_bin`:DSH 入口与版本;入口落在 npx 缓存(`_npx/<hash>/`)时 **warn** —— 清缓存就会失效,建议全局安装 DSH 或固定 `DSH_BIN`;
|
|
38
|
+
- `dsh_version`:按上表给版本定档 —— `verified` 为 ok;`partial` / `unsupported` / `unverified`(如 0.2.x)/ 读不到版本为 **warn**;
|
|
38
39
|
- `profile`:是否存在、是否漂移、路由来源;
|
|
39
40
|
- `team`:`on` / `off` / `requested but unavailable`(版本或 profile 不含 team 包);
|
|
40
41
|
- `config`:当前 permission、空闲回收、共享进程等生效值。
|
package/docs/zh/limits.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
因为改名会打断一堆已经落盘的东西。
|
|
19
19
|
|
|
20
|
-
本服务首次启动时会在 `~/.dsh/profiles/dsh-mcp/` 建一个 DSH profile。0.
|
|
20
|
+
本服务首次启动时会在 `~/.dsh/profiles/dsh-mcp/` 建一个 DSH profile。0.5.0 起团队模式按需开启:开启时 profile 切到 `dsh-team`(同样由本服务自动创建),默认或关闭时仍用 `dsh-mcp`。两者共用同一套 `DSH_MCP_*` 变量与 `# dsh-mcp:managed` 标记。
|
|
21
21
|
|
|
22
22
|
这个目录名被另外三样持久化标识引用着:
|
|
23
23
|
|
package/docs/zh/reference.md
CHANGED
|
@@ -25,8 +25,10 @@
|
|
|
25
25
|
| `DSH_MCP_SHARED_PROCESS` | `1`(开启) | **默认开启**:同 `(cwd, permission)` 的会话共享一个 ACP 进程。显著省内存与冷启动(实测第二个会话 `session/new` 约 300ms,独立进程约 4s),`dsh_doctor` 的 RSS 也按 PID 去重。代价:进程崩溃会连坐同组会话(统一写 error 哨兵,之后各自 resume),所以 `dsh_takeover` 杀这类进程时要求 `force=true`。设 `0` 回到"一会话一进程" |
|
|
26
26
|
| `DSH_MCP_AUDIT_FILE` | 空(关闭) | 元数据审计 JSONL 路径。只记事件、会话/回合 ID、决定与计数;**不记** prompt、正文、思考、工具输入。10MB 滚动到 `.1` |
|
|
27
27
|
| `DSH_MCP_LIST_PROBE_TTL_MS` | `10000` | `dsh_list` 磁盘探测结果的缓存时长(探测要 spawn 一个 DSH 进程,实测约 1 秒) |
|
|
28
|
-
| `DSH_MCP_SENTINEL_TTL_MS` | `604800000`(7 天) |
|
|
28
|
+
| `DSH_MCP_SENTINEL_TTL_MS` | `604800000`(7 天) | 启动时清理超过此时长的哨兵**与审核记录**;`0` 关闭清理 |
|
|
29
29
|
| `DSH_MCP_SENTINEL_INCLUDE_REASONING` | 未设置 | `1` 会把 `result.thinking` 一并写进哨兵(**打破"思考不落盘"的保证**);answer/error/title/工具输入仍会脱敏 |
|
|
30
|
+
| `DSH_MCP_REVIEW` | `advisory` | 调用方审核策略。`advisory`(默认)给每个结果/哨兵附审核单(清单 + 证据 + `dsh_review` 入口),**但不阻塞**后续派活;`off` 关闭自动审核单。单会话/单次派活可用 `review` 参数覆盖 |
|
|
31
|
+
| `DSH_MCP_REVIEWS_DIR` | `<状态文件同目录>/reviews` | 审核裁决的落盘目录(`<此目录>/<会话id>/<run_id>.review.json`)。不可写会让服务启动时明确失败 |
|
|
30
32
|
| `DSH_MCP_PERMISSION` | `workspace-write` | `dsh_start` 未指定时的权限档。设为 `danger-full-access` 还需要 `DSH_MCP_ALLOW_FULL_ACCESS=1` |
|
|
31
33
|
| `DSH_MCP_ALLOW_FULL_ACCESS` | `0` | 是否允许 `permission=danger-full-access`。默认拒绝:该档把整机写权限交给调用方 |
|
|
32
34
|
| `DSH_MCP_REASONING_EFFORT` | `max` | 默认思考深度 |
|