dsh-plugin-wsl-env 0.15.0 → 0.16.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 (52) hide show
  1. package/CHANGELOG.md +87 -14
  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 +4 -5
  9. package/cordis.patch.yml +8 -1
  10. package/docs/ARCHITECTURE.md +42 -35
  11. package/docs/COMPATIBILITY.md +121 -0
  12. package/docs/CONFIGURATION.md +254 -273
  13. package/docs/LIMITATIONS.md +61 -26
  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/VALUE.zh.md +144 -0
  28. package/docs/adr/0002-native-agent.md +348 -0
  29. package/docs/root-fs-routing.md +186 -0
  30. package/docs/screenshots/picker.png +0 -0
  31. package/docs/screenshots/preview.png +0 -0
  32. package/docs/screenshots/sandbox.png +0 -0
  33. package/docs/screenshots/terminal.png +0 -0
  34. package/lib/agent-exec.d.ts +2 -3
  35. package/lib/agent-shared.js +1 -0
  36. package/lib/agent.d.ts +19 -8
  37. package/lib/agent.js +73 -13
  38. package/lib/fs-routing.d.ts +2 -1
  39. package/lib/fs-routing.js +13 -1
  40. package/lib/fs-substrate.d.ts +11 -9
  41. package/lib/fs-substrate.js +6 -9
  42. package/lib/fsio-agent.d.ts +2 -3
  43. package/lib/fsio-agent.js +2 -3
  44. package/lib/index.d.ts +20 -13
  45. package/lib/index.js +18 -11
  46. package/lib/session-runtime.d.ts +5 -3
  47. package/lib/session-runtime.js +5 -4
  48. package/locale/en.json +1 -1
  49. package/locale/zh.json +1 -1
  50. package/package.json +13 -4
  51. package/scripts/diagnose.sh +1 -1
  52. package/scripts/doctor.mjs +3 -3
@@ -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
 
@@ -146,14 +146,15 @@ The former Windows-side share backend (`substrate: "share"`) is retired — a
146
146
  profile carrying the value is refused at boot, and no file tool crosses the
147
147
  9p share. The substrate's facts:
148
148
 
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).
149
+ - **Reads, writes, identities** run through the resident on the filesystem
150
+ backing the distro path, typically ext4 for distro-owned files. Symlink and
151
+ mode behavior also depends on that filesystem; identities use distro-side
152
+ `realpath` and publication uses the kernel's rename or link.
152
153
  - **Guarded writes** carry their intent to the publication: a `createIfAbsent`
153
154
  publishes with a no-replace link, closing the check-then-write window for
154
155
  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`.
156
+ and the agent re-verifies it before the rename. A mismatch returns
157
+ `FS_STALE_VERSION`, but a change after that check can still be overwritten.
157
158
  - **Kernel enforcement** is stage two: a mutation runs on a confined resident —
158
159
  one long-lived agent per (distro, policy), whose bwrap profile binds exactly
159
160
  what the mode grants — so the kernel refuses what the check would have, and a
@@ -245,12 +246,14 @@ selector read that value.
245
246
  distro can still run a Windows program through WSL interop, for example
246
247
  `/mnt/c/.../*.exe`. Bubblewrap does not govern that process.
247
248
 
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
249
+ `maskWindowsDrive` or the `masked` and `disabled` `interopVisibility` stances
250
+ shadow `/mnt` in confined commands, a narrowing rather than a closure. Other
251
+ mounts and unrestricted file reads are not covered by the mask, and `binfmt`
252
+ interop dispatches on file *content*, so a command
251
253
  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
254
+ a Windows process. Either way the provider reports `partial`. Disabling WSL
255
+ interop requires a separate distro-level change (`[interop] enabled=false` in
256
+ `wsl.conf`); the `disabled` stance does not make that change. The mask is
254
257
  reported on the result's sandbox fact as `windowsDrive: "masked"` (or
255
258
  `"visible"`), but that is this plugin's own extra field: the pinned harness's
256
259
  sandbox type and its model-facing notes carry `mode`, `denied`, `enforcement` and
@@ -338,35 +341,35 @@ and pnpm rewrites `link:\\wsl.localhost\...` into a broken
338
341
  distro checkout.
339
342
 
340
343
  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
344
+ [`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
345
  step with the checkout, and `pnpm run sync:windows` runs that script.
343
346
 
344
347
  ## Test layers
345
348
 
346
349
  | Layer | Location | Needs a harness |
347
350
  |---|---|---|
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 |
351
+ | Style and packaging checks | [`test/style.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/style.mjs) | no |
352
+ | Syntax pass | [`test/syntax.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/syntax.mjs) | no |
353
+ | 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
354
  | Type check | `tsc --noEmit` over `test/**` and `types/*.d.ts` (`src/` is checked by the build itself) | no |
352
355
  | 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 |
356
+ | 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 |
357
+ | 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 |
358
+ | 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 |
359
+ | 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
360
  | Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
358
361
 
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)
362
+ `pnpm test` runs `lint:style`, `lint:links`, `test:syntax`, `lint:build`,
363
+ `lint:types` and `test:unit`, in that order.
364
+ [`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
365
  waits for the app to exit and then runs the whole real-machine suite once —
363
366
  `--include-fs` prepends `run.sh`.
364
367
 
365
368
  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
369
+ [`test/composition.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/composition.test.mjs),
370
+ [`test/fs.boot.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/fs.boot.test.mjs),
371
+ [`test/fsio-agent.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/fsio-agent.test.mjs) and
372
+ [`test/provider.test.mjs`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/provider.test.mjs) import
370
373
  pinned `@deepseek-ai/*` development dependencies (the composition and boot tests run
371
374
  the loader's real patch algorithm). One `pnpm install` reproduces that tree before
372
375
  the first run; the runtime package itself still ships zero dependencies. CI runs
@@ -376,9 +379,9 @@ Node 24 only — the threshold flags need Node 22.8. Node 20 is absent on purpos
376
379
  probes are a manual step, because a hosted runner has no WSL distro and no profile to
377
380
  mount.
378
381
 
379
- The probe scripts share [`test/probe/env.sh`](../test/probe/env.sh), which derives
382
+ 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
383
  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.
384
+ 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
385
  No probe carries a user name, and the YAML overlays are templates with `@NAME@`
383
386
  placeholders that the scripts substitute into generated copies.
384
387
 
@@ -431,9 +434,11 @@ src/fsio-text.ts the peer's pure text mechanics, replicated pending
431
434
  src/git-route.ts which git spawns belong inside the distro (pure)
432
435
  src/index.ts WslShellExecutor (ctx.shell) and WslFileSystem (ctx.fs)
433
436
  src/listing.ts pure directory listing and breadcrumb helpers
437
+ src/native-agent.ts native binary selection and preparation
434
438
  src/paths.ts pure conversion between the three path formats
435
439
  src/picker.ts WslDirectoryPicker (ctx.directoryPicker)
436
440
  src/ports.ts the listening-port snapshot behind DSH_WSL_PORTS
441
+ src/preflight.ts session-start environment advisories
437
442
  src/preset-choice.ts whether a new session adopts the WSL preset (pure)
438
443
  src/probe-cache.ts which probe verdicts may be remembered (pure)
439
444
  src/sandbox.ts the distro-side bwrap confinement shared by both
@@ -441,6 +446,8 @@ src/sandbox.ts the distro-side bwrap confinement shared by both
441
446
  src/sandbox-core.ts the probe-and-confinement core, free of DSH peers
442
447
  src/search-exec.ts the agent-backed execution handle for a search
443
448
  src/search-route.ts which execution world a search spawn belongs to (pure)
449
+ src/session-runtime.ts shared session runtime and agent lifecycle
450
+ src/session-scope.ts systemd scope creation and cleanup
444
451
  src/shell.ts the ctx.shell subpath entry point
445
452
  src/shell-env.ts registers the DSH_WSL_* environment variables
446
453
  src/subprocess.ts WslSubprocessRuntime (ctx.subprocess), the GUI
@@ -462,11 +469,11 @@ src/wsl.ts wsl.exe interop primitives (no DSH imports)
462
469
  - [`REQUIREMENTS.zh.md`](REQUIREMENTS.zh.md) is the requirements register: every
463
470
  documented behaviour commitment numbered, with where it is specified and what
464
471
  verifies it, plus the open items (Chinese, maintainer-facing, not packaged).
465
- - [`archive/engineering-record.zh.md`](archive/engineering-record.zh.md) is the
472
+ - [`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
473
  full engineering record, in Chinese: measured contracts, debugging rounds, and
467
474
  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
475
+ - [`archive/README.md`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/docs/archive/README.md) and
476
+ [`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
477
  same three archived files, the second so a reader who does not read Chinese can
471
478
  still tell what is there; the records themselves are Chinese.
472
479
  - [`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.