dsh-any-agent 0.3.4 → 0.3.6

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