dsh-any-agent 0.3.2 โ†’ 0.3.4

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 CHANGED
@@ -1,50 +1,51 @@
1
- # ๐Ÿณ dsh-any-agent
1
+ <div align="center">
2
2
 
3
- **English** | [ไธญๆ–‡](README.zh.md)
3
+ <img src="https://raw.githubusercontent.com/xmtlzzz/dsh-any-agent/main/docs/brand/header.jpg" width="100%" alt="dsh-any-agent โ€” a blue-haired chibi whale-girl eating white rice beside a coding laptop">
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/dsh-any-agent.svg)](https://www.npmjs.com/package/dsh-any-agent)
6
- [![node](https://img.shields.io/node/v/dsh-any-agent.svg)](https://nodejs.org)
7
- [![license](https://img.shields.io/npm/l/dsh-any-agent.svg)](LICENSE)
8
- [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](package.json)
5
+ **Give any MCP client a coding buddy that sticks around.**
9
6
 
10
- > **Give any MCP client a coding buddy that sticks around** โ€” powered by DeepSeek Harness.
7
+ **[Website](https://dsh-any-agent.xmtlz.dev/)** ยท **English** ยท [็ฎ€ไฝ“ไธญๆ–‡](README.zh.md)
11
8
 
12
- An MCP server that drives DSH as a **long-lived agent runtime** instead of wrapping a CLI. Open a conversation, hand it a goal, and it writes code, runs scripts and spawns its own subagents โ€” while you watch, cut in, and collect the result whenever you like. ๐Ÿ› ๏ธ
9
+ [![npm version](https://img.shields.io/npm/v/dsh-any-agent.svg?style=flat-square)](https://www.npmjs.com/package/dsh-any-agent)
10
+ [![node](https://img.shields.io/node/v/dsh-any-agent.svg?style=flat-square)](https://nodejs.org)
11
+ [![license](https://img.shields.io/npm/l/dsh-any-agent.svg?style=flat-square)](LICENSE)
12
+ [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg?style=flat-square)](package.json)
13
13
 
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`.
14
+ [Website](https://dsh-any-agent.xmtlz.dev/) ยท [Quick start](#quick-start) ยท [Features](#-why-its-different) ยท [Tools](#-tools) ยท [Usage notes](USAGE-NOTES.en.md) ยท [Changelog](CHANGELOG.md)
15
15
 
16
- ๐Ÿงช 660 offline checks green on 0.3.2 (619 of them in the client-registration suites) ยท ๐Ÿ”Œ MCP over stdio ยท ๐Ÿ“ฆ Zero dependencies ยท ๐Ÿ“œ MIT
16
+ </div>
17
17
 
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
- >
20
- > ๐Ÿ†• **Setting this up on a fresh machine?** [USAGE-MIGRATION.md](USAGE-MIGRATION.md) is a step-by-step walkthrough for a new environment: prerequisites, how to find your `DSH_BIN`, ready-to-paste config blocks for Claude Code / Codex / Cursor / Windsurf / Cline / Zed, and a first-turn checklist.
18
+ ๐Ÿงช 673 offline checks green on 0.3.4 on this machine (691 when all nine client fixtures exist); 517 of them are in the client-registration suites (535 with all fixtures) ยท ๐Ÿ”Œ MCP over stdio ยท ๐Ÿ“ฆ Zero dependencies ยท ๐Ÿ“œ MIT
19
+
20
+ An MCP server that drives DeepSeek Harness as a **long-lived coding agent**. Hand it a goal, let it write code and run scripts, steer it mid-flight, and collect the result when you are ready.
21
+
22
+ **Claude Code ยท Claude Desktop ยท Codex ยท Cursor ยท Windsurf ยท Zed ยท Gemini CLI ยท opencode ยท VS Code** โ€” or any client that speaks MCP over stdio.
23
+
24
+ ## Quick start
25
+
26
+ Requires **Node.js โ‰ฅ 22.15** and **DSH installed with a working model route and credentials**. See [Requirements](#-requirements) for details.
27
+
28
+ ```bash
29
+ npm install -g dsh-any-agent
30
+ dsh-any-agent-setup
31
+ dsh-any-agent-setup --yes
32
+ ```
21
33
 
22
- ## ๐Ÿ“‘ Contents
34
+ The second command previews the changes; `--yes` applies them. Reconnect your client's MCP server, then ask your agent to start a DSH conversation in your project, dispatch a task, and collect the result. The [dispatch workflow](#-workflow-dispatch-then-collect-later) shows the tool sequence.
23
35
 
36
+ > **Before your first task:** read the [Usage Notes](USAGE-NOTES.en.md), especially the model-config and external-MCP permission boundaries. External MCP tools bypass DSH's sandbox. For a fresh machine, follow the [migration guide](USAGE-MIGRATION.md).
37
+
38
+ <details>
39
+ <summary>Full contents</summary>
40
+
41
+ - **Quick start** โ€” [Install and connect](#quick-start) ยท [Changelog](CHANGELOG.md)
24
42
  - **Start here** โ€” [The problem](#-the-problem-this-solves) ยท [Why it's different](#-why-its-different) ยท [Why not just wrap the DSH CLI](#-why-not-just-wrap-the-dsh-cli) ยท [Requirements](#-requirements) ยท [Setup](#-setup)
25
43
  - **Using it** โ€” [Tools](#-tools) ยท [Dispatch then collect](#-workflow-dispatch-then-collect-later) ยท [Completion notifications](#-workflow-get-notified-on-completion-no-polling) ยท [Many conversations](#-workflow-many-conversations-at-once)
26
- - **Behavior** โ€” [Permissions](#-permissions) ยท [Reasoning](#-reasoning-hidden-by-default) ยท [Logging and privacy](#-logging-and-privacy) ยท [Workspace & GUI](#workspace-visibility-in-the-dsh-gui)
44
+ - **Behavior** โ€” [Permissions](#-permissions) ยท [Reasoning](#-reasoning-hidden-by-default) ยท [Logging and privacy](#-logging-and-privacy) ยท [Workspace & GUI](#-workspace-visibility-in-the-dsh-gui)
27
45
  - **Hard parts** โ€” [The write lock](#-the-write-lock-one-writer-per-conversation-at-a-time)
28
46
  - **Reference** โ€” [Environment](#-environment-variables) ยท [Tests](#-tests) ยท [Known limitations](#-known-limitations) ยท [Why the old profile name?](#-why-is-the-profile-still-called-dsh-mcp)
29
47
 
30
-
31
- ```
32
- any MCP client โ† Claude Code, Codex, Cursor, Zed, Cline, your ownโ€ฆ
33
- (Claude/Codex/Cursor/โ€ฆ) โ”‚
34
- โ”‚ MCP over stdio โ”‚ newline-delimited JSON-RPC (ACP)
35
- โ–ผ โ–ผ
36
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
37
- โ”‚dsh-any-agentโ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ dsh --profile dsh-mcp โ”‚ one long-lived process
38
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ per conversation
39
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
40
- โ”‚
41
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
42
- โ”‚ DSH's own tools โ”‚ files, shell, search,
43
- โ”‚ โ”‚ skills, subagents, workflows
44
- โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
45
- โ”‚ dsh-mcp-client โ”‚ โ†’ your other MCP servers
46
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
47
- ```
48
+ </details>
48
49
 
49
50
  ## ๐ŸŽฏ The problem this solves
50
51
 
@@ -63,7 +64,7 @@ DSH (DeepSeek Harness) can do all of that โ€” it has file/shell tools, skills, s
63
64
 
64
65
  ## โœจ Why it's different
65
66
 
66
- | | |
67
+ | Capability | What you get |
67
68
  |---|---|
68
69
  | ๐Ÿ”„ **A dead process never kills a conversation** | The identity lives on disk. A crash, an idle reap, even restarting this whole server costs you nothing โ€” the next call transparently resumes it, memory intact (verified across processes). |
69
70
  | ๐Ÿ—ฃ๏ธ **You are never stuck waiting** | Steer a conversation mid-flight โ€” **interject** to stop and redirect (~100 ms to reach idle, measured over four cancelled turns), or **queue** a remark for after the current turn. Interrupting never damages the conversation. |
@@ -89,36 +90,62 @@ DSH ships two programmable entry points. The `sdk` profile looks simpler, but it
89
90
 
90
91
  **Core idea: the process is a cache, not the identity.** A conversation's identity is DSH's `sessionId`, persisted under `~/.dsh/sessions`. If the process is reaped, crashes, or the whole MCP server restarts, the next call transparently `session/resume`s it โ€” history intact.
91
92
 
93
+ <details>
94
+ <summary>How the clients, server and DSH connect</summary>
95
+
96
+ ```
97
+ any MCP client โ† Claude Code, Codex, Cursor, Zed, Cline, your ownโ€ฆ
98
+ (Claude/Codex/Cursor/โ€ฆ) โ”‚
99
+ โ”‚ MCP over stdio โ”‚ newline-delimited JSON-RPC (ACP)
100
+ โ–ผ โ–ผ
101
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
102
+ โ”‚dsh-any-agentโ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ dsh --profile dsh-mcp โ”‚ one long-lived process
103
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ per conversation
104
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
105
+ โ”‚
106
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
107
+ โ”‚ DSH's own tools โ”‚ files, shell, search,
108
+ โ”‚ โ”‚ skills, subagents, workflows
109
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
110
+ โ”‚ dsh-mcp-client โ”‚ โ†’ your other MCP servers
111
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
112
+ ```
113
+
114
+ **MCP compatibility.** The server uses only `initialize`, `ping`, `tools/list` and `tools/call`. It has also been verified with a bare-bones client that negotiates an older protocol version and never sends `notifications/initialized`.
115
+
116
+ </details>
117
+
92
118
  ## ๐Ÿ“ฆ Requirements
93
119
 
94
120
  - **Node.js โ‰ฅ 22.15** โ€” the server uses `zstdDecompressSync` from `node:zlib` to read DSH session headers, and that API does not exist before 22.15
95
121
  - **DSH installed** (`@deepseek-ai/dsh`), with working credentials under `~/.dsh`
96
- - A DSH **profile named `dsh-mcp`** derived from the shipped `acp` template, containing **your own** provider/model config (this repo deliberately ships no provider config)
122
+ - A working provider/model route in your local DSH configuration. The server prepares its `dsh-mcp` ACP profile automatically from that route; no provider config or credentials are bundled.
97
123
 
98
124
  ## ๐Ÿš€ Setup
99
125
 
100
- ### 1. Create the DSH profile
126
+ ### 1. Prepare the DSH profile
127
+
128
+ On first launch, the server prepares a `dsh-mcp` profile from the shipped ACP template and copies the model route from your existing DSH configuration. Credentials stay with DSH. Use `DSH_MCP_ROUTE_SOURCE` to choose a source profile, or `DSH_MCP_BOOTSTRAP=0` to manage the profile yourself. See [Environment variables](#-environment-variables).
129
+
130
+ <details>
131
+ <summary>Manual profile setup and ACP configuration details</summary>
101
132
 
102
133
  ```bash
103
- dsh dsh-mcp --from-default-profile acp
134
+ dsh --profile dsh-mcp --from-default-profile acp
104
135
  ```
105
136
 
106
- Then edit `~/.dsh/profiles/dsh-mcp/cordis.patch.yml` and declare your provider and model. Start from [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml).
137
+ Then edit `~/.dsh/profiles/dsh-mcp/cordis.patch.yml`, remove its `# dsh-mcp:managed` marker if present, and declare your provider and model. Start from [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml).
107
138
 
108
139
  > โš ๏ธ **Two traps here** (both learned the hard way):
109
140
  >
110
141
  > 1. **ACP's model does NOT come from `agent-default-model`.** It comes from the `dsh-acp` plugin's own `config.provider` / `config.model`, which the `acp` bundle hard-codes to `deepseek-official`. If you don't override the `acp` row, your sessions silently run on the wrong provider and fail with `no API key for provider route "deepseek-official"`.
111
142
  > 2. **ACP's `reasoning_effort` defaults to empty** (= "Provider default"), not to the maximum. This server explicitly sets it to `max` on every `session/new` and after every `session/resume` โ€” because a fresh process does not remember the previous choice.
112
143
 
113
- ### 2. Register the server
144
+ </details>
114
145
 
115
- **From npm, one command (recommended)**
146
+ ### 2. Register the server
116
147
 
117
- ```bash
118
- npm install -g dsh-any-agent
119
- dsh-any-agent-setup # detect the clients you have; writes nothing
120
- dsh-any-agent-setup --yes # apply
121
- ```
148
+ Run the three commands in [Quick start](#quick-start), then reconnect your client. For a manual configuration, expand the client examples below.
122
149
 
123
150
  `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
151
 
@@ -128,12 +155,17 @@ dsh-any-agent-setup --yes # apply
128
155
  - **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
156
  - **idempotent** โ€” re-running only changes what differs, and a second `--yes` writes nothing and adds no `.bak`
130
157
 
158
+ **Measured impact scope (2026-11-02, plan mode on this machine):** 5 of the 9 clients were detected (Claude Code, Codex, Cursor, Zed, VS Code) and 4 were skipped; `--json` reported `mode=plan, changed=5, skipped=4, failed=0`. The SHA-256, size and mtime of every existing client config were unchanged. A real `--yes` is additive: only the single `dsh-any-agent` entry is written, every other byte is preserved, and a `.bak-dsh-<timestamp>` copy is taken first; `--remove` deletes only that entry.
159
+
131
160
  ```bash
132
161
  dsh-any-agent-setup --yes --only cursor,zed # just these
133
162
  dsh-any-agent-setup --yes --permission read-only
134
163
  dsh-any-agent-setup --remove --yes # take it back out; only its own entry is removed
135
164
  ```
136
165
 
166
+ <details>
167
+ <summary>Advanced flags and client configuration paths</summary>
168
+
137
169
  **Three new flags**
138
170
 
139
171
  | Flag | What it does |
@@ -162,6 +194,11 @@ For the exact path on your machine, read the `path` field for each client in `--
162
194
 
163
195
  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
196
 
197
+ </details>
198
+
199
+ <details>
200
+ <summary>Registration checks and round-trip verification</summary>
201
+
165
202
  **What `setup` will and will not touch**
166
203
 
167
204
  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.
@@ -209,6 +246,7 @@ Removal is not accepted because it *looks* right: the check is that **taking our
209
246
 
210
247
  Afterwards every JSON config still parses and `codex mcp list` still reads the file; no client needs hand repair.
211
248
 
249
+ </details>
212
250
 
213
251
  **`cwd` is optional: it defaults to the project the client has open**
214
252
 
@@ -249,6 +287,9 @@ A restore is itself destructive, so it copies the **current** file aside as `.ba
249
287
  | Unregister from just some | `dsh-any-agent-setup --remove --yes --only codex` |
250
288
  | Undo one bad write | copy `.bak-dsh-<stamp>` back over the config |
251
289
 
290
+ <details>
291
+ <summary>What removal changes: verification details</summary>
292
+
252
293
  **What `--remove` actually does โ€” measured**
253
294
 
254
295
  Unregistering touches exactly one key per client and leaves everything else alone. Measured on a real machine, before โ†’ after:
@@ -268,10 +309,15 @@ Nothing outside those entries is read, rewritten or deleted. Your credentials, o
268
309
 
269
310
  To verify by hand afterwards: `claude mcp list`, `codex mcp list`, and inspect `~/.cursor/mcp.json` / the `context_servers` key of Zed's settings.
270
311
 
312
+ </details>
313
+
271
314
  **The recorded path is absolute**
272
315
 
273
316
  `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.
274
317
 
318
+ <details>
319
+ <summary>Manual client configuration: Claude Code, Codex and generic MCP</summary>
320
+
275
321
  **From a local checkout**
276
322
 
277
323
  Clone or unzip anywhere, then point `command` at `node` and `args` at `<checkout>/bin/dsh-any-agent.mjs` (shown below).
@@ -315,6 +361,8 @@ For a local checkout, use `"command": "node"` and `"args": ["/absolute/path/to/s
315
361
 
316
362
  > Cursor, Windsurf, Zed, Cline, Continue and most other clients use this same `mcpServers` shape โ€” if it can launch a stdio MCP server, it works.
317
363
 
364
+ </details>
365
+
318
366
  ### 3. Try it
319
367
 
320
368
  ```bash
@@ -330,12 +378,12 @@ node test/smoke.mjs # no LLM calls, verifies the whole plumbing
330
378
  | `dsh_start` | Create a conversation: workspace, permission tier, approval policy, **reasoning effort (default `max`)**, model |
331
379
  | `dsh_send` | Hand it a task. **Defaults to `wait=false`: returns a `run_id` + `sentinel_file` immediately**, the turn runs in the background and the sentinel file notifies you when it's done (recommended). `wait=true` blocks until the turn ends; hitting `timeout_ms` (default `0` = wait forever) **only downgrades to background โ€” the turn is never cancelled and the result is never lost** |
332
380
  | `dsh_list` | All conversations with live state: `running` / `idle` / `detached`, elapsed time, current tool, output so far. `only_running=true` for active ones |
333
- | `dsh_read` | **Cursor-based incremental read** of what a *running* conversation is producing right now |
334
- | `dsh_get` | Conversation details; with `run_id`, the **final result of that dispatch** (the "collect the result later" entry point) |
335
- | `dsh_interject` | Interject while it's busy: `interject` = stop and redirect, `queue` = wait for the current turn, then speak. **This round writes a sentinel too**, and the reply carries its `sentinel_file` |
381
+ | `dsh_read` | **Cursor-based incremental read** of what a *running* conversation is producing right now. The cursor is a **revision number**: an entry whose text grew since your last read reappears โ€” match it by `seq` and replace its text, don't append again |
382
+ | `dsh_get` | Conversation details; with `run_id`, the **final result of that dispatch** (the "collect the result later" entry point). Run records live in memory (last 20) โ€” use the sentinel file after a restart |
383
+ | `dsh_interject` | Interject while it's busy: `interject` = stop and redirect, `queue` = wait for the current turn, then speak. **This round writes a sentinel too**, and the reply carries its `sentinel_file`. On an idle conversation it behaves like `wait=true` and **blocks until the turn ends** |
336
384
  | `dsh_takeover` | **Preempt the write lock.** The only way out when another DSH process holds it (DSH has no steal API โ€” preempting means killing the holder). It classifies the holder first: our own orphaned child โ†’ taken automatically; another live instance โ†’ needs `force`; **GUI/unknown โ†’ never killed**, reported instead |
337
385
  | `dsh_interrupt` | Stop the current turn (the conversation stays healthy) |
338
- | `dsh_release` | Hand the conversation back (frees the write lock so you can open it in your own DSH GUI). Still resumable |
386
+ | `dsh_release` | Hand the conversation back (frees the write lock so you can open it in your own DSH GUI). Still resumable; `forget=true` writes a tombstone โ€” by-id lookups are refused until you delete `$DSH_HOME/.dsh-any-agent-forgotten/<id>.json` |
339
387
  | `dsh_approval_decide` | Adjudicate a pending approval (only when `on_approval=ask`) |
340
388
  | `dsh_status` | Lightweight status check |
341
389
 
@@ -384,7 +432,7 @@ Design notes (especially under **concurrent multi-session** use):
384
432
  - **Latch semantics**: the file is written once and kept. Even if the run finishes *before* you arm the waiter, `[ -f ]` is immediately true โ€” you never miss it.
385
433
  - **Atomic write**: `tmp` + `rename`; if the file exists its contents are complete. Each run uses its own tmp, so concurrent writes never clobber.
386
434
  - **No server push**: the service only answers requests over MCP (it sends no notifications). "Completion notification" is entirely the sentinel file plus your background waiter.
387
- - **โ˜… No reasoning in the sentinel**: the payload carries the answer and thinking *statistics* only โ€” `result.thinking` is stripped and replaced by `thinking_omitted: true`, because the project's rule is that reasoning never reaches disk. `dsh_get(run_id)` still returns it from memory while the record lives. `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` breaks that guarantee deliberately.
435
+ - **โ˜… No reasoning in the sentinel**: the payload carries the answer, thinking *statistics*, usage and (credential-redacted) tool inputs, but never reasoning text โ€” `result.thinking` is stripped and replaced by a boolean `thinking_omitted` (`true` when reasoning was removed, `false` when there was none), because this server's rule is that reasoning never reaches its own disk artifacts. `dsh_get(run_id)` still returns it from memory while the record lives. `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` breaks that guarantee deliberately. Note that **DSH's own session log does contain reasoning** โ€” see [Logging and privacy](#๏ธ-logging-and-privacy).
388
436
 
389
437
  > Cleanup: sentinels contain full answers, so the service **prunes sentinels older than 7 days at startup** (tune with `DSH_MCP_SENTINEL_TTL_MS`; `0` disables). Your waiter should still `rm` the file after consuming it.
390
438
 
@@ -396,7 +444,7 @@ Design notes (especially under **concurrent multi-session** use):
396
444
  - โ€ฆ | id=33427096โ€ฆ | ๐Ÿ”ต running (8s, 34 chars this turn) | cwd=โ€ฆ
397
445
  2. dsh_read(id, cursor=0) # read the first, note the cursor
398
446
  3. dsh_read(id2, cursor=0) # switch to the second
399
- 4. dsh_read(id, cursor=<prev>) # switch back, deltas only
447
+ 4. dsh_read(id, cursor=<prev>) # switch back; entries that grew since <prev> reappear (match by seq)
400
448
  ```
401
449
 
402
450
  ## ๐Ÿคซ Reasoning: hidden by default
@@ -407,6 +455,8 @@ DSH separates reasoning from answer text at the model layer, and ACP exposes the
407
455
  - `reasoning=marker` / `summary` / `full` โ€” progressively more content (mind your context budget)
408
456
  - `dsh_read(include_reasoning=true)` โ€” live reasoning of the *currently running* turn only; it is never buffered and is discarded when the turn ends
409
457
 
458
+ > โš ๏ธ This server never writes reasoning to its own logs, state file or sentinels โ€” but **DSH's own session log (`~/.dsh/sessions/**/session.v4.jsonl.zstd`) does contain the full reasoning text**, even when `reasoning=hide`. That is DSH's storage (it is what makes resume possible) and is outside this server's control; account for it when analyzing sensitive data.
459
+
410
460
  ## ๐Ÿ” Permissions
411
461
 
412
462
  | Tier | Approval default | Use for |
@@ -417,6 +467,8 @@ DSH separates reasoning from answer text at the model layer, and ACP exposes the
417
467
 
418
468
  `on_approval` can be `auto-allow`, `auto-deny`, or `ask` (which exposes a pending approval for `dsh_approval_decide` to adjudicate).
419
469
 
470
+ **`read-only` is enforced, not merely a default**: it always runs with `auto-deny`; passing `auto-allow` or `ask` for a read-only conversation is rejected at `dsh_start`, and the approval layer refuses any permission request from a read-only conversation. Without that, DSH can ask for a `sandbox_permissions` escalation and a permissive approval policy would let the write through (found by the adversarial audit).
471
+
420
472
  ## ๐ŸชŸ Workspace visibility in the DSH GUI
421
473
 
422
474
  Two different things โ€” don't confuse them:
@@ -455,20 +507,20 @@ Silent by default โ€” **not one byte is written**.
455
507
  | `debug` | more of our own events; still no DSH output |
456
508
  | `DSH_MCP_LOG_STDERR=1` | additionally forward DSH's raw stderr (**may contain reasoning**) |
457
509
 
458
- **Reasoning never reaches disk** โ€” with exactly one deliberate exception:
510
+ **This server never writes reasoning to disk** โ€” with exactly one deliberate exception:
459
511
 
460
- - Not in logs (silent by default), and the state file holds metadata only (id, cwd, permissions, counters) โ€” a smoke-test assertion guards this.
461
- - **The async completion sentinel does persist the answer** (it must, so a caller can collect offline) but **strips `result.thinking`** and marks `thinking_omitted: true`. A test asserts that even when `reasoning=full` is requested, no reasoning text appears in the sentinel โ€” while `dsh_get(run_id)` still returns it from memory. Set `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` to break this on purpose.
512
+ - Not in logs (silent by default). The state file and the per-conversation metadata sidecars under `.state/meta/` hold metadata only (id, cwd, permissions, counters, title hint) โ€” a smoke-test assertion guards the registry.
513
+ - **The async completion sentinel persists the answer, usage and tool inputs** (it must, so a caller can collect offline) but **strips `result.thinking`** and always reports `thinking_omitted` (true when reasoning was removed, false when there was none). Tool inputs are passed through the same credential redaction as approvals, but the sentinel still contains commands and file contents โ€” treat it as sensitive. A test asserts that even when `reasoning=full` is requested, no reasoning text appears in the sentinel โ€” while `dsh_get(run_id)` still returns it from memory. Set `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` to break this on purpose.
462
514
  - Sentinels are pruned after 7 days by default (`DSH_MCP_SENTINEL_TTL_MS`; `0` keeps them forever).
463
515
 
464
- (DSH itself writes its own session log under `~/.dsh/sessions`; that is what makes resume possible and is outside this server's control.)
516
+ > โš ๏ธ **DSH writes its own session log under `~/.dsh/sessions` and that log contains the full reasoning text** (even with `reasoning=hide`). It is what makes resume possible and is outside this server's control. The absolute "reasoning never reaches disk" claim applies to *this server's* artifacts only.
465
517
 
466
518
  ## โš™๏ธ Environment variables
467
519
 
468
520
  | Variable | Default | Purpose |
469
521
  |---|---|---|
470
522
  | `DSH_BIN` | auto-detected | Path to `@deepseek-ai/dsh/lib/bin.js` |
471
- | `DSH_MCP_PROFILE` | `dsh-mcp` | DSH profile to drive. โš ๏ธ The stale name is deliberate โ€” see [Why is the profile still called `dsh-mcp`?](#why-is-the-profile-still-called-dsh-mcp) |
523
+ | `DSH_MCP_PROFILE` | `dsh-mcp` | DSH profile to drive. โš ๏ธ The stale name is deliberate โ€” see [Why is the profile still called `dsh-mcp`?](#-why-is-the-profile-still-called-dsh-mcp) |
472
524
  | `DSH_MCP_BOOTSTRAP` | on | Whether this server may create/sync the DSH profile on first start. `0` makes it drive an existing profile only and never write a file |
473
525
  | `DSH_MCP_ROUTE_SOURCE` | unset | Pin where model routes are copied from during bootstrap: a profile name (`desktop`, `web`, โ€ฆ) or `__home__` for `$DSH_HOME/cordis.patch.yml`. Unset = try home โ†’ desktop โ†’ web โ†’ headless โ†’ acp |
474
526
  | `DSH_MCP_STATE` | `<repo>/.state/conversations.json` | Conversation registry, survives restarts |
@@ -503,18 +555,21 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
503
555
  | Suite | Checks | Covers |
504
556
  |---|---|---|
505
557
  | `smoke` | 29 | handshake, tool table, conversation creation, config, registry purity |
506
- | `boundary` | 42 | protocol edges (double initialize, malformed lines, unknown method), argument validation, unknown ids, cursor edges, lifecycle idempotence, unicode |
558
+ | `boundary` | 46 | protocol edges (double initialize, malformed lines, unknown method), argument validation (including read-only + permissive approval rejection, unknown provider/model, and no stale write-lock marker on a failed create), unknown ids, cursor edges, lifecycle idempotence, unicode |
507
559
  | `integration` | 30 | resume-with-memory across processes, interrupt, both interject modes |
508
560
  | `async` | 16 | fire-and-forget + later collection |
509
561
  | `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
510
562
  | `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) |
511
563
  | `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) |
512
564
  | `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) |
513
- | `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 |
565
+ | `approval-details` | 27 | **approval review context**: the pending-approval snapshot carries the command, reason, cwd and original approval ID โ€” not just a bare ID โ€” so a caller can actually adjudicate; missing `rawInput` is correlated from the earlier `tool_call` update by ID; credential-shaped fields and common inline literals are redacted; closing a conversation cancels pending approvals. Verified end-to-end over real stdio MCP |
566
+ | `read-cursor` | 10 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`), and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
567
+ | `sentinel-redaction` | 11 | **sentinel disk hygiene**: `result.thinking` is stripped, `thinking_omitted` is always a boolean, and `tools_used[].input` passes the same credential redaction as approvals (no tokens) |
568
+ | `forget-tombstone` | 7 | **`forget` is terminal**: `release(forget=true)` writes a tombstone, by-id lookups fail with a recovery path, lists exclude it, and deleting the tombstone restores the normal unknown-session path (no tokens) |
514
569
  | `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) |
515
570
  | `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) |
516
- | `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) |
517
- | `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) |
571
+ | `config-safety` | 404 | **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) |
572
+ | `real-configs` | 66 (48 with 5/9 fixtures) | **round-trip on the real thing**: copies this machine's actual client configs (an 182693-byte `~/.claude.json` holding hundreds of `projects` entries and case-colliding keys, a 238-line Codex TOML, Zed's full `settings.json`) into a temp dir, registers, then removes, and requires every file back **byte for byte**; entry-presence is checked by key (not a bare substring), and missing fixtures are reported as skipped rather than green (no tokens) |
518
573
  | `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 |
519
574
  | `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) |
520
575
  | `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) |
@@ -522,17 +577,17 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
522
577
  | `capability` | 22 | writing code, running scripts, **spawning its own subagents** (verified on disk via child session headers) |
523
578
  | `workspace-effect` | 24 | workspace actually effective when no path is given |
524
579
  | `acceptance` | 36 | two folders ร— two conversations doing a read-only IDA Pro analysis |
525
- | `multi` | 19 | **several instances and non-ASCII paths**: reproduces "two instances share the registry and the later save drops the earlier instance's conversation", then proves **that conversation is recoverable by id alone from the session store and actually usable**; **recovery never silently escalates privileges** (a read-only conversation stays read-only); no `.tmp` residue; and **CJK / emoji workspace paths** create sessions, do real work, and place files in the right directory |
580
+ | `multi` | 19 | **several instances and non-ASCII paths**: reproduces "two instances share the registry and the later save drops the earlier instance's conversation", then proves **that conversation is recoverable by id alone from the session store and actually usable** (registry loss, not an explicit `forget` โ€” forget is a terminal tombstone); **recovery never silently escalates privileges** (a read-only conversation stays read-only); no `.tmp` residue; and **CJK / emoji workspace paths** create sessions, do real work, and place files in the right directory |
526
581
  | `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 |
527
582
  | `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) |
528
583
 
529
- **The offline half is 660 checks, all green on 0.3.2** โ€” `guards` 31, `prune` 11, `approval-details` 24, `sandbox-guard` 18, `setup` 41, `removal-scope` 24, `config-safety` 403, `real-configs` 66, `package-check` 14, `roots` 12, `workspace-titles` 15. Every suite exited 0; none needs a model, a DSH install or a provider.
584
+ **The offline suites total 691 checks when all nine client fixtures exist (673 on a machine with five), all green on 0.3.4** โ€” `guards` 31, `prune` 11, `approval-details` 27, `sandbox-guard` 18, `setup` 41, `removal-scope` 24, `config-safety` 404, `real-configs` 66 (48 with 5/9), `package-check` 14, `roots` 12, `workspace-titles` 15, `read-cursor` 10, `sentinel-redaction` 11, `forget-tombstone` 7. Every suite exited 0; none needs a model, a DSH install or a provider.
530
585
 
531
- 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.
586
+ That is **535 assertions in the client-registration suites alone** (`setup`, `removal-scope`, `config-safety`, `real-configs`; 517 on a machine with five client fixtures), which is where the 0.3 work actually is.
532
587
 
533
- 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`.
588
+ The model-backed suites (`smoke` 29, `boundary` 46, `integration` 30, `async` 16, `sentinel` 36, `timeout` 38, `lock` 79, `cycle` 36, `concurrency` 31, `capability` 22, `workspace-effect` 24, `acceptance` 36, `multi` 19, `permission` 11, `list` 13) total 466 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`. `acceptance` now **skips explicitly** when the DSH profile has no IDA MCP (`mcp__ida__*`).
534
589
 
535
- > 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.
590
+ > Counts are the numbers each suite prints itself. `smoke`/`prune`/`guards`/`sandbox-guard`/`approval-details`/`boundary`/`setup`/`removal-scope`/`config-safety`/`package-check`/`read-cursor`/`sentinel-redaction`/`forget-tombstone` run without LLM calls; the rest need a working provider.
536
591
 
537
592
  ### ๐Ÿงฏ Why `sandbox-guard` exists
538
593
 
@@ -603,3 +658,13 @@ DSH conversations carry a **cross-process write lock**: **it never expires while
603
658
  Because renaming it would break things that already exist on disk.
604
659
 
605
660
  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:
661
+
662
+ ## Star History
663
+
664
+ <a href="https://www.star-history.com/?repos=xmtlzzz%2Fdsh-any-agent&type=date&legend=top-left">
665
+ <picture>
666
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=xmtlzzz/dsh-any-agent&type=date&theme=dark&legend=top-left" />
667
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=xmtlzzz/dsh-any-agent&type=date&legend=top-left" />
668
+ <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=xmtlzzz/dsh-any-agent&type=date&legend=top-left" />
669
+ </picture>
670
+ </a>