dsh-plugin-wsl-env 0.15.0 → 0.17.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 (61) hide show
  1. package/CHANGELOG.md +144 -15
  2. package/CONTRIBUTING.md +25 -33
  3. package/README.md +10 -10
  4. package/SECURITY.md +6 -4
  5. package/SUPPORT.md +8 -8
  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 +51 -12
  9. package/cordis.patch.yml +23 -10
  10. package/docs/ARCHITECTURE.md +56 -41
  11. package/docs/COMPATIBILITY.md +121 -0
  12. package/docs/CONFIGURATION.md +275 -273
  13. package/docs/LIMITATIONS.md +114 -38
  14. package/docs/MIGRATION.md +16 -13
  15. package/docs/PARITY.md +4 -3
  16. package/docs/PERFORMANCE.md +11 -9
  17. package/docs/QUICKSTART.md +46 -21
  18. package/docs/README.zh.md +7 -7
  19. package/docs/RELEASING.md +257 -0
  20. package/docs/REQUIREMENTS.zh.md +77 -0
  21. package/docs/ROADMAP-1.0.md +105 -61
  22. package/docs/ROADMAP.md +15 -9
  23. package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +3 -3
  24. package/docs/UPSTREAM-FSIO-EXPORT.md +7 -7
  25. package/docs/UPSTREAM-OPEN-IN-APP.md +57 -0
  26. package/docs/UPSTREAM-SESSION-NOTICE.md +4 -4
  27. package/docs/UPSTREAM-TERMINAL-TITLE.md +10 -12
  28. package/docs/VALUE.zh.md +144 -0
  29. package/docs/adr/0002-native-agent.md +348 -0
  30. package/docs/root-fs-routing.md +186 -0
  31. package/docs/screenshots/picker.png +0 -0
  32. package/docs/screenshots/preview.png +0 -0
  33. package/docs/screenshots/sandbox.png +0 -0
  34. package/docs/screenshots/terminal.png +0 -0
  35. package/lib/agent-exec.d.ts +7 -3
  36. package/lib/agent-protocol.d.ts +18 -5
  37. package/lib/agent-protocol.js +11 -6
  38. package/lib/agent-shared.js +1 -0
  39. package/lib/agent.d.ts +28 -9
  40. package/lib/agent.js +74 -14
  41. package/lib/fs-routing.d.ts +2 -1
  42. package/lib/fs-routing.js +13 -1
  43. package/lib/fs-substrate.d.ts +11 -9
  44. package/lib/fs-substrate.js +6 -9
  45. package/lib/fsio-agent.d.ts +2 -3
  46. package/lib/fsio-agent.js +2 -3
  47. package/lib/git-exec.d.ts +117 -0
  48. package/lib/git-exec.js +105 -0
  49. package/lib/git-route.d.ts +43 -13
  50. package/lib/git-route.js +54 -15
  51. package/lib/index.d.ts +20 -13
  52. package/lib/index.js +18 -11
  53. package/lib/session-runtime.d.ts +5 -3
  54. package/lib/session-runtime.js +5 -4
  55. package/lib/subprocess.d.ts +21 -7
  56. package/lib/subprocess.js +63 -9
  57. package/locale/en.json +1 -1
  58. package/locale/zh.json +1 -1
  59. package/package.json +13 -4
  60. package/scripts/diagnose.sh +1 -1
  61. package/scripts/doctor.mjs +23 -3
package/CHANGELOG.md CHANGED
@@ -6,15 +6,106 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  The reasoning and the measurements behind each entry are in the archived
8
8
  engineering record,
9
- [docs/archive/engineering-record.zh.md](docs/archive/engineering-record.zh.md)
9
+ [docs/archive/engineering-record.zh.md](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/engineering-record.zh.md)
10
10
  (Chinese, repository only — it is history, never a second README). A `§`
11
11
  reference below points at that record's numbering unless it names another
12
12
  document: the 0.1.0 entry's `design-history §` references are to
13
- [docs/archive/design-history.zh.md](docs/archive/design-history.zh.md), which
13
+ [docs/archive/design-history.zh.md](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/design-history.zh.md), which
14
14
  keeps the superseded designs under the section numbers they had while they
15
15
  were part of the record.
16
16
 
17
- ## [Unreleased]
17
+ ## [0.17.0] - 2026-11-01
18
+
19
+ ### Changed
20
+
21
+ - **Git snapshot spawns are now agent-first with a one-shot fallback.**
22
+ Protocol v6 adds a stdin payload to the EXEC frame (a seventh header field
23
+ flagging one trailing base64 line), which is what lets `workspace-changes`'
24
+ snapshot calls — including `check-ignore -z --stdin` — run on the warm
25
+ resident agent instead of a one-shot `wsl.exe`. Measured on `ubuntu`
26
+ (2026-11-01): ~232 ms per one-shot git call (nearly all `wsl.exe` process
27
+ startup) against ~1.6 ms on the resident — a 6-8 call snapshot saves well
28
+ over a second. The per-stream caps map onto the agent's single wire cap with
29
+ a client-side per-stream slice (`lib/git-exec.js`), and the environment
30
+ rides the agent's per-request `env` (`env --` argv prefix, no `SETENV`
31
+ pollution) carrying only the translated `GIT_*` names. Both agents speak v6
32
+ (the shell agent decodes the payload through command substitution, which
33
+ strips trailing newlines — NUL separators survive; the native binary
34
+ delivers bytes verbatim), so host library and shipped binaries upgrade
35
+ together; the doctor's handshake reports any mismatch.
36
+
37
+ - **WSL change review no longer requires Windows git.** When
38
+ `resolveExecutable('git')` fails on the host, the provider answers with a
39
+ distro sentinel (`/usr/bin/git`); every workspace-changes spawn — the
40
+ `rev-parse` discovery included — then routes into the distro, and the
41
+ snapshot facade translates the discovery call's absolute-path output lines
42
+ into UNC form (`translateDiscoveryOutput`). A machine with Windows git is
43
+ unchanged (host discovery + `safe.directory` whitelist). The
44
+ `resolveExecutable` override is scoped to the one name and only fires on
45
+ the super's failure.
46
+
47
+ - **The doctor probes the Windows git trust.** A new `git.win-safe` check
48
+ reports the `safe.directory` whitelist state through interop `git.exe`:
49
+ `ok` when the `\\wsl.localhost` share is whitelisted, a warn with the
50
+ copy-paste remedy when Windows git does not trust it, and an info (nothing
51
+ to do) when there is no Windows git — the distro fallback covers WSL change
52
+ review entirely.
53
+
54
+ ### Documentation
55
+
56
+ - The architecture and upstream-promotion documents reflect the v6 protocol
57
+ and the shell-menu candidates; the promotion map records the measurement
58
+ that superseded the git one-shot decision.
59
+ - The full real-machine suite re-ran 15/0 on 2026-11-01 after the protocol
60
+ change landed, and again after the documentation refresh. The safe.directory
61
+ prerequisite for WSL change review is now documented with its one-time
62
+ remedy.
63
+
64
+ - **The shell menu now offers host shells instead of a single entry.**
65
+ `terminal-controller`'s `shellCandidates` grows from `[]` to
66
+ `[wsl.exe, powershell, cmd]` while the default profile stays
67
+ `shell: { path: wsl.exe, name: WSL }`. `wsl.exe` dedupes into the default's
68
+ `wsl` kind; `powershell` and `cmd` are launches this terminal really starts —
69
+ host sessions already run PowerShell outright (`hostSessions`), and a manually
70
+ selected shell is titled after itself, which moves the tab-title workaround
71
+ from a user-layer edit into the plugin's own composition
72
+ ([#8769](https://github.com/deepseek-ai/deepseek-harness/discussions/8769)
73
+ remains the real fix for the default entry's title).
74
+
75
+ ## [0.16.0] - 2026-10-31
76
+
77
+ The schema-validated routing row: the root-plane router stops silently
78
+ ignoring a mistyped or wrong-typed distro key, and the first pass of the
79
+ community-optimization plan stamps the routing documentation and the 1.0
80
+ roadmap with dated, current evidence.
81
+
82
+ ### Changed
83
+
84
+ - **`fs-routing` row config is now validated at load time.**
85
+ `WslRoutingFileSystem.Config` no longer blindly reuses
86
+ `LocalFileSystem.Config`: it is a `z.intersect` of the host row's schema and
87
+ the distro-side keys (`distro`, `wslPath`, `distroCwd`, `sandbox`,
88
+ `maskWindowsDrive`, `restrictToDistro`, `watchMaxDepth`). The Loader now
89
+ refuses to mount the row when a key fails its schema — previously a typo or
90
+ a wrong type was silently ignored and the distro side ran on defaults. A
91
+ `null` field value still falls back to the schema default, which matches how
92
+ the Loader treats optional keys. Composition tests mount the row through the
93
+ real Loader to assert both the accepted defaults and the rejected values.
94
+
95
+ ### Fixed
96
+
97
+ - `docs/root-fs-routing.md` claimed a 12-probe suite with no routing probe, a
98
+ unit file outside the test lists, and a 10/10 composition test. All three
99
+ had drifted: the suite is 15 probes with `--include-fs` and includes
100
+ `fs-routing.sh`, `test/fs-routing.test.mjs` entered both test lists on
101
+ 2026-10-19 (OI-7), and the composition test is 12/12. A header note stamps
102
+ the 2026-10-31 review.
103
+ - `docs/ROADMAP-1.0.md` still described WSL-101 as implemented in the working
104
+ tree; it shipped in 0.13.0, and the status now records which acceptance leg
105
+ has real-machine evidence and which leg (concurrent two-distro) is still
106
+ missing. WSL-104's "routing probe has never been executed" claim is replaced
107
+ by the dated 2026-10-31 single-machine 15/0 result; the metrics row's probe
108
+ count moves to the current 15-with-fs breakdown.
18
109
 
19
110
  ## [0.15.0] - 2026-10-27
20
111
 
@@ -64,6 +155,44 @@ fixes the run surfaced, and the interop stance in one configuration key.
64
155
  fixture, and the picker probe now carries that fixture against the real
65
156
  composition. The LIMITATIONS entry retires.
66
157
 
158
+ ### Fixed
159
+
160
+ - **The nightly sandbox probe no longer crashes when the runner workspace sits
161
+ outside the C: drive.** The probe's bwrap-profile builder imported
162
+ `lib/bwrap.js` by a raw Windows path; on a self-hosted runner whose
163
+ workspace is on another drive the ESM loader rejects the `d:` scheme and
164
+ the first nightly run failed. The import now goes through
165
+ `pathToFileURL` (same hardening applied to the watcher probe).
166
+
167
+ - **The nightly probes job now tests the WSL topology, not the presence of
168
+ the binary.** The scheduled runs land on GitHub-hosted `windows-latest`
169
+ runners — no self-hosted runner carries that label, so the hosted fleet
170
+ picks the job up — and a hosted Windows image ships `wsl.exe` on PATH with
171
+ no distro behind it. The old `command -v wsl.exe` guard therefore always
172
+ passed, and the sandbox leg died inside wsl.exe itself: the last run
173
+ aborted on the `wslpath` translation with wsl.exe's own UTF-16 "no
174
+ installed distributions" surfacing as null bytes and exit 127 under the
175
+ step's `-e -o pipefail`. The guard now requires the default distro to
176
+ actually launch (`WSL_UTF8=1 wsl.exe -e true`); where it does not, the job
177
+ reports `skip all` — the honest no-op CONTRIBUTING now states. Where a
178
+ default distro does launch, the sandbox probe runs inside it through
179
+ `wsl.exe -e`, which is where the script's header and CONTRIBUTING already
180
+ place it: bubblewrap exists only there, so the leg could never pass from
181
+ Git Bash. The leg also picks up the `USER` resolution its sibling got in
182
+ the preceding fix (that hardening claimed `sandbox.sh` but changed only
183
+ `session-sandbox.sh`; a non-login `wsl.exe -e` shell needs it too). The
184
+ probe holds all 14 expectations under `env -u USER` in a distro.
185
+
186
+ ### Added
187
+
188
+ - **`scripts/open-wsl-vscode.ps1` — open a WSL folder in VS Code through
189
+ Remote-WSL.** Opening the UNC share in VS Code is the degraded mode: files
190
+ cross 9p, the terminal is PowerShell, extensions run on Windows. The
191
+ helper translates a WSL UNC path (or a Linux path + distro) into the
192
+ `vscode-remote://wsl+<distro>` folder URI, so VS Code's server runs inside
193
+ the distro — local ext4, distro shell, distro extensions. The quickstart
194
+ documents it next to the workspace-opening step.
195
+
67
196
  ### Changed
68
197
 
69
198
  - **The doctor gains a cross-distro sessions view (WSL-309) and marks live
@@ -281,7 +410,7 @@ entry points.
281
410
  complete `preset-wsl` block, applies `distro`/`sessionSandbox`/`sandbox`/
282
411
  `maskWindowsDrive`/`agentBackend`, and `--check` fails loudly with the exact
283
412
  block to paste when a layer carries the nested id patch. See
284
- [docs/CONFIGURATION.md](docs/CONFIGURATION.md#overriding-the-rows-inside-preset-wsl).
413
+ [docs/CONFIGURATION.md](docs/CONFIGURATION.md#configure-wsl-shell-and-wsl-fs).
285
414
 
286
415
  - **Session-notice upstream payload ready.** The exact title, body and `gh`
287
416
  command for the session-visible notice proposal are in
@@ -582,7 +711,7 @@ from it.
582
711
  longer the mechanism. The three deliberate deviations from the shipped copy
583
712
  — the `wsl-env` realm wrapping every row, the fixed shell-tool choice, and
584
713
  the preset's identity — are unchanged, and the transformation is now
585
- [scripts/build-preset-wsl.mjs](scripts/build-preset-wsl.mjs) rather than the
714
+ [scripts/build-preset-wsl.mjs](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/scripts/build-preset-wsl.mjs) rather than the
586
715
  machine-local generator the block used to name: it takes the shipped file and
587
716
  prints the row, and its output is the committed block byte for byte.
588
717
  - `docs/UPSTREAM-TERMINAL-FONT.md` closes: the `0.2.1-rc.2` line made the
@@ -2184,7 +2313,7 @@ could not work, and one where the cause of a failure was invisible.
2184
2313
  is re-probed on the next command, and concurrent first commands share one probe. The
2185
2314
  policy lives in [`lib/probe-cache.js`](lib/probe-cache.js), which is pure, so the
2186
2315
  regression is pinned on every platform including CI
2187
- ([`test/probe-cache.test.mjs`](test/probe-cache.test.mjs)).
2316
+ ([`test/probe-cache.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe-cache.test.mjs)).
2188
2317
  - `restrictToDistro` refused another distro's share with `FS_SANDBOX_DENIED`. The
2189
2318
  file-tool layer turns that code into a denial marker plus an escalation offer
2190
2319
  (`mapError` in `dsh-tool-fs`), and no wider permission can lift this fence: it is a
@@ -2259,7 +2388,7 @@ Repository and release engineering. No runtime behaviour changed.
2259
2388
 
2260
2389
  ### Added
2261
2390
 
2262
- - `npm run lint:style` ([`test/style.mjs`](test/style.mjs)): a dependency-free gate
2391
+ - `npm run lint:style` ([`test/style.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/style.mjs)): a dependency-free gate
2263
2392
  over the rules `.editorconfig` states, plus three packaging checks — every `files`
2264
2393
  entry still matches tracked content, every `exports` target exists, and exactly one
2265
2394
  readme candidate sits at the package root. That last one is the regression guard
@@ -2269,14 +2398,14 @@ Repository and release engineering. No runtime behaviour changed.
2269
2398
  spawning half of `lib/wsl.js` and the four service modules are covered by the
2270
2399
  probes instead, which is why the function threshold is lower than the others.
2271
2400
  - `.github/workflows/release.yml`: a `v*` tag is checked against `package.json`
2272
- ([`scripts/check-release-tag.mjs`](scripts/check-release-tag.mjs)), gated, then
2401
+ ([`scripts/check-release-tag.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/scripts/check-release-tag.mjs)), gated, then
2273
2402
  published with `--provenance` through npm trusted publishing, and the GitHub
2274
2403
  Release is created from the changelog section
2275
- ([`scripts/changelog-section.mjs`](scripts/changelog-section.mjs)). No npm token is
2404
+ ([`scripts/changelog-section.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/scripts/changelog-section.mjs)). No npm token is
2276
2405
  stored in the repository.
2277
2406
  - The governance files a public repository is expected to carry:
2278
2407
  [`SECURITY.md`](SECURITY.md), [`CONTRIBUTING.md`](CONTRIBUTING.md),
2279
- [`SUPPORT.md`](SUPPORT.md), [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md),
2408
+ [`SUPPORT.md`](SUPPORT.md), [`CODE_OF_CONDUCT.md`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/CODE_OF_CONDUCT.md),
2280
2409
  `CODEOWNERS`, a pull-request template, bug-report and contact-link issue templates,
2281
2410
  and a Dependabot configuration for the pinned workflow actions.
2282
2411
  - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) describes the plugin as it is
@@ -2285,16 +2414,16 @@ Repository and release engineering. No runtime behaviour changed.
2285
2414
  has to be reconstructed from a changelog and a historical record.
2286
2415
  - [`docs/RELEASING.md`](docs/RELEASING.md): the release checklist, including the
2287
2416
  operational facts that cost time to learn.
2288
- - [`docs/archive/README.en.md`](docs/archive/README.en.md): an English index of the
2417
+ - [`docs/archive/README.en.md`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/README.en.md): an English index of the
2289
2418
  Chinese archive.
2290
2419
  - `.nvmrc` names the Node version the coverage job uses.
2291
2420
 
2292
2421
  ### Changed
2293
2422
 
2294
2423
  - The probes no longer carry a user name, a home directory or a distro name.
2295
- [`test/probe/env.sh`](test/probe/env.sh) derives them from the machine
2424
+ [`test/probe/env.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/env.sh) derives them from the machine
2296
2425
  (`%USERPROFILE%`, `%LOCALAPPDATA%`, `WSL_DISTRO_NAME`) and
2297
- [`test/probe/env.mjs`](test/probe/env.mjs) does the same for the Node-side probes,
2426
+ [`test/probe/env.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/env.mjs) does the same for the Node-side probes,
2298
2427
  which learn their own location from `import.meta.url`. The YAML overlays are now
2299
2428
  templates with `@NAME@` placeholders, and the scripts substitute real values into
2300
2429
  generated copies that `.gitignore` keeps out of the tree. The two profile layers
@@ -2516,7 +2645,7 @@ and the package installs as a bundle that contributes its own profile layer.
2516
2645
  - The README is now **current state only**. The five sections describing designs
2517
2646
  that this one replaced — the per-process `DSH_WSL` switch and the three
2518
2647
  abandoned tool-naming routes — moved to
2519
- [docs/archive/design-history.zh.md](docs/archive/design-history.zh.md), which
2648
+ [docs/archive/design-history.zh.md](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/design-history.zh.md), which
2520
2649
  keeps their original numbering and records where each conclusion lives now.
2521
2650
  The README renumbered accordingly; its §0.1 explains the split. The one part of
2522
2651
  the old desktop section that is still current (regenerating the generated
@@ -2593,7 +2722,7 @@ Documentation that a reader would have acted on, and that was no longer true:
2593
2722
  current description of the plugin while its conclusions had been superseded
2594
2723
  three times over — most sharply by the sandbox work below — which is exactly
2595
2724
  the ambiguity a second README creates. It is now
2596
- [docs/archive/engineering-record.zh.md](docs/archive/engineering-record.zh.md):
2725
+ [docs/archive/engineering-record.zh.md](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/engineering-record.zh.md):
2597
2726
  the same content, renamed out of the README slot and banner-headed as history,
2598
2727
  with README.md the single statement of current state. Nothing was deleted from
2599
2728
  the record; §0.1 names the conclusions that are now historical.
package/CONTRIBUTING.md CHANGED
@@ -91,9 +91,8 @@ Three patterns keep the check honest:
91
91
  upstream seam gap, carries the reason inline, and the gap is written up — in a
92
92
  `docs/UPSTREAM-*.md` tracking doc when the fix is an upstream ask (the
93
93
  `LocalBashExecutor.spawnSpec` seam has one), or in the source comment itself
94
- when the honest fix needs a protocol decision this package owns (the exit-4
95
- relay's unknown entry kind is the other site, deliberately left visible). It
96
- is a published finding, not a silencer.
94
+ when the honest fix needs a protocol decision this package owns. The current
95
+ exception is the `spawnSpec` seam; it is a published finding, not a silencer.
97
96
 
98
97
  ## The build
99
98
 
@@ -194,6 +193,7 @@ terminal if you would rather not see the prompt.
194
193
  | `pnpm run probe` | filesystem behaviour against a real distro | yes |
195
194
  | `pnpm run probe:sandbox` | what bubblewrap confines and what it does not | no |
196
195
  | `pnpm run probe:sandbox-shell` | the confined executor through a real boot | yes |
196
+ | `pnpm run probe:interop-shell` | resident and one-shot interop for drive and copied executables, with and without the `/mnt` mask | yes |
197
197
  | `pnpm run probe:terminal` | the terminal provider | yes |
198
198
  | `pnpm run probe:substrate` | the agent filesystem substrate over a real `wsl.exe` transport | no; inside the distro |
199
199
  | `pnpm run probe:watch` | the in-distro watcher over a real directory | no; inside the distro |
@@ -226,13 +226,15 @@ still need Windows and WSL2, but no harness boot and no profile: run them from a
226
226
  distro terminal with Node available, and they reach `wsl.exe` through interop.
227
227
 
228
228
  Two conveniences for the real-machine set. `test/probe/run-all-when-closed.sh
229
- --include-fs` runs all twelve probes once the app has exited. And `ci.yml` has a
230
- `probes` job you dispatch by hand: `sandbox` runs whenever WSL is present, then
231
- `mode`, `terminal`, `picker` and `missing-wsl` where the harness app is
232
- installed. It is named for a self-hosted Windows runner although its `runs-on`
233
- carries the hosted `windows-latest` label, and it is not a green no-op on a
234
- hosted runner — the `sandbox` leg fails, not skips, where the default distro has
235
- no bubblewrap.
229
+ --include-fs` runs all fifteen probes once the app has exited. The interop shell
230
+ matrix uses the `wslfs` profile, systemd and a lingering user manager; it fails
231
+ when Windows interop or its temporary marker cannot be verified. And `ci.yml` has a
232
+ scheduled and manually dispatched `probes` job: `sandbox` runs whenever a default distro
233
+ actually launches, then `mode`, `terminal`, `picker` and `missing-wsl` where the
234
+ harness app is installed. It is named for a self-hosted Windows runner although
235
+ its `runs-on` carries the GitHub-hosted `windows-latest` label; when the hosted
236
+ runner has no usable distro the job is a green no-op (`skip all`). Where a default
237
+ distro launches without bubblewrap, the `sandbox` leg fails, not skips.
236
238
 
237
239
  ## Adding a test
238
240
 
@@ -245,19 +247,13 @@ Choose the layer by what the test needs.
245
247
 
246
248
  Unit tests run under `node --test`. The `test:unit` and the `test:coverage` lists
247
249
  in `package.json` name their files explicitly; a test that only touches this
248
- repository belongs in both. Two lags in those lists today are hygiene to fix in
249
- passing, not a pattern to copy: each names one file twice
250
- (`test/fsio-text.test.mjs` in `test:unit`, `test/fs.boot.test.mjs` in
251
- `test:coverage`), and `test/fs-routing.test.mjs` is in neither
252
- (`docs/REQUIREMENTS.zh.md` tracks that as OI-7).
250
+ repository belongs in both. Keep the lists synchronized without duplicate entries.
253
251
 
254
252
  A test that imports a pinned `@deepseek-ai/*` package goes in the same lists and
255
253
  needs no guarding — CI runs `pnpm install --frozen-lockfile` over the locked tree
256
- before `pnpm test`. Five files import such packages at runtime today —
254
+ before `pnpm test`. For example,
257
255
  `test/provider.test.mjs` is what covers the assembled service rows, and
258
- `test/composition.test.mjs` runs the loader's real patch algorithm — and a sixth
259
- (`test/terminal-activity.test.mjs`) reaches a peer only through a JSDoc type
260
- import, which `checkJs` still resolves.
256
+ `test/composition.test.mjs` runs the loader's real patch algorithm.
261
257
 
262
258
  Where a peer-free seam exists, prefer it. `test/sandbox.test.mjs` runs against
263
259
  `lib/sandbox-core.js`, with a stand-in for the peer's `SandboxUnavailableError`
@@ -269,14 +265,13 @@ must skip itself — print a `SKIP` line and exit 0 — the way that file does o
269
265
  `win32`, where its fake `wsl.exe` (a POSIX script) cannot execute.
270
266
 
271
267
  Probes are shell scripts, plus the small plugin files they load: the `*-probe.mjs`
272
- and `*-probe.yml` files in the same directory. Two shapes. The harness probes
273
- (`run.sh`, `terminal.sh`, `sandbox-shell.sh`, `picker.sh`, `fs-routing.sh`,
274
- `missing-wsl.sh`, `sandbox-off.sh`) boot a throwaway profile whose setup steps
275
- are in the header of `test/probe/run.sh`; the six others reuse the profile
276
- `run.sh` creates rather than making their own. The distro probes (`substrate.sh`, `watch.sh`, `agent.sh`,
277
- `exec.sh`) boot nothing: they run from a distro terminal and drive the resident
278
- agent through the `wsl.exe` interop transport. `sandbox.sh` and `mode.sh` sit
279
- outside both groups — no harness boot, no profile.
268
+ and `*-probe.yml` files in the same directory. Harness probes including `run.sh`,
269
+ `terminal.sh`, `sandbox-shell.sh`, `interop-shell.sh`, `picker.sh`,
270
+ `fs-routing.sh`, `missing-wsl.sh` and `sandbox-off.sh` use the throwaway profile
271
+ whose setup is documented in the header of `test/probe/run.sh`. The distro probes
272
+ (`substrate.sh`, `watch.sh`, `agent.sh`, `exec.sh`) boot no harness: they run
273
+ from a distro terminal and drive the resident agent through the `wsl.exe` interop
274
+ transport. `sandbox.sh` and `mode.sh` need neither a harness boot nor a profile.
280
275
 
281
276
  Keep probe output idempotent. Several probes delete their scratch files at the
282
277
  start, because a leftover file from an earlier run changes the result.
@@ -321,12 +316,9 @@ On Windows 11 + WSL2 (Ubuntu 26.04) the following has been verified:
321
316
  outside the session workspace is refused inside the distro and the following
322
317
  wider-permission request succeeds.
323
318
 
324
- Counts: 33 unit test files under `test/` — 32 of them registered in the
325
- `test:unit` list, see the OI-7 note above — which need the pinned packages
326
- installed, and over 1000 `assert` call sites across them; 19 filesystem-probe
327
- assertions; 15 shell-probe checks; 9 picker checks; 6 missing-executable checks;
328
- 5 mode checks; 5 opt-out checks and 10 sandbox expectations plus the recorded
329
- escape; and 3 terminal assertions.
319
+ The current unit and coverage test lists are in `package.json`; probe assertions
320
+ and prerequisites are in the individual scripts. Verification results describe
321
+ the tested environment and are not a substitute for rerunning the applicable gates.
330
322
 
331
323
  ## Good first issues and the contributor path
332
324
 
package/README.md CHANGED
@@ -6,19 +6,19 @@
6
6
  [![npm](https://img.shields.io/npm/v/dsh-plugin-wsl-env)](https://www.npmjs.com/package/dsh-plugin-wsl-env)
7
7
  [![license](https://img.shields.io/npm/l/dsh-plugin-wsl-env)](LICENSE)
8
8
 
9
- A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that runs a session inside a WSL 2 distribution on Windows.
9
+ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that opens the project workspace inside a WSL 2 distribution on Windows.
10
10
 
11
- The harness process stays on Windows. Commands, file operations, the folder picker and the GUI terminal use the distro. No helper service or daemon is installed, no port is opened, and confined commands run under `bubblewrap`.
11
+ The harness process stays on Windows. Command execution, shell scripts and file access all run in the distro's Linux environment for the full Linux shell toolchain. No helper service or daemon is installed, no port is opened, and confined commands run under `bubblewrap`.
12
12
 
13
13
  Windows + WSL 2 only.
14
14
 
15
- **New here?** Start with [docs/QUICKSTART.md](docs/QUICKSTART.md) — five minutes, prerequisites, recipes and the honest compatibility table.
15
+ **New here?** Start with [docs/QUICKSTART.md](docs/QUICKSTART.md) — prerequisites, recipes and the compatibility table.
16
16
 
17
17
  ## Why this plugin
18
18
 
19
19
  A Windows coding tool that works on WSL projects has three options: run the whole tool inside the distro, connect to a helper server inside the distro, or let the plugin provide the distro environment. This plugin is the third option. One workflow covers both kinds of session, with the distro's real tools and the command sandbox intact.
20
20
 
21
- - **Real Linux environment.** Commands run in the distro's own login shell. Its `PATH`, `nvm`, `cargo`, `pyenv` and rc files apply. File tools read and write the distro's real files on ext4.
21
+ - **Real Linux environment.** Commands run in the distro's own login shell. Its `PATH`, `nvm`, `cargo`, `pyenv` and rc files apply. File tools access distro paths through an in-distro agent; the backing filesystem depends on the path.
22
22
  - **No service in the distro.** There is no helper server, no daemon and no listening port. The only persisted artifact is a content-addressed copy of the native agent in `~/.cache/dsh-wsl/agent/`.
23
23
  - **Sandboxed by default.** Confined commands run under `bubblewrap` inside the distro. If `bubblewrap` is missing, the command fails; it does not run unconfined.
24
24
  - **Per-session.** A session opened on a distro folder gets the distro environment. A session opened on a Windows folder keeps the normal Windows tools. Both can run in one harness process.
@@ -74,7 +74,7 @@ Install `bubblewrap` before the first command:
74
74
  wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap
75
75
  ```
76
76
 
77
- From a checkout, `pnpm run doctor` is the one-command environment check: Windows/WSL versions, the distro and its shell, bubblewrap usability, systemd / the user manager / Linger, the optional tools, peer versions, the agent cache, a fresh-agent handshake, stale session scopes and the known profile-patch trap. Each failure prints one copy-paste remedy; `--json` is for scripts, and `--fix` sweeps stale scopes (package installation needs `--fix --install` and an explicit confirmation). `pnpm run diagnose` is its read-only alias. `pnpm run bootstrap -- <distro>` remains the narrower tool-install helper.
77
+ From a checkout, `pnpm run doctor` is the one-command environment check: Windows/WSL versions, the distro and its shell, bubblewrap usability, systemd / the user manager / Linger, the optional tools, peer versions, the agent cache, a fresh-agent handshake, stale session scopes and the known profile-patch trap. Each failure prints one copy-paste remedy; `--json` is for scripts, and `--fix` sweeps stale scopes (package installation needs `--fix --install` and an explicit confirmation). `pnpm run diagnose` is its read-only alias. `pnpm run bootstrap <distro>` remains the narrower tool-install helper.
78
78
 
79
79
  Upgrade by running the `add` command again. Uninstall with `dsh plugin --profile wsl remove dsh-plugin-wsl-env`; the agent cache under `~/.cache/dsh-wsl` can then be deleted. If you added a local `workspace-files-wsl` workaround to your profile to restore previews, remove it after upgrading: the plugin no longer mounts that row.
80
80
 
@@ -137,11 +137,11 @@ wsl preset (per session)
137
137
  | `workspace-write` | the above, plus the session workspace; `/tmp` is the distro's real `/tmp` with the session runtime |
138
138
  | `danger-full-access` | no sandbox |
139
139
 
140
- With `sessionSandbox: false`, each command gets a private `/tmp` instead of the distro's real one.
140
+ With `sessionSandbox: false`, a `workspace-write` command gets a private `/tmp` instead of the distro's real one. `read-only` does not mount a writable `/tmp`.
141
141
 
142
142
  `bubblewrap` is required. If it is missing or broken, confined commands report `SANDBOX_UNAVAILABLE` and do not run. Setting `sandbox: false` on the owning row disables confinement; the model is told that the operation is unsandboxed.
143
143
 
144
- Enforcement is reported as `partial`. A command inside the distro can still start a Windows executable through `/mnt/c`, and `bubblewrap` cannot control Windows processes. `maskWindowsDrive: true` hides `/mnt` from the confined view, which narrows the hole but does not close it. The complete fix is distro-level: set `[interop] enabled=false` in `/etc/wsl.conf`. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) and [SECURITY.md](SECURITY.md).
144
+ Enforcement is reported as `partial`. A command inside the distro can still start a Windows executable through `/mnt/c`, and `bubblewrap` cannot control Windows processes. `maskWindowsDrive: true` hides `/mnt` from the confined view unless overridden by `interopVisibility: visible`; this narrows the hole but does not close it. Disabling WSL interop requires a separate distro-level change: set `[interop] enabled=false` in `/etc/wsl.conf`. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) and [SECURITY.md](SECURITY.md).
145
145
 
146
146
  ## Troubleshooting
147
147
 
@@ -149,9 +149,9 @@ Enforcement is reported as `partial`. A command inside the distro can still star
149
149
  |---|---|
150
150
  | every command reports `SANDBOX_UNAVAILABLE` | install `bubblewrap` in the distro, or set `sandbox: false` |
151
151
  | the session refuses before the first command | run `pnpm run doctor`; enable systemd and linger, or set `sessionSandbox: false` on `wsl-shell` and `wsl-fs` |
152
- | `glob` and `grep` are slow in the distro | install `ripgrep` with `pnpm run bootstrap -- <distro> --install` |
152
+ | `glob` and `grep` are slow in the distro | run `pnpm run bootstrap <distro>` and use its printed install command to install `ripgrep` |
153
153
  | `dsh plugin add` prints nothing | the plugin is already installed; this is success |
154
- | a profile patch for `wsl-shell`/`wsl-fs` changes nothing | those rows are nested inside `preset-wsl`. Run `pnpm run preset -- --check <patch>`; it fails with the complete block to paste |
154
+ | a profile patch for `wsl-shell`/`wsl-fs` changes nothing | those rows are nested inside `preset-wsl`. Run `pnpm run preset --check <patch>`; it fails with the complete block to paste |
155
155
  | changes under `lib/` have no effect | run `pnpm run sync:windows`, then restart the app |
156
156
 
157
157
  More error codes and refusals are in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#error-codes). Everything the plugin does not do is in [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
@@ -174,7 +174,7 @@ Real-machine probes run on Windows + WSL only. See [CONTRIBUTING.md](CONTRIBUTIN
174
174
 
175
175
  | Document | Content |
176
176
  |---|---|
177
- | [docs/QUICKSTART.md](docs/QUICKSTART.md) | five-minute first run, recipes, compatibility |
177
+ | [docs/QUICKSTART.md](docs/QUICKSTART.md) | first run, recipes, compatibility |
178
178
  | [docs/PERFORMANCE.md](docs/PERFORMANCE.md) | ext4 agent vs 9p, measured latency, how to reproduce |
179
179
  | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | every configuration key and how to override it |
180
180
  | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | services, path routing, sandbox, error codes |
package/SECURITY.md CHANGED
@@ -88,14 +88,16 @@ This is a documented design boundary, not a vulnerability by itself. The command
88
88
  distro, where `bubblewrap` and `node` must be installed; the README states it in
89
89
  its Sandbox section.
90
90
 
91
- The strict profile (`maskWindowsDrive: true` with `sandbox: true`) narrows this:
92
- confined profiles shadow `/mnt` with an empty tmpfs, so the Windows executables
93
- cannot be launched from there. It does not close the hole — a Windows executable
91
+ The strict profile (`maskWindowsDrive: true` with `sandbox: true`, without an
92
+ overriding `interopVisibility: visible`) narrows this:
93
+ confined profiles shadow `/mnt` with an empty tmpfs, so executables at paths
94
+ under `/mnt` cannot be launched from there. Other mounts and unrestricted file
95
+ reads are not covered. It does not close the hole — a Windows executable
94
96
  written into the workspace still runs, because WSL's `binfmt_misc` dispatches on
95
97
  content, not location — so the reported enforcement stays `partial`. The complete
96
98
  closure is distro-level (`[interop] enabled=false` in `/etc/wsl.conf`). The three
97
99
  states and their evidence are tabulated in
98
- [docs/CONFIGURATION.md](docs/CONFIGURATION.md#the-strict-profile).
100
+ [docs/CONFIGURATION.md](docs/CONFIGURATION.md#windows-interop).
99
101
 
100
102
  A report is still welcome if you find an escape that does not depend on interop,
101
103
  if the masked profile is bypassable from `/mnt`, or if the plugin ever reports
package/SUPPORT.md CHANGED
@@ -6,15 +6,15 @@
6
6
  |---|---|
7
7
  | Operating system | Windows 10 and Windows 11 |
8
8
  | Linux side | WSL2 with at least one distro, and systemd as PID 1 with a lingering user manager (`loginctl enable-linger`) for the shipped session runtime. Set `sessionSandbox: false` on the preset's `wsl-shell` to keep the per-command sandbox instead |
9
- | DSH packages | the `0.2.1-rc.3`-or-newer packages the `peerDependencies` floors declare; an app on a newer harness installs unchanged |
9
+ | DSH packages | `peerDependencies` declares a `0.2.1-rc.3` floor; newer versions require compatibility validation |
10
+ | Plugin version | the latest published version only |
11
+ | Node | 22.19+ (`^22.19.0`), or 24 and later (`>=24.0.0`) |
10
12
 
11
13
  The DSH row is checked weekly: `.github/workflows/peer-canary.yml` resolves the
12
14
  `next` dist-tag of every pinned peer, installs that line into a scratch copy and
13
15
  runs `pnpm test` plus the composition boot test. A successful run prints the
14
16
  exact version delta in its summary; update the row above from that summary. A
15
17
  red run means the plugin has not been validated against the new line yet.
16
- | Plugin version | the latest published version only |
17
- | Node | 22.19+ (`^22.19.0`), or 24 and later (`>=24.0.0`) |
18
18
 
19
19
  Node 20 is outside the range on purpose: the floor is the harness host's own, and
20
20
  CI covers 22 and 24. `engines` in `package.json` is the authoritative statement,
@@ -60,10 +60,10 @@ issue for one.
60
60
 
61
61
  ## Compatibility expectations
62
62
 
63
- The upstream packages are a release candidate, so the peer versions are pinned
64
- exactly. One example is `@deepseek-ai/dsh-fs: 0.2.1-rc.2`. An upstream change can
65
- require a new release of this plugin. The exact pins are what make that break
66
- loudly instead of quietly.
63
+ The DSH peer ranges start at `0.2.1-rc.3`; development dependencies pin that
64
+ version exactly for testing. Cordis and Schemastery use caret peer ranges.
65
+ The accepted range does not guarantee compatibility with every newer release:
66
+ an upstream change can require a new release of this plugin.
67
67
 
68
68
  There is no long-term support branch. Fixes go to the latest version.
69
69
 
@@ -93,7 +93,7 @@ what it cannot see:
93
93
 
94
94
  | Item | How to get it |
95
95
  |---|---|
96
- | The diagnose report | `pnpm run diagnose -- <distro>` (read-only; safe to paste) |
96
+ | The diagnose report | `pnpm run diagnose <distro>` (read-only; review and redact before sharing) |
97
97
  | Plugin version | `npm ls dsh-plugin-wsl-env`, or the git tag you built from |
98
98
  | DSH version | the version of the harness you run |
99
99
  | The composed configuration | `dsh --profile <name> --dump-config`, in particular the `dsh-plugin-wsl-env` layer and the rows it patches |
@@ -24,8 +24,12 @@
24
24
  # SETENV|<key>|<b64 value> -> export the variable for later requests;
25
25
  # <key> must be a POSIX identifier, else
26
26
  # ERR|protocol
27
- # EXEC|<id>|<b64 cwd>|<timeout seconds|0>|<n args>|<output cap bytes|0>
28
- # followed by <n args> lines of base64, one argv word each
27
+ # EXEC|<id>|<b64 cwd>|<timeout seconds|0>|<n args>|<output cap bytes|0>|<stdin flag|0>
28
+ # followed by <n args> lines of base64, one argv word each, and — when
29
+ # the stdin flag is 1 — one more base64 line: the child's entire stdin.
30
+ # The agent feeds those bytes to the child and closes its stdin, so the
31
+ # child sees EOF at the payload's end. Absent or 0 means the child reads
32
+ # an empty stdin and immediate EOF (the v5 shape).
29
33
  # -> ACK|<id> the moment the request is dequeued for execution — the host
30
34
  # arms its stuck-request watchdog on this line, so time spent queued
31
35
  # behind an earlier request never counts against a budget
@@ -50,9 +54,8 @@
50
54
  # of its stderr (reason: notfound notdir perm loop exists stale io),
51
55
  # which the host's adapter maps onto the FsError codes the tool layer
52
56
  # knows. A `replace` write carrying an expected version re-verifies it
53
- # against a fresh stat one syscall before the rename and refuses with
54
- # `stale` on a mismatch — the concurrent writer wins, the model's
55
- # stale write does not land.
57
+ # against a fresh stat before the rename and refuses with `stale` on
58
+ # a mismatch; a later concurrent write can still be overwritten.
56
59
  #
57
60
  # Confinement is NOT this script's business: the host wraps argv in the bwrap
58
61
  # profile (lib/sandbox.js) before the frame is sent, so the agent executes
@@ -63,7 +66,14 @@
63
66
 
64
67
  set -u
65
68
 
66
- PROTO_VERSION=5
69
+ # v6: EXEC gains an optional seventh header field flagging one stdin payload
70
+ # line after the argv words. One documented limitation, the same one SETENV
71
+ # and every argv word already carry: command substitution strips TRAILING
72
+ # NEWLINES from the decoded payload, so a stdin feed ending in `\n` reaches
73
+ # the child without it (NUL bytes — `check-ignore -z` separators — survive).
74
+ # Every known consumer parses line-oriented stdin that tolerates a missing
75
+ # final newline.
76
+ PROTO_VERSION=6
67
77
  TMPDIR_AGENT=$(mktemp -d "${TMPDIR:-/tmp}/wsl-agent.XXXXXX")
68
78
  OUT_FILE="$TMPDIR_AGENT/out"
69
79
  ERR_FILE="$TMPDIR_AGENT/err"
@@ -255,16 +265,23 @@ run_capture() {
255
265
  fi
256
266
  fi
257
267
  CURRENT_ID=$req_id
268
+ # Protocol v6 stdin: when the EXEC dispatch staged a payload file, the child
269
+ # reads its bytes and sees EOF at the file's end; otherwise /dev/null keeps
270
+ # the child off the agent's own stdin (the host pipe).
271
+ spawn_in=/dev/null
272
+ if [ -n "${EXEC_STDIN:-}" ]; then
273
+ spawn_in=$EXEC_STDIN
274
+ fi
258
275
  # The inflight marker pauses the lease watchdog for this request and
259
276
  # refreshes the temp dir's mtime for the boot sweep; removing it after the
260
277
  # answer re-arms both.
261
278
  : >"$TMPDIR_AGENT/inflight"
262
279
  if [ "$mode" = subshell ]; then
263
- ( "$@" ) >"$spawn_out" 2>"$spawn_err" &
280
+ ( "$@" ) >"$spawn_out" 2>"$spawn_err" <"$spawn_in" &
264
281
  elif [ -n "$SETSID" ]; then
265
- ( exec setsid "$@" ) >"$spawn_out" 2>"$spawn_err" &
282
+ ( exec setsid "$@" ) >"$spawn_out" 2>"$spawn_err" <"$spawn_in" &
266
283
  else
267
- "$@" >"$spawn_out" 2>"$spawn_err" &
284
+ "$@" >"$spawn_out" 2>"$spawn_err" <"$spawn_in" &
268
285
  fi
269
286
  CURRENT_PID=$!
270
287
  # Dispatched: the child exists now, so the host may arm its execution
@@ -510,8 +527,8 @@ sweep_staging() {
510
527
  fs_write() {
511
528
  # $1 = base64 path, $2 = base64 mode (`-` keeps none), $3 = base64
512
529
  # `replace` or `no-replace`, $4 = expected version (`-` for none) — the
513
- # version string this write was based on, re-verified one syscall before the
514
- # rename so a concurrent writer wins instead of being clobbered — and $5 =
530
+ # version string this write was based on, re-verified before the rename;
531
+ # a later concurrent write can still be overwritten — and $5 =
515
532
  # content STILL base64. Mirrors fsio's POSIX publication: a private 0700
516
533
  # sibling staging dir, a 0600 temp, fsync, chmod, then an atomic rename — or
517
534
  # a hard link that refuses to replace for guarded creates.
@@ -676,7 +693,18 @@ while IFS= read -r line; do
676
693
  timeout_s=${rest%%|*}
677
694
  rest=${rest#*|}
678
695
  nargs=${rest%%|*}
679
- maxout=${rest#*|}
696
+ rest=${rest#*|}
697
+ maxout=${rest%%|*}
698
+ # Protocol v6: an optional seventh field flags one stdin payload line
699
+ # after the argv words. Absent (a stale peer) or 0 keeps the v5 shape —
700
+ # the child reads an empty stdin and immediate EOF.
701
+ stdin_flag=0
702
+ case $rest in
703
+ *\|*) stdin_flag=${rest#*|} ;;
704
+ *) stdin_flag=0 ;;
705
+ esac
706
+ case $stdin_flag in ''|*[!01]*) stdin_flag=0 ;; esac
707
+ EXEC_STDIN=
680
708
  cwd=$(b64dec "$cwd_b64")
681
709
  if ! cd "$cwd" 2>/dev/null; then
682
710
  printf 'ERR|%s|cwd|%s\n' "$req_id" "$(printf '%s' "$cwd" | base64 | tr -d '\n')"
@@ -705,9 +733,20 @@ while IFS= read -r line; do
705
733
  printf 'ERR|%s|protocol|%s\n' "$req_id" "$(printf '%s' "an argv payload failed to decode" | base64 | tr -d '\n')"
706
734
  continue
707
735
  fi
736
+ if [ "$stdin_flag" = 1 ]; then
737
+ IFS= read -r stdin_b64 || exit 0
738
+ if ! b64dec_strict "$stdin_b64"; then
739
+ printf 'ERR|%s|protocol|%s\n' "$req_id" "$(printf '%s' "the stdin payload failed to decode" | base64 | tr -d '\n')"
740
+ continue
741
+ fi
742
+ EXEC_STDIN="$TMPDIR_AGENT/stdin.$req_id"
743
+ printf '%s' "$DEC" >"$EXEC_STDIN"
744
+ fi
708
745
  # Dispatched: run_capture prints the ACK once the child exists, so the
709
746
  # host's watchdog and USR1 cancel both target a real process.
710
747
  run_capture "$req_id" "$timeout_s" "$maxout" exec "$@"
748
+ [ -n "$EXEC_STDIN" ] && rm -f -- "$EXEC_STDIN"
749
+ EXEC_STDIN=
711
750
  ;;
712
751
  FS\|*)
713
752
  header=${line#FS|}