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/cordis.patch.yml CHANGED
@@ -36,17 +36,20 @@
36
36
  # `wsl.exe -d <distro> --cd <linux dir>` for the directory the Session
37
37
  # supplies. Ordinary `spawn()` — every command, the host ripgrep search,
38
38
  # every other consumer — reaches the shipped implementation untouched;
39
- # 2. the controller is given the one shell profile it should resolve, so the
40
- # window's default is a distro shell rather than `cmd.exe` inside a UNC
41
- # workspace. Its shell menu is reduced to that single entry on purpose:
42
- # shell discovery also runs against the root provider and receives no
43
- # directory, so offering host executables there would advertise shells this
44
- # terminal never starts.
39
+ # 2. the controller is given the composition's default shell profile (the
40
+ # distro shell, so a directory-less launch lands in the distro rather than
41
+ # `cmd.exe` inside a UNC workspace) plus host candidates. The default must
42
+ # stay static — the controller resolves it without a Session — but the
43
+ # candidates make the shell menu honest: `wsl.exe` dedupes into the
44
+ # default's `wsl` kind, and `powershell`/`cmd` are launches this terminal
45
+ # really starts (host sessions run PowerShell outright, and a manually
46
+ # selected shell is titled after itself — the sanctioned bridge over the
47
+ # fixed tab title, see docs/UPSTREAM-TERMINAL-TITLE.md).
45
48
  #
46
49
  # Consequence: a terminal's EXECUTION WORLD follows the Session — a WSL-folder
47
50
  # session runs the distro's shell, a Windows-folder session runs PowerShell in
48
- # its own directory (`hostSessions` on `subprocess-wsl`) — while the shell menu
49
- # and the tab title stay the composition's single `WSL` profile, which no
51
+ # its own directory (`hostSessions` on `subprocess-wsl`) — while the DEFAULT
52
+ # entry and its title stay the composition's single `WSL` profile, which no
50
53
  # Session identity reaches. The title that implies is documented in
51
54
  # docs/LIMITATIONS.md and bridged upstream in docs/UPSTREAM-TERMINAL-TITLE.md.
52
55
  # ═════════════════════════════════════════════════════════════════════════════
@@ -60,7 +63,10 @@
60
63
  path: wsl.exe
61
64
  name: WSL
62
65
  args: []
63
- shellCandidates: []
66
+ shellCandidates:
67
+ - wsl.exe
68
+ - powershell
69
+ - cmd
64
70
 
65
71
  - insert:
66
72
  - id: subprocess-wsl
@@ -191,7 +197,8 @@
191
197
  # the distro's shell and fs (the realm is declared on the group);
192
198
  # 2. `tool-bash` is enabled and `tool-pwsh` disabled outright, where the
193
199
  # shipped copy derives the choice from `process.platform`;
194
- # 3. the identity: row `preset-wsl`, preset `wsl`, `order: 2`.
200
+ # 3. the identity and display metadata: row `preset-wsl`, preset `wsl`,
201
+ # `name`/`description` for the mode selector, `order: 2`.
195
202
  # Regenerate it when an app update changes the preset: take
196
203
  # `packages/bundle/web-app/presets/standard.patch.yml` from the harness tree
197
204
  # the peer pins track and run `node scripts/build-preset-wsl.mjs <that
@@ -202,6 +209,12 @@
202
209
  name: '@deepseek-ai/dsh-agent-preset'
203
210
  config:
204
211
  id: wsl
212
+ # Display metadata: `wsl` is not a registry built-in id, so the mode
213
+ # selector falls back to the raw id and "no description" unless the
214
+ # declaration carries its own copy (registry policy: user-authored
215
+ # metadata is never localized — write it in the deployment language).
216
+ name: WSL 模式
217
+ description: 在 WSL 的 Linux 实例中打开项目工作区:命令执行、shell 脚本和文件访问均在 Linux 空间内完成,以获得全套 Linux shell 工具链,受限命令经 bubblewrap 沙箱隔离。
205
218
  order: 2
206
219
  plugins:
207
220
  - id: wsl-env
@@ -3,7 +3,7 @@
3
3
  This file describes the plugin as it is today. It covers what the code does and
4
4
  where each piece lives. The reasoning behind the design, including the designs that
5
5
  were rejected, is recorded in [`CHANGELOG.md`](../CHANGELOG.md) and in
6
- [`archive/engineering-record.zh.md`](archive/engineering-record.zh.md).
6
+ [`archive/engineering-record.zh.md`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/engineering-record.zh.md).
7
7
 
8
8
  ## What "provider" means here
9
9
 
@@ -19,7 +19,7 @@ root-plane services, and adds three smaller integrations.
19
19
  | Service | Class | File | What it does |
20
20
  |---|---|---|---|
21
21
  | `ctx.shell` | `WslShellExecutor` | [`lib/index.js`](../lib/index.js) | Runs each command in the distro's login shell over the resident in-distro agent (the default), wrapped in a distro-side `bwrap` sandbox; while the agent is out, or with `agent: false`, the same command falls back to one `wsl.exe -d <distro> --cd <linux dir> --exec <login shell> -lc <cmd>` process. |
22
- | `ctx.fs` | `WslFileSystem` | [`lib/index.js`](../lib/index.js) | Serves the file tools from real distro files on the resident in-distro agent, on ext4 — the former Windows-side share substrate is retired (see below). |
22
+ | `ctx.fs` | `WslFileSystem` | [`lib/index.js`](../lib/index.js) | Serves WSL-routed file operations through the resident in-distro agent; the former Windows-side share substrate is retired (see below). |
23
23
  | `ctx.subprocess` | `WslSubprocessRuntime` | [`lib/subprocess.js`](../lib/subprocess.js) | Opens the GUI terminal inside the distro, in the session workspace, and rewrites the file-search and git-snapshot spawns into the distro (see below). |
24
24
 
25
25
  `WslShellExecutor` extends the shipped `LocalBashExecutor`, and `WslFileSystem`
@@ -34,7 +34,7 @@ a subclass that routes by coordinate:
34
34
 
35
35
  | Service | Class | File | What it does |
36
36
  |---|---|---|---|
37
- | `ctx.fs` (root plane) | `WslRoutingFileSystem` | [`lib/fs-routing.js`](../lib/fs-routing.js) | The root filesystem, routed by coordinate: a distro UNC identity is served by the resident agent (real watch events, ext4 reads), a drive path stays host-native. Replaces the shipped `fs-sandbox` row, whose write fence has no root-plane consumers — the model's writes ride the preset filesystems. [`root-fs-routing.md`](root-fs-routing.md) is the design. |
37
+ | `ctx.fs` (root plane) | `WslRoutingFileSystem` | [`lib/fs-routing.js`](../lib/fs-routing.js) | The root filesystem, routed by coordinate: a distro UNC identity is served inside the distro (inotify where available, polling otherwise), a drive path stays host-native. Replaces the shipped `fs-sandbox` row, whose write fence has no root-plane consumers — the model's writes ride the preset filesystems. [`root-fs-routing.md`](root-fs-routing.md) is the design. |
38
38
 
39
39
  The GUI file tree and previews keep the shipped `workspace-files` row. Its
40
40
  client half is the browser `file` resource provider the Sidebar previews need,
@@ -56,7 +56,7 @@ One further module ships built but deliberately unwired:
56
56
  [`lib/file-reference-wsl.js`](../lib/file-reference-wsl.js), the distro-side
57
57
  traversal strategy for `@` completion. It is built and tested, but **not** wired
58
58
  into `cordis.patch.yml` — the seam it consumes is an upstream proposal
59
- ([`upstream/rfc-wsl-workspaces.md`](upstream/rfc-wsl-workspaces.md)), and
59
+ ([`upstream/rfc-wsl-workspaces.md`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/upstream/rfc-wsl-workspaces.md)), and
60
60
  reaching the funnels without it would mean subclassing past module-internal
61
61
  functions.
62
62
 
@@ -64,7 +64,7 @@ functions.
64
64
 
65
65
  | Subpath | Class or name | File | What it does |
66
66
  |---|---|---|---|
67
- | `dsh-plugin-wsl-env/picker` | `WslDirectoryPicker` | [`lib/picker.js`](../lib/picker.js) | Adds the installed distros to the folder picker, next to the Windows home directory, and lists distro levels from inside the distro (the resident's `ls`, one-shot `wsl.exe` when it is out). |
67
+ | `dsh-plugin-wsl-env/picker` | `WslDirectoryPicker` | [`lib/picker.js`](../lib/picker.js) | Adds the installed distros to the folder picker, next to the Windows home directory, and lists distro levels from inside the distro (the resident's `find`, one-shot `wsl.exe` when it is out). |
68
68
  | `dsh-plugin-wsl-env/auto-preset` | `auto-preset` | [`lib/auto-preset.js`](../lib/auto-preset.js) | Binds the `wsl` agent preset when a new session's workspace is inside a distro. |
69
69
  | `dsh-plugin-wsl-env/shell-env` | `wsl-shell-env` | [`lib/shell-env.js`](../lib/shell-env.js) | Contributes `DSH_WSL_DISTRO`, `DSH_WSL_SHELL`, `DSH_WSL_HOME` and `DSH_WSL_PORTS` to the managed `DSH_*` namespace, choosing the session workspace's distro (default/pinned facts are probed at mount; other distros are warmed when their session appears). The port list is a snapshot per active distro, refreshed every `portsRefreshMs` (10000 by default), so a server started moments ago may not be listed yet. |
70
70
 
@@ -109,9 +109,15 @@ So the layer disables the shipped `subprocess` row and inserts
109
109
  `dsh-plugin-wsl-env/subprocess` in its place at the app level. The subclass
110
110
  overrides two methods: `spawnTerminal` rewrites a `wsl.exe` launch into
111
111
  `wsl.exe -d <distro> --cd <linux dir>`, and `spawn()` rewrites two kinds of
112
- launches into the distro — the file-search tool's packaged ripgrep (agent-first,
113
- with the one-shot `wsl.exe --exec` handle as the fallback when the agent is out)
114
- and git snapshot commands. Both spawn decisions are pure coordinate functions
112
+ launches into the distro — the file-search tool's packaged ripgrep and git
113
+ snapshot commands (each agent-first on the warm resident — protocol v6 carries
114
+ a git call's stdin, and a one-shot call costs ~230 ms of `wsl.exe` startup
115
+ against the resident's ~2 ms — with the one-shot `wsl.exe --exec` handle as
116
+ the fallback when the agent is out). When the HOST has no git,
117
+ `resolveExecutable` answers git with a distro sentinel path, and the snapshot
118
+ command's discovery call (`rev-parse --show-toplevel ...`) routes too, its
119
+ absolute-path output translated back into UNC form — so full WSL change review
120
+ works without Windows git. Both spawn decisions are pure coordinate functions
115
121
  (`lib/search-route.js`, `lib/git-route.js`); every other `spawn()` — including
116
122
  the one-shot fallback's own `wsl.exe -lc <command>` (the default agent path
117
123
  carries the command over the resident instead of spawning) — reaches the shipped
@@ -120,9 +126,11 @@ implementation untouched, as do the pwsh executor and the LSP host.
120
126
  The terminal's execution world follows the session (`hostSessions`, default on):
121
127
  a WSL-folder session runs the distro shell, a Windows-folder session runs
122
128
  `powershell.exe` in its own directory. What still follows the app configuration
123
- is the shell menu and the tab title, both derived from the composition's single
124
- `WSL` shell profile — so a host session's tab reads `WSL` over a PowerShell
125
- process. See [LIMITATIONS.md](LIMITATIONS.md) and
129
+ is the shell menu's DEFAULT entry and its title, both derived from the
130
+ composition's single `WSL` shell profile — so a new tab reads `WSL` whatever it
131
+ runs, including a host session's PowerShell. The menu also lists host
132
+ candidates (`powershell`, `cmd`; `wsl.exe` dedupes into the default), and a
133
+ manually selected shell is titled after itself. See [LIMITATIONS.md](LIMITATIONS.md) and
126
134
  [UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md).
127
135
 
128
136
  ## Path coordinates
@@ -146,14 +154,15 @@ The former Windows-side share backend (`substrate: "share"`) is retired — a
146
154
  profile carrying the value is refused at boot, and no file tool crosses the
147
155
  9p share. The substrate's facts:
148
156
 
149
- - **Reads, writes, identities** run on ext4 through the resident; symlinks and
150
- mode bits are native (every identity is a distro-side `realpath`; the
151
- publication rename is the kernel's).
157
+ - **Reads, writes, identities** run through the resident on the filesystem
158
+ backing the distro path, typically ext4 for distro-owned files. Symlink and
159
+ mode behavior also depends on that filesystem; identities use distro-side
160
+ `realpath` and publication uses the kernel's rename or link.
152
161
  - **Guarded writes** carry their intent to the publication: a `createIfAbsent`
153
162
  publishes with a no-replace link, closing the check-then-write window for
154
163
  creates outright; an overwrite or edit carries its version into the write op
155
- and the agent re-verifies it one syscall before the rename — a concurrent
156
- writer wins, the stale write refuses with `FS_STALE_VERSION`.
164
+ and the agent re-verifies it before the rename. A mismatch returns
165
+ `FS_STALE_VERSION`, but a change after that check can still be overwritten.
157
166
  - **Kernel enforcement** is stage two: a mutation runs on a confined resident —
158
167
  one long-lived agent per (distro, policy), whose bwrap profile binds exactly
159
168
  what the mode grants — so the kernel refuses what the check would have, and a
@@ -245,12 +254,14 @@ selector read that value.
245
254
  distro can still run a Windows program through WSL interop, for example
246
255
  `/mnt/c/.../*.exe`. Bubblewrap does not govern that process.
247
256
 
248
- `maskWindowsDrive` is the one mitigation the profile can offer, and it is a
249
- narrowing rather than a closure: it takes the drive's data and its executables
250
- off the table, but `binfmt` interop dispatches on file *content*, so a command
257
+ `maskWindowsDrive` or the `masked` and `disabled` `interopVisibility` stances
258
+ shadow `/mnt` in confined commands, a narrowing rather than a closure. Other
259
+ mounts and unrestricted file reads are not covered by the mask, and `binfmt`
260
+ interop dispatches on file *content*, so a command
251
261
  that can write the workspace can still write a PE there and have it launched as
252
- a Windows process. Either way the provider reports `partial` — the complete
253
- closure is distro-level (`[interop] enabled=false` in `wsl.conf`). The mask is
262
+ a Windows process. Either way the provider reports `partial`. Disabling WSL
263
+ interop requires a separate distro-level change (`[interop] enabled=false` in
264
+ `wsl.conf`); the `disabled` stance does not make that change. The mask is
254
265
  reported on the result's sandbox fact as `windowsDrive: "masked"` (or
255
266
  `"visible"`), but that is this plugin's own extra field: the pinned harness's
256
267
  sandbox type and its model-facing notes carry `mode`, `denied`, `enforcement` and
@@ -338,35 +349,35 @@ and pnpm rewrites `link:\\wsl.localhost\...` into a broken
338
349
  distro checkout.
339
350
 
340
351
  The Windows copy at `default-workspace/dsh-plugin-wsl` is a runtime mirror.
341
- [`test/probe/sync-to-windows.sh`](../test/probe/sync-to-windows.sh) keeps it in
352
+ [`test/probe/sync-to-windows.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/sync-to-windows.sh) keeps it in
342
353
  step with the checkout, and `pnpm run sync:windows` runs that script.
343
354
 
344
355
  ## Test layers
345
356
 
346
357
  | Layer | Location | Needs a harness |
347
358
  |---|---|---|
348
- | Style and packaging checks | [`test/style.mjs`](../test/style.mjs) | no |
349
- | Syntax pass | [`test/syntax.mjs`](../test/syntax.mjs) | no |
350
- | Build consistency | [`test/build-freshness.mjs`](../test/build-freshness.mjs) | no; it re-runs the real build and byte-compares the committed `lib/`, so it also catches an orphan there |
359
+ | Style and packaging checks | [`test/style.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/style.mjs) | no |
360
+ | Syntax pass | [`test/syntax.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/syntax.mjs) | no |
361
+ | Build consistency | [`test/build-freshness.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/build-freshness.mjs) | no; it re-runs the real build and byte-compares the committed `lib/`, so it also catches an orphan there |
351
362
  | Type check | `tsc --noEmit` over `test/**` and `types/*.d.ts` (`src/` is checked by the build itself) | no |
352
363
  | Unit tests | `test/*.test.mjs` | no |
353
- | Sandbox probe | [`test/probe/sandbox.sh`](../test/probe/sandbox.sh) | no; it applies the profile arguments directly |
354
- | Harness probes | [`test/probe/run.sh`](../test/probe/run.sh), `terminal.sh`, `sandbox-shell.sh`, `picker.sh`, `fs-routing.sh`, `missing-wsl.sh`, `sandbox-off.sh` | yes: Windows, WSL2, a mounted profile, and a linked checkout |
355
- | Distro probes | [`test/probe/substrate.sh`](../test/probe/substrate.sh), `watch.sh`, `agent.sh`, `exec.sh` | Windows and WSL2, but no harness boot and no profile: they run from a distro terminal with Node available, reaching `wsl.exe` through interop |
356
- | Host probe | [`test/probe/mode.sh`](../test/probe/mode.sh) | Windows Node, no harness |
364
+ | Sandbox probe | [`test/probe/sandbox.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/sandbox.sh) | no; it applies the profile arguments directly |
365
+ | Harness probes | [`test/probe/run.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/run.sh), `terminal.sh`, `sandbox-shell.sh`, `interop-shell.sh`, `picker.sh`, `fs-routing.sh`, `missing-wsl.sh`, `sandbox-off.sh` | yes: Windows, WSL2, a mounted profile, and a linked checkout |
366
+ | Distro probes | [`test/probe/substrate.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/substrate.sh), `watch.sh`, `agent.sh`, `exec.sh` | Windows and WSL2, but no harness boot and no profile: they run from a distro terminal with Node available, reaching `wsl.exe` through interop |
367
+ | Host probe | [`test/probe/mode.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/mode.sh) | Windows Node, no harness |
357
368
  | Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
358
369
 
359
- `pnpm test` runs the first five rows in order: `lint:style`, `test:syntax`,
360
- `lint:build`, `lint:types`, `test:unit`.
361
- [`test/probe/run-all-when-closed.sh`](../test/probe/run-all-when-closed.sh)
370
+ `pnpm test` runs `lint:style`, `lint:links`, `test:syntax`, `lint:build`,
371
+ `lint:types` and `test:unit`, in that order.
372
+ [`test/probe/run-all-when-closed.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/run-all-when-closed.sh)
362
373
  waits for the app to exit and then runs the whole real-machine suite once —
363
374
  `--include-fs` prepends `run.sh`.
364
375
 
365
376
  The unit tests come in two kinds: most import only Node builtins, while
366
- [`test/composition.test.mjs`](../test/composition.test.mjs),
367
- [`test/fs.boot.test.mjs`](../test/fs.boot.test.mjs),
368
- [`test/fsio-agent.test.mjs`](../test/fsio-agent.test.mjs) and
369
- [`test/provider.test.mjs`](../test/provider.test.mjs) import
377
+ [`test/composition.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/composition.test.mjs),
378
+ [`test/fs.boot.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/fs.boot.test.mjs),
379
+ [`test/fsio-agent.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/fsio-agent.test.mjs) and
380
+ [`test/provider.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/provider.test.mjs) import
370
381
  pinned `@deepseek-ai/*` development dependencies (the composition and boot tests run
371
382
  the loader's real patch algorithm). One `pnpm install` reproduces that tree before
372
383
  the first run; the runtime package itself still ships zero dependencies. CI runs
@@ -376,9 +387,9 @@ Node 24 only — the threshold flags need Node 22.8. Node 20 is absent on purpos
376
387
  probes are a manual step, because a hosted runner has no WSL distro and no profile to
377
388
  mount.
378
389
 
379
- The probe scripts share [`test/probe/env.sh`](../test/probe/env.sh), which derives
390
+ The probe scripts share [`test/probe/env.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/env.sh), which derives
380
391
  the Windows user, the distro name and the UNC spelling of the checkout from the
381
- machine, and [`test/probe/env.mjs`](../test/probe/env.mjs) for the Node-side probes.
392
+ machine, and [`test/probe/env.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/env.mjs) for the Node-side probes.
382
393
  No probe carries a user name, and the YAML overlays are templates with `@NAME@`
383
394
  placeholders that the scripts substitute into generated copies.
384
395
 
@@ -431,9 +442,11 @@ src/fsio-text.ts the peer's pure text mechanics, replicated pending
431
442
  src/git-route.ts which git spawns belong inside the distro (pure)
432
443
  src/index.ts WslShellExecutor (ctx.shell) and WslFileSystem (ctx.fs)
433
444
  src/listing.ts pure directory listing and breadcrumb helpers
445
+ src/native-agent.ts native binary selection and preparation
434
446
  src/paths.ts pure conversion between the three path formats
435
447
  src/picker.ts WslDirectoryPicker (ctx.directoryPicker)
436
448
  src/ports.ts the listening-port snapshot behind DSH_WSL_PORTS
449
+ src/preflight.ts session-start environment advisories
437
450
  src/preset-choice.ts whether a new session adopts the WSL preset (pure)
438
451
  src/probe-cache.ts which probe verdicts may be remembered (pure)
439
452
  src/sandbox.ts the distro-side bwrap confinement shared by both
@@ -441,6 +454,8 @@ src/sandbox.ts the distro-side bwrap confinement shared by both
441
454
  src/sandbox-core.ts the probe-and-confinement core, free of DSH peers
442
455
  src/search-exec.ts the agent-backed execution handle for a search
443
456
  src/search-route.ts which execution world a search spawn belongs to (pure)
457
+ src/session-runtime.ts shared session runtime and agent lifecycle
458
+ src/session-scope.ts systemd scope creation and cleanup
444
459
  src/shell.ts the ctx.shell subpath entry point
445
460
  src/shell-env.ts registers the DSH_WSL_* environment variables
446
461
  src/subprocess.ts WslSubprocessRuntime (ctx.subprocess), the GUI
@@ -462,11 +477,11 @@ src/wsl.ts wsl.exe interop primitives (no DSH imports)
462
477
  - [`REQUIREMENTS.zh.md`](REQUIREMENTS.zh.md) is the requirements register: every
463
478
  documented behaviour commitment numbered, with where it is specified and what
464
479
  verifies it, plus the open items (Chinese, maintainer-facing, not packaged).
465
- - [`archive/engineering-record.zh.md`](archive/engineering-record.zh.md) is the
480
+ - [`archive/engineering-record.zh.md`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/engineering-record.zh.md) is the
466
481
  full engineering record, in Chinese: measured contracts, debugging rounds, and
467
482
  the conclusions as they stood then.
468
- - [`archive/README.md`](archive/README.md) and
469
- [`archive/README.en.md`](archive/README.en.md) are two English indexes of the
483
+ - [`archive/README.md`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/README.md) and
484
+ [`archive/README.en.md`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/README.en.md) are two English indexes of the
470
485
  same three archived files, the second so a reader who does not read Chinese can
471
486
  still tell what is there; the records themselves are Chinese.
472
487
  - [`README.md`](../README.md) is the only statement of current state.
@@ -0,0 +1,121 @@
1
+ # Compatibility matrix
2
+
3
+ Measured evidence for the support tiers in [`ROADMAP-1.0.md`](ROADMAP-1.0.md)
4
+ (WSL-202). Every row below was executed on real WSL2 distros on one machine
5
+ (x86_64, Windows 11, WSL 2, kernel 6.18) on **2026-10-26**, with the probe
6
+ suite in [`test/probe/`](https://github.com/Big-Dao/dsh-plugin-wsl-env/tree/main/test/probe/) run from inside each distro against a
7
+ fresh `--depth 1` clone. The aarch64 native agent is additionally executed per
8
+ push under qemu in CI — see the `native packages` job in
9
+ [`.github/workflows/ci.yml`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/.github/workflows/ci.yml); VM-only by
10
+ decision — see "Scope decisions" below.
11
+
12
+ ## The matrix
13
+
14
+ | | Ubuntu (host, Tier 1) | Debian 13.5 | Fedora 44 | openSUSE Tumbleweed | Arch | Alpine 3.22 |
15
+ |---|---|---|---|---|---|---|
16
+ | init | systemd | systemd | systemd | systemd | systemd | busybox/OpenRC |
17
+ | libc | glibc | glibc | glibc | glibc | glibc | musl |
18
+ | bwrap | 0.12 | 0.12 | 0.12 | 0.13.1 | 0.13.1 | 0.12 |
19
+ | node | 24 | 20.19 | 22.23 | 26.12 | 26.12 | 22.23 |
20
+ | `sandbox.sh` | 14/14 | 14/14 | 14/14 | 14/14 | 14/14 | 15/15 |
21
+ | `agent.sh` | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 | 11/11 |
22
+ | `exec.sh` | 5/5 | 5/5 | 5/5 | 5/5 | 5/5 | 5/5 |
23
+ | `substrate.sh` | 8/8 | 8/8 | 8/8 | 8/8 | 8/8 | 8/8 |
24
+ | `watch.sh` | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 | 5/5 |
25
+ | `session-sandbox.sh` | 10p 1s | 10p 1s | 3p 1r 1s | 3p 1r 1s | 3p 1r 1s | 3p 1d 1s |
26
+
27
+ `p` = passed, `s` = skipped, `r` = race, `d` = declared limitation. Verdicts
28
+ per row follow [`ROADMAP-1.0.md`](ROADMAP-1.0.md): Ubuntu, Debian, Fedora,
29
+ openSUSE and Arch are **supported**; Alpine is **supported** for everything
30
+ the non-systemd scope claims, with the narrower `sessionSandbox: auto` scope.
31
+
32
+ **Debian is verified end to end**: the four harness-level probes
33
+ (`run.sh`, `terminal.sh`, `picker.sh`, `fs-routing.sh`) also pass against a
34
+ `wslfs-debian` profile (2026-10-26), on top of the distro-internal suite —
35
+ the full stack, not just the in-distro half.
36
+
37
+ ## The found first-run trap: missing workspace root
38
+
39
+ The Debian harness-level runs surfaced one real finding: **the confined
40
+ resident refuses to start when the workspace root does not exist yet**. The
41
+ bwrap profile binds the workspace root (`--bind <root> <root>`), bwrap
42
+ requires the bind source to exist, and on a freshly imported distro the
43
+ probe's workspace (`~/.dsh-fsprobe`) did not. `mkdir -p` of the root turns
44
+ all ten failing steps green. Every prior Ubuntu run had the directory left
45
+ over from earlier probes, which is why the trap never fired there. Any
46
+ workspace whose directory is created lazily by its first write hits the same
47
+ wall on a fresh distro. Fixed in aa12b49: the confined factory's prepare
48
+ hook now creates the root (`mkdir -p`) before bwrap runs, the handshake
49
+ failure carries the resident's stderr when one still occurs, and
50
+ `substrate.sh` gained a fresh-root leg that spawns the resident against an
51
+ absent workspace root on every run, verified on both Debian (glibc) and
52
+ Alpine (musl).
53
+
54
+ ## The two explained differences
55
+
56
+ - **`session-sandbox.sh`, background-survival leg (Fedora, openSUSE, Arch).**
57
+ The same systemd distros split on this check when driven through `wsl.exe`
58
+ (Debian passes, the other three do not): the probe allows a 0.5 s window and
59
+ the WSL session teardown races it. This is a probe-context artifact, not a
60
+ plugin claim — the plugin's background-process guarantee rides on systemd
61
+ transient scopes, whose legs the probe itself marks "run from a WSL
62
+ terminal". On Alpine the same leg fails **by declaration**: without systemd
63
+ the plugin does not promise background survival, and `sessionSandbox: auto`
64
+ reports the narrower per-command scope instead.
65
+
66
+ - **`watch.sh` on Alpine (5/5).** Two checks exercised the inotify fast path
67
+ on the other distros and the polling fallback there; both behaviours are
68
+ shipped and both are asserted, so the lower count reflects fewer exercised
69
+ paths on that kernel/libc, not a weaker promise.
70
+
71
+ ## Tier 2 setup notes (the traps, so the next import does not rediscover them)
72
+
73
+ | Distro | Setup that mattered |
74
+ |---|---|
75
+ | Debian | First launch can wedge before OOBE completes (`wsl.exe` exec and `--terminate` both hang). `wsl --terminate <distro>` once it is Stopped, then start as root to bypass OOBE. systemd is PID 1 out of the box. |
76
+ | Alpine | Import the **minirootfs** tarball (`wsl --import`), not the standard/cloud images. Create a normal user and set `[user] default=…` in `/etc/wsl.conf` — the imported default is root, and a root agent writes 0600 files the probe user cannot read. Install `coreutils util-linux` (busybox `mktemp`/`ps` lack the used options) and `shadow` for `useradd`. |
77
+ | Fedora / openSUSE / Arch | systemd PID 1 out of the box. openSUSE: `zypper --non-interactive` (the plain `-y` still prompts); Arch: `pacman -Sy --needed`. |
78
+
79
+ Dependencies used everywhere: `bash bubblewrap git nodejs` (node 20–26 all
80
+ pass; the plugin's `engines` floor applies to the harness host, not to probe
81
+ nodes). The probe runner per distro, from inside the distro:
82
+
83
+ ```sh
84
+ git clone --depth 1 https://github.com/Big-Dao/dsh-plugin-wsl-env.git
85
+ cd dsh-plugin-wsl-env
86
+ for p in sandbox session-sandbox agent exec substrate watch; do bash test/probe/$p.sh; done
87
+ ```
88
+
89
+ The harness-level probes (`run.sh`, `terminal.sh`, `picker.sh`,
90
+ `fs-routing.sh`) additionally need the app closed and the throwaway `wslfs`
91
+ profile from `run.sh`'s header; on a second distro, copy that profile and set
92
+ its `distro:` key.
93
+
94
+ ## Scope decisions
95
+
96
+ - **aarch64 is verified in VM only; physical hardware is out of scope
97
+ (decided 2026-10-27).** The qemu-user CI leg executes the full 18-check
98
+ suite against the shipped binary on every push, which covers the agent's
99
+ own code paths; what only real hardware can add is exposure to aarch64
100
+ silicon's edge cases, which no Windows-on-ARM device in this project's
101
+ reach provides. The support table marks aarch64 "supported (executed in
102
+ CI)" and stays that way.
103
+
104
+ - **Windows 10 is not separately verified (decided 2026-10-27).** The plugin's
105
+ entire surface is `wsl.exe`, the distro, and a UNC share; on WSL2 those are
106
+ the same components on Windows 10 as on Windows 11, and the version-specific
107
+ behaviour lives upstream in WSL itself, not in anything this plugin mounts.
108
+ Windows 11 is what the matrix above measures; a Win10 report is welcomed
109
+ through the support channels, but no separate verification is planned.
110
+
111
+ ## Not yet measured
112
+
113
+ - Non-systemd session scopes driven by a real WSL terminal session (the
114
+ probe's skipped legs).
115
+ - ~~Container images~~ — closed 2026-10-27: `test/agent-container.mjs` runs
116
+ the real shell agent inside `debian:stable-slim`, `alpine:3`, `fedora:latest`,
117
+ `opensuse/tumbleweed` and `archlinux` containers in CI (the
118
+ `agent-containers` job, one per image), 7 checks each — handshake with the
119
+ in-place digest, PING, EXEC with exit and both streams, SETENV, and the
120
+ presence of `sha256sum`/`base64` where the families actually differ
121
+ (busybox on Alpine vs coreutils elsewhere). 35/35 on first execution.