dsh-any-agent 0.3.6 → 0.3.8
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 +48 -2
- package/DESIGN.md +9 -1
- package/README.md +10 -6
- package/README.zh.md +8 -5
- package/USAGE-MIGRATION.en.md +160 -0
- package/bin/dsh-any-agent.mjs +1 -1
- package/package.json +3 -1
- package/src/hub.mjs +7 -6
- package/src/tools.mjs +9 -9
- package/src/workspace.mjs +62 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,51 @@ 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.
|
|
10
|
+
Post-0.3.8 hardening.
|
|
11
|
+
|
|
12
|
+
## [0.3.8] — 2026-11-02
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `npm test` now works without a local DSH: smoke still checks handshake and
|
|
17
|
+
the tool table, then skips session-dependent sections instead of failing, so
|
|
18
|
+
all offline suites run. `npm run test:all` is the explicit full-suite entry.
|
|
19
|
+
- Added the `model-suites` workflow for a self-hosted runner with DSH and
|
|
20
|
+
provider credentials; hosted runners print an explicit skip.
|
|
21
|
+
- Added an English `USAGE-MIGRATION.en.md` and linked it from the English
|
|
22
|
+
README and `/en/` install section.
|
|
23
|
+
|
|
24
|
+
### Testing
|
|
25
|
+
|
|
26
|
+
- Removed the remaining always-true `check(..., true)` assertions; cleanup
|
|
27
|
+
checks now assert actual release/forget results or timeout state.
|
|
28
|
+
- `prune` runs with an isolated `DSH_HOME` so it cannot touch real tombstones.
|
|
29
|
+
- Added 8 static accessibility tests for both locales (28 website tests total
|
|
30
|
+
counting the existing site/release/demo suites).
|
|
31
|
+
- `readHeaderAsync` now uses `StringDecoder` for multi-byte UTF-8 boundaries.
|
|
32
|
+
|
|
33
|
+
### Website
|
|
34
|
+
|
|
35
|
+
- `robots.txt` now mirrors the complete Cloudflare Managed crawler list, not
|
|
36
|
+
just the four most common bots.
|
|
37
|
+
|
|
38
|
+
## [0.3.7] — 2026-11-02
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **Session-header lookup no longer blocks the event loop.** `Hub.get()`,
|
|
43
|
+
`Hub.load()` and `pruneStaleTombstones()` now use a streaming
|
|
44
|
+
`readHeaderAsync()` that stops at the first newline, with a 5 s timeout and
|
|
45
|
+
1 MiB header cap. Large session logs can no longer stall MCP requests,
|
|
46
|
+
heartbeat renewal, or approval handling while recovering an unknown
|
|
47
|
+
conversation. The synchronous reader remains only in the explicit
|
|
48
|
+
workspace CLI/test-cleanup path.
|
|
49
|
+
- **Website robots policy mirrors Cloudflare Managed content.** The generated
|
|
50
|
+
`robots.txt` now states `Content-Signal: search=yes, ai-train=no` and
|
|
51
|
+
disallows GPTBot/ClaudeBot/Google-Extended/CCBot, so non-Cloudflare hosts
|
|
52
|
+
and the repository agree with the production policy.
|
|
53
|
+
- Website tests now cover the English locale, robots policy, hreflang, CSP
|
|
54
|
+
hash, and bilingual pages (20 tests).
|
|
11
55
|
|
|
12
56
|
## [0.3.6] — 2026-11-02
|
|
13
57
|
|
|
@@ -652,7 +696,9 @@ First public release, published to npm as [`dsh-any-agent`](https://www.npmjs.co
|
|
|
652
696
|
profiles, sessions and client configs. The product name changed; the on-disk
|
|
653
697
|
contract did not. See the README FAQ.
|
|
654
698
|
|
|
655
|
-
[Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.
|
|
699
|
+
[Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.8...HEAD
|
|
700
|
+
[0.3.8]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.7...v0.3.8
|
|
701
|
+
[0.3.7]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.6...v0.3.7
|
|
656
702
|
[0.3.6]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.5...v0.3.6
|
|
657
703
|
[0.3.5]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.4...v0.3.5
|
|
658
704
|
[0.3.4]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.3...v0.3.4
|
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.
|
|
5
|
+
> 本文只谈**功能与接口**,不写实现代码;其中 M2/M3 工具(`dsh_rename` 等)是分期计划,不是当前 0.3.8 的缺失功能。技术判断最初基于 DSH 0.1.7-rc.2,当前验证基线是 **DSH 0.2.1-rc.5**(见 §2 证据)。
|
|
6
|
+
|
|
7
|
+
## 当前实现状态(0.3.8)
|
|
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
|
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
🧪 815 checks green on 0.3.
|
|
18
|
+
🧪 815 checks green on 0.3.8 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
|
|
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>
|
|
@@ -550,11 +550,15 @@ Silent by default — **not one byte is written**.
|
|
|
550
550
|
## 🧪 Tests
|
|
551
551
|
|
|
552
552
|
```bash
|
|
553
|
-
|
|
554
|
-
|
|
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 |
|
|
@@ -595,11 +599,11 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
595
599
|
| `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
600
|
| `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
601
|
|
|
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.
|
|
602
|
+
**`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.8; none needs a provider (smoke skips its session-creation sections when DSH is absent).
|
|
599
603
|
|
|
600
604
|
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.
|
|
601
605
|
|
|
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.
|
|
606
|
+
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.8 — 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
607
|
|
|
604
608
|
> 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.
|
|
605
609
|
|
package/README.zh.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
🧪 0.3.
|
|
18
|
+
🧪 0.3.8 本机 815 项检查全绿(九种客户端配置齐全时 851 项;其中 519 项属于客户端注册器套件,齐全时 555 项) · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
|
|
19
19
|
|
|
20
20
|
一个 MCP 服务端,把 DeepSeek Harness 当作**长驻的编码 agent** 来驱动。开个会话、扔个目标,它自己写代码、跑脚本、派子代理;你随时能看、能插话、能回头验收。
|
|
21
21
|
|
|
@@ -565,11 +565,14 @@ node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话
|
|
|
565
565
|
## 🧪 测试
|
|
566
566
|
|
|
567
567
|
```bash
|
|
568
|
-
|
|
569
|
-
|
|
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
|
| 套件 | 检查数 | 覆盖 |
|
|
@@ -610,11 +613,11 @@ node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目
|
|
|
610
613
|
| `list` | 13 | **`dsh_list` 的开销**(实测驱动):含磁盘探测的默认调用约 1 秒(要 spawn 一个 DSH 进程),**连续调用命中缓存降到个位数毫秒**;`only_running=true` 与 `include_closed=false` **跳过探测**且语义等价(不把磁盘上的未打开会话算进来) |
|
|
611
614
|
| `reasoning-check` | 8 | **思考可见性**:`reasoning=hide` 不返回思考正文但统计非零;`reasoning=full` 返回正文且渲染出思考段 |
|
|
612
615
|
|
|
613
|
-
**`node test/run.mjs` 在本机 5/9 客户端配置时跑 815 项(九种齐全时 851 项)** —— smoke 29 + 离线套件 786 项(九种齐全时 822 项)。0.3.
|
|
616
|
+
**`node test/run.mjs` 在本机 5/9 客户端配置时跑 815 项(九种齐全时 851 项)** —— smoke 29 + 离线套件 786 项(九种齐全时 822 项)。0.3.8 上全部退出码 0;都不需要 provider(没有 DSH 时 smoke 自动跳过建会话段)。
|
|
614
617
|
|
|
615
618
|
其中 **519 项属于客户端注册器套件**(`setup` 41、`removal-scope` 24、`config-safety` 406、`real-configs` 48;九种齐全时 555 项)—— 0.3 的活其实都在这里。
|
|
616
619
|
|
|
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.
|
|
620
|
+
需要模型的套件(`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.8** —— 它们要可用 provider,发版前请自己重跑一遍:`node test/run.mjs --all`。`acceptance` 在没有 IDA MCP(`mcp__ida__*`)时会明确 **skip**。
|
|
618
621
|
|
|
619
622
|
> 三个总数是刻意分开的:`node test/run.mjs`(815/851)、CI 离线 job(786/822,不含 smoke)、需要模型的集合(名义 474)。数字取各套件自己打印的汇总,不在这里手抄;不要把一个数字套到另一个 runner 上。
|
|
620
623
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Moving to a new machine / first-time setup (English)
|
|
2
|
+
|
|
3
|
+
This is the short English version of `USAGE-MIGRATION.md`. It covers a fresh
|
|
4
|
+
install, registering the MCP server with your client, and the checks worth
|
|
5
|
+
doing before you trust it with real work.
|
|
6
|
+
|
|
7
|
+
## 1. Prerequisites
|
|
8
|
+
|
|
9
|
+
| Item | Requirement |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Node.js | **≥ 22.15** (`zstdDecompressSync` is required) |
|
|
12
|
+
| DSH | DeepSeek Harness installed and able to answer a normal chat turn |
|
|
13
|
+
| Provider | A model route configured in DSH, with credentials available through `~/.dsh/.credentials.yaml` or environment variables |
|
|
14
|
+
|
|
15
|
+
> The order matters. This server does **not** ship a provider or credentials.
|
|
16
|
+
> On first start it copies the model route from an existing DSH profile. If DSH
|
|
17
|
+
> itself is not configured, the first call fails with a provider error.
|
|
18
|
+
|
|
19
|
+
## 2. Install and register
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g dsh-any-agent
|
|
23
|
+
|
|
24
|
+
# Preview only: reads configs, writes nothing
|
|
25
|
+
dsh-any-agent-setup
|
|
26
|
+
|
|
27
|
+
# Apply
|
|
28
|
+
dsh-any-agent-setup --yes
|
|
29
|
+
|
|
30
|
+
# Limit to some clients, or choose a permission tier
|
|
31
|
+
dsh-any-agent-setup --yes --only cursor,zed
|
|
32
|
+
dsh-any-agent-setup --yes --permission workspace-write
|
|
33
|
+
|
|
34
|
+
# Remove only this server's entry
|
|
35
|
+
dsh-any-agent-setup --remove --yes
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Supported clients: **Claude Code, Claude Desktop, Codex, Cursor, Windsurf,
|
|
39
|
+
Zed, Gemini CLI, opencode, VS Code**. Clients that are not installed are
|
|
40
|
+
reported as skipped, not failed.
|
|
41
|
+
|
|
42
|
+
The writer is deliberately conservative:
|
|
43
|
+
|
|
44
|
+
- preview mode never writes and never starts a child process;
|
|
45
|
+
- every changed file is backed up as `<file>.bak-dsh-<stamp>`;
|
|
46
|
+
- JSON/JSONC/TOML edits are text-level and only touch the
|
|
47
|
+
`dsh-any-agent` entry (indentation, key order, CRLF, BOM and comments are
|
|
48
|
+
preserved);
|
|
49
|
+
- malformed or read-only configs are refused rather than overwritten;
|
|
50
|
+
- a second `--yes` is a no-op;
|
|
51
|
+
- `setup` does **not** invoke client CLIs such as `claude mcp get` (that can
|
|
52
|
+
make Claude Code rewrite `~/.claude.json`).
|
|
53
|
+
|
|
54
|
+
## 3. Manual configuration (optional)
|
|
55
|
+
|
|
56
|
+
If your client is not covered by setup, add a stdio MCP server. For example:
|
|
57
|
+
|
|
58
|
+
Codex `~/.codex/config.toml`:
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
[mcp_servers.dsh-any-agent]
|
|
62
|
+
command = "dsh-any-agent"
|
|
63
|
+
args = []
|
|
64
|
+
startup_timeout_sec = 20.0
|
|
65
|
+
tool_timeout_sec = 120.0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Generic JSON client:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"dsh-any-agent": {
|
|
74
|
+
"command": "dsh-any-agent",
|
|
75
|
+
"args": []
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`setup --yes` writes the absolute node path, the entry file, and
|
|
82
|
+
`DSH_MCP_PERMISSION` / `DSH_MCP_LOG`; the examples above assume
|
|
83
|
+
`dsh-any-agent` is on `PATH`.
|
|
84
|
+
|
|
85
|
+
## 4. First start and profile bootstrap
|
|
86
|
+
|
|
87
|
+
On first start the server creates `~/.dsh/profiles/dsh-mcp/` and copies the
|
|
88
|
+
model route from an existing DSH profile (`home` → `desktop` → `web` →
|
|
89
|
+
`headless` → `acp`, first usable one). It uses a built-in zero-dependency YAML
|
|
90
|
+
subset parser, so no extra npm package is needed.
|
|
91
|
+
|
|
92
|
+
What is copied: `apiKeyEnv`, `baseURL`, `api`, `models`, reasoning metadata.
|
|
93
|
+
What is stripped: literal API keys, nested `headers`, and URL userinfo. The
|
|
94
|
+
generated profile includes the three permission presets but **never**
|
|
95
|
+
`defaultPreset` (that would override the permission tier at session creation).
|
|
96
|
+
|
|
97
|
+
Credentials remain in DSH's own credential store; this project never owns them.
|
|
98
|
+
Set `DSH_MCP_BOOTSTRAP=0` to disable profile writing entirely, or
|
|
99
|
+
`DSH_MCP_ROUTE_SOURCE=<profile>` / `__home__` to pin the route source.
|
|
100
|
+
|
|
101
|
+
## 5. Permission tiers
|
|
102
|
+
|
|
103
|
+
| Tier | Meaning |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `read-only` | DSH's sandbox is read-only; approvals are forced to `auto-deny` |
|
|
106
|
+
| `workspace-write` | Writes are limited to the workspace by DSH's sandbox |
|
|
107
|
+
| `danger-full-access` | No workspace sandbox |
|
|
108
|
+
|
|
109
|
+
The server's raw default is `danger-full-access` when `DSH_MCP_PERMISSION` is
|
|
110
|
+
not set; `setup` writes `workspace-write` into the client config. Out-of-
|
|
111
|
+
workspace approval requests default to `auto-allow`; pass `on_approval=ask`
|
|
112
|
+
when creating a session if you want `dsh_approval_decide` to adjudicate them.
|
|
113
|
+
|
|
114
|
+
`read-only` is a hard boundary at this layer: `read-only + auto-allow/ask` is
|
|
115
|
+
rejected, and the server reads back the session's recorded permission tier
|
|
116
|
+
after create/resume. External MCP tools mounted in DSH are outside DSH's
|
|
117
|
+
sandbox; read `USAGE-NOTES.en.md` before using them with untrusted data.
|
|
118
|
+
|
|
119
|
+
## 6. Verify
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
dsh-any-agent --version
|
|
123
|
+
dsh-any-agent-setup --help
|
|
124
|
+
dsh-any-agent-setup # preview should show your clients
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
In the client, ask the agent to call `dsh_start` with a small project
|
|
128
|
+
directory, then `dsh_send` a short prompt and `dsh_get` the result. If the
|
|
129
|
+
client cannot see the server, restart the client after setup.
|
|
130
|
+
|
|
131
|
+
## 7. Common problems
|
|
132
|
+
|
|
133
|
+
- **`DSH CLI entry not found`** — set `DSH_BIN` to
|
|
134
|
+
`.../node_modules/@deepseek-ai/dsh/lib/bin.js`, or install DSH in a location
|
|
135
|
+
the server can discover.
|
|
136
|
+
- **`no adapter registered for provider` / no API key** — DSH itself is not
|
|
137
|
+
configured, or the profile route was not copied. Open DSH once, configure a
|
|
138
|
+
model, then restart the MCP server.
|
|
139
|
+
- **`session ... is already owned by an active write handle`** — the session
|
|
140
|
+
is held by another process (often the DSH GUI). Use `dsh_status` to see
|
|
141
|
+
`lock_holder`; use `dsh_release` on the other instance or `dsh_takeover`
|
|
142
|
+
when the holder is a stale dsh-any-agent process. Never kill a GUI process.
|
|
143
|
+
- **Nothing happens when waiting for completion** — use the returned
|
|
144
|
+
`sentinel_file`; a turn that is interrupted by a process exit may not land
|
|
145
|
+
one. Inspect the workspace and re-dispatch.
|
|
146
|
+
- **Old state after upgrading** — the registry moved from
|
|
147
|
+
`<repo>/.state/conversations.json` to
|
|
148
|
+
`$DSH_HOME/.dsh-any-agent/conversations.json`; the first 0.3.x start
|
|
149
|
+
migrates it automatically. Use `DSH_MCP_STATE` to override.
|
|
150
|
+
|
|
151
|
+
## 8. Upgrade / rollback
|
|
152
|
+
|
|
153
|
+
`npm install -g dsh-any-agent@latest` upgrades the server. Client configs are
|
|
154
|
+
not rewritten by an upgrade; run `dsh-any-agent-setup` again only if you want
|
|
155
|
+
to refresh the entry or permission env. `dsh-any-agent-restore` can restore a
|
|
156
|
+
client config from a `.bak-dsh-*` backup.
|
|
157
|
+
|
|
158
|
+
For the full Chinese guide, environment-variable reference, write-lock
|
|
159
|
+
measurements and website deployment notes, see `USAGE-MIGRATION.md`,
|
|
160
|
+
`README.md` and `USAGE-NOTES.en.md`.
|
package/bin/dsh-any-agent.mjs
CHANGED
|
@@ -59,7 +59,7 @@ const { createHandlers } = await import('../src/tools.mjs');
|
|
|
59
59
|
const log = createLogger();
|
|
60
60
|
|
|
61
61
|
const hub = new Hub({ log });
|
|
62
|
-
hub.load();
|
|
62
|
+
await hub.load();
|
|
63
63
|
hub.startReaper();
|
|
64
64
|
// 启动时清一次过期哨兵(哨兵内含正文,长期不清理会无声堆积)
|
|
65
65
|
const prunedSentinels = hub.pruneSentinels();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Drive DeepSeek Harness (DSH) as a long-lived coding agent from any MCP client — Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Zed, Gemini CLI, opencode, VS Code. Resume-aware, interruptible, approval-capable, and it only ever touches the one config entry it owns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"USAGE-NOTES.en.md",
|
|
37
37
|
"USAGE-NOTES.md",
|
|
38
38
|
"USAGE-MIGRATION.md",
|
|
39
|
+
"USAGE-MIGRATION.en.md",
|
|
39
40
|
"CHANGELOG.md",
|
|
40
41
|
"LICENSE",
|
|
41
42
|
"DESIGN.md",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"start": "node ./bin/dsh-any-agent.mjs",
|
|
46
47
|
"setup": "node ./bin/setup.mjs",
|
|
47
48
|
"test": "node ./test/run.mjs",
|
|
49
|
+
"test:all": "node ./test/run.mjs --all",
|
|
48
50
|
"test:smoke": "node ./test/smoke.mjs",
|
|
49
51
|
"test:guards": "node ./test/guards.mjs",
|
|
50
52
|
"test:prune": "node ./test/prune.mjs",
|
package/src/hub.mjs
CHANGED
|
@@ -134,14 +134,15 @@ function isForgotten(id) {
|
|
|
134
134
|
* 留着只会让测试/临时会话在 `$DSH_HOME/.dsh-any-agent-forgotten` 里堆积。
|
|
135
135
|
* 会话文件还在的墓碑必须保留 —— 那正是 forget 的效力所在。
|
|
136
136
|
*/
|
|
137
|
-
function pruneStaleTombstones() {
|
|
137
|
+
async function pruneStaleTombstones() {
|
|
138
138
|
let removed = 0;
|
|
139
139
|
try {
|
|
140
140
|
if (!existsSync(FORGOTTEN_DIR)) return 0;
|
|
141
141
|
for (const f of readdirSync(FORGOTTEN_DIR)) {
|
|
142
142
|
if (!f.endsWith('.json')) continue;
|
|
143
143
|
const id = f.slice(0, -5);
|
|
144
|
-
|
|
144
|
+
// 异步读会话头:启动路径不能被大日志的同步解压卡住。
|
|
145
|
+
if (await findSessionHeader(id)) continue;
|
|
145
146
|
try {
|
|
146
147
|
rmSync(join(FORGOTTEN_DIR, f), { force: true });
|
|
147
148
|
removed++;
|
|
@@ -1209,7 +1210,7 @@ export class Hub {
|
|
|
1209
1210
|
/** `dsh_list` 磁盘探测的短缓存(探测要 spawn 一个 DSH 进程,实测约 1 秒)。 */
|
|
1210
1211
|
#probeCache = null;
|
|
1211
1212
|
|
|
1212
|
-
load() {
|
|
1213
|
+
async load() {
|
|
1213
1214
|
// ★ 启动即确认三个关键目录可写。以前"写失败只记 silent 日志"会让服务
|
|
1214
1215
|
// 看起来正常,但注册表/哨兵/锁登记全部丢失,异步等待永远醒不过来。
|
|
1215
1216
|
assertWritableDir(dirname(STATE_FILE), '状态目录');
|
|
@@ -1234,7 +1235,7 @@ export class Hub {
|
|
|
1234
1235
|
}
|
|
1235
1236
|
}
|
|
1236
1237
|
|
|
1237
|
-
const stale = pruneStaleTombstones();
|
|
1238
|
+
const stale = await pruneStaleTombstones();
|
|
1238
1239
|
if (stale) this.log.info(`[hub] 清理 ${stale} 个失效的 forget 墓碑`);
|
|
1239
1240
|
if (!existsSync(STATE_FILE)) return;
|
|
1240
1241
|
try {
|
|
@@ -1373,7 +1374,7 @@ export class Hub {
|
|
|
1373
1374
|
}
|
|
1374
1375
|
}
|
|
1375
1376
|
|
|
1376
|
-
get(id) {
|
|
1377
|
+
async get(id) {
|
|
1377
1378
|
// ★ forget 是终态:墓碑存在时,任何按 id 的查询都必须明确失败,
|
|
1378
1379
|
// 否则 DSH 会话存储里的记录会让它悄悄复活("forget 不持久")。
|
|
1379
1380
|
if (isForgotten(id)) {
|
|
@@ -1392,7 +1393,7 @@ export class Hub {
|
|
|
1392
1393
|
if (!c) {
|
|
1393
1394
|
// 最后一道:注册表里没有,但会话可能就在磁盘上 —— 别的实例建的、注册表被并发写覆盖丢的、
|
|
1394
1395
|
// 或者根本是别的 profile / GUI 建的。**会话存储才是权威,注册表只是缓存。**
|
|
1395
|
-
const header = findSessionHeader(id);
|
|
1396
|
+
const header = await findSessionHeader(id);
|
|
1396
1397
|
if (header?.cwd) {
|
|
1397
1398
|
// ★ 权限档必须取自**会话自己的记录**,不能一律套默认值:
|
|
1398
1399
|
// 一个当初用 read-only 建的会话,若注册表丢了、被我们以默认档接管,
|
package/src/tools.mjs
CHANGED
|
@@ -403,7 +403,7 @@ export function createHandlers(hub, opts = {}) {
|
|
|
403
403
|
if (typeof args.prompt !== 'string' || args.prompt.trim() === '') {
|
|
404
404
|
throw new Error('prompt 不能为空:至少包含一个非空白字符');
|
|
405
405
|
}
|
|
406
|
-
const conv = hub.get(args.conversation_id);
|
|
406
|
+
const conv = await hub.get(args.conversation_id);
|
|
407
407
|
const opts = { reasoning: args.reasoning ?? DEFAULT_REASONING };
|
|
408
408
|
if (args.timeout_ms) opts.timeoutMs = args.timeout_ms;
|
|
409
409
|
|
|
@@ -489,7 +489,7 @@ export function createHandlers(hub, opts = {}) {
|
|
|
489
489
|
},
|
|
490
490
|
|
|
491
491
|
async dsh_read(args) {
|
|
492
|
-
const conv = hub.get(args.conversation_id);
|
|
492
|
+
const conv = await hub.get(args.conversation_id);
|
|
493
493
|
const r = conv.readOut({
|
|
494
494
|
cursor: args.cursor ?? 0,
|
|
495
495
|
includeReasoning: args.include_reasoning === true,
|
|
@@ -515,7 +515,7 @@ export function createHandlers(hub, opts = {}) {
|
|
|
515
515
|
},
|
|
516
516
|
|
|
517
517
|
async dsh_get(args) {
|
|
518
|
-
const conv = hub.get(args.conversation_id);
|
|
518
|
+
const conv = await hub.get(args.conversation_id);
|
|
519
519
|
const snap = conv.snapshot();
|
|
520
520
|
let run = null;
|
|
521
521
|
const meta = [];
|
|
@@ -575,7 +575,7 @@ export function createHandlers(hub, opts = {}) {
|
|
|
575
575
|
if (typeof args.message !== 'string' || args.message.trim() === '') {
|
|
576
576
|
throw new Error('message 不能为空:至少包含一个非空白字符');
|
|
577
577
|
}
|
|
578
|
-
const conv = hub.get(args.conversation_id);
|
|
578
|
+
const conv = await hub.get(args.conversation_id);
|
|
579
579
|
const opts = { mode: args.mode ?? 'interject', reasoning: args.reasoning ?? DEFAULT_REASONING };
|
|
580
580
|
if (args.timeout_ms) opts.timeoutMs = args.timeout_ms;
|
|
581
581
|
const r = await conv.interject(args.message, opts);
|
|
@@ -600,7 +600,7 @@ export function createHandlers(hub, opts = {}) {
|
|
|
600
600
|
},
|
|
601
601
|
|
|
602
602
|
async dsh_interrupt(args) {
|
|
603
|
-
const conv = hub.get(args.conversation_id);
|
|
603
|
+
const conv = await hub.get(args.conversation_id);
|
|
604
604
|
const r = await conv.interrupt();
|
|
605
605
|
return { structured: r, text: r.interrupted ? '已发送中断请求。' : `未中断:${r.reason}` };
|
|
606
606
|
},
|
|
@@ -620,7 +620,7 @@ export function createHandlers(hub, opts = {}) {
|
|
|
620
620
|
`\n会话日志都在磁盘上,之后派活会自动 resume;没有一个可释放时 released=0。`,
|
|
621
621
|
};
|
|
622
622
|
}
|
|
623
|
-
const conv = hub.get(args.conversation_id);
|
|
623
|
+
const conv = await hub.get(args.conversation_id);
|
|
624
624
|
const r = await conv.close({ forget: args.forget === true });
|
|
625
625
|
return {
|
|
626
626
|
structured: { conversation_id: args.conversation_id, ...r },
|
|
@@ -631,19 +631,19 @@ export function createHandlers(hub, opts = {}) {
|
|
|
631
631
|
},
|
|
632
632
|
|
|
633
633
|
async dsh_takeover(args) {
|
|
634
|
-
const conv = hub.get(args.conversation_id);
|
|
634
|
+
const conv = await hub.get(args.conversation_id);
|
|
635
635
|
const r = await conv.takeover({ force: args.force === true });
|
|
636
636
|
return { structured: r, text: `${r.ok ? '✅' : '❌'} ${r.message}` };
|
|
637
637
|
},
|
|
638
638
|
|
|
639
639
|
async dsh_approval_decide(args) {
|
|
640
|
-
const conv = hub.get(args.conversation_id);
|
|
640
|
+
const conv = await hub.get(args.conversation_id);
|
|
641
641
|
const r = conv.decideApproval(args.approval_id, args.decision);
|
|
642
642
|
return { structured: r, text: `审批 ${r.approval_id} 已裁决为 ${r.decision}。` };
|
|
643
643
|
},
|
|
644
644
|
|
|
645
645
|
async dsh_status(args) {
|
|
646
|
-
const conv = hub.get(args.conversation_id);
|
|
646
|
+
const conv = await hub.get(args.conversation_id);
|
|
647
647
|
const snap = conv.snapshot();
|
|
648
648
|
const run = conv.getRun();
|
|
649
649
|
const bits = [`状态=${snap.state}`, `轮数=${snap.turns}`];
|
package/src/workspace.mjs
CHANGED
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
* 或手动执行回填时做一次读-改-写,窗口很小;但仍与 GUI 存在理论上的覆盖可能。
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { copyFileSync, readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, readdirSync, rmSync, realpathSync } from 'node:fs';
|
|
18
|
+
import { copyFileSync, createReadStream, readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, readdirSync, rmSync, realpathSync } from 'node:fs';
|
|
19
19
|
import { randomUUID } from 'node:crypto';
|
|
20
20
|
import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
21
21
|
import { tmpdir } from 'node:os';
|
|
22
|
-
import {
|
|
22
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
23
|
+
import { createZstdDecompress, zstdDecompressSync } from 'node:zlib';
|
|
23
24
|
import { DSH_HOME, REGISTER_WORKSPACE } from './config.mjs';
|
|
24
25
|
|
|
25
26
|
const WORKSPACE_FILE = join(DSH_HOME, 'storages', 'workspace.json');
|
|
@@ -220,7 +221,7 @@ export function registerWorkspace(cwd, sessionId) {
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
/** 读取一个会话头的第一个 zstd
|
|
224
|
+
/** 读取一个会话头的第一个 zstd 帧(就是头行)。仅用于 CLI/测试清理,不用于 MCP 热路径。 */
|
|
224
225
|
function readHeader(file) {
|
|
225
226
|
try {
|
|
226
227
|
const text = zstdDecompressSync(readFileSync(file)).toString('utf8');
|
|
@@ -230,6 +231,62 @@ function readHeader(file) {
|
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
/**
|
|
235
|
+
* 异步读取会话头:流式解压,拿到第一行就销毁流。
|
|
236
|
+
*
|
|
237
|
+
* 为什么需要它:MCP 服务在 `Hub.get()` 里找回磁盘会话时必须读头;
|
|
238
|
+
* `readHeader()` 会把整份日志同步解压,超大日志会阻塞事件循环、停掉
|
|
239
|
+
* 心跳(多实例下可能被误判 stale-mcp)。这里只解到第一个换行,
|
|
240
|
+
* 不把整份日志读进内存,也不阻塞事件循环。
|
|
241
|
+
*
|
|
242
|
+
* 超时/读失败返回 null;调用方按“找不到会话头”处理。
|
|
243
|
+
*/
|
|
244
|
+
export async function readHeaderAsync(file, { timeoutMs = 5000, maxHeaderBytes = 1024 * 1024 } = {}) {
|
|
245
|
+
return new Promise((resolve) => {
|
|
246
|
+
let settled = false;
|
|
247
|
+
let buf = '';
|
|
248
|
+
let timer = null;
|
|
249
|
+
const decoder = new StringDecoder('utf8');
|
|
250
|
+
const rs = createReadStream(file);
|
|
251
|
+
const zs = createZstdDecompress();
|
|
252
|
+
const finish = (value) => {
|
|
253
|
+
if (settled) return;
|
|
254
|
+
settled = true;
|
|
255
|
+
if (timer) clearTimeout(timer);
|
|
256
|
+
try { rs.destroy(); } catch {}
|
|
257
|
+
try { zs.destroy(); } catch {}
|
|
258
|
+
resolve(value);
|
|
259
|
+
};
|
|
260
|
+
const parseLine = (line) => {
|
|
261
|
+
try {
|
|
262
|
+
const value = JSON.parse(String(line).trim());
|
|
263
|
+
finish(value && typeof value === 'object' ? value : null);
|
|
264
|
+
} catch {
|
|
265
|
+
finish(null);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
269
|
+
timer.unref?.();
|
|
270
|
+
rs.on('error', () => finish(null));
|
|
271
|
+
zs.on('error', () => finish(null));
|
|
272
|
+
zs.on('data', (chunk) => {
|
|
273
|
+
// 用 StringDecoder 处理跨 chunk 的多字节 UTF-8(中文/emoji 路径),
|
|
274
|
+
// 避免 chunk.toString() 把字符截成乱码。
|
|
275
|
+
buf += decoder.write(chunk);
|
|
276
|
+
const nl = buf.search(/\r?\n/);
|
|
277
|
+
if (nl >= 0) parseLine(buf.slice(0, nl));
|
|
278
|
+
else if (buf.length > maxHeaderBytes) finish(null);
|
|
279
|
+
});
|
|
280
|
+
zs.on('end', () => {
|
|
281
|
+
buf += decoder.end();
|
|
282
|
+
const line = buf.split(/\r?\n/).find(Boolean);
|
|
283
|
+
if (line) parseLine(line);
|
|
284
|
+
else finish(null);
|
|
285
|
+
});
|
|
286
|
+
rs.pipe(zs);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
233
290
|
/**
|
|
234
291
|
* 扫描磁盘上所有会话,按 header.cwd 归组。数据源是会话头,不是目录名(目录名有损)。
|
|
235
292
|
*
|
|
@@ -272,7 +329,7 @@ export function scanSessions() {
|
|
|
272
329
|
*
|
|
273
330
|
* 会对 id 做形状校验(只允许 UUID / `session-` 前缀那类字符),避免 `..` 之类逃出会话目录。
|
|
274
331
|
*/
|
|
275
|
-
export function findSessionHeader(sessionId) {
|
|
332
|
+
export async function findSessionHeader(sessionId) {
|
|
276
333
|
if (typeof sessionId !== 'string' || !/^[A-Za-z0-9._-]{4,80}$/.test(sessionId)) return null;
|
|
277
334
|
if (!existsSync(SESSIONS_ROOT)) return null;
|
|
278
335
|
let buckets;
|
|
@@ -284,7 +341,7 @@ export function findSessionHeader(sessionId) {
|
|
|
284
341
|
for (const bucket of buckets) {
|
|
285
342
|
const f = join(SESSIONS_ROOT, bucket, sessionId, 'session.v4.jsonl.zstd');
|
|
286
343
|
if (!existsSync(f)) continue;
|
|
287
|
-
const header =
|
|
344
|
+
const header = await readHeaderAsync(f);
|
|
288
345
|
if (header?.id === sessionId) return header;
|
|
289
346
|
}
|
|
290
347
|
return null;
|