dsh-any-agent 0.1.2 → 0.3.0
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/README.md +144 -77
- package/README.zh.md +148 -34
- package/bin/restore.mjs +150 -0
- package/bin/setup.mjs +204 -235
- package/package.json +4 -3
- package/src/clients.mjs +830 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ An MCP server that drives DSH as a **long-lived agent runtime** instead of wrapp
|
|
|
13
13
|
|
|
14
14
|
**Works with any MCP client.** Nothing here is Codex- or Claude-specific: the server speaks plain MCP over stdio and uses only the four standard methods (`initialize`, `ping`, `tools/list`, `tools/call`). Verified against a bare-bones client that negotiates an older protocol version and never sends `notifications/initialized`.
|
|
15
15
|
|
|
16
|
-
🧪
|
|
16
|
+
🧪 632 offline checks green on 0.3.0 (619 of them in the client-registration suites) · 🔌 MCP over stdio · 📦 Zero dependencies · 📜 MIT
|
|
17
17
|
|
|
18
18
|
> 📌 **Read the [Usage Notes](USAGE-NOTES.en.md) first** — the practical gotchas that will actually bite you: the ACP model-config trap, external MCP tools bypassing DSH's sandbox, the workspace/GUI registry cache, cost control, and a troubleshooting table.
|
|
19
19
|
>
|
|
@@ -120,23 +120,51 @@ dsh-any-agent-setup # detect the clients you have; writes nothing
|
|
|
120
120
|
dsh-any-agent-setup --yes # apply
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
`setup` finds the MCP clients on your machine — **Claude Code, Codex, Cursor, Windsurf, Zed** — and writes the entry into each one it finds, skipping the rest. It is safe by construction:
|
|
123
|
+
`setup` finds the MCP clients on your machine — **Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Zed, Gemini CLI, opencode, VS Code** — and writes the entry into each one it finds, skipping the rest. It is safe by construction:
|
|
124
124
|
|
|
125
|
-
- **dry-run by default**; `--yes` is required to write anything
|
|
126
|
-
- every file is backed up to `<file>.bak-dsh-<timestamp>` first
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
- **idempotent** — re-running only changes what differs
|
|
125
|
+
- **dry-run by default**; `--yes` is required to write anything, and a preview starts **no subprocess at all** — it only reads the files it would touch
|
|
126
|
+
- every file is backed up to `<file>.bak-dsh-<timestamp>` first, holding the exact **pre-write bytes**
|
|
127
|
+
- the config is parsed before and after writing, so a broken config never lands; malformed JSON and a read-only file are refused rather than forced, while a missing directory is created safely
|
|
128
|
+
- **text-level writes**: only the bytes of setup's own entry are replaced, so indentation (2-space, 4-space, tabs), key order, CRLF endings, a UTF-8 BOM and a missing trailing newline all survive untouched
|
|
129
|
+
- **idempotent** — re-running only changes what differs, and a second `--yes` writes nothing and adds no `.bak`
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
132
|
dsh-any-agent-setup --yes --only cursor,zed # just these
|
|
133
133
|
dsh-any-agent-setup --yes --permission read-only
|
|
134
|
-
dsh-any-agent-setup --remove --yes # take it back out
|
|
134
|
+
dsh-any-agent-setup --remove --yes # take it back out; only its own entry is removed
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
**Three new flags**
|
|
138
|
+
|
|
139
|
+
| Flag | What it does |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `--json` | **stdout carries one parseable JSON object and nothing else** — every human-readable line goes to stderr. Shape: `{ ok, mode, clients: [{ id, label, path, status, summary, error? }], changed, skipped, failed }` with `mode` being `plan`, `apply` or `remove`. This is a contract, not a convenience: a script can pipe stdout straight into a JSON parser |
|
|
142
|
+
| `--home <dir>` | use this directory instead of the real `HOME` / `USERPROFILE` / `APPDATA`, so a register-then-remove cycle can be rehearsed without touching a live config |
|
|
143
|
+
| `--force` | only two things: (a) create a directory or file that does not exist yet (it does that by default too — the flag makes it reliable for automation, including when the client itself was not detected); (b) overwrite a same-named entry that exists but is not the shape this program expects. It never deletes somebody else's entry and never skips validation |
|
|
144
|
+
|
|
145
|
+
**Which file each client actually gets**
|
|
146
|
+
|
|
147
|
+
A `.bak-dsh-<timestamp>` backup lands beside every file before it is written. Paths are generic examples; "its own entry" means the single entry/section named `dsh-any-agent`.
|
|
148
|
+
|
|
149
|
+
| Client (`--only` id) | Config file | Container key | Install / update | `--remove` | Undo |
|
|
150
|
+
|---|---|---|---|---|---|
|
|
151
|
+
| Claude Code (`claude-code`) | `~/.claude.json` | `mcpServers` | its own entry only, with `type: "stdio"` | deletes its own entry only | copy the backup back |
|
|
152
|
+
| Claude Desktop (`claude-desktop`) | Windows `%APPDATA%\Claude\claude_desktop_config.json`; macOS `~/Library/Application Support/Claude/claude_desktop_config.json`; Linux `~/.config/Claude/claude_desktop_config.json` | `mcpServers` | adds or rewrites its own entry only | deletes its own entry only | copy the backup back |
|
|
153
|
+
| Codex (`codex`) | `~/.codex/config.toml` | `[mcp_servers.dsh-any-agent]` | appends its two TOML sections line-wise | deletes those two sections and nothing else | copy the backup back |
|
|
154
|
+
| Cursor (`cursor`) | `~/.cursor/mcp.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
|
|
155
|
+
| Windsurf (`windsurf`) | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
|
|
156
|
+
| Zed (`zed`) | Windows `%APPDATA%\Zed\settings.json`; elsewhere `~/.config/zed/settings.json` | `context_servers` | its own entry only | its own entry only | copy the backup back |
|
|
157
|
+
| Gemini CLI (`gemini`) | `~/.gemini/settings.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
|
|
158
|
+
| opencode (`opencode`) | `~/.config/opencode/opencode.json` | `mcp` | its own entry only, written in opencode's shape: `{ type: "local", command: [exe, ...args], enabled: true }` — one array, not separate `command`/`args` | its own entry only | copy the backup back |
|
|
159
|
+
| VS Code (`vscode`) | Windows `%APPDATA%\Code\User\mcp.json`; elsewhere the same `mcp.json` under your VS Code user directory | `servers` — not `mcpServers` | its own entry only | its own entry only | copy the backup back |
|
|
160
|
+
|
|
161
|
+
For the exact path on your machine, read the `path` field for each client in `--json` output — that is the file this run would write, on your OS, which beats any table in a README.
|
|
162
|
+
|
|
163
|
+
Three shapes worth spelling out, because getting them wrong is silent. **VS Code** stores MCP servers under `servers`, not `mcpServers`; writing the usual key there produces a config the editor ignores. **opencode** takes a single `command` array (`{ type: "local", command: [exe, ...args], enabled: true }`) rather than separate `command`/`args` fields. **Claude Code** is the reason this release does not use a JSON library at all: `~/.claude.json` really does hold a `projects` map with keys that differ only by letter case (`.../Rust` next to `.../rust`), so a naive `JSON.parse` fails on it and any whole-file rewrite would destroy the user's project list. A byte-range scanner that never builds a value object handles it, and an earlier design that shelled out to `claude mcp add` was dropped for the same reason — that command rewrites the file in its own formatting with no backup you could restore.
|
|
164
|
+
|
|
137
165
|
**What `setup` will and will not touch**
|
|
138
166
|
|
|
139
|
-
It is additive. Verified against a real machine holding existing MCP servers (Cursor with four, Codex with two, Claude Code with five, Zed with 33 settings keys): every pre-existing entry survived
|
|
167
|
+
It is additive, and it only ever touches its own entry. Verified against a real machine holding existing MCP servers (Cursor with four, Codex with two, Claude Code with five, Zed with 33 settings keys): every pre-existing entry survived **byte for byte**, key order included, and the only diff was one new entry per client.
|
|
140
168
|
|
|
141
169
|
```
|
|
142
170
|
Cursor 4 -> 5 entries existing entries byte-identical
|
|
@@ -145,7 +173,96 @@ Claude Code 5 -> 6 servers all four originals still listed
|
|
|
145
173
|
Zed 33 -> 33 keys only context_servers gained a child
|
|
146
174
|
```
|
|
147
175
|
|
|
148
|
-
|
|
176
|
+
What changed since 0.1.x: JSON configs used to be **re-serialised whole** (values preserved, layout lost), which is why the docs said whitespace was not guaranteed. Writes are text-level now — apart from its own entry, the file comes back byte-identical, whether it was indented with 4 spaces, 2 spaces or tabs, laid out as one long line, missing its trailing newline, using CRLF, or carrying a UTF-8 BOM.
|
|
177
|
+
|
|
178
|
+
Codex's `config.toml` gets the same treatment line-wise: your other `[tables]` and their comments, the top-level keys (`model`, `approval_policy`, `sandbox_mode`, …) and CRLF endings come back exactly as they were — `--remove` deletes only the two `[mcp_servers.dsh-any-agent]` sections and leaves the rest intact.
|
|
179
|
+
|
|
180
|
+
The coverage for each of those claims is in [`test/`](test/) (`test/config-safety.mjs` asserts them one by one, 403 checks).
|
|
181
|
+
|
|
182
|
+
**Checked against a real home, read-only.** Running `setup --json` (plan mode) against an actual developer profile reported all nine clients and left every existing config untouched — identical size, mtime and SHA-256, no `.bak-dsh-*` created, no new file created:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
~/.claude.json 182693 B unchanged
|
|
186
|
+
~/.cursor/mcp.json 1396 B unchanged
|
|
187
|
+
~/.codex/config.toml 6647 B unchanged
|
|
188
|
+
~/.gemini/settings.json 465 B unchanged
|
|
189
|
+
~/.config/opencode/opencode.json 441 B unchanged
|
|
190
|
+
%APPDATA%/Zed/settings.json 5647 B unchanged
|
|
191
|
+
%APPDATA%/Claude/claude_desktop_config.json 247 B unchanged
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The same run with `--home <tmp>` put **every** reported path inside that directory, `APPDATA` included — which is what makes the register/remove cycle safe to rehearse.
|
|
195
|
+
|
|
196
|
+
**A full register -> remove round trip on the real home, not just a preview.** All seven configs were backed up, `--yes` was run for real, then `--remove --yes`, and everything was verified byte by byte:
|
|
197
|
+
|
|
198
|
+
| Client | Before | After register | After remove | Other MCP servers |
|
|
199
|
+
|---|---|---|---|---|
|
|
200
|
+
| ~/.claude.json | 182693 B | 182693 B (already current) | byte-identical to baseline | untouched; 67 top-level keys, 60 projects entries and both case-only duplicate keys still there |
|
|
201
|
+
| ~/.cursor/mcp.json | 1396 B | 1389 B | 1009 B | stitch / obsidian-mcp-server / codegraph / rea identical field by field |
|
|
202
|
+
| ~/.codex/config.toml | 6647 B | 6647 B (already current) | 6239 B | every other table, comment and top-level key kept |
|
|
203
|
+
| %APPDATA%/Zed/settings.json | 5647 B | 5647 B (already current) | 5240 B | all 32 top-level settings keys kept |
|
|
204
|
+
| ~/.gemini/settings.json | 465 B | 845 B | 465 B | restored byte for byte |
|
|
205
|
+
| ~/.config/opencode/opencode.json | 441 B | 869 B | 441 B | `$schema` and the pencil entry kept |
|
|
206
|
+
| %APPDATA%/Claude/claude_desktop_config.json | 247 B | 627 B | 247 B | restored byte for byte |
|
|
207
|
+
|
|
208
|
+
Removal is not accepted because it *looks* right: the check is that **taking our one entry out of the backup with the same editing engine reproduces the file now on disk, byte for byte**. All seven files pass. That proves removal is the strict inverse of registration and cannot quietly take anything else with it.
|
|
209
|
+
|
|
210
|
+
Afterwards every JSON config still parses and `codex mcp list` still reads the file; no client needs hand repair.
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
**Backups and rollback**
|
|
214
|
+
|
|
215
|
+
Both registering and removing snapshot the file first: `<config>.bak-dsh-<timestamp>`, holding the exact pre-operation bytes. Both directions, because the state you want back differs — the pre-register backup undoes a bad install, the pre-remove backup reinstalls after a bad removal.
|
|
216
|
+
|
|
217
|
+
After every real write, each client entry in `--json` carries a `backup` field, and the human-readable output prints `backup: <path>` under each written entry.
|
|
218
|
+
|
|
219
|
+
The copies live next to each client's config, so picking the right one by hand is error-prone. There is a companion command:
|
|
220
|
+
|
|
221
|
+
| What | Command |
|
|
222
|
+
|---|---|
|
|
223
|
+
| List backups, newest first | `dsh-any-agent-restore --list` |
|
|
224
|
+
| Restore the newest backup per file | `dsh-any-agent-restore --latest` |
|
|
225
|
+
| Restore one batch | `dsh-any-agent-restore --stamp 2026-10-30T01-35` |
|
|
226
|
+
| Restore one specific copy | `dsh-any-agent-restore --file <backup path>` |
|
|
227
|
+
| See what would happen | add `--dry-run` |
|
|
228
|
+
|
|
229
|
+
A restore is itself destructive, so it copies the **current** file aside as `.bak-dsh-pre-restore-<timestamp>` before overwriting — a restore can be undone.
|
|
230
|
+
|
|
231
|
+
**Install / update / remove**
|
|
232
|
+
|
|
233
|
+
| Task | Command |
|
|
234
|
+
|---|---|
|
|
235
|
+
| Preview what would change | `dsh-any-agent-setup` |
|
|
236
|
+
| Register with every client found | `dsh-any-agent-setup --yes` |
|
|
237
|
+
| Register with just some | `dsh-any-agent-setup --yes --only cursor,zed` |
|
|
238
|
+
| Change the permission tier in place | `dsh-any-agent-setup --yes --permission read-only` |
|
|
239
|
+
| Update after upgrading the package | `dsh-any-agent-setup --yes` (idempotent; only differing entries are rewritten) |
|
|
240
|
+
| Unregister from every client | `dsh-any-agent-setup --remove --yes` |
|
|
241
|
+
| Unregister from just some | `dsh-any-agent-setup --remove --yes --only codex` |
|
|
242
|
+
| Undo one bad write | copy `.bak-dsh-<stamp>` back over the config |
|
|
243
|
+
|
|
244
|
+
**What `--remove` actually does — measured**
|
|
245
|
+
|
|
246
|
+
Unregistering touches exactly one key per client and leaves everything else alone. Measured on a real machine, before → after:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
Cursor { keep, dsh-any-agent } -> { keep }
|
|
250
|
+
the hand-written entry survived untouched
|
|
251
|
+
Claude Code 6 servers -> 5 servers
|
|
252
|
+
all five originals still listed
|
|
253
|
+
Zed 33 top-level keys -> 33 keys
|
|
254
|
+
only context_servers lost its child, now empty
|
|
255
|
+
Codex our two TOML blocks -> removed
|
|
256
|
+
model, approval_policy, sandbox_mode, node_repl and rea all intact
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Nothing outside those entries is read, rewritten or deleted. Your credentials, other MCP servers and unrelated settings are not touched. The package itself is *not* uninstalled by `--remove` — that is `npm uninstall -g dsh-any-agent`, and the two are independent: you can keep the package installed with no clients registered, or unregister without removing it.
|
|
260
|
+
|
|
261
|
+
To verify by hand afterwards: `claude mcp list`, `codex mcp list`, and inspect `~/.cursor/mcp.json` / the `context_servers` key of Zed's settings.
|
|
262
|
+
|
|
263
|
+
**The recorded path is absolute**
|
|
264
|
+
|
|
265
|
+
`setup` writes the absolute path of the installation it ran from. **If you move or delete that directory, every client will fail with `CONNECTION_CLOSED`** until you either re-run `setup` from the new location or `--remove` the stale entries. This bites when you install into a temporary folder to try things out, then clean it up: the configs still point into the void.
|
|
149
266
|
|
|
150
267
|
**From a local checkout**
|
|
151
268
|
|
|
@@ -384,7 +501,12 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
384
501
|
| `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
|
|
385
502
|
| `prune` | 11 | **sentinel retention**: prunes over-age and crash-leftover files, keeps fresh ones, removes empty shells, `TTL=0` disables pruning, and never touches the conversation registry beside it (no tokens, runs every time) |
|
|
386
503
|
| `guards` | 31 | **environment-variable guards**: odd numbers always land on the safe side — `IDLE_TTL_MS` values like `-1` or garbage mean "never reap" (never give the write lock away), a too-small reap interval falls back to the default (no busy loop), and the **lost-holder threshold is floored at 30 s** (so another instance cannot "legitimately" preempt and kill our turn) (no tokens, runs every time) |
|
|
504
|
+
| `sandbox-guard` | 18 | **the test sandbox itself**: a "throwaway HOME" that resolves to the real user profile, to the filesystem root, or to anywhere outside the OS temp dir is refused with an error instead of being written to and deleted. Every other suite builds its HOME through `test/sandbox-home.mjs` and can only destroy what it created there (no tokens, runs every time) |
|
|
387
505
|
| `approval-details` | 24 | **approval review context**: the pending-approval snapshot carries the command, reason, cwd and original approval ID — not just a bare ID — so a caller can actually adjudicate; credential-shaped fields and common inline literals are redacted in both the structured context and the displayed title. Verified end-to-end over real stdio MCP |
|
|
506
|
+
| `setup` | 41 | **the registration CLI**: preview writes nothing, `--remove` takes out only its own entry, an absent client is skipped rather than failed, the Codex TOML block round-trips, and the command recorded in a config is actually spawned and must answer `initialize` (no tokens) |
|
|
507
|
+
| `removal-scope` | 24 | **unregistering touches only its own entry**: configs holding unrelated MCP servers, top-level Codex settings and sibling Zed settings are registered and then removed, asserting every other value is byte-identical; removing something never written is a no-op with no write (no tokens) |
|
|
508
|
+
| `config-safety` | 403 | **the property the 0.3 line is about**, per client: preview leaves files byte-identical with no `.bak`; register-then-remove restores every other entry byte-for-byte, keeps key order and drops no top-level key; 4-space / 2-space / tab indentation, single-line JSON, missing trailing newline, CRLF and a UTF-8 BOM all survive a write; malformed JSON and read-only files are refused without damage; the second `--yes` is a no-op with no new backup; every `.bak-dsh-<stamp>` appears once and equals the pre-write bytes (no tokens) |
|
|
509
|
+
| `real-configs` | 66 | **round-trip on the real thing**: copies this machine's actual client configs (an 182693-byte `~/.claude.json` holding hundreds of `projects` entries and case-colliding keys, a 238-line Codex TOML, Zed's full `settings.json`) into a temp dir, registers, then removes, and requires every file back **byte for byte**; prints `skipped` explicitly on CI where those files do not exist, so a missing fixture is never green (no tokens) |
|
|
388
510
|
| `timeout` | 38 | **timeout semantics**: `wait=false` is unaffected by `timeout_ms`; a `wait=true` expiry merely downgrades to background (turn not cancelled, result not lost, `busy` never lies); `timeout_ms<=0` waits forever; a failed resume invalidates the process instead of wedging the conversation; an empty prompt yields a clear error |
|
|
389
511
|
| `lock` | 79 | **write lock and preemption**: four holder classifications, `writeMarker` never overwriting a live holder, malformed/missing registration edge cases; **end-to-end** with two real MCP instances fighting over one conversation → clear error → refusal → `force` takeover; **a crash releases the lock automatically**, **a wedged holder is preempted without `force`**; plus the "one turn, one sentinel" invariant (inline turns and idle interjects included) |
|
|
390
512
|
| `cycle` | 36 | **the async dispatch lifecycle**: dispatch → collect the sentinel → idle reap → dispatch again, three rounds with no lock error; re-dispatch right on the reap boundary (widening the race); an immediate re-dispatch after `dsh_release`; and an assertion that reaping leaves no unattributable lock (this suite caught the reaper collecting a freshly spawned process as if it were idle) |
|
|
@@ -396,9 +518,19 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
396
518
|
| `permission` | 11 | ★ **whether the permission tiers actually take effect** (a safety property): it ignores our own return values (the very thing that used to lie) and reads **the session's own record** (`permissions.preset` / `sandboxMode` in the projection cache), checking all three tiers and that their recorded values differ. **This suite caught a silent privilege escalation**: a `defaultPreset` in the profile overrides `DSH_PERMISSION_MODE` at session creation |
|
|
397
519
|
| `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) |
|
|
398
520
|
|
|
399
|
-
**
|
|
521
|
+
**The offline half is 632 checks, all green on 0.3.0** — `guards` 31, `prune` 11, `approval-details` 24, `sandbox-guard` 18, `setup` 41, `removal-scope` 24, `config-safety` 403, `real-configs` 66, `package-check` 14. Every suite exited 0; none needs a model, a DSH install or a provider.
|
|
522
|
+
|
|
523
|
+
That is **619 assertions in the client-registration suites alone** (`setup`, `removal-scope`, `config-safety`, `real-configs`), which is where the 0.3 work actually is.
|
|
524
|
+
|
|
525
|
+
The model-backed suites (`smoke` 29, `boundary` 42, `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) total 462 and were last measured in full for 0.1.2 — they need a working provider, so re-run them yourself before trusting a release: `node test/run.mjs --all`.
|
|
526
|
+
|
|
527
|
+
> Counts are the numbers each suite prints itself. `smoke`/`prune`/`guards`/`sandbox-guard`/`approval-details`/`boundary`/`setup`/`removal-scope`/`config-safety`/`package-check` run without LLM calls; the rest need a working provider.
|
|
528
|
+
|
|
529
|
+
### 🧯 Why `sandbox-guard` exists
|
|
400
530
|
|
|
401
|
-
|
|
531
|
+
On 2026-10-29 a throwaway verification script stored its sandbox path in a **PowerShell** variable named `$home`. `$home` is a read-only automatic variable, so the assignment failed silently (the script ran with `$ErrorActionPreference = 'Continue'`), `$env:USERPROFILE` was pointed at the sandbox name anyway, and the cleanup line — `Remove-Item $home -Recurse -Force` — resolved to the **real user profile**. The run timed out at 120 s mid-delete, taking part of `~/.codex`, `~/.cursor` and `~/.claude` with it.
|
|
532
|
+
|
|
533
|
+
Nothing in the test suites was ever green-but-wrong about the *writer*; what was missing was any assertion about the *sandbox*. `sandbox-guard` is that assertion: it refuses the real home, its subdirectories, case variants, the filesystem root, and any path outside `os.tmpdir()`. `test/sandbox-home.mjs` is now the only place a sandbox is created or destroyed, and `setup`/`removal-scope` no longer call `mkdtempSync` or `rmSync` themselves — the suite fails if they go back to doing so.
|
|
402
534
|
|
|
403
535
|
### 📏 Numbers in this README must be reproducible
|
|
404
536
|
|
|
@@ -463,68 +595,3 @@ DSH conversations carry a **cross-process write lock**: **it never expires while
|
|
|
463
595
|
Because renaming it would break things that already exist on disk.
|
|
464
596
|
|
|
465
597
|
When you first start this server it creates a DSH profile under `~/.dsh/profiles/dsh-mcp/`. That directory name is referenced by three other persisted things:
|
|
466
|
-
|
|
467
|
-
| Persisted identifier | Where | Breaks if renamed |
|
|
468
|
-
|---|---|---|
|
|
469
|
-
| the profile directory | `~/.dsh/profiles/dsh-mcp/` | existing sessions can no longer resolve their provider (`no adapter registered for provider "…"`) |
|
|
470
|
-
| `DSH_MCP_*` environment variables | your client config | every deployment would need editing at once |
|
|
471
|
-
| the `# dsh-mcp:managed` marker line | first line of the generated `cordis.patch.yml` | the bootstrap would stop recognising the file as its own and never sync it again |
|
|
472
|
-
|
|
473
|
-
So the **product** is `dsh-any-agent`; the **persisted identifiers** keep the old name on purpose. This is a deliberate compatibility decision, not an oversight. If you are setting this up from scratch and want a different profile name, set `DSH_MCP_PROFILE` — the server will use whatever you give it.
|
|
474
|
-
|
|
475
|
-
## 📦 Releasing
|
|
476
|
-
|
|
477
|
-
Checklist for cutting a version. The ordering matters; two of these steps exist because skipping them shipped a broken release.
|
|
478
|
-
|
|
479
|
-
```bash
|
|
480
|
-
# 1. Bump the version. SERVER_VERSION reads package.json, so nothing else
|
|
481
|
-
# needs editing -- that duplication used to drift.
|
|
482
|
-
npm version patch --no-git-tag-version
|
|
483
|
-
# then update CHANGELOG.md by hand: move [Unreleased] into a new section
|
|
484
|
-
|
|
485
|
-
# 2. Prove it offline (same suites CI runs, no model, no DSH)
|
|
486
|
-
node test/guards.mjs && node test/prune.mjs
|
|
487
|
-
node test/approval-details.mjs && node test/setup.mjs
|
|
488
|
-
node test/package-check.mjs
|
|
489
|
-
|
|
490
|
-
# 3. Confirm the tarball itself: packs, installs, starts
|
|
491
|
-
npm pack --dry-run
|
|
492
|
-
# then, in a scratch dir: npm install <tgz> && node_modules/.bin/dsh-any-agent --version
|
|
493
|
-
|
|
494
|
-
# 4. Tag and push (CI must be green before publishing)
|
|
495
|
-
git add -A && git commit -m "chore: release x.y.z"
|
|
496
|
-
git tag vx.y.z && git push origin main --tags
|
|
497
|
-
|
|
498
|
-
# 5. Publish
|
|
499
|
-
npm publish
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
**Do not trust a 404 after publishing.** npm processes new versions asynchronously (`PUT 202`, not `200`). Querying the registry a few seconds later still returns 404 and looks like failure. Confirm success from the local npm debug log first:
|
|
503
|
-
|
|
504
|
-
```
|
|
505
|
-
grep 'http fetch PUT 202' "$LOCALAPPDATA/npm-cache/_logs/<latest>.log"
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
then wait a minute or two and check `npm view dsh-any-agent dist-tags`.
|
|
509
|
-
|
|
510
|
-
**If a published version is broken, deprecate it — do not leave it installable.** `0.1.0` declared `node >=20` while actually requiring 22.15; anyone pinning it got a server that could not start:
|
|
511
|
-
|
|
512
|
-
```bash
|
|
513
|
-
npm deprecate "dsh-any-agent@0.1.0" \
|
|
514
|
-
"Node 20 cannot start this version. Upgrade to 0.1.1+ (requires Node >=22.15)."
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
`deprecate` prints a warning on install without unpublishing, which is the right tool: the tarball stays for anyone already depending on it. Note this needs an interactive OTP — granular access tokens with bypass-2FA are not allowed to perform it (npm restricted that in July 2026), so run it yourself rather than from CI.
|
|
518
|
-
|
|
519
|
-
**Badges lag.** shields.io caches for five minutes (`max-age=300`), so a fresh publish shows the old version, the old `node` floor, and sometimes `license: package not found` — that last string is shields' generic failure text for a license it could not read, not a missing license. Wait and hard-refresh before assuming the metadata is wrong.
|
|
520
|
-
|
|
521
|
-
## 💬 Support
|
|
522
|
-
|
|
523
|
-
- **Bug reports, feature requests, setup questions** — [open an issue](https://github.com/xmtlzzz/dsh-any-agent/issues); all of it is welcome there
|
|
524
|
-
- **Before reporting a failure** — check the [troubleshooting table](USAGE-MIGRATION.md) and the [Usage Notes](USAGE-NOTES.en.md); most setup failures are one of a handful of known causes
|
|
525
|
-
|
|
526
|
-
This is a one-maintainer project, so a clear reproduction (the exact call, the error text, your DSH version) gets a much faster answer than a description.
|
|
527
|
-
|
|
528
|
-
## 📜 License
|
|
529
|
-
|
|
530
|
-
MIT — see [LICENSE](LICENSE).
|
package/README.zh.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
**任何 MCP 客户端都能用。** 这里没有任何 Codex 或 Claude 专属的东西:服务端只讲标准 MCP over stdio,只用四个标准方法(`initialize`、`ping`、`tools/list`、`tools/call`)。已用一个"原始"客户端验证过——它协商更旧的协议版本、且从不发 `notifications/initialized`。
|
|
15
15
|
|
|
16
|
-
🧪
|
|
16
|
+
🧪 0.3.0 离线 632 项检查全绿(其中 619 项属于客户端注册器套件) · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
|
|
17
17
|
|
|
18
18
|
> 📌 **建议先读 [使用注意事项](USAGE-NOTES.md)** —— 会真正踩到的坑:ACP 模型配置陷阱、**外部 MCP 工具的副作用不受 DSH 沙箱约束**、工作区与 GUI 的注册表缓存、成本控制、以及一张故障速查表。
|
|
19
19
|
>
|
|
@@ -135,23 +135,51 @@ dsh-any-agent-setup # 检测你装了哪些客户端,不写任何
|
|
|
135
135
|
dsh-any-agent-setup --yes # 真正写入
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
`setup` 会找出你机器上的 MCP 客户端 —— **Claude Code、Codex、Cursor、Windsurf、Zed** —— 逐个写进去,没装的跳过。它的安全性是结构性保证的:
|
|
138
|
+
`setup` 会找出你机器上的 MCP 客户端 —— **Claude Code、Claude Desktop、Codex、Cursor、Windsurf、Zed、Gemini CLI、opencode、VS Code** —— 逐个写进去,没装的跳过。它的安全性是结构性保证的:
|
|
139
139
|
|
|
140
|
-
- **默认 dry-run**,必须加 `--yes`
|
|
141
|
-
- 每个文件写前备份成 `<file>.bak-dsh
|
|
142
|
-
- JSON
|
|
143
|
-
-
|
|
144
|
-
-
|
|
140
|
+
- **默认 dry-run**,必须加 `--yes` 才写;预览模式下**不启动任何子进程**,只读取它将要触及的那些文件
|
|
141
|
+
- 每个文件写前备份成 `<file>.bak-dsh-<时间戳>`,内容是**写入前的原始字节**
|
|
142
|
+
- 写入前后各解析一次,坏配置绝不落盘;坏 JSON 与只读文件一律拒绝而不是硬写,目录不存在则安全创建
|
|
143
|
+
- **文本级写入**:只替换自己那个条目的字节,缩进(2 空格 / 4 空格 / Tab)、键顺序、CRLF 行尾、UTF-8 BOM、有无末尾换行都原样保留
|
|
144
|
+
- **幂等**,重复运行只改变有差异的部分;第二次 `--yes` 不写盘、不新增 `.bak`
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
147
|
dsh-any-agent-setup --yes --only cursor,zed # 只装这几个
|
|
148
148
|
dsh-any-agent-setup --yes --permission read-only
|
|
149
|
-
dsh-any-agent-setup --remove --yes #
|
|
149
|
+
dsh-any-agent-setup --remove --yes # 全部移除,只摘自己那条
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
**新增的三个开关**
|
|
153
|
+
|
|
154
|
+
| 开关 | 作用 |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `--json` | **stdout 只有一个可解析的 JSON 对象,人类可读日志全部走 stderr**。结构:`{ ok, mode, clients: [{ id, label, path, status, summary, error? }], changed, skipped, failed }`,其中 `mode` 取 `plan` / `apply` / `remove`。这是**契约**,不是便利功能:脚本可以把 stdout 直接喂给 JSON 解析器 |
|
|
157
|
+
| `--home <dir>` | 用指定的 home 目录代替真实 `HOME` / `USERPROFILE` / `APPDATA`,注册—注销可以整套演练而不碰真实配置 |
|
|
158
|
+
| `--force` | 只做两件事:(a) 目录或文件不存在时创建它们(默认也会创建,这个开关让它在自动化里、以及检测不到客户端时也照做);(b) 目标文件里已有同名条目、但形状不是本程序期望的样子时,允许覆盖它。它**绝不删别人的条目**,也**绝不跳过校验** |
|
|
159
|
+
|
|
160
|
+
**每个客户端到底会碰哪个文件**
|
|
161
|
+
|
|
162
|
+
写入前一律先落 `.bak-dsh-<时间戳>` 备份。表中路径均为通用示例,「自己那条」指名为 `dsh-any-agent` 的那一个条目 / 那一段。
|
|
163
|
+
|
|
164
|
+
| 客户端(`--only` 取值) | 配置文件 | 容器键 | 安装 / 更新 | 卸载(`--remove`) | 撤销 |
|
|
165
|
+
|---|---|---|---|---|---|
|
|
166
|
+
| Claude Code(`claude-code`) | `~/.claude.json` | `mcpServers` | 只增 / 改自己那条,且带 `type: "stdio"` | 删掉自己那条 | 把备份复制回去 |
|
|
167
|
+
| Claude Desktop(`claude-desktop`) | Windows `%APPDATA%\Claude\claude_desktop_config.json`;macOS `~/Library/Application Support/Claude/claude_desktop_config.json`;Linux `~/.config/Claude/claude_desktop_config.json` | `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
|
|
168
|
+
| Codex(`codex`) | `~/.codex/config.toml` | `[mcp_servers.dsh-any-agent]` | 按行追加自己那两段 TOML | 只删这两段 | 把备份复制回去 |
|
|
169
|
+
| Cursor(`cursor`) | `~/.cursor/mcp.json` | `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
|
|
170
|
+
| Windsurf(`windsurf`) | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
|
|
171
|
+
| Zed(`zed`) | Windows `%APPDATA%\Zed\settings.json`;其他平台 `~/.config/zed/settings.json` | `context_servers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
|
|
172
|
+
| Gemini CLI(`gemini`) | `~/.gemini/settings.json` | `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
|
|
173
|
+
| opencode(`opencode`) | `~/.config/opencode/opencode.json` | `mcp` | 只增 / 改自己那条,且按 opencode 的形状写:`{ type: "local", command: [exe, ...args], enabled: true }` —— 一个数组,不是分开的 `command`/`args` | 删掉自己那条 | 把备份复制回去 |
|
|
174
|
+
| VS Code(`vscode`) | Windows `%APPDATA%\Code\User\mcp.json`;其他平台是 VS Code 用户目录下的同名 `mcp.json` | `servers` —— **不是** `mcpServers` | 只增 / 改自己那条 | 删掉自己那条 | 把备份复制回去 |
|
|
175
|
+
|
|
176
|
+
具体路径以 `--json` 输出里每个客户端的 `path` 字段为准 —— 那是本次运行在你这个系统上真正要写的文件,比任何文档都准。
|
|
177
|
+
|
|
178
|
+
有三种形状值得单独点出来,写错了是**静默失效**。**VS Code** 的 MCP 配置放在 `servers` 下,不是 `mcpServers`;按常见写法写进去,编辑器会直接忽略。**opencode** 用的是**一个** `command` 数组(`{ type: "local", command: [exe, ...args], enabled: true }`),不是分开的 `command`/`args`。**Claude Code** 则是本次发布**完全不用 JSON 库**的原因:`~/.claude.json` 的 `projects` 映射里确实存在**只差大小写**的重复键(`.../Rust` 和 `.../rust` 并存),朴素的 `JSON.parse` 在那里会失败,而任何整份重写都会毁掉用户的项目列表。所以改用按字符区间扫描、**从不构造值对象**的扫描器;早先那版「调 `claude mcp add`」也出于同样理由被放弃 —— 那条命令会用它自己的格式重写整个文件,且不产生我们可以回滚的备份。
|
|
179
|
+
|
|
152
180
|
**setup 会动什么、不会动什么**
|
|
153
181
|
|
|
154
|
-
|
|
182
|
+
它只做新增,而且只动自己那一条。在真正装着别的 MCP 服务器的机器上验证过(Cursor 4 个、Codex 2 个段、Claude Code 5 个、Zed 33 个配置键):所有既有条目**逐字节不变**,键顺序不变,唯一的差异是每个客户端多了一个条目。
|
|
155
183
|
|
|
156
184
|
```
|
|
157
185
|
Cursor 4 -> 5 个条目 既有条目逐字节相同
|
|
@@ -160,7 +188,100 @@ Claude Code 5 -> 6 个服务 原有四个都还在列表里
|
|
|
160
188
|
Zed 33 -> 33 个键 只有 context_servers 多了个子项
|
|
161
189
|
```
|
|
162
190
|
|
|
163
|
-
|
|
191
|
+
和 0.1.x 的区别:过去 JSON 配置是**整份重新序列化**的(值保留、排版丢失),所以「白空格不保证原样」。现在写入是文本级的 —— 除了自己那一条,文件其余部分逐字节相同:4 空格缩进、2 空格缩进、Tab、单行 JSON、没有末尾换行、CRLF 行尾、带 UTF-8 BOM,一律原样。
|
|
192
|
+
|
|
193
|
+
Codex 的 `config.toml` 同理,是按行处理的:其它 `[表]` 及其注释、顶层键(`model`、`approval_policy`、`sandbox_mode` 等)、CRLF 行尾都原样保留 —— `--remove` 只删 `[mcp_servers.dsh-any-agent]` 那两段,其余一律不动。
|
|
194
|
+
|
|
195
|
+
这些断言的详细覆盖见 [`test/`](test/)(`test/config-safety.mjs` 共 403 项,逐条断言上面每一条)。
|
|
196
|
+
|
|
197
|
+
**在真实 home 上做过只读验证。** 用 `setup --json`(纯计划模式)跑一遍开发机上的真实配置:9 个客户端全部报出,而现存的每份配置都**原封未动** —— size、mtime、SHA-256 全同,没有新增任何 `.bak-dsh-*`,也没有新建任何文件:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
~/.claude.json 182693 B 未变
|
|
201
|
+
~/.cursor/mcp.json 1396 B 未变
|
|
202
|
+
~/.codex/config.toml 6647 B 未变
|
|
203
|
+
~/.gemini/settings.json 465 B 未变
|
|
204
|
+
~/.config/opencode/opencode.json 441 B 未变
|
|
205
|
+
%APPDATA%/Zed/settings.json 5647 B 未变
|
|
206
|
+
%APPDATA%/Claude/claude_desktop_config.json 247 B 未变
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
同一条命令加 `--home <临时目录>` 时,报出的**每一个** path 都落进了那个目录(`APPDATA` 也跟着走)—— 注册 / 注销整套演练之所以安全,靠的就是这条。
|
|
210
|
+
|
|
211
|
+
**在真实 home 上做过完整的注册 → 撤销往返(不只是预览)。** 备份全部 7 份配置后,实际执行 `--yes` 再执行 `--remove --yes`,然后逐字节核对:
|
|
212
|
+
|
|
213
|
+
| 客户端 | 注册前 | 注册后 | 撤销后 | 其它 MCP 服务器 |
|
|
214
|
+
|---|---|---|---|---|
|
|
215
|
+
| ~/.claude.json | 182693 B | 182693 B(原本已是最新) | 逐字节回到基线 | 未动;67 个顶层键、60 个 projects 条目、两个只差大小写的重复键都还在 |
|
|
216
|
+
| ~/.cursor/mcp.json | 1396 B | 1389 B | 1009 B | stitch / obsidian-mcp-server / codegraph / rea 逐字段相同 |
|
|
217
|
+
| ~/.codex/config.toml | 6647 B | 6647 B(原本已是最新) | 6239 B | 其它 `[表]`、注释、顶层键全部保留 |
|
|
218
|
+
| %APPDATA%/Zed/settings.json | 5647 B | 5647 B(原本已是最新) | 5240 B | 32 个顶层设置键全部保留 |
|
|
219
|
+
| ~/.gemini/settings.json | 465 B | 845 B | 465 B | 逐字节还原 |
|
|
220
|
+
| ~/.config/opencode/opencode.json | 441 B | 869 B | 441 B | `$schema` 与 pencil 条目保留 |
|
|
221
|
+
| %APPDATA%/Claude/claude_desktop_config.json | 247 B | 627 B | 247 B | 逐字节还原 |
|
|
222
|
+
|
|
223
|
+
撤销的验收方式不是「看起来对」,而是**从备份原文里用同一套编辑引擎摘掉 `dsh-any-agent` 这一条,结果必须与磁盘上的当前文件逐字节相同** —— 7 个文件全部通过。这条证明撤销是注册的严格逆运算,不会顺手带走别的东西。
|
|
224
|
+
|
|
225
|
+
撤销后所有 JSON 仍可解析、`codex mcp list` 仍能正常读取配置,客户端不需要人工修复。
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
**备份与回滚**
|
|
229
|
+
|
|
230
|
+
注册和删除**都会**在动文件之前先存一份:`<配置文件>.bak-dsh-<时间戳>`,内容就是操作前的原始字节。两个方向都存,因为要退回的状态不同——注册前那份用来「装错了退回原样」,删除前那份用来「删错了装回去」。
|
|
231
|
+
|
|
232
|
+
每次实际写入后,`--json` 的每个客户端条目里都带 `backup` 字段,直接告诉你备份在哪;人类可读的输出也会在每条下面打印 `backup: <路径>`。
|
|
233
|
+
|
|
234
|
+
备份散落在各客户端目录里,手动翻文件容易挑错,所以附带一个还原命令:
|
|
235
|
+
|
|
236
|
+
| 要做的事 | 命令 |
|
|
237
|
+
|---|---|
|
|
238
|
+
| 看有哪些备份(新的在前) | `dsh-any-agent-restore --list` |
|
|
239
|
+
| 退回每个文件最新的那份 | `dsh-any-agent-restore --latest` |
|
|
240
|
+
| 退回某一批次 | `dsh-any-agent-restore --stamp 2026-10-30T01-35` |
|
|
241
|
+
| 退回指定的一份 | `dsh-any-agent-restore --file <备份路径>` |
|
|
242
|
+
| 先看会做什么 | 任意命令加 `--dry-run` |
|
|
243
|
+
|
|
244
|
+
还原本身也是破坏性操作,所以覆盖之前会把**当前**状态另存为 `.bak-dsh-pre-restore-<时间戳>`——退错了还能再退回来。
|
|
245
|
+
|
|
246
|
+
**安装 / 更新 / 删除**
|
|
247
|
+
|
|
248
|
+
| 要做的事 | 命令 |
|
|
249
|
+
|---|---|
|
|
250
|
+
| 预览会改什么(不写) | `dsh-any-agent-setup` |
|
|
251
|
+
| 注册到所有检测到的客户端 | `dsh-any-agent-setup --yes` |
|
|
252
|
+
| 只注册某几个 | `dsh-any-agent-setup --yes --only cursor,zed` |
|
|
253
|
+
| 就地改权限档 | `dsh-any-agent-setup --yes --permission read-only` |
|
|
254
|
+
| 升级包之后更新配置 | `dsh-any-agent-setup --yes`(幂等,只重写有差异的条目) |
|
|
255
|
+
| 从所有客户端移除 | `dsh-any-agent-setup --remove --yes` |
|
|
256
|
+
| 只从某几个移除 | `dsh-any-agent-setup --remove --yes --only codex` |
|
|
257
|
+
| 撤销某次误写 | 把 `.bak-dsh-<时间戳>` 复制回原文件 |
|
|
258
|
+
|
|
259
|
+
**`--remove` 到底动了什么 —— 实测**
|
|
260
|
+
|
|
261
|
+
注销只删掉每个客户端里的**那一个键**,其余一概不碰。在真机上测得的前后对比:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
Cursor { keep, dsh-any-agent } -> { keep }
|
|
265
|
+
手工写的那条原样保留
|
|
266
|
+
Claude Code 6 个服务 -> 5 个服务
|
|
267
|
+
原有五个都还在列表里
|
|
268
|
+
Zed 33 个顶层键 -> 33 个键
|
|
269
|
+
只有 context_servers 少了子项,现在是空对象
|
|
270
|
+
Codex 我们的两个 TOML 段 -> 已移除
|
|
271
|
+
model、approval_policy、sandbox_mode、node_repl、rea 全部完好
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
这些条目之外的东西它一概不读、不改、不删。你的凭据、其他 MCP 服务器、无关设置都不受影响。
|
|
275
|
+
|
|
276
|
+
注意:**`--remove` 不会卸载包本身** —— 那是 `npm uninstall -g dsh-any-agent`,两件事互相独立:你可以保留包但不注册任何客户端,也可以只注销而不卸载。
|
|
277
|
+
|
|
278
|
+
事后可自行核验:`claude mcp list`、`codex mcp list`,以及看 `~/.cursor/mcp.json` 和 Zed 设置里的 `context_servers` 键。
|
|
279
|
+
|
|
280
|
+
**记录的是绝对路径**
|
|
281
|
+
|
|
282
|
+
`setup` 写进去的是它运行时所处安装目录的**绝对路径**。**如果你移动或删除了那个目录,所有客户端都会报 `CONNECTION_CLOSED`**,直到你在新位置重新跑一遍 `setup`,或者用 `--remove` 把失效条目清掉。
|
|
283
|
+
|
|
284
|
+
最容易踩到的情况:装到临时目录里试用,然后清理掉 —— 配置还指着那个已经不存在的路径。
|
|
164
285
|
|
|
165
286
|
**从本地目录安装**
|
|
166
287
|
|
|
@@ -400,7 +521,12 @@ node test/cleanup.mjs # 单独跑收尾清理(只针对临时目录;--d
|
|
|
400
521
|
| `sentinel` | 36 | 完成哨兵:原子性、闩锁语义、多会话并发不串台、被取消也落地、**思考不进文件** |
|
|
401
522
|
| `prune` | 11 | **哨兵留存**:清超期与崩溃残留、留新、清空壳目录、`TTL=0` 关闭清理、且绝不碰同目录的会话注册表(不花 token,每次必跑) |
|
|
402
523
|
| `guards` | 31 | **环境变量守卫**:奇怪的数值一律压到安全侧 —— `IDLE_TTL_MS` 的 `-1`/非法值都当"永不回收"(绝不主动让出写锁)、回收间隔过小压回默认(防空转紧循环)、**失联阈值下限 30 秒**(防别的实例"合法地"抢占并杀掉我们的回合)(不花 token,每次必跑) |
|
|
524
|
+
| `sandbox-guard` | 18 | **测试沙箱自身**:「临时 HOME」如果落到真实用户目录、它的子目录、盘符根目录,或 OS 临时目录之外,一律报错拒绝,绝不被写入和删除。其余套件都通过 `test/sandbox-home.mjs` 拿目录、只销毁自己建的那个(不花 token,每次必跑) |
|
|
403
525
|
| `approval-details` | 24 | **审批审查上下文**:待决审批的快照带命令、理由、cwd 与原始审批 ID —— 不只是一个裸 ID,调用方才能真正裁决;凭据形状的字段与常见内联字面量在结构化上下文和展示标题里都会脱敏。经真实 stdio MCP 端到端验证 |
|
|
526
|
+
| `setup` | 41 | **注册 CLI**:预览不写盘、`--remove` 只摘自己那条、没装客户端算跳过不算失败、Codex TOML 段往返、以及把写进配置里的命令真的拉起一次并要求应答 `initialize`(不花 token) |
|
|
527
|
+
| `removal-scope` | 24 | **注销只动自己那条**:给一份装着别的 MCP 服务器、顶层 Codex 设置和 Zed 兄弟设置的配置,注册再注销,断言其余每个值逐字节不变;删一个从没写过的东西 = 无写入、无报错(不花 token) |
|
|
528
|
+
| `config-safety` | 403 | **0.3 这条线的核心性质**,逐客户端验证:预览后文件逐字节不变且不产生 `.bak`;注册再注销后其余条目逐字节还原、键顺序不变、顶层键不丢;4 空格 / 2 空格 / Tab 缩进、单行 JSON、无末尾换行、CRLF、UTF-8 BOM 都能在写入后保留;坏 JSON 与只读文件被拒绝且原文件无损;第二次 `--yes` 空跑不新增备份;每个 `.bak-dsh-<时间戳>` 只出现一次且内容等于写入前字节(不花 token) |
|
|
529
|
+
| `real-configs` | 66 | **拿真家伙做往返**:把本机**真实**的客户端配置复制进临时目录(18 万字节、含几百个 `projects` 条目与大小写重复键的 `~/.claude.json`,238 行的 Codex TOML,Zed 的完整 `settings.json`),注册再撤销,要求每个文件**逐字节**还原;CI 上没有这些文件时明确打印 `skipped`,绝不假绿(不花 token) |
|
|
404
530
|
| `timeout` | 38 | **超时语义**:`wait=false` 不受 `timeout_ms` 影响、`wait=true` 到期只转后台(不取消/不丢结果、`busy` 不说谎)、`timeout_ms<=0` 一直等、resume 失败会作废进程而不永久卡死、空 prompt 给清晰错误 |
|
|
405
531
|
| `lock` | 79 | **写锁与抢占**:四类持有者判定、`writeMarker` 不覆盖活着的持有者、登记文件损坏/目录缺失等边界;**端到端**两实例抢锁 → 清晰报错 → 拒绝 → `force` 夺锁;**崩溃自动释放锁**、**卡死持有者无 force 自动抢占**;以及"一个回合 = 一个哨兵"的不变量(含内联回合与空闲插话) |
|
|
406
532
|
| `cycle` | 36 | **异步派活的生命周期**:异步派活 → 收哨兵 → 空闲回收 → 再派活,连做 3 轮必须全程无锁错误;贴着回收窗口立刻重派(放大竞态);`dsh_release` 后立刻重派;并断言回收后**不留下无法归因的锁**(这条实测抓到过"刚拉起的进程被 reaper 当空闲收掉"的竞态) |
|
|
@@ -412,9 +538,19 @@ node test/cleanup.mjs # 单独跑收尾清理(只针对临时目录;--d
|
|
|
412
538
|
| `permission` | 11 | ★ **权限档是否真的生效**(安全属性):不看我们的返回值(那正是会说谎的地方),而是读**会话自己记录的事实**(投影缓存里的 `permissions.preset` / `sandboxMode`),三档逐一核对且三档记录值互不相同。**这条测试抓到过无声提权**:profile 里 `defaultPreset` 会在会话创建时盖掉 `DSH_PERMISSION_MODE` |
|
|
413
539
|
| `list` | 13 | **`dsh_list` 的开销**(实测驱动):含磁盘探测的默认调用约 1 秒(要 spawn 一个 DSH 进程),**连续调用命中缓存降到个位数毫秒**;`only_running=true` 与 `include_closed=false` **跳过探测**且语义等价(不把磁盘上的未打开会话算进来) |
|
|
414
540
|
|
|
415
|
-
|
|
541
|
+
**离线那半边是 632 项检查,0.3.0 上全绿** —— `guards` 31、`prune` 11、`approval-details` 24、`sandbox-guard` 18、`setup` 41、`removal-scope` 24、`config-safety` 403、`real-configs` 66、`package-check` 14。九套全部退出码 0,都不需要模型、不需要装 DSH、也不需要 provider。
|
|
542
|
+
|
|
543
|
+
其中 **619 项属于客户端注册器套件**(`setup`、`removal-scope`、`config-safety`、`real-configs`)—— 0.3 的活其实都在这里。
|
|
544
|
+
|
|
545
|
+
需要模型的套件(`smoke` 29、`boundary` 42、`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)合计 462 项,**完整实测停在 0.1.2** —— 它们要可用 provider,发版前请自己重跑一遍:`node test/run.mjs --all`。
|
|
416
546
|
|
|
417
|
-
>
|
|
547
|
+
> 数字取各套件自己打印的汇总,不在这里手抄。`smoke`/`prune`/`guards`/`sandbox-guard`/`approval-details`/`boundary`/`setup`/`removal-scope`/`config-safety`/`package-check` 不调用模型;其余需要可用的 provider。
|
|
548
|
+
|
|
549
|
+
### 🧯 为什么会有 `sandbox-guard`
|
|
550
|
+
|
|
551
|
+
2026-10-29 的一次性验证脚本把沙箱路径存进了 **PowerShell** 的 `$home`。`$home` 是只读自动变量,赋值静默失败(脚本设了 `$ErrorActionPreference = 'Continue'`),`$env:USERPROFILE` 照样被改成了那个假路径,于是收尾那句 `Remove-Item $home -Recurse -Force` 落在**真实用户目录**上。那次运行在 120 秒超时被杀在半路,删掉了 `~/.codex`、`~/.cursor`、`~/.claude` 的一部分。
|
|
552
|
+
|
|
553
|
+
此前所有套件对**写入方**的断言都是真的,缺的是对**沙箱本身**的断言。`sandbox-guard` 就是这条断言:真实 HOME、它的子目录、大小写变体、盘符根目录、`os.tmpdir()` 之外的位置,全部拒绝。现在 `test/sandbox-home.mjs` 是唯一创建与销毁沙箱的地方,`setup`/`removal-scope` 自己不再调用 `mkdtempSync` 或 `rmSync` —— 一旦退回旧写法,套件立刻失败。
|
|
418
554
|
|
|
419
555
|
### 📏 本文档里的数字必须可复现
|
|
420
556
|
|
|
@@ -523,25 +659,3 @@ grep 'http fetch PUT 202' "$LOCALAPPDATA/npm-cache/_logs/<最新的>.log"
|
|
|
523
659
|
```
|
|
524
660
|
|
|
525
661
|
然后等一两分钟再查 `npm view dsh-any-agent dist-tags`。
|
|
526
|
-
|
|
527
|
-
**已发布的版本如果有问题,用 deprecate 而不是放着不管。** `0.1.0` 声明 `node >=20` 但实际需要 22.15,锁版本装它的人会得到一个起不来的服务:
|
|
528
|
-
|
|
529
|
-
```bash
|
|
530
|
-
npm deprecate "dsh-any-agent@0.1.0" \
|
|
531
|
-
"Node 20 cannot start this version. Upgrade to 0.1.1+ (requires Node >=22.15)."
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
`deprecate` 只在安装时打印警告、不删包 —— 这是对的工具:已经依赖它的人仍然能装到那个 tarball。注意这条**需要交互式 OTP**:带 bypass-2FA 的 granular token 不允许执行(npm 2026 年 7 月收紧的),所以要你自己跑,别放在 CI 里。
|
|
535
|
-
|
|
536
|
-
**徽章有延迟。** shields.io 缓存五分钟(`max-age=300`),所以刚发完会显示旧版本号、旧的 `node` 下限,有时还会显示 `license: package not found` —— 那个字符串是 shields 读不到 license 时的通用失败文案,不是真的没有 license。等一会并强制刷新,再判断元数据是不是错的。
|
|
537
|
-
|
|
538
|
-
## 💬 支持
|
|
539
|
-
|
|
540
|
-
- **Bug、功能请求、部署提问** —— [提 issue](https://github.com/xmtlzzz/dsh-any-agent/issues),都往这里放
|
|
541
|
-
- **报故障之前** —— 先看 [新环境常见故障表](USAGE-MIGRATION.md) 和 [使用注意事项](USAGE-NOTES.md),绝大多数部署失败就那么几种原因
|
|
542
|
-
|
|
543
|
-
单人维护的项目,所以带着**可复现的信息**(具体哪次调用、完整报错原文、DSH 版本)会快很多。
|
|
544
|
-
|
|
545
|
-
## 📜 许可
|
|
546
|
-
|
|
547
|
-
MIT —— 见 [LICENSE](LICENSE)。
|