dsh-plugin-wsl-env 0.8.0 → 0.10.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +488 -12
  2. package/CONTRIBUTING.md +125 -62
  3. package/README.md +99 -68
  4. package/SECURITY.md +18 -2
  5. package/SUPPORT.md +26 -10
  6. package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
  7. package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
  8. package/agent/wsl-agent.sh +83 -16
  9. package/cordis.patch.yml +50 -35
  10. package/docs/ARCHITECTURE.md +203 -55
  11. package/docs/CONFIGURATION.md +185 -18
  12. package/docs/LIMITATIONS.md +125 -45
  13. package/docs/PARITY.md +68 -38
  14. package/docs/PEER-PARITY.md +103 -33
  15. package/docs/README.zh.md +105 -64
  16. package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
  17. package/docs/UPSTREAM-TERMINAL-TITLE.md +56 -14
  18. package/examples/preset-wsl-session-sandbox.cordis.patch.yml +178 -0
  19. package/lib/agent-protocol.d.ts +10 -4
  20. package/lib/agent-protocol.js +10 -3
  21. package/lib/agent.d.ts +84 -0
  22. package/lib/agent.js +167 -15
  23. package/lib/bwrap.d.ts +8 -0
  24. package/lib/bwrap.js +4 -1
  25. package/lib/fs-routing.d.ts +40 -4
  26. package/lib/fs-routing.js +80 -12
  27. package/lib/fsio-agent.js +1 -1
  28. package/lib/index.d.ts +134 -0
  29. package/lib/index.js +202 -9
  30. package/lib/native-agent.d.ts +124 -0
  31. package/lib/native-agent.js +198 -0
  32. package/lib/ports.d.ts +13 -2
  33. package/lib/ports.js +18 -5
  34. package/lib/preflight.d.ts +91 -0
  35. package/lib/preflight.js +117 -0
  36. package/lib/probe-cache.d.ts +12 -1
  37. package/lib/probe-cache.js +11 -3
  38. package/lib/sandbox-core.d.ts +70 -0
  39. package/lib/sandbox-core.js +187 -18
  40. package/lib/session-runtime.d.ts +161 -0
  41. package/lib/session-runtime.js +220 -0
  42. package/lib/session-scope.d.ts +124 -0
  43. package/lib/session-scope.js +191 -0
  44. package/lib/shell-env.js +10 -3
  45. package/lib/watcher.d.ts +25 -12
  46. package/lib/watcher.js +120 -32
  47. package/lib/workspace-files-route.d.ts +8 -4
  48. package/lib/workspace-files-route.js +42 -14
  49. package/lib/workspace-files-wsl.js +25 -8
  50. package/lib/wsl.js +10 -4
  51. package/locale/preflight/en.json +6 -0
  52. package/locale/preflight/zh.json +6 -0
  53. package/package.json +44 -27
  54. package/scripts/diagnose.sh +92 -0
@@ -1,54 +1,90 @@
1
- # Upstream proposal: export the fsio module from `dsh-fs-local`
1
+ # Upstream proposal: export the fsio text helpers from `dsh-fs-local`
2
2
 
3
- Status: draft, targeting `deepseek-harness` @ `packages/fs/fs-local` (version
4
- line `0.2.0-rc.x`). This is the Shape-A upstream half of the in-distro I/O
5
- refactor: with the fsio module exported, `dsh-plugin-wsl-env` deletes its
6
- in-plugin replicas (`lib/fsio-text.js`) and imports the originals.
3
+ Status: draft, not yet filed upstream — no discussion link yet (the filing
4
+ convention is [UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md)).
5
+ Targeting `deepseek-harness` @ `packages/fs/fs-local`, the `0.2.1-rc.x` line
6
+ this plugin pins exactly (`@deepseek-ai/dsh-fs-local@0.2.1-rc.2`, in both the
7
+ dev and peer dependency blocks of `package.json`). This is the Shape-A
8
+ upstream half of the in-distro I/O refactor: with the fsio text mechanics
9
+ importable, `dsh-plugin-wsl-env` deletes its copies of them.
7
10
 
8
11
  ## The change
9
12
 
10
- The fsio module already exists, is fully documented, and is pure (no peers
11
- beyond types). It is only not reachable: `package.json` exports `"."` and
12
- `"./package.json"`, and `lib/index.js` does not re-export the helpers. Two
13
- lines of surface, no behaviour change:
13
+ The fsio module already exists and is fully documented in the shipped
14
+ declarations (`lib/types/fsio.d.ts`). Two facts keep it out of reach:
14
15
 
15
- ```diff
16
- --- a/packages/fs/fs-local/package.json
17
- +++ b/packages/fs/fs-local/package.json
18
- @@
19
- "exports": {
20
- ".": {
21
- "types": "./lib/types/index.d.ts",
22
- "default": "./lib/index.js"
23
- },
24
- + "./fsio": {
25
- + "types": "./lib/types/fsio.d.ts",
26
- + "default": "./lib/fsio.js"
27
- + },
28
- "./src/*": "./src/*",
29
- "./package.json": "./package.json"
30
- },
31
- ```
16
+ - `package.json` exports exactly three entries — `"."`, `"./src/*"` and
17
+ `"./package.json"` (lines 16–23). `"./src/*"` is not a way in: `files` is
18
+ `["lib/index.js", "lib/types/**/*.d.ts"]` (lines 24–27), so the sources are
19
+ in the repository but not in the tarball (`npm pack --dry-run` lists nine
20
+ files, none of them under `src/`).
21
+ - The published runtime is one bundled file. The fsio code is a region of
22
+ `lib/index.js` (`//#region lib/types/fsio.js`, line 90) — there is no
23
+ `lib/fsio.js` — and that file's only export statement is
24
+ `export { LocalFileSystem, LocalFileSystem as default };` (line 909).
25
+
26
+ So the reachable seam is the package entry, not a new subpath: re-export the
27
+ four text helpers from it, in the runtime and in the declarations.
28
+
29
+ (A `"./fsio"` subpath does not work as-is — the runtime file it would name does
30
+ not exist. Pointing its `types` at `lib/types/fsio.d.ts` instead would publish
31
+ the whole fsio surface — `readWholeText`, `readWholeBytes`, `readByteWindow`,
32
+ `streamWholeText`, `writeFileAtomic`, `probe`, `probeNoFollow`,
33
+ `listDirectory`, `resolveLocalTarget`, `readForEdit`, `readTextForDiff`, … —
34
+ while the runtime side offered four names. Types and runtime must promise the
35
+ same surface.)
32
36
 
33
37
  ```diff
34
38
  --- a/packages/fs/fs-local/lib/index.js
35
39
  +++ b/packages/fs/fs-local/lib/index.js
36
- @@
40
+ @@ -906,4 +906,11 @@
41
+ }
42
+ };
43
+ //#endregion
37
44
  -export { LocalFileSystem, LocalFileSystem as default };
38
45
  +export {
39
- + LocalFileSystem,
40
- + LocalFileSystem as default,
41
- + applyLiteralEdit,
42
- + detectLineEndings,
43
- + normalizeLineEndings,
44
- + restoreLineEndings,
46
+ + LocalFileSystem,
47
+ + LocalFileSystem as default,
48
+ + applyLiteralEdit,
49
+ + detectLineEndings,
50
+ + normalizeLineEndings,
51
+ + restoreLineEndings,
45
52
  +};
46
53
  ```
47
54
 
48
- (The module layout differs between the repo's `src/` and the built `lib/`;
49
- the export list is what matters. `readForEdit` and `readTextForDiff` stay
50
- internal — they are I/O orchestration, which the plugin drives against its own
51
- substrate — only the pure text mechanics are needed.)
55
+ ```diff
56
+ --- a/packages/fs/fs-local/lib/types/index.d.ts
57
+ +++ b/packages/fs/fs-local/lib/types/index.d.ts
58
+ @@ -66,4 +66,5 @@
59
+ private versionAfterWrite;
60
+ }
61
+ export default LocalFileSystem;
62
+ +export { applyLiteralEdit, detectLineEndings, normalizeLineEndings, restoreLineEndings } from './fsio.ts';
63
+ //# sourceMappingURL=index.d.ts.map
64
+
65
+ ```
66
+
67
+ Both files are generated; the change itself belongs in the sources that emit
68
+ them (`src/index.ts` re-exporting from the fsio module, and that module
69
+ exporting `detectLineEndings`). The artifact diffs are shown because they are
70
+ what a consumer resolves — and they apply as-is to the published `0.2.1-rc.2`
71
+ files.
72
+
73
+ `detectLineEndings` is the one genuinely new name: it is not in
74
+ `lib/types/fsio.d.ts` at all — in the bundle it is a non-exported function
75
+ (`function detectLineEndings(raw)`, line 587). Declaring it is a
76
+ `declare function detectLineEndings(raw: string): LineEndings;` beside its two
77
+ siblings, plus the name in the export list at `fsio.d.ts:223` (`LineEndings`
78
+ itself is already exported, `:166`). The other three are already declared
79
+ there: `normalizeLineEndings` (`:173`), `restoreLineEndings` (`:182`),
80
+ `applyLiteralEdit` (`:219`), with the first two exported at `:223`. If upstream
81
+ would rather not widen the surface at all, leave `detectLineEndings` out and the
82
+ consumer keeps its four-line local copy.
83
+
84
+ `readForEdit` and `readTextForDiff` are not part of this. They are exported by
85
+ the fsio module (`fsio.d.ts:191`, `:207`), but they are I/O orchestration —
86
+ which the plugin drives against its own substrate — so the proposal simply does
87
+ not re-export them.
52
88
 
53
89
  ## Why the plugin needs it
54
90
 
@@ -56,27 +92,100 @@ substrate — only the pure text mechanics are needed.)
56
92
  `lib/fs-substrate.js`) re-runs fsio's orchestration inside a WSL distro: reads,
57
93
  writes, identities and edits execute where the files live, and the host side
58
94
  keeps only the validation and edit mechanics. Those mechanics —
59
- `applyLiteralEdit`, the line-ending trio, the binary NUL sample, the fatal UTF-8
60
- decode — are replicated today (`lib/fsio-text.js`) because the package does not
61
- export them. The replica is byte-for-byte faithful, including error messages,
62
- because the tool layer matches on both the code and the text the model reads;
63
- but a replica is a fork surface: every upstream change to the edit algorithm
64
- has to be re-ported by hand.
95
+ `applyLiteralEdit`, the line-ending trio, the binary NUL sample, the fatal
96
+ UTF-8 decode — are replicated today in `src/fsio-text.ts` (built to
97
+ `lib/fsio-text.js`) because the package does not export them. The replica is
98
+ byte-for-byte faithful, including error messages, because the tool layer
99
+ matches on both the code and the text the model reads — and that faithfulness
100
+ is verified, not assumed: [PEER-PARITY.md](PEER-PARITY.md) records the
101
+ function-by-function and string-by-string comparison against the pinned peer's
102
+ shipped code (verified 2026-10-03; the read/edit split re-verified
103
+ 2026-10-06). But a replica is a fork surface: every upstream change to the edit
104
+ algorithm has to be re-ported by hand.
65
105
 
66
- With the export in place the plugin deletes `lib/fsio-text.js` and imports:
106
+ With the export in place the plugin imports the four functions and deletes its
107
+ copies:
67
108
 
68
109
  ```js
69
- import { applyLiteralEdit, detectLineEndings, normalizeLineEndings, restoreLineEndings } from "@deepseek-ai/dsh-fs-local/fsio";
110
+ import { applyLiteralEdit, detectLineEndings, normalizeLineEndings, restoreLineEndings } from "@deepseek-ai/dsh-fs-local";
70
111
  ```
71
112
 
113
+ The rest of the module stays. `BINARY_SAMPLE_BYTES`, `FsCodedError`,
114
+ `notTextError`, `decodeUtf8`, `decodeUtf8Stream` and `throwIfAborted`
115
+ (`src/fsio-text.ts:23`, `:30`, `:53`, `:67`, `:86`, `:179`) have no counterpart
116
+ in the fsio declarations, and the module's other consumers keep importing them
117
+ locally (`src/fsio-agent.ts:27-36`, `src/fs-substrate.ts:37`,
118
+ `src/index.ts:57`). `lib/fsio-text.js` is not deleted; four of its ten exports
119
+ are.
120
+
121
+ Importing from the entry costs this consumer nothing new: it already imports
122
+ `LocalFileSystem` from the same entry (`src/index.ts:45`,
123
+ `src/fs-routing.ts:35`) and `@deepseek-ai/dsh-fs` (`src/index.ts:46`). The
124
+ entry's non-builtin static imports are `chokidar` (`lib/index.js:3`),
125
+ `@deepseek-ai/schemastery` (`:6`) and the `@deepseek-ai/dsh-fs` peer (`:7`) —
126
+ all three already in this consumer's graph; `koffi` loads lazily (`await
127
+ import("koffi")`, `:25`), so no native dependency is evaluated at import time.
128
+
72
129
  ## Why this is safe upstream
73
130
 
74
- - No new API surface beyond re-exporting functions that already exist and are
75
- documented in `lib/types/fsio.d.ts`.
131
+ - Additive: three of the four names are already declared in the shipped
132
+ `lib/types/fsio.d.ts`; `detectLineEndings` is one new pure function the edit
133
+ path already calls (`readForEdit`, `lib/index.js:629` — the class reaches it
134
+ through `editText`).
76
135
  - No behaviour change: the class's own call sites are untouched.
77
- - The functions are pure (text in, text out, structured errors), so exporting
78
- them cannot widen the I/O contract.
79
- - Semver: additive export, a minor bump.
136
+ - The functions do no I/O and hold no state — text in, text out, structured
137
+ errors — so exporting them cannot widen the I/O contract.
138
+ - They are pure but **not peer-free**: the module they live in imports
139
+ `FsError`, `FsTargetKey` and `FsVersion` from the `@deepseek-ai/dsh-fs` peer
140
+ (`lib/index.js:7`) and uses them throughout (`FsError` thrown at `:700`,
141
+ `:703`, `:704`; `FsTargetKey` at `:175`, `:196`, `:207`; `FsVersion` at
142
+ `:146`), so importers of these helpers take that peer at runtime. They do not
143
+ take cordis — the bundle imports none. The plugin accepts the peer
144
+ dependency: both packages are already runtime imports of it, and its tool
145
+ layer maps the `code` token and the message, not the error class.
146
+ - Semver: an additive export. On the `0.2.0-rc.x` line it ships in the next
147
+ pre-release — the `0.2` position does not move, so this is not a minor bump
148
+ of a released line; and because the consuming plugin pins the exact version,
149
+ adoption is an explicit pin bump, never an automatic upgrade.
150
+
151
+ ## Landing it (consumer side)
152
+
153
+ 1. Bump both pins in `package.json` (`devDependencies` and `peerDependencies`)
154
+ to the peer that carries the export.
155
+ 2. `pnpm run build` — `lib/` is committed, and `test/build-freshness.mjs`
156
+ enforces that it is a byte-identical, closed image of `src/`.
157
+ 3. Re-run the comparison in [PEER-PARITY.md](PEER-PARITY.md) (its Maintenance
158
+ section) against the new pin before trusting anything else.
159
+ 4. Delete the four replicas and point the call sites at the peer, then
160
+ `pnpm test`.
161
+
162
+ Files that move with it:
163
+
164
+ - `src/fsio-text.ts` — drop `applyLiteralEdit`, `detectLineEndings`,
165
+ `normalizeLineEndings`, `restoreLineEndings`; keep the rest.
166
+ - `src/fsio-agent.ts` (imports `:27-36`, `detectLineEndings` used at `:528`)
167
+ and `src/fs-substrate.ts` (`:37`) — take the four from the peer. Where they
168
+ land decides the module's peer story: importing them directly keeps
169
+ `src/fsio-text.ts` peer-free — and `test/fsio-text.test.mjs`, which imports
170
+ nothing but node builtins and the local module (`:9-18`), runnable without
171
+ the peer — while re-exporting them through it does not. Either way the
172
+ "peer-free" self-descriptions in `src/fsio-agent.ts:14-16` and
173
+ `src/fs-substrate.ts:25` need a pass.
174
+ - `lib/*` — the rebuilt artifacts.
175
+ - `test/fsio-text.test.mjs` (the replica's tests, including the
176
+ `detectLineEndings` checks at `:69-72`) and `test/fsio-agent.test.mjs:22`,
177
+ which imports two of the four from `lib/fsio-text.js`. If that test file is
178
+ dropped or renamed, also the `test:coverage` and `test:unit` lists at
179
+ `package.json:114` and `:116`.
180
+ - The documents that describe the replica as current: `docs/ARCHITECTURE.md:173`
181
+ and `:416` (the two entries marked "replicated … pending the … export"),
182
+ `docs/PEER-PARITY.md:3` and `:17` (plus its Maintenance section),
183
+ `docs/upstream/promotion-map.md:82` — and this file, whose status then becomes
184
+ "landed".
185
+
186
+ Done when: the pinned peer resolves the four names from both its runtime entry
187
+ and its declarations, the plugin imports them there, `pnpm test` passes, and no
188
+ document still calls `lib/fsio-text.js` a replica of them.
80
189
 
81
190
  ## Follow-up (separate proposal, not required)
82
191
 
@@ -2,17 +2,28 @@
2
2
 
3
3
  Status: proposed upstream as
4
4
  [deepseek-ai/deepseek-harness#8769](https://github.com/deepseek-ai/deepseek-harness/discussions/8769)
5
- (targeting `packages/api/terminal-controller`, version line `0.2.0-rc.x`). This
6
- is the record of a gap this plugin worked around, could not work around, and
7
- now documents: the GUI terminal titles every tab from the composition's single
8
- shell profile, so a per-session shell choice is invisible in the title.
5
+ (targeting `packages/api/terminal-controller`, version line `0.2.1-rc.x`; the
6
+ gap was re-checked against `dsh-v0.2.1-rc.2` on 2026-10-17 and is unchanged —
7
+ `packages/api/terminal-controller/src/index.ts:327` still builds the terminal
8
+ info with `title: shell.name`, and the spawn spec still carries no display-name
9
+ field). The
10
+ plugin worked around the execution half of this gap — a Windows-folder
11
+ session's terminal runs a host shell (`subprocess-wsl.hostSessions`) — and
12
+ could not work around the title half. This is the record: the GUI titles every
13
+ tab from the composition's single shell profile, so a per-session shell choice
14
+ is invisible in the title.
9
15
 
10
16
  ## The gap
11
17
 
12
- `TerminalController.spawn` fixes a terminal's display title from the resolved
13
- shell profile before the provider is ever consulted:
18
+ The display title is fixed from the resolved shell profile and never revisited.
19
+ The private `TerminalController.spawn` builds `info`, with `title: shell.name`,
20
+ only after `await subprocess.spawnTerminal(...)` has returned, and it does not
21
+ read back what that call started:
14
22
 
15
23
  ```ts
24
+ const handle = await subprocess.spawnTerminal({
25
+ argv: [shell.path, ...shell.args], cwd: environment.cwd, cols: request.cols, ...
26
+ })
16
27
  const info: WebTerminalInfo = {
17
28
  id: request.id, shell, title: shell.name, cwd: environment.cwd, ...
18
29
  }
@@ -48,10 +59,18 @@ Three escape hatches were evaluated against `0.2.0-rc.2` and each is closed:
48
59
  inserts `terminal-controller`, and the client model that the
49
60
  `dsh-client-ui-sidebar-terminal` entry waits for mounts with
50
61
  `inject = ['remote', 'remote.terminal']` — the `terminal` namespace the host
51
- controller registers in its constructor. Disabling the row fails web boot:
52
- `1 entry did not activate: @deepseek-ai/dsh-client-ui-sidebar-terminal:
53
- pending (waiting for service: webTerminals)`. Verified live against
54
- `dsh-desktop 0.2.0-rc.2`.
62
+ controller registers in its constructor. Disabling the row fails web boot
63
+ with this two-line error:
64
+
65
+ ```text
66
+ web boot: 1 entry did not activate
67
+ @deepseek-ai/dsh-client-ui-sidebar-terminal: pending (waiting for service: webTerminals)
68
+ ```
69
+
70
+ Verified live against `dsh-desktop 0.2.0-rc.2` (Electron 44.0.0); the
71
+ row-id and `inject` facts the argument rests on are readable in the
72
+ `dsh-v0.2.1-rc.2` tree and did not change with the terminal controller's
73
+ 0.2.1 rework (`attach`/`write` → sequenced `TerminalInput`).
55
74
  3. **Per-session config.** The `shell` profile is composition state; `!!js`
56
75
  expressions evaluate once at config resolution and reach no Session
57
76
  identity.
@@ -62,7 +81,7 @@ Any one of these closes the gap; the first is the smallest and keeps the
62
81
  profile name as the fallback:
63
82
 
64
83
  - `SubprocessTerminalSpawnSpec` gains an optional `displayTitle` (or the handle
65
- exposes the launched `argv`), and `create()` uses it for `info.title` when
84
+ exposes the launched `argv`), and `spawn()` uses it for `info.title` when
66
85
  present — falling back to `shell.name`. A provider that rewrites a launch
67
86
  states the program it started; every other launch keeps the profile name.
68
87
  - Alternatively, the controller derives the title from the effective program
@@ -71,11 +90,34 @@ profile name as the fallback:
71
90
 
72
91
  With either, this plugin deletes the limitation paragraph in
73
92
  [LIMITATIONS.md](LIMITATIONS.md) and the `WSL`-over-PowerShell title, for any
74
- provider that rewrites launches — not just WSL.
93
+ provider that rewrites launches — not just WSL. The same limitation is stated
94
+ as current fact in four places. Three go stale the day the proposal lands; the
95
+ fourth is already out of step and needs the corrections above whenever it is
96
+ next touched:
97
+
98
+ - the GUI-terminal comment block in [`cordis.patch.yml`](../cordis.patch.yml);
99
+ - the closing paragraph of "Why the terminal provider is app-level" in
100
+ [ARCHITECTURE.md](ARCHITECTURE.md);
101
+ - R4's `残差` (residual) column in [REQUIREMENTS.zh.md](REQUIREMENTS.zh.md);
102
+ - the `WSL` tab-title bullet under "Terminals" in
103
+ [LIMITATIONS.md](LIMITATIONS.md) — the paragraph named just above. It says
104
+ the controller titles a tab *before* the provider rewrites the launch, the
105
+ reverse of the ordering corrected at the top of this file, and it carries the
106
+ one-line `shellCandidates` workaround that drops the row's `shell` block.
75
107
 
76
108
  ## Workarounds until then
77
109
 
78
110
  - Double-click a tab's title to rename it (a shipped client feature; the
79
111
  controller's `rename` Remote API accepts 1–120 characters).
80
- - Add `shellCandidates` to the `terminal-controller` row in a user layer: a
81
- manually selected shell is titled after itself.
112
+ - Re-declare the `terminal-controller` row in a user layer with a non-empty
113
+ `shellCandidates`: a manually selected shell is titled after itself. A patch
114
+ config replaces the whole config rather than deep-merging it
115
+ (`packages/boot/app-boot/src/config-schema/document.ts`), so the override has
116
+ to carry this plugin's `shell` block verbatim — `path: wsl.exe`, `name: WSL`,
117
+ `args: []` — or the terminal stops being a distro terminal. Expect the menu
118
+ to grow the host shells discovery finds on the root provider, which receives
119
+ no directory: executables this terminal never starts (the second bullet of
120
+ the GUI-terminal comment in [`cordis.patch.yml`](../cordis.patch.yml)).
121
+ [LIMITATIONS.md](LIMITATIONS.md) still carries the older one-line form of
122
+ this tip, which drops the `shell` block; the version above is the one that
123
+ works.
@@ -0,0 +1,178 @@
1
+ # A profile-patch fragment for `$DSH_HOME/profiles/<name>/cordis.patch.yml`.
2
+ # It is the plugin's ENTIRE `preset-wsl` row (a row's config is replaced whole,
3
+ # never deep-merged) with `sessionSandbox: true` on the `wsl-shell` AND
4
+ # `wsl-fs` plugins.
5
+ #
6
+ # Paste it in place of any existing `- id: preset-wsl` entry in that profile
7
+ # layer, then restart the app. Regenerate it from the plugin's
8
+ # `cordis.patch.yml` whenever the shipped preset changes.
9
+
10
+ - id: preset-wsl
11
+ name: '@deepseek-ai/dsh-agent-preset'
12
+ config:
13
+ id: wsl
14
+ order: 2
15
+ plugins:
16
+ - id: wsl-env
17
+ name: cordis:group
18
+ group: true
19
+ isolate:
20
+ shell: true
21
+ fs: true
22
+ config:
23
+ # ── the WSL environment for this preset only ────────────────────
24
+ # No `cwd`: an empty default workdir means the distro user's home,
25
+ # asked of the distro itself. Pin a directory here to override it —
26
+ # a value that is right on one machine is wrong on every other.
27
+ #
28
+ # Commands run in one persistent systemd user scope per session
29
+ # (ADR 0001): a background dev server survives the command that
30
+ # started it, `/tmp` is the distro's real `/tmp`, and `ps` sees the
31
+ # session. This needs systemd as PID 1 (`[boot] systemd=true` in
32
+ # /etc/wsl.conf) and a lingering user manager (`loginctl
33
+ # enable-linger <user>`); the preflight refuses with those remedies
34
+ # when one is missing. Set `sessionSandbox: false` for the old
35
+ # per-command shape. Bubblewrap is still required for the mount
36
+ # profile; without it the providers fail closed.
37
+ - id: wsl-shell
38
+ name: 'dsh-plugin-wsl-env'
39
+ config:
40
+ distro: ''
41
+ loginShell: true
42
+ timeoutMs: 120000
43
+ maxTimeoutMs: 600000
44
+ sessionSandbox: true
45
+ agentBackend: auto
46
+ - id: wsl-fs
47
+ name: 'dsh-plugin-wsl-env/fs'
48
+ config:
49
+ distro: ''
50
+ restrictToDistro: true
51
+ # agent (the default): reads/writes/identities run inside the
52
+ # distro over the resident agent (lib/fs-substrate.js) —
53
+ # native symlinks and mode bits, the write guard survives to
54
+ # publication, and confined mutations are kernel-enforced.
55
+ # share: the opt-out — the Windows-side host fs stack over
56
+ # the 9p share.
57
+ substrate: agent
58
+ # Confined writes ride the session runtime (ADR 0001), so they
59
+ # share the command's real /tmp and workspace mounts.
60
+ sessionSandbox: true
61
+ - id: persona
62
+ name: '@deepseek-ai/dsh-persona'
63
+ config:
64
+ prefix: You are a coding agent powered by the {{model}} model.
65
+ - id: agent-instructions
66
+ name: '@deepseek-ai/dsh-agent-instructions'
67
+ config:
68
+ maxBytes: 65536
69
+ - id: time-context
70
+ name: '@deepseek-ai/dsh-time-context'
71
+ - id: tool-bash
72
+ name: '@deepseek-ai/dsh-tool-bash'
73
+ disabled: false
74
+ - id: tool-pwsh
75
+ name: '@deepseek-ai/dsh-tool-pwsh'
76
+ disabled: true
77
+ - id: tool-fs
78
+ name: '@deepseek-ai/dsh-tool-fs'
79
+ - id: tool-fs-search
80
+ name: '@deepseek-ai/dsh-tool-fs-search'
81
+ config:
82
+ sampleOverCapGlobResults: false
83
+ - id: tool-jobs
84
+ name: '@deepseek-ai/dsh-tool-jobs'
85
+ - id: tool-schedule
86
+ name: '@deepseek-ai/dsh-tool-schedule'
87
+ - id: skill-filesystem
88
+ name: '@deepseek-ai/dsh-skill-filesystem'
89
+ - id: tool-skill
90
+ name: '@deepseek-ai/dsh-tool-skill'
91
+ - id: command-goal
92
+ name: '@deepseek-ai/dsh-command-goal'
93
+ - id: tool-goal
94
+ name: '@deepseek-ai/dsh-tool-goal'
95
+ - id: planning
96
+ name: cordis:group
97
+ group: true
98
+ isolate:
99
+ planMode: true
100
+ config:
101
+ - id: plan-mode
102
+ name: '@deepseek-ai/dsh-plan-mode'
103
+ config:
104
+ section: |
105
+ You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
106
+
107
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
108
+
109
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
110
+
111
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
112
+
113
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
114
+
115
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
116
+ - id: compaction
117
+ name: cordis:group
118
+ group: true
119
+ isolate:
120
+ compaction: true
121
+ toolResultPruner: true
122
+ config:
123
+ - id: compaction-basic
124
+ name: '@deepseek-ai/dsh-compaction-basic'
125
+ - id: command-compact
126
+ name: '@deepseek-ai/dsh-command-compact'
127
+ - id: tool-result-pruner
128
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
129
+ config:
130
+ thresholdChars: 8192
131
+ headChars: 4096
132
+ tailChars: 1024
133
+ - id: delegation
134
+ name: cordis:group
135
+ group: true
136
+ isolate:
137
+ workflowEngine: true
138
+ config:
139
+ - id: tool-subagent-control
140
+ name: '@deepseek-ai/dsh-tool-subagent-control'
141
+ - id: tool-subagent-list-agents
142
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
143
+ - id: tool-subagent
144
+ name: '@deepseek-ai/dsh-tool-subagent'
145
+ config:
146
+ provider: spawn
147
+ toolName: subagent
148
+ modelSelectionSettings: true
149
+ - id: tool-subagent-fork
150
+ name: '@deepseek-ai/dsh-tool-subagent'
151
+ config:
152
+ provider: fork
153
+ toolName: subagent_fork
154
+ - id: workflow-ptc
155
+ name: '@deepseek-ai/dsh-workflow-ptc'
156
+ config:
157
+ provider: spawn
158
+ - id: tool-workflow
159
+ name: '@deepseek-ai/dsh-tool-workflow'
160
+ - id: tool-ask-user
161
+ name: '@deepseek-ai/dsh-tool-ask-user'
162
+ - id: tool-todo
163
+ name: '@deepseek-ai/dsh-tool-todo'
164
+ config:
165
+ allowParallelInProgress: true
166
+ - id: tool-web
167
+ name: '@deepseek-ai/dsh-tool-web'
168
+ config:
169
+ fetch: true
170
+ searchTimeoutMs: 60000
171
+ - id: present
172
+ name: '@deepseek-ai/dsh-tool-present'
173
+ - id: tool-plugin-manager
174
+ name: '@deepseek-ai/dsh-plugin-manager/tools'
175
+ disabled: true
176
+ - id: tool-plugin-market
177
+ name: '@deepseek-ai/dsh-plugin-market/tools'
178
+ disabled: true
@@ -14,7 +14,7 @@
14
14
  * @module dsh-plugin-wsl/agent-protocol
15
15
  */
16
16
  /** The protocol version this host side speaks; the agent must match. */
17
- export declare const PROTOCOL_VERSION = 4;
17
+ export declare const PROTOCOL_VERSION = 5;
18
18
  /**
19
19
  * Upper bound on one `read` op's payload, set by the HOST looping over windowed
20
20
  * requests. Keeping each frame's decoded payload here means the agent's RES
@@ -127,15 +127,21 @@ export interface FsFrameRequest {
127
127
  */
128
128
  export declare function encodeFsFrame({ id, op, args, timeoutMs }: FsFrameRequest): string[];
129
129
  /**
130
- * `HELLO|name|version|digest` — `digest` is the sha256 of the script file the
131
- * agent is executing (empty when it could not hash it); the host compares it
132
- * against its own shipped copy.
130
+ * `HELLO|name|version|digest|pid` — `digest` is the sha256 of the script file
131
+ * the agent is executing (empty when it could not hash it); the host compares
132
+ * it against its own shipped copy. `pid` is the agent shell's PID inside the
133
+ * distro, present from protocol 5 on: the host uses it to deliver an
134
+ * out-of-band `USR1` cancellation while the agent is blocked in `wait`, which
135
+ * the serial stdin loop cannot do by frame. It is absent when the agent
136
+ * itself runs inside a PID namespace (the confined resident), where that PID
137
+ * is not addressable from a second `wsl.exe`.
133
138
  */
134
139
  export interface HelloMessage {
135
140
  type: "hello";
136
141
  name: string;
137
142
  version: number;
138
143
  digest: string;
144
+ pid?: number;
139
145
  }
140
146
  /** `PONG` — the liveness answer to `PING`. */
141
147
  export interface PongMessage {
@@ -14,7 +14,7 @@
14
14
  * @module dsh-plugin-wsl/agent-protocol
15
15
  */
16
16
  /** The protocol version this host side speaks; the agent must match. */
17
- export const PROTOCOL_VERSION = 4;
17
+ export const PROTOCOL_VERSION = 5;
18
18
  /**
19
19
  * Upper bound on one `read` op's payload, set by the HOST looping over windowed
20
20
  * requests. Keeping each frame's decoded payload here means the agent's RES
@@ -124,8 +124,15 @@ export function encodeFsFrame({ id, op, args, timeoutMs = 0 }) {
124
124
  */
125
125
  export function parseAgentLine(line) {
126
126
  if (line.startsWith("HELLO|")) {
127
- const [, name, version, digest] = line.split("|");
128
- return { type: "hello", name: name ?? "", version: Number(version), digest: digest ?? "" };
127
+ const [, name, version, digest, pid] = line.split("|");
128
+ const agentPid = Number(pid);
129
+ return {
130
+ type: "hello",
131
+ name: name ?? "",
132
+ version: Number(version),
133
+ digest: digest ?? "",
134
+ ...(Number.isSafeInteger(agentPid) && agentPid > 0 ? { pid: agentPid } : {}),
135
+ };
129
136
  }
130
137
  if (line === "PONG")
131
138
  return { type: "pong" };