dsh-any-agent 0.3.3 → 0.3.5

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 ADDED
@@ -0,0 +1,628 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ Post-0.3.5 hardening (remaining completeness-audit items).
11
+
12
+ ## [0.3.5] — 2026-11-02
13
+
14
+ Completeness-audit follow-up. Data-safety and privilege boundaries first,
15
+ then documentation/CI truthfulness.
16
+
17
+ ### Security
18
+
19
+ - **Unknown permission on an adopted external session now fails closed to
20
+ `read-only`.** It previously fell back to `danger-full-access` when the
21
+ projection cache had no permission record.
22
+ - **Runtime permission read-back.** After `session/new` and every
23
+ `session/resume`, the session's own recorded `permissions.preset` is read
24
+ back; a mismatch (for example a profile `defaultPreset`) fails the call
25
+ instead of silently escalating. `dsh_get` exposes `permission_verified`.
26
+ - **Workspace-write semantics documented honestly.** Out-of-workspace
27
+ requests are auto-allowed by default; pass `on_approval=ask` to adjudicate.
28
+
29
+ ### Fixed
30
+
31
+ - **Temp-path prefix bug could delete real sessions.** `isTempPath` now
32
+ compares path segments, so `tmpdir()+'Suffix'` and `/tmpfoo` are no longer
33
+ treated as the OS temp directory. Automatic cleanup only deletes
34
+ test-created sessions (`tmpdir()/dsh-any-agent-*` and the Desktop test
35
+ folders); `tempOnly` remains an explicit manual flag.
36
+ - **`setup` no longer invokes `claude mcp get`.** That probe made Claude Code
37
+ rewrite `~/.claude.json` (dropping `projects[*].history` and adding keys).
38
+ - **First-start profile bootstrap works in the published package.** The
39
+ undeclared `js-yaml` dependency is replaced by a built-in YAML subset parser
40
+ (`src/yaml-lite.mjs`); generated profiles include the three permission
41
+ presets but never `defaultPreset`. A route without a default provider/model
42
+ fails with an actionable error instead of writing an empty `acp` config.
43
+ - **State/sentinel/lock storage moved to `$DSH_HOME/.dsh-any-agent`.** The
44
+ old in-package `.state` location is migrated once (including metadata
45
+ sidecars). Startup now fails loudly if the directories are unwritable, and
46
+ a sentinel write failure is surfaced as `sentinel_error`.
47
+ - **Corrupt `workspace.json` is no longer overwritten** with an empty
48
+ document; register/prune report the error and leave the file byte-identical.
49
+ The temp filename is unique per writer.
50
+ - **`defaultLaunch` uses `fileURLToPath`**, so install paths containing
51
+ spaces or non-ASCII characters no longer become percent-encoded.
52
+ - **`release(all=true)` now skips busy conversations by default** (pass
53
+ `force=true` to interrupt them); the interject timeout description matches
54
+ the busy-path behavior.
55
+ - Numeric env guards: negative sentinel TTL no longer deletes every sentinel;
56
+ empty permission/reasoning-effort values fall back to defaults.
57
+ - `ACP clientInfo.version` now comes from `package.json`.
58
+
59
+ ### Tests
60
+
61
+ - New offline suites: `bootstrap-profile`, `workspace-safety`,
62
+ `permission-adopt`, `state-safety` (81 checks total). `package-check` is
63
+ part of `node test/run.mjs`; `acceptance` is part of `--all` (it self-skips
64
+ without IDA); `reasoning-check` now asserts rather than only printing.
65
+ - CI runs every offline suite and the website build/tests; stale counts in
66
+ the SCOPE header and READMEs were corrected (815 checks on five client
67
+ fixtures, 851 with nine; CI offline 786/822).
68
+
69
+ ## [0.3.4] — 2026-11-02
70
+
71
+ Docs-only release. No runtime code changed; this publishes the updated
72
+ README (Star History, corrected setup wording) and the website source so the
73
+ npm package page matches the repository. The site is deployed separately at
74
+ <https://dsh-any-agent.xmtlz.dev/>.
75
+
76
+ ### Changed
77
+
78
+ - README / README.zh: add the Star History chart and keep the measured
79
+ setup impact-scope note.
80
+ - Website: the release version is read at runtime from npm (fallback:
81
+ GitHub release/tag) instead of being baked at build time; the CSP allows
82
+ those endpoints; canonical and Open Graph URLs use the public domain.
83
+ - Website wording now states that copying the model route into the ACP
84
+ profile needs a YAML parser (js-yaml), that external MCP servers are
85
+ configured in the DSH profile, and that `setup --yes` writes absolute
86
+ node/entry paths plus env.
87
+
88
+ ## [0.3.3] — 2026-11-02
89
+
90
+ An adversarial audit of the 0.3.2 MCP surface found one safety boundary that
91
+ could be bypassed and several places where the implementation and the docs
92
+ disagreed. This release closes them, with regressions for each.
93
+
94
+ ### Security
95
+
96
+ - **`read-only` is now a hard boundary.** `dsh_start` rejects
97
+ `permission=read-only` combined with `on_approval=auto-allow`/`ask`, and the
98
+ approval layer refuses every permission request from a read-only
99
+ conversation. Previously DSH could ask for a `sandbox_permissions` escalation
100
+ and a permissive policy would let the write through while the session record
101
+ still said `read-only`.
102
+
103
+ ### Fixed
104
+
105
+ - **`dsh_read` no longer loses streamed text.** The cursor is a revision
106
+ number: text appended to an already-read entry is delivered again, so a
107
+ polling caller sees the rest of the message. `limit` is clamped to 1..500.
108
+ - **`dsh_release(forget=true)` is terminal.** It writes a tombstone under
109
+ `$DSH_HOME/.dsh-any-agent-forgotten`; by-id lookups fail with a recovery
110
+ path, lists exclude the session, and stale tombstones are pruned at startup.
111
+ - **A failed `dsh_start` no longer leaves a write-lock marker.** It was
112
+ visible as an orphan holder after an invalid provider/model.
113
+ - **Real approval context.** The permission request's `toolCallId` is
114
+ correlated with the earlier `tool_call` update, so
115
+ `pending_approval_details` carries the command and reason instead of an
116
+ opaque id.
117
+ - **Sentinels redact tool inputs** with the approval credential rules, and
118
+ `thinking_omitted` is always a boolean.
119
+ - **Empty prompts fail synchronously** instead of producing an accepted
120
+ receipt for a turn that immediately errors.
121
+ - **`dsh_start` rejects an unknown provider/model** instead of silently
122
+ falling back to the profile default model.
123
+ - **Pending approvals are cancelled and cleared** when a turn ends or a
124
+ conversation closes.
125
+ - **`queue`-mode interject honors `timeout_ms`** (0 = wait forever).
126
+ - Tests: `read-cursor`, `sentinel-redaction` and `forget-tombstone`
127
+ regressions; `real-configs` checks entry keys rather than a bare substring;
128
+ test teardown removes only the conversation ids it created; acceptance
129
+ skips explicitly when no IDA MCP is configured.
130
+
131
+ ### Changed
132
+
133
+ - `dsh_read` cursor semantics: entries reappear when their text grows; match
134
+ by `seq` and replace the text.
135
+ - Documentation now states that **DSH's own session log contains reasoning**
136
+ (even with `reasoning=hide`); the "never reaches disk" guarantee covers this
137
+ server's logs, sentinels and registry only.
138
+ - README links the official site at <https://dsh-any-agent.xmtlz.dev/> and
139
+ records the measured config impact scope for the nine supported clients.
140
+
141
+ ## [0.3.2] — 2026-10-30
142
+
143
+ Two fixes and one feature, all from driving this server from Claude Code
144
+ rather than from reading the code.
145
+
146
+ ### Added
147
+
148
+ - **`cwd` is optional and defaults to the project the client has open.** The
149
+ client already knows which folder the user is working in, but nothing asked
150
+ it. `initialize` now records the client's `roots` capability, and
151
+ `dsh_start` without `cwd` sends one `roots/list` request and converts the
152
+ returned `file://` URI to a path. "DSH works where the client is looking"
153
+ becomes the default, which also removes the usual way to land on the home
154
+ directory by building the path by hand. A client that does not advertise
155
+ roots, or does not answer in time, gets an actionable error naming an example
156
+ `cwd` — it never guesses a directory. `projectDir()` is exposed on the
157
+ server object for callers that want the raw value.
158
+ - **The home directory is rejected as a working directory, with the reason.**
159
+ DSH's own ACL sandbox requires its temp directory to be *outside* the
160
+ workspace. On Windows `%TEMP%` is `C:/Users/<name>/AppData/Local/Temp`,
161
+ inside the home directory, so `cwd: C:/Users/<name>` made the first shell
162
+ command fail with `Windows ACL: temp root must be outside the workspace` —
163
+ several layers away from the cause and easy to mistake for a broken install.
164
+ The combination is now refused up front, naming both escapes: a narrower
165
+ project directory, or `permission=read-only` / `danger-full-access`.
166
+ - `snapshot()` returns `title_hint` alongside `title`. DSH's own title is a
167
+ truncated prefix of the first message (13–15 characters, e.g.
168
+ `这是一次连通性冒烟测试,请`), which is what the GUI shows. `title` is
169
+ unchanged — it stays the field the GUI reads, so the two agree — and the
170
+ hint is returned separately rather than replacing it.
171
+
172
+ ### Changed
173
+
174
+ - The `title_hint` tool description now states what happens: DSH's generated
175
+ title wins for display, and the hint is returned alongside it, instead of
176
+ the vaguer "fallback display".
177
+
178
+ ### Tests
179
+
180
+ - `test/roots.mjs` — 12 checks over a real stdio handshake: that the server
181
+ sends `roots/list` when the client advertises it, that an answered root
182
+ becomes the working directory, that an unsupported client gets the
183
+ actionable message, and that a nonexistent explicit `cwd` still reports
184
+ cleanly.
185
+ - `test/workspace-titles.mjs` — 15 checks: the home-directory guard's
186
+ case-insensitive and trailing-separator handling (asserted only where the
187
+ filesystem actually has it), that `read-only` is still allowed, and that the
188
+ two title fields do not overwrite each other.
189
+ - 660 offline checks across eleven suites, green on Ubuntu and Windows for
190
+ Node 22 and 24.
191
+
192
+ ## [0.3.1] — 2026-10-30
193
+
194
+ 0.3.0 could overwrite a client config the user had deliberately made
195
+ read-only. Caught by CI on Linux, not on Windows, which is the interesting
196
+ part.
197
+
198
+ ### Fixed
199
+
200
+ - **A read-only config was silently replaced on POSIX.** Writes go through
201
+ write-to-temp then `rename`, and `rename` only requires the *directory* to
202
+ be writable — not the target file. A `chmod 444` `~/.codex/config.toml`
203
+ was therefore replaced as if the bit were not there. Windows `rename`
204
+ returns EPERM for a read-only target, so the same code passed on
205
+ `windows-latest` and failed on `ubuntu-latest`; the bug had been in 0.3.0
206
+ the whole time. Writability is now checked with `access(W_OK)` before
207
+ writing, so both platforms refuse.
208
+ - **`--force` never actually overrode anything.** It was parsed, documented
209
+ and passed to neither the read-only check nor the writer. It now clears the
210
+ read-only bit, writes, and **restores the original mode** — verified, so it
211
+ does not leave a file whose permissions were quietly changed.
212
+
213
+ ### Changed
214
+
215
+ - A file the current identity cannot write to is reported as
216
+ `config file is read-only, refusing to overwrite it (use --force to
217
+ override)` rather than a generic write failure, and no backup or temp file
218
+ is left behind.
219
+
220
+ ### Tests
221
+
222
+ - 633 offline checks across nine suites, all green, on Ubuntu and Windows for
223
+ Node 22 and 24. The read-only section now asserts refusal, an untouched
224
+ file, no stray temp file, and that `--force` writes *and* restores the
225
+ mode. The root skip remains for hosts where permission bits genuinely do
226
+ not apply.
227
+
228
+ ## [0.3.0] — 2026-10-30
229
+
230
+ **Configuration impact is now a bounded, auditable property.** Registering this
231
+ server with any of nine MCP clients changes exactly one entry per client and
232
+ nothing else — including in configs with comments, CRLF endings, a BOM or keys
233
+ that collide only by letter case. `package.json` reports `0.3.0`; both
234
+ `dsh-any-agent --version` and `dsh-any-agent-setup --version` print it.
235
+
236
+ ### Added
237
+
238
+ - **Nine clients, up from five.** `dsh-any-agent-setup` now detects and
239
+ registers with Claude Code (`claude-code`), Claude Desktop
240
+ (`claude-desktop`), Codex (`codex`), Cursor (`cursor`), Windsurf
241
+ (`windsurf`), Zed (`zed`), Gemini CLI (`gemini`), opencode (`opencode`)
242
+ and VS Code (`vscode`) — these ids are what `--only` and the `--json`
243
+ output use. A client that is not installed is still skipped, never failed.
244
+ - **Per-client config shapes, written correctly rather than generically.**
245
+ VS Code keeps MCP servers under `servers` (not `mcpServers`); opencode
246
+ takes one `command` array (`{ type: "local", command: [exe, ...args],
247
+ enabled: true }`) instead of separate `command`/`args`; Zed uses
248
+ `context_servers`; Codex gets the two `[mcp_servers.dsh-any-agent]`
249
+ sections. Claude Code is now edited like every other JSON client — a
250
+ text-level `mcpServers` edit of `~/.claude.json` with a `type: "stdio"`
251
+ entry — rather than registered through `claude mcp add`.
252
+ - **A JSON scanner that does not build a value object.** `~/.claude.json`
253
+ really does hold a `projects` map with keys differing only in letter case
254
+ (`.../Rust` next to `.../rust`), which is enough to break a naive
255
+ `JSON.parse`; rewriting the file wholesale would destroy the user's project
256
+ list. The scanner walks the text into a tree of byte ranges (tolerating
257
+ comments and trailing commas, because Zed and VS Code configs allow them),
258
+ so the case-colliding keys are never materialised — only the range of our
259
+ own entry is touched.
260
+ - **JSON writes are text-level now: exactly one entry is rewritten, in place.**
261
+ Earlier versions re-serialised the whole file, so values survived but
262
+ formatting did not. Setup now locates its own entry in the existing text and
263
+ replaces only that span, so indentation (2-space, 4-space, tabs), key order,
264
+ CRLF line endings, a UTF-8 BOM and a missing trailing newline all come back
265
+ byte-identical apart from the entry itself.
266
+ - `--json` — **stdout is one parseable object and nothing else; every
267
+ human-readable line moves to stderr.** Shape: `{ ok, mode, clients: [{ id,
268
+ label, path, status, summary, error? }], changed, skipped, failed }` with
269
+ `mode` being `plan`, `apply` or `remove`. That split is the contract — a
270
+ caller can pipe stdout straight into a JSON parser. Exit code still reports
271
+ failure (`0` / `1`).
272
+ - `--home <dir>` — point setup at a sandbox (or any) home directory instead of
273
+ the real `HOME`/`USERPROFILE`/`APPDATA`, so registering, re-registering and
274
+ removing can be exercised without touching a live config.
275
+ - `--force` — exactly two effects: create a missing directory or file (the
276
+ default does that too; the flag makes it dependable under automation and
277
+ when the client itself is not detected), and overwrite a same-named entry
278
+ whose shape is not what this program expects. It never removes another
279
+ entry and never skips validation.
280
+
281
+ ### Changed
282
+
283
+ - **The blast radius is now stated as a property, not a promise.** For every
284
+ client, setup writes one entry named `dsh-any-agent` and `--remove` deletes
285
+ exactly that entry. Nothing else in the file is re-serialised, re-indented or
286
+ reordered.
287
+ - **Codex (`config.toml`) is edited line-wise.** Other `[tables]`, their
288
+ comments, top-level keys (`model`, `approval_policy`, `sandbox_mode`) and
289
+ CRLF endings are carried over unchanged; `--remove` puts the file back the
290
+ way it was.
291
+ - **`dsh-any-agent-restore`** — a companion CLI for the backups. Setups and
292
+ removals both leave `<file>.bak-dsh-<stamp>` copies of the pre-operation
293
+ bytes, which is only useful if you can find the right one and put it back:
294
+ `--list` shows every copy newest-first, `--latest` / `--stamp` / `--file`
295
+ restore one, and `--dry-run` previews. A restore backs up the current file
296
+ as `.bak-dsh-pre-restore-<stamp>` first, so a mistaken restore can itself be
297
+ undone. Covered by 9 checks in `test/config-safety.mjs`, including
298
+ recovering a deliberately corrupted config.
299
+ - **Backups can no longer overwrite each other.** Two runs inside the same
300
+ millisecond produced the same filename, so the second copy silently replaced
301
+ the first — and the replaced one could be the only state worth going back to.
302
+ A colliding name now gets a `-2`, `-3` suffix instead.
303
+ - **The backup path is reported.** `--json` gained a `backup` field per client
304
+ and the human output prints `backup: <path>` under each written entry;
305
+ before this, a backup was created but you had to guess where it went.
306
+ - **A backup before every write**: `<file>.bak-dsh-<timestamp>`, one per run
307
+ per file, holding the exact pre-write bytes. No backup is left behind by a
308
+ preview.
309
+ - **Measured on seven real configs, not fixtures.** Registering added exactly
310
+ one `dsh-any-agent` entry per client and preserved every pre-existing line —
311
+ comments, CRLF endings, a 238-line Codex config, the 182693-byte
312
+ `~/.claude.json` — and a subsequent `--remove` restored all seven files
313
+ byte for byte.
314
+ - **The round trip was run on the real home, with backups.** `--yes` then
315
+ `--remove --yes` were executed against the developer machine's actual
316
+ configs after backing all seven up. Removal is verified as the strict inverse
317
+ of registration: taking our single entry out of the backup with the same
318
+ editing engine must reproduce the file then on disk, byte for byte. All seven
319
+ match. Other MCP servers were unchanged field by field (Cursor still has
320
+ stitch / obsidian-mcp-server / codegraph / rea), Zed still has all 32
321
+ top-level settings keys, and `~/.claude.json` still parses with its 67
322
+ top-level keys, 60 project entries and both case-only duplicate keys.
323
+ `codex mcp list` reads the file normally afterwards, so nothing needs hand
324
+ repair.
325
+ - **One deliberate change to an existing entry**: because the launch command is
326
+ derived from where setup runs, registering from this checkout replaced the
327
+ bundled `node/node.exe` in the pre-existing Cursor entry with the system
328
+ `node.exe`. That is the one entry we own; no other server was touched.
329
+ - **Preview mode is a hard guarantee, not a report.** Without `--yes` nothing
330
+ is written and no subprocess that writes is started. The Claude Code path
331
+ used to shell out to `claude mcp add`, which writes immediately — and, worse,
332
+ rewrites `~/.claude.json` in its own formatting with no backup we could
333
+ restore. It is now a text-level edit like every other client, and a preview
334
+ is a pure read: no CLI is invoked at all.
335
+ - **`claude mcp get` is used only as a read-only confirmation.** When the
336
+ `claude` CLI happens to be installed, a completed write is followed by one
337
+ `claude mcp get` probe and the summary says
338
+ `(confirmed by claude mcp get)`; if the client has not picked the entry up
339
+ yet the summary says so instead. The probe never writes.
340
+ - README (both languages): a per-client table of config file paths and exactly
341
+ what install / update / remove / undo touch, replacing the older "JSON is
342
+ re-serialised" caveat.
343
+ - `test/sandbox-home.mjs` + `test/sandbox-guard.mjs` (18 checks) — a test
344
+ sandbox can no longer point at real user data. The guard refuses the real
345
+ `home`, its subdirectories, case variants, the filesystem root, and any path
346
+ outside `os.tmpdir()`; `setup` and `removal-scope` now create and destroy
347
+ their HOME only through this module and fail if they go back to calling
348
+ `mkdtempSync`/`rmSync` directly.
349
+ - `test/removal-scope.mjs` — 24 checks proving unregistering touches only
350
+ its own entry. Given configs holding unrelated MCP servers, top-level Codex
351
+ settings (`model`, `approval_policy`, `sandbox_mode`) and Zed settings
352
+ (`theme`, `vim_mode`, a sibling `context_servers` child), it registers and
353
+ then removes, and asserts every other value is byte-identical afterwards.
354
+ Also covers removing something that was never there: no error, no write.
355
+ - README (both languages): a table for install / update / remove / undo, the
356
+ measured before-after of `--remove` per client, and an explicit note that
357
+ `--remove` does **not** uninstall the package.
358
+ - `dsh-any-agent-setup --help` now states that only one entry per client is
359
+ added and exactly that entry is removed, that other MCP servers are never
360
+ read or rewritten, and that the recorded path is absolute — moving or
361
+ deleting the installation leaves clients reporting `CONNECTION_CLOSED`
362
+ until setup is re-run from the new location.
363
+
364
+ ### Fixed
365
+
366
+ - **Setup used to reformat a config it was only supposed to add one key to.**
367
+ The previous writer parsed each JSON client config and re-emitted it with
368
+ `JSON.stringify(..., null, 2)`, which silently normalised indentation, key
369
+ order, line endings and the trailing newline of every unrelated entry — the
370
+ values survived, the formatting did not. Writes now replace only the bytes
371
+ belonging to setup's own entry, and the invariant is asserted in
372
+ `test/config-safety.mjs` rather than described in prose.
373
+ - **A throwaway verification script deleted parts of the operator's real
374
+ home directory.** Not a bug in `setup` — a bug in the harness used to
375
+ verify it. The script kept its sandbox path in `$home`, which is a
376
+ read-only automatic variable in PowerShell; with
377
+ `$ErrorActionPreference = 'Continue'` the failed assignment was silent, so
378
+ `$env:USERPROFILE` still pointed at the intended sandbox name while the
379
+ cleanup line `Remove-Item $home -Recurse -Force` resolved to the real
380
+ profile. Three runs, each killed by a 120 s timeout mid-delete, removed part
381
+ of `~/.codex`, `~/.cursor` and `~/.claude` (the package's own config
382
+ entries survived; user data around them did not).
383
+
384
+ Evidence it was a sandbox-escape rather than a write: stray
385
+ `~/.cursor/mcp.json.bak-dsh-*` files contained the fixture the script had
386
+ written (`{"mcpServers":{"keep":...}}`), i.e. it was writing to the real
387
+ profile. `sandbox-guard` now makes that impossible to do silently.
388
+
389
+ ### Tests
390
+
391
+ - **632 offline checks, all green on 0.3.0** — `guards` 31, `prune` 11,
392
+ `approval-details` 24, `sandbox-guard` 18, `setup` 41, `removal-scope`
393
+ 24, `config-safety` 403, `real-configs` 66, `package-check` 14. Every
394
+ suite exited 0; none needs a model, a DSH install or a provider. **619 of
395
+ those assertions are in the client-registration suites** (`setup`,
396
+ `removal-scope`, `config-safety`, `real-configs`), which is where this
397
+ release's work actually is. The model-backed suites (462 checks) were last
398
+ measured in full for 0.1.2 and are re-run with `node test/run.mjs --all`.
399
+ - `test/real-configs.mjs` — **66 checks against real configs rather than
400
+ fixtures.** It copies this machine's actual client files into a temp
401
+ directory — the 182693-byte `~/.claude.json` with its hundreds of
402
+ `projects` entries and case-colliding keys, a 238-line Codex
403
+ `config.toml`, Zed's full `settings.json` — registers, then removes, and
404
+ requires every file back byte for byte. Where those files do not exist (CI),
405
+ the suite prints `skipped` explicitly instead of passing vacuously.
406
+ - `test/config-safety.mjs` — **403 checks**, the fixture matrix for the
407
+ property this release is about, per client and against a throwaway home: a
408
+ preview leaves every file byte-identical and creates no backup;
409
+ register-then-remove restores every other entry byte-for-byte and leaves no
410
+ top-level key behind; 4-space, 2-space and tab indentation, single-line
411
+ JSON, a missing trailing newline, CRLF endings and a UTF-8 BOM all survive a
412
+ write; malformed JSON and a read-only file are refused without damaging the
413
+ original, while a missing directory is created safely; the second `--yes`
414
+ is a no-op with no new backup; removing an entry that was never written
415
+ writes nothing and exits `0`; and every `.bak-dsh-<stamp>` appears once
416
+ and equals the pre-write bytes.
417
+ - `test/removal-scope.mjs` — 24 checks; `test/setup.mjs` — 41 checks,
418
+ including spawning the command written into a config and requiring a valid
419
+ `initialize` answer.
420
+ - **Verified against a real home, read-only.** Running `setup --json` (plan
421
+ mode) over an operator's actual profile left all seven existing client
422
+ configs — `~/.claude.json` (182693 B), `~/.cursor/mcp.json` (1396 B),
423
+ `~/.codex/config.toml` (6647 B), `~/.gemini/settings.json` (465 B),
424
+ `~/.config/opencode/opencode.json` (441 B), Zed's `settings.json`
425
+ (5647 B) and Claude Desktop's `claude_desktop_config.json` (247 B) — with
426
+ identical size, mtime and SHA-256, created no `.bak-dsh-*` file and created
427
+ no new config. All nine clients were reported. With `--home <tmp>` every
428
+ reported path moved inside that directory, `APPDATA` included.
429
+
430
+ ## [0.1.2] — 2026-10-29
431
+
432
+ One-line fix, but it decides whether `dsh-any-agent-setup` can register
433
+ Claude Code on Windows at all.
434
+
435
+ ### Fixed
436
+
437
+ - **Registration with Claude Code failed on any path containing a space.**
438
+ `process.execPath` is `C:\Program Files\nodejs\node.exe` on a default
439
+ Windows install, and setup passed it to the `claude` CLI through a shell
440
+ without quoting. cmd split the argument:
441
+
442
+ Command: C:\Program
443
+ Args: Files\nodejs\node.exe D:\...\bin\dsh-any-agent.mjs
444
+
445
+ Nothing reported an error. `claude mcp add` exited 0, and the JSON written
446
+ to the config was correct — the only symptom was `CONNECTION_CLOSED` when
447
+ Claude Code tried to start the server. Cursor, Zed and Codex were unaffected
448
+ because they receive a JSON/TOML value rather than a shell argument.
449
+ Arguments containing whitespace are now quoted.
450
+
451
+ ### Added
452
+
453
+ - `test/setup.mjs` grew a ninth group that closes the gap which let the above
454
+ through: it takes the entry setup just wrote, spawns it exactly as a client
455
+ would, and requires a valid `initialize` response. Every earlier check
456
+ asserted file contents; none asked whether the command in that file runs.
457
+ 35 → 41 checks.
458
+
459
+ ### Documented
460
+
461
+ - **What setup does and does not touch**, with before/after counts measured on
462
+ a machine holding real MCP servers: Cursor 4→5 entries, Codex 2→3 sections,
463
+ Claude Code 5→6 servers, Zed 33→33 keys, every pre-existing entry unchanged.
464
+ Also states the one thing that does change: JSON configs are re-serialised,
465
+ so values are preserved but whitespace is not.
466
+ - **A release checklist**, including the two traps hit while shipping 0.1.0
467
+ and 0.1.1 — npm returning 202 while the registry still 404s for a minute,
468
+ and shields.io serving five-minute-stale badges (`license: package not
469
+ found` is its generic failure text, not a missing licence) — plus the
470
+ `npm deprecate` step for a broken published version and the fact that it
471
+ requires interactive OTP and therefore cannot run in CI.
472
+
473
+ ## [0.1.1] — 2026-10-29
474
+
475
+ A correctness release. Two of the fixes matter if you are on Node 20 or were
476
+ relying on `dsh-any-agent-setup` to preview before writing.
477
+
478
+ ### Fixed
479
+
480
+ - **`engines` was wrong: this needs Node ≥ 22.15, not ≥ 20.** `src/workspace.mjs`
481
+ imports `zstdDecompressSync` from `node:zlib` to read DSH session headers;
482
+ that API does not exist before 22.15, and a named import of a missing export
483
+ is a load-time error. On Node 20 the server exited with code 1 before it
484
+ could answer a single request, and `package.json` had been telling people
485
+ Node 20 was fine. Both `engines` and the README now say 22.15.
486
+ - **`dsh-any-agent-setup` wrote files during a dry run.** Every writer path
487
+ called the backup-and-write helper unconditionally and only *reported*
488
+ "would write" afterwards, so a preview run with no `--yes` modified the
489
+ client configs it found and left `.bak-dsh-*` files behind. The Claude Code
490
+ path shelled out to `claude mcp add` during the preview, which writes
491
+ immediately. All three paths now return a preview and stop before touching
492
+ disk, and a dry run performs no subprocess that can mutate state.
493
+ - **Claude Code registration broke on paths containing spaces.** On Windows
494
+ `process.execPath` is `C:\Program Files\nodejs\node.exe`, and the setup
495
+ command handed it to the `claude` CLI through a shell without quoting, so it
496
+ was split into `C:\Program` plus a stray `Files\nodejs\node.exe` argument.
497
+ The written config looked correct and Claude Code reported
498
+ `CONNECTION_CLOSED`. Every argument is now quoted when it contains a space.
499
+
500
+ ### Added
501
+
502
+ - `dsh-any-agent --version` / `--help`, answered before the hub starts, so
503
+ asking for a version spawns no reaper and touches no registry. Same for
504
+ `dsh-any-agent-setup`.
505
+ - `test/setup.mjs` — 41 checks over the client-config writer, run against a
506
+ throwaway HOME: dry run is byte-identical, existing entries survive, writes
507
+ are idempotent, `--remove` takes out only its own entry, a corrupt config is
508
+ refused rather than rewritten, absent clients are skipped, the Codex TOML
509
+ block round-trips, and — added after the quoting bug — the command written
510
+ into the config is spawned for real and must answer `initialize`.
511
+ - `test/package-check.mjs` — catches what only a tarball reveals: a missing
512
+ `bin` field, a path without `./`, a missing shebang, `private:true`, test or
513
+ state paths in `files`, and an `engines` floor that contradicts what the
514
+ source actually imports.
515
+ - GitHub Actions: guards, prune, approval-details and setup across Node 22/24
516
+ on Linux and Windows, plus a packaging job that packs, installs into a
517
+ scratch project and asserts the tarball carries no state, tests or
518
+ credentials.
519
+
520
+ ### Changed
521
+
522
+ - `SERVER_VERSION` now comes from `package.json` instead of a hard-coded copy
523
+ in `src/mcp.mjs`, so `npm version` cannot leave it stale.
524
+
525
+ ## [0.1.0] — 2026-10-29
526
+
527
+ First public release, published to npm as [`dsh-any-agent`](https://www.npmjs.com/package/dsh-any-agent).
528
+
529
+ ### Added
530
+
531
+ - **MCP server over stdio** that drives DeepSeek Harness (DSH) as a long-lived
532
+ background coding agent. Speaks only the four standard MCP methods
533
+ (`initialize`, `ping`, `tools/list`, `tools/call`), so any client that can
534
+ launch a stdio server works — no host-specific extensions.
535
+ - **11 `dsh_*` tools**: `dsh_start`, `dsh_send`, `dsh_list`, `dsh_read`,
536
+ `dsh_get`, `dsh_interject`, `dsh_interrupt`, `dsh_takeover`,
537
+ `dsh_release`, `dsh_approval_decide`, `dsh_status`.
538
+ - **Resume across processes.** Conversation identity is DSH's `sessionId` on
539
+ disk, not the process. A crash, an idle reap, or restarting this whole server
540
+ costs nothing: the next call transparently resumes the session, memory intact.
541
+ - **Asynchronous dispatch with sentinel files.** `dsh_send(wait=false)` returns
542
+ a `run_id` and a `sentinel_file` immediately; the turn writes that file
543
+ atomically when it finishes, so a background task in the host wakes the caller
544
+ instead of polling. Sentinels are namespaced per conversation, latched, and
545
+ carry the answer but never the reasoning.
546
+ - **Mid-turn steering.** `dsh_interject` redirects a running turn (cancel, then
547
+ immediately start a new one with history preserved); `dsh_interject(mode=queue)`
548
+ defers a remark until the current turn ends. `dsh_interrupt` stops a turn
549
+ without damaging the conversation.
550
+ - **Write-lock handling.** DSH conversations carry a cross-process write lock
551
+ that never expires while the holder lives and has no steal API. `dsh_status`
552
+ reports `lock_holder` (`self` / `stale-mcp` / `live-mcp` / `none`), and
553
+ `dsh_takeover` preempts only the cases that are provably safe — it never kills
554
+ a GUI, because GUI conversations live inside the single `dsh web` process.
555
+ - **Approval bridging** to the calling client, with credential-shaped fields
556
+ redacted from both the structured context and the displayed title.
557
+ - **First-start profile bootstrap.** On first use the server scaffolds
558
+ `~/.dsh/profiles/dsh-mcp/` from the shipped `acp` template and copies model
559
+ routes from an existing profile. Only route *declarations* are copied
560
+ (`apiKeyEnv`, `baseURL`, `api`, `models`, `reasoningEfforts`); `apiKey`
561
+ — a field the adapter never reads — and `headers` are stripped. No credential
562
+ is ever requested, stored or written by this project.
563
+ - **Reasoning hidden by default.** Only statistics (characters, duration, chunk
564
+ count) reach the caller; reasoning never reaches disk, with one deliberate
565
+ opt-out (`DSH_MCP_SENTINEL_INCLUDE_REASONING=1`).
566
+ - **Silent by default.** Not one byte on stderr, so nothing pollutes the host's
567
+ logs; stdout carries the protocol and nothing else.
568
+ - **Workspace maintenance CLI** (`dsh-any-agent-workspaces`): `--list`,
569
+ `--backfill`, `--prune-temp`, `--prune-empty`, `--purge-test-sessions`.
570
+ - Documentation: `README.md` / `README.zh.md`, `USAGE-NOTES.md` /
571
+ `USAGE-NOTES.en.md` (gotchas), `USAGE-MIGRATION.md` (new-environment setup),
572
+ `DESIGN.md` (protocol-level design notes).
573
+
574
+ ### Verified
575
+
576
+ - **528 checks green** across 18 suites (492 in-suite + 36 acceptance), including
577
+ resume-with-memory across processes, sentinel atomicity under concurrency,
578
+ four lock-holder classifications, permission-tier enforcement read back from
579
+ the session's own record, and real model calls through a provider.
580
+ - **Zero runtime dependencies** — Node built-ins only
581
+ (`child_process`, `crypto`, `fs`, `module`, `os`, `path`, `url`, `zlib`).
582
+ - Installation from the published tarball: `npm install -g dsh-any-agent`
583
+ creates the `dsh-any-agent` / `.cmd` / `.ps1` shims, the server answers
584
+ `initialize` with `serverInfo: dsh-any-agent@0.1.0`, exposes all 11 tools, and
585
+ returns a real model reply with zero bytes on stderr.
586
+ - Documentation claims were re-measured before release rather than trusted.
587
+ Three figures did not survive: `dsh_send(wait=false)` was documented as
588
+ returning "in ~1 ms" (measured 81 ms), interruption as converging in
589
+ "~18 ms" (no test produced it; four samples of the `waited_ms` field gave
590
+ 91/102/105/119 ms), and a conversation as costing "~120 MB"
591
+ (measured 295–476 MB, and self-contradictory in the original sentence).
592
+ The README's Tests section now states the rule: every number is either
593
+ printed by a suite or measured with a command the reader can re-run, and
594
+ measured values carry their method.
595
+
596
+ ### Known limitations
597
+
598
+ - No token-level streaming into model context (an MCP limitation).
599
+ - True mid-turn steering is impossible — ACP rejects concurrent prompts, so
600
+ interjection is implemented as cancel-then-restart.
601
+ - Image prompts are unsupported (ACP advertises `promptCapabilities.image: false`).
602
+ - Restarting the MCP server takes its child processes with it: in-flight turns
603
+ are interrupted and no sentinel lands. The write lock *is* released, so a
604
+ restart always gets it back.
605
+ - A conversation that dies before its first successful turn may never have
606
+ materialized on disk; resume then fails with a clear error.
607
+ - No renaming — DSH's title subsystem has no external rename API.
608
+ - `session/list` returns only `{sessionId, cwd}` and excludes already-open
609
+ sessions; titles are filled in from DSH's projection cache.
610
+
611
+ ### Notes
612
+
613
+ - The DSH profile is intentionally still named `dsh-mcp`, as are the
614
+ `DSH_MCP_*` environment variables and the `# dsh-mcp:managed` marker line.
615
+ These are persisted identifiers: renaming them would disconnect existing
616
+ profiles, sessions and client configs. The product name changed; the on-disk
617
+ contract did not. See the README FAQ.
618
+
619
+ [Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.5...HEAD
620
+ [0.3.5]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.4...v0.3.5
621
+ [0.3.4]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.3...v0.3.4
622
+ [0.3.3]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.2...v0.3.3
623
+ [0.3.2]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.1...v0.3.2
624
+ [0.3.1]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.0...v0.3.1
625
+ [0.3.0]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.1.2...v0.3.0
626
+ [0.1.2]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.1.1...v0.1.2
627
+ [0.1.1]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.1.0...v0.1.1
628
+ [0.1.0]: https://github.com/xmtlzzz/dsh-any-agent/releases/tag/v0.1.0
package/DESIGN.md CHANGED
@@ -1,8 +1,8 @@
1
- # dsh-any-agent 功能设计(v0.2 草案)
1
+ # dsh-any-agent 功能设计(v0.2 草案,历史文档)
2
2
 
3
3
  > 目标:做一个 MCP Server,让 **Codex / Claude Code** 能调用 **DeepSeek Harness (DSH)** 当"外包工程师"——写代码、搭脚手架、跑任务,并且 DSH 自己还能挂它自己的 MCP。
4
4
  >
5
- > 本文只谈**功能与接口**,不写实现代码。所有技术判断都基于对本机 DSH 0.1.7-rc.2 的实测(见 §2 证据)。
5
+ > 本文只谈**功能与接口**,不写实现代码;其中 M2/M3 工具(`dsh_rename` 等)是分期计划,不是当前 0.3.5 的缺失功能。技术判断最初基于 DSH 0.1.7-rc.2,当前验证基线是 **DSH 0.2.1-rc.5**(见 §2 证据)。
6
6
 
7
7
  ---
8
8