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
@@ -1,293 +1,274 @@
1
1
  # Configuration
2
2
 
3
- Every value lives in a profile row, and a row is overridden by id in your own
4
- profile layer: `$DSH_HOME/profiles/<name>/cordis.patch.yml`. Start from
5
- [`examples/profile.cordis.patch.yml`](../examples/profile.cordis.patch.yml), and keep
6
- [`cordis.patch.yml`](../cordis.patch.yml) open beside it — that file is the commented
7
- reference for every value the plugin ships.
8
-
9
- Which rows a patch can reach depends on where they sit. `fs-routing`,
10
- `directory-picker-wsl`, `subprocess-wsl`, `wsl-shell-env`
11
- and `preset-wsl` are top-level rows, so `- id: <row>` in your layer patches them.
12
- `wsl-shell` and `wsl-fs` are not top-level: they are plugins of the `wsl` agent
13
- preset, nested inside the `preset-wsl` row's `config.plugins`. A patch that
14
- targets them by id matches nothing — the loader logs `patch: entry "wsl-shell"
15
- not found` (a warning, not an error), the profile boots anyway, and the value you
16
- wrote is not in force. See
17
- [Overriding the rows inside `preset-wsl`](#overriding-the-rows-inside-preset-wsl).
18
-
19
- In the table below, *(shipped)* marks the values set by `cordis.patch.yml`. The rest
20
- are schema defaults, listed because they are the ones worth knowing. `wsl-shell-env`
21
- has its own table at the end of this page.
22
-
23
- | Row | Key | Default | Meaning |
3
+ ## Profile patches
4
+
5
+ Store profile settings in `$DSH_HOME/profiles/<name>/cordis.patch.yml`. Use
6
+ [`examples/profile.cordis.patch.yml`](../examples/profile.cordis.patch.yml) as a
7
+ starting point. The bundled [`cordis.patch.yml`](../cordis.patch.yml) lists the
8
+ rows and values shipped by this plugin.
9
+
10
+ A profile layer can target a top-level row by `id`. Plugin rows at that level
11
+ include `fs-routing`, `directory-picker-wsl`, `subprocess-wsl`, `wsl-shell-env`,
12
+ `wsl-preflight`, `auto-preset`, and `preset-wsl`. The `wsl-shell` and `wsl-fs`
13
+ rows are nested inside `preset-wsl.config.plugins`; a profile entry targeting
14
+ either id directly is ignored with a `patch: entry <id> not found` warning.
15
+
16
+ A row's `config` is replaced as a whole. Include any non-default values from
17
+ the bundled row that you want to retain. This matters most for `preset-wsl`,
18
+ whose config contains the complete preset and its nested plugins.
19
+
20
+ In the tables below, **shipped** marks a value explicitly set in the bundled
21
+ row. Other values are schema defaults. An omitted key uses the schema default.
22
+
23
+ ## Configure `wsl-shell` and `wsl-fs`
24
+
25
+ These rows are nested inside `preset-wsl`. Use the helper to change their
26
+ settings:
27
+
28
+ ```bash
29
+ pnpm run preset --distro Ubuntu --session-sandbox false
30
+ ```
31
+
32
+ Supported edits are `--distro`, `--session-sandbox`, `--sandbox`,
33
+ `--mask-windows-drive`, and `--agent-backend`. Without `--out`, the helper
34
+ prints the full `preset-wsl` block to merge into the profile manually. Use
35
+ `pnpm run preset --check <file>` to detect profile layers that target nested
36
+ rows directly; on detecting them it prints the block to use. `--out` replaces
37
+ the entire destination file, including other overrides, even when it already
38
+ contains a `preset-wsl` row. It refuses to replace an existing file without
39
+ that row unless you pass `--force`. Prefer manual merging for existing profiles.
40
+
41
+ To edit the preset manually, replace the entire `preset-wsl` row. Its config is
42
+ not deep-merged: preserve `id`, `order`, and every item in `plugins`. Copy the
43
+ current block from `cordis.patch.yml` or from
44
+ `dsh --profile <name> --dump-config`, then change the nested values. Re-copy it
45
+ after a plugin upgrade that changes the preset.
46
+
47
+ ### `wsl-shell`
48
+
49
+ | Key | Default / shipped value | Description |
50
+ |---|---|---|
51
+ | `distro` | `''` (shipped) | Empty or `auto` follows the request workdir's distro. `default` selects WSL's default distro; a name pins commands to that distro. |
52
+ | `shell` | `''` | Empty resolves the distro user's login shell. Set an absolute Linux path to pin a shell. |
53
+ | `loginShell` | `true` (shipped) | Use login semantics for POSIX shells. Non-POSIX shells use their own command form. |
54
+ | `agent` | `true` | Use the resident agent. If it is unavailable, commands use the one-shot `wsl.exe` path. Set `false` to always use that path. Cwd and sandbox refusals are returned directly. |
55
+ | `agentBackend` | `auto` (shipped) | `auto` selects the packaged static-musl binary at `agent/native/linux-<arch>/dsh-wsl-agent` or the `DSH_WSL_AGENT_NATIVE` override; otherwise it uses the shell agent. `native` fails if it cannot prepare; `shell` forces the shell agent. Applies to commands, confined writes, and shared reads. |
56
+ | `sessionSandbox` | `false`; shipped `true` | `true` runs the session in a persistent systemd user scope. Requires `agent: true`, systemd as PID 1 (`[boot] systemd=true` in `/etc/wsl.conf`), a user manager, `systemd-run`, and Linger (`loginctl enable-linger <user>`). `false` uses a per-command sandbox. `auto` uses the scope when available and otherwise keeps commands confined per command. The result reports `sessionScope: 'session'` or `'per-command'`. |
57
+ | `sandbox` | `true` | Confine commands with `bwrap`. A missing or unusable `bwrap` fails closed. `false` runs commands without confinement. |
58
+ | `interopVisibility` | Unset; effective default is `visible` | `visible`, `masked`, or `disabled`. When set, this takes precedence over `maskWindowsDrive`. See [Windows interop](#windows-interop). |
59
+ | `maskWindowsDrive` | `false` | For confined commands, mount an empty tmpfs over `/mnt`. See [Windows interop](#windows-interop). |
60
+ | `cwd` | `''` | Default workdir. Empty uses the distro user's home. |
61
+ | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000` (shipped) | Default per-call timeout and the maximum timeout a call may request. |
62
+ | `wslPath` | `wsl.exe` | Path to `wsl.exe`. |
63
+ | `hostCwd` | Unset | Windows workdir for the `wsl.exe` process. Defaults to `%SystemRoot%` when available. |
64
+ | `forwardEnv` | `[]` | Additional host environment names to forward through `WSLENV`. See [Environment forwarding](#environment-forwarding). |
65
+
66
+ ### `wsl-fs`
67
+
68
+ | Key | Default / shipped value | Description |
69
+ |---|---|---|
70
+ | `distro` | `''` (shipped) | Empty or `auto` follows the target path's distro. `default` selects WSL's default distro; a name pins the filesystem. |
71
+ | `restrictToDistro` | `true` (shipped) | With a pinned distro, refuse paths in another distro's share with `FS_OUTSIDE_DISTRO`. |
72
+ | `sandbox` | `true` | Check writes against the sandbox policy. Confined writes fail closed if `bwrap` is unavailable. |
73
+ | `sessionSandbox` | `false`; shipped `true` | Use the same session runtime as `wsl-shell`. `auto` falls back to a per-policy confined resident when the session runtime is unavailable. |
74
+ | `maskWindowsDrive` | `false` | Hide `/mnt` from confined mutations. Reads are unaffected. |
75
+ | `substrate` | `agent` (shipped) | File operations use the in-distro agent. The legacy `share` value is refused at startup. |
76
+ | `cwd` | `''` | Base directory for relative paths. Empty uses the distro user's home. |
77
+ | `watchMaxDepth` | `0` | Watch the full tree. A positive depth uses bounded polling and can miss changes below the bound until a change above it triggers the callback. |
78
+ | `wslPath` | `wsl.exe` | Path to `wsl.exe`. |
79
+ | `diffBasisMaxBytes` | `10485760` | UTF-8 byte limit per side of an overwrite diff. Larger files are written without a diff. |
80
+
81
+ ### `fs-routing`
82
+
83
+ This top-level row serves root-level `ctx.fs` calls. It routes distro UNC paths
84
+ to the agent-backed filesystem and drive paths to the host filesystem. Its
85
+ distro settings are independent of `wsl-fs`.
86
+ The Loader validates the host and distro settings before mounting this row;
87
+ values rejected by the schema prevent the router from starting. A `null`
88
+ field value uses its schema default.
89
+
90
+ | Key | Default / shipped value | Description |
91
+ |---|---|---|
92
+ | `distro` | `''` (shipped) | Empty or `auto` routes each UNC path to its named distro. `default` selects WSL's default distro; a name pins the route. |
93
+ | `wslPath` | `wsl.exe` (shipped) | Path to `wsl.exe`, used to resolve the default distro. |
94
+ | `sandbox` | `true` (shipped) | Check routed mutations against `ctx.sandboxPolicy`; fail closed if `bwrap` is unavailable. The row reports `sandboxMode: undefined` whether the fence is enabled or disabled. |
95
+ | `maskWindowsDrive` | `false` (shipped) | Hide `/mnt` from confined routed mutations. |
96
+ | `restrictToDistro` | `true` (shipped) | Refuse paths in another distro's share when `distro` is pinned. |
97
+ | `watchMaxDepth` | `0` (shipped) | Watch the full tree. A positive depth uses bounded polling. |
98
+ | `distroCwd` | `''` | Default distro workdir; empty uses the distro user's home. |
99
+ | `cwd` | `process.cwd()` | Working directory for the host filesystem backend. |
100
+ | `diffBasisMaxBytes` | `10485760` | Shared overwrite-diff limit for the host and routed backends. |
101
+
102
+ The root router is app-level, not session-owned, so it does not use
103
+ `sessionSandbox`.
104
+
105
+ ### Picker and terminal rows
106
+
107
+ | Row | Key | Default / shipped value | Description |
108
+ |---|---|---|---|
109
+ | `directory-picker-wsl` | `preferredDistro` | `''` (shipped) | Distro shown first and used for the Home location. |
110
+ | | `includeHostHome` | `true` (shipped) | Include the Windows home directory. |
111
+ | | `maxEntries` | `1000` (shipped) | Maximum entries returned for one directory. |
112
+ | | `wslPath` | `wsl.exe` | Path to `wsl.exe`. |
113
+ | | `distroCacheMs` | `5000` | Cache lifetime for the distro list. |
114
+ | `subprocess-wsl` | `distro` | `''` (shipped) | Empty or `auto` follows the launch directory's distro. `default` selects WSL's default; a name pins the distro. |
115
+ | | `shell` | `''` | Shell to pin inside the distro. Empty lets `wsl.exe` select the login shell. |
116
+ | | `loginShell` | `true` | Login semantics for a pinned shell. |
117
+ | | `hostSessions` | `true` | A session opened on a Windows folder gets host PowerShell in that folder. WSL-folder sessions use the distro shell. Set `false` to use the composition-owned terminal for every session. |
118
+ | | `terminalIdleReclaim` | `true` | Reclaim idle distro terminals based on their marked processes. An unknown activity state pauses reclamation. |
119
+ | | `cwd` | `''` | Fallback launch directory when none is supplied. |
120
+ | | `wslPath` | `wsl.exe` | Path to `wsl.exe`. |
121
+ | | `hostCwd` | Unset | Windows workdir for the `wsl.exe` process. Defaults to `%SystemRoot%` when available. |
122
+ | | `forwardEnv` | `[]` | Additional host environment names to forward through `WSLENV`. |
123
+
124
+ ### Other top-level rows
125
+
126
+ | Row | Key | Default | Description |
24
127
  |---|---|---|---|
25
- | `wsl-shell` | `distro` | `''` *(shipped)* | distro mode: empty/`auto` follows the request workdir, `default` forces WSL's default, and a name pins every command |
26
- | | `shell` | `''` | pin a shell inside the distro; empty means the user's login shell |
27
- | | `loginShell` | `true` *(shipped)* | use `<shell> -lc`, which sources your profile, instead of a bare `-c`. Applies only to the POSIX family; a non-POSIX shell (fish, csh) still gets a bare `-c` |
28
- | | `agent` | `true` | run commands over the resident in-distro agent (one long-lived process; in-distro timeout); the one-shot `wsl.exe` path is fallen back to only when the agent is out of service. Anything else — a cwd refusal, a sandbox refusal — is the command's own verdict and is raised, not retried on the other path |
29
- | | `sessionSandbox` | `true` *(shipped)* | `true`, `false`, or `auto`. With `true`, run this session's commands in one persistent systemd user scope (ADR 0001): a background dev server survives the command that started it, `/tmp` is the distro's real `/tmp` under `workspace-write`, and `ps` sees the session. The executor holds the runtime for the session and stops the scope from the preset's disposer, which reaps the cgroup. Requires `agent: true`, systemd as PID 1, a reachable user manager and `Linger` (`loginctl enable-linger <user>`); the preflight refuses with the remedy when systemd, the user manager, `systemd-run` or `Linger` is missing (a success is cached; a failure is re-probed, so following the remedy needs no app restart). `auto` uses the scope when those prerequisites answer, and otherwise falls back to the per-command `bwrap` profile — still confined, never unconfined — reporting `sessionScope: "per-command"` in the result (`"session"` when the scope ran); a failed `auto` verdict is re-probed at most once a minute. `false` is the per-command shape. On in the shipped preset |
30
- | | `agentBackend` | `auto` | the session runtime's in-distro agent. `auto` uses the native agent when a verified source is available — the packaged per-arch static-musl binary at `agent/native/linux-<arch>/dsh-wsl-agent` (x86_64 and aarch64; runs on glibc and musl distros), or the `DSH_WSL_AGENT_NATIVE` development override — and otherwise the shipped shell agent; `native` fails the first command when it cannot prepare instead of silently falling back; `shell` forces the shell agent. Confined writes share the runtime, so they follow the same selection. The native backend is the default (static musl x86_64/aarch64); the shell agent is the fallback, and `native` refuses instead of falling back. This key covers the session runtime (commands and confined writes) and, since A7, the shared read agent (unconfined reads), which also follows this selection with the shell fallback |
31
- | | `sandbox` | `true` | confine commands with `bubblewrap`; `false` disables the sandbox. Needs a usable `bwrap` inside the distro and fails closed without one (`SANDBOX_UNAVAILABLE`, never an unconfined run) — see [Turning the sandbox off](#turning-the-sandbox-off) |
32
- | | `interopVisibility` | `visible` | the stance in one key (WSL-314): `visible` / `masked` / `disabled` — see the stance table below. Set at all, the stance takes precedence over the legacy `maskWindowsDrive` boolean (unset lets it answer); `disabled` masks in-session and the doctor's remedy prints the distro-level `[interop] line for the full closure (a config key does not write distro config) |
33
- | | `maskWindowsDrive` | `false` | shadow `/mnt` with an empty tmpfs in every confined profile: the Windows drive's files cannot be read or exfiltrated and its executables cannot be launched through interop — the hole that lets a "sandboxed" command reach your full Windows token. A narrowing, not a closure (a command can still write an executable into the workspace and run it — binfmt dispatches on content, not location), so enforcement stays `partial` and the result's sandbox fact reports `windowsDrive: "masked"`. The complete closure is distro-level: `[interop] enabled=false` in `wsl.conf` |
34
- | | `cwd` | `''` | default working directory; empty means the distro user's home |
35
- | | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000` *(shipped)* | per-call time limit, and the ceiling a call may ask for |
36
- | `wsl-fs` | `distro` | `''` *(shipped)* | distro mode: empty/`auto` follows each target path, `default` forces WSL's default, and a name pins the filesystem |
37
- | | `restrictToDistro` | `true` *(shipped)* | under a fixed `distro` (`default` or a name), refuse a path in **another distro**'s share (`FS_OUTSIDE_DISTRO`). Under `auto`, a UNC target is served by its own distro, so the fence has nothing foreign to refuse. `/mnt/c` is a directory *inside* the selected distro and is not affected |
38
- | | `sandbox` | `true` | check `writeText` and `editText` against the policy. Needs a usable `bwrap` in the distro too, and fails closed the same way: a distro without one refuses confined mutations (`FS_IO_ERROR`, with the install guidance) rather than downgrading them |
39
- | | `maskWindowsDrive` | `false` | as on `wsl-shell`: confined fs mutations publish through a resident whose profile shadows `/mnt` (same honest limits) |
40
- | | `sessionSandbox` | `true` *(shipped)* | `true`, `false`, or `auto`. With `true`, run confined mutations in the same session runtime the shell uses for the same `(distro, workspace, policy)` — one agent, one real `/tmp`, one workspace mount view; requires the same systemd/linger setup and the preflight refuses with the remedy. `auto` uses the runtime when the prerequisites answer and otherwise falls back to the per-policy confined resident (still kernel-confined). Set `false` for the per-policy confined resident. The app-level `fs-routing` backend is not session-owned and keeps this off |
41
- | | `substrate` | `agent` *(shipped)* | which I/O substrate serves the file tools. Only the resident in-distro agent (`lib/fs-substrate.js`) exists: reads, writes and identities run on ext4 — native symlinks, native mode bits, and the mutation guard survives to the write's atomic publication, kernel-enforced by the confined resident when the policy is confined. The former `share` opt-out (the Windows-side host stack over the 9p share) is refused at boot; delete the line. The comment shipped beside this key in `cordis.patch.yml` still calls `share` the opt-out — that comment is out of date, and the boot refusal is what runs |
42
- | | `cwd` | `''` | base directory for relative paths; empty means the distro user's home |
43
- | | `watchMaxDepth` | `0` | how deep the in-distro watch scan walks; `0` walks the whole tree and enables the inotify backend. A positive value keeps the polling scan (inotify cannot bound its walk); a change below the bound is seen only when another change above it fires the same callback |
44
- | `fs-routing` | `distro` | `''` *(shipped)* | distro mode for the root-plane filesystem: empty/`auto` follows each target's UNC distro, `default` forces WSL's default, and a name pins. **A row of its own**, with its own copy of the distro knobs — changing `wsl-fs` does not change this one, nor the other way round |
45
- | | `wslPath` | `wsl.exe` *(shipped)* | path to `wsl.exe`; used here only to resolve the default distro |
46
- | | `sandbox` | `true` *(shipped)* | fence routed mutations by `ctx.sandboxPolicy`; same `bwrap` requirement and fail-closed refusal as `wsl-fs`. The routing row reports no `sandboxMode` — `undefined` whether the fence is on or off |
47
- | | `maskWindowsDrive` | `false` *(shipped)* | as on `wsl-shell`, for routed mutations |
48
- | | `restrictToDistro` | `true` *(shipped)* | under a fixed `distro` (`default` or a name), refuse a path in another distro's share (`FS_OUTSIDE_DISTRO`); under `auto`, each UNC target is served by its own distro |
49
- | | `watchMaxDepth` | `0` *(shipped)* | how deep the routed watch scan walks; `0` walks the whole tree and enables the inotify backend; a positive value keeps the polling scan |
50
- | | `distroCwd` | `''` | the routed backend's default workdir; empty means the distro user's home. `cwd` belongs to the host backend only — the distro side reads `distroCwd` — while `diffBasisMaxBytes` is shared: it bounds the overwrite diff on both sides |
51
- | `directory-picker-wsl` | `preferredDistro` | `''` *(shipped)* | the distro listed first in the picker |
52
- | | `includeHostHome` | `true` *(shipped)* | also list the Windows home directory |
53
- | | `maxEntries` | `1000` *(shipped)* | maximum entries listed per directory |
54
- | `subprocess-wsl` | `distro` | `''` *(shipped)* | distro mode for the GUI terminal: empty/`auto` follows the launch directory, `default` forces WSL's default, and a name pins |
55
- | | `shell` | `''` | pin a shell; empty lets `wsl.exe` decide |
56
- | | `loginShell` | `true` | whether a pinned shell uses login semantics |
57
- | | `hostSessions` | `true` | give a session opened on a Windows folder a host `powershell.exe` in its own directory instead of the distro shell at `/mnt/<drive>/…`; `false` restores the composition-owned distro terminal for every session |
58
- | | `terminalIdleReclaim` | `true` | let the controller reclaim an idle distro terminal: the launch is marked with a per-terminal `DSH_TERMINAL_ID`, and the handle's `inspectActivity` counts the marked processes through the resident agent — one (the shell at its prompt) is idle, more than one (a running command or a nested shell) is busy, `unknown` (agent out) pauses reclamation. `false` restores close-by-hand |
59
-
60
- The GUI file tree and previews use the shipped `workspace-files` row, not a plugin
61
- row. Its caps are the peer defaults (`maxBytes` 2 MiB, `maxFileBytes` 32 MiB,
62
- `maxLines` 5000, `maxEntries` 2000); override them with `- id: workspace-files`
63
- in your own layer.
64
-
65
- ## Overriding the rows inside `preset-wsl`
66
-
67
- `wsl-shell` and `wsl-fs` are mounted as plugins of the `wsl` agent preset, inside
68
- the `preset-wsl` row's `config.plugins`. The loader indexes top-level entry ids —
69
- plus the plugin lists of rows that are themselves groups — and `preset-wsl` is a
70
- plain entry whose plugins sit inside its `config`, so those two ids are not
71
- addressable from a patch layer at all. A layer of
72
-
73
- ```yaml
74
- - id: wsl-shell
75
- config:
76
- distro: Ubuntu
77
-
78
- - id: wsl-fs
79
- config:
80
- restrictToDistro: false
128
+ | `auto-preset` | `preset` | `wsl` | Preset to bind when a session workspace is inside a distro. An explicit operator selection is preserved. |
129
+ | | `attempts` | `6` | Number of attempts to bind the preset. |
130
+ | | `retryMs` | `150` | Delay between attempts, in milliseconds. |
131
+ | `wsl-preflight` | `distro` | `''` | Distro to check. Empty or `auto` follows the session workspace. |
132
+ | | `wslPath` | `wsl.exe` | Path to `wsl.exe`. |
133
+ | | `sandbox` | `true` | Run the session-start `bwrap` check. |
134
+ | | `sessionSandbox` | `true` | Also check systemd, the user manager, `systemd-run`, and Linger. Set false when the profile does not use a session runtime. |
135
+ | `wsl-shell-env` | `distro` | `''` | Distro used to populate the `DSH_WSL_*` environment facts. |
136
+ | | `portsRefreshMs` | `10000` | Listening-port snapshot interval, in milliseconds. |
137
+
138
+ The GUI file tree and previews use the bundled `workspace-files` row through
139
+ `fs-routing`; they do not use a separate WSL provider row. To change its
140
+ peer-defined limits, override `workspace-files` by id in the profile layer.
141
+
142
+ ## Shared command settings
143
+
144
+ The shell row inherits output and process settings from
145
+ `@deepseek-ai/dsh-bash-local`:
146
+
147
+ | Key | Default | Description |
148
+ |---|---|---|
149
+ | `maxOutputBytes` | `64000` | Per-stream captured output limit. The agent enforces this limit inside the distro and marks truncated results. |
150
+ | `maxSpillBytes` | `67108864` | Per-stream spill-file limit on the one-shot path. |
151
+ | `graceMs` | `3000` | Grace period between SIGTERM and SIGKILL. |
152
+
153
+ With `agent: true`, captured output is limited inside the distro and no spill
154
+ file is written. `maxSpillBytes` and `graceMs` apply when `agent: false` or
155
+ when the resident agent is unavailable and execution uses one-shot `wsl.exe`.
156
+
157
+ ## Environment forwarding
158
+
159
+ WSL imports a host environment variable only when its name is listed in
160
+ `WSLENV`. The providers forward `NO_COLOR`, `TERM`, `PAGER`, `GIT_PAGER`,
161
+ `LANG`, `LC_ALL`, and managed `DSH_*` names by default. `forwardEnv` adds names
162
+ to that list; `PATH` is never forwarded.
163
+
164
+ For `wsl-shell`, `forwardEnv` applies to the one-shot `wsl.exe` path. The
165
+ resident agent receives only the managed `DSH_*` environment, so custom names
166
+ in `forwardEnv` do not reach commands running through that agent. The
167
+ `subprocess-wsl` setting applies to its WSL launches.
168
+
169
+ ## Distro selection
170
+
171
+ For command and terminal rows, an empty `distro` or `distro: auto` follows
172
+ the request's UNC workdir. When the request does not name a distro, WSL's
173
+ default distro is used. For `wsl-fs`, `auto` follows each target path. For
174
+ `fs-routing`, each UNC target selects its own distro.
175
+
176
+ `distro: default` always selects WSL's default distro. A distro name pins the
177
+ row to that distro. With `restrictToDistro: true`, a path in another distro's
178
+ share is refused with `FS_OUTSIDE_DISTRO`. The Windows drive mounted at
179
+ `/mnt/c` is inside the selected distro and is not rejected by this fence.
180
+
181
+ ## Windows interop
182
+
183
+ `interopVisibility` controls whether `/mnt` is masked for confined commands;
184
+ it does not itself disable Windows interop. If unset, `maskWindowsDrive: false`
185
+ gives the `visible` behavior.
186
+
187
+ | Value | Effect |
188
+ |---|---|
189
+ | `visible` | Leaves `/mnt` visible. Confined commands can read Windows files and launch Windows executables through WSL interop. |
190
+ | `masked` | Mounts an empty tmpfs over `/mnt` in confined profiles. Commands cannot access paths under `/mnt`; other mounts and unrestricted file reads are not covered. |
191
+ | `disabled` | Applies the same `/mnt` mask as `masked`; it does not disable interop or edit `wsl.conf`. Configure the distro separately as described below. |
192
+
193
+ `maskWindowsDrive: true` is equivalent to `interopVisibility: masked`.
194
+ When `interopVisibility` is set, it takes precedence over
195
+ `maskWindowsDrive`.
196
+
197
+ Masking narrows access; it does not close interop. WSL can still run a Windows
198
+ executable written into the workspace because `binfmt_misc` dispatches by file
199
+ content. Enforcement therefore remains `partial`, and results report
200
+ `windowsDrive: 'masked'`. To disable interop for the distro, add this section
201
+ to `/etc/wsl.conf`:
202
+
203
+ ```ini
204
+ [interop]
205
+ enabled=false
81
206
  ```
82
207
 
83
- changes nothing and does not fail: the log carries
84
- `patch: entry "wsl-shell" not found` and `patch: entry "wsl-fs" not found`, and the
85
- composed config keeps the shipped values.
208
+ Then run `wsl.exe --shutdown`. This distro-level change is required to close
209
+ interop.
86
210
 
87
- The helper does the extraction and the common edits for you:
211
+ To apply the masked profile to both nested rows:
88
212
 
89
213
  ```bash
90
- pnpm run preset -- --distro Ubuntu --session-sandbox false \
91
- --out "$DSH_HOME/profiles/<name>/cordis.patch.yml"
214
+ pnpm run preset --sandbox true --mask-windows-drive true
92
215
  ```
93
216
 
94
- `--distro`, `--session-sandbox`, `--sandbox`, `--mask-windows-drive` and
95
- `--agent-backend` are the supported edits; without `--out` the helper prints the
96
- complete block instead of writing it. `pnpm run preset -- --check <file>` reads an
97
- existing layer and **fails with the exact block to paste** when it finds a
98
- top-level `- id: wsl-shell`/`wsl-fs` entry — the silent no-op is caught before
99
- boot. The generated block is byte-for-byte the shipped row plus your edits, so a
100
- plugin upgrade only needs a re-run.
101
-
102
- The route that works is to override the `preset-wsl` row as a whole. A row's
103
- `config` is replaced whole, never deep-merged, so the replacement has to carry
104
- `id`, `order` and the entire `plugins` list — every plugin, the `wsl-env` group
105
- and everything after it. Copy the whole `- id: preset-wsl` block out of
106
- [`cordis.patch.yml`](../cordis.patch.yml) (or out of
107
- `dsh --profile <name> --dump-config`), paste it into your layer, and edit only the
108
- values you want:
109
-
110
- ```yaml
111
- - id: preset-wsl
112
- config:
113
- id: wsl
114
- order: 2
115
- plugins:
116
- - id: wsl-env
117
- name: cordis:group
118
- group: true
119
- isolate:
120
- shell: true
121
- fs: true
122
- config:
123
- - id: wsl-shell
124
- name: 'dsh-plugin-wsl-env'
125
- config:
126
- distro: Ubuntu # ← the edit
127
- loginShell: true
128
- timeoutMs: 120000
129
- maxTimeoutMs: 600000
130
- - id: wsl-fs
131
- name: 'dsh-plugin-wsl-env/fs'
132
- config:
133
- distro: ''
134
- restrictToDistro: false # ← the edit
135
- substrate: agent
136
- # … and the rest of the group, verbatim — do not leave a plugin out …
137
- ```
217
+ Merge the printed block into the profile. The `visible` and `masked` behavior
218
+ is covered by `test/probe/interop-shell.sh` for both resident and one-shot
219
+ commands. The distro-level disabled state requires a manual check because it
220
+ changes WSL configuration.
138
221
 
139
- Leaving a plugin out of the replacement drops it: the preset would mount only the
140
- plugins you listed. The keys described above are the only reason to rewrite this
141
- row, so treat the `preset-wsl` block as one unit and re-copy it after a plugin
142
- upgrade that changes it.
222
+ ## Sandbox and preflight
143
223
 
144
- For the root-plane filesystem — the row that serves `ctx.fs` outside any session,
145
- `fs-routing` — no rewrite is needed: it is a top-level row, and
146
- `restrictToDistro`, `sandbox` and `maskWindowsDrive` can be set on it directly.
147
- That governs different traffic from `wsl-fs`; see
148
- [`docs/root-fs-routing.md`](root-fs-routing.md).
224
+ `sandbox: true` is the default for `wsl-shell`, `wsl-fs`, and
225
+ `fs-routing`. Confined commands and mutations require a usable `bwrap` inside
226
+ the distro. If it is missing or unusable, commands fail with
227
+ `SANDBOX_UNAVAILABLE` and writes fail with `FS_IO_ERROR`; neither operation
228
+ falls back to an unconfined run. The error distinguishes a missing package from
229
+ a present but unusable `bwrap`.
149
230
 
150
- ## Other keys
231
+ Install the prerequisite with the bootstrap helper:
151
232
 
152
- You can override the other keys in the same way, with one split: every row in the
153
- table below except `wsl-shell` and `wsl-fs` is top-level, so `- id: <row>` in your
154
- own layer patches it — those two are inside `preset-wsl` and take the rewrite
155
- above. They all keep their defaults:
233
+ ```bash
234
+ scripts/bootstrap.sh <distro> --install
235
+ ```
156
236
 
157
- | Row | Key | Default | Meaning |
158
- |---|---|---|---|
159
- | `wsl-shell` (in `preset-wsl`) | `wslPath` | `wsl.exe` | path to `wsl.exe`, for an installation that is not on `PATH` |
160
- | | `hostCwd` | unset → `%SystemRoot%` | the Windows directory the `wsl.exe` process itself starts in |
161
- | | `forwardEnv` | `[]` | extra host environment variable names to forward into the distro through `WSLENV`. This is the one key here that widens what crosses the boundary: `NO_COLOR`, `TERM`, `PAGER`, `GIT_PAGER`, `LANG`, `LC_ALL` and every managed `DSH_*` name ride along without it, and `PATH` is deliberately never forwarded. It applies to the **one-shot `wsl.exe` path**; the resident agent's environment stays pinned to the managed `DSH_*` namespace and deliberately does not forward these names, so a secret listed here does not widen every model command in the session |
162
- | `wsl-fs` (in `preset-wsl`) | `wslPath` | `wsl.exe` | as above; used only to resolve the default distro |
163
- | | `diffBasisMaxBytes` | `10485760` | UTF-8 byte limit per side of an overwrite diff; a larger file publishes without a diff |
164
- | `fs-routing` | `cwd`, `diffBasisMaxBytes` | `process.cwd()`, `10485760` | `cwd` configures the host side only (the distro side reads `distroCwd`); `diffBasisMaxBytes` is shared by both — it also bounds the routed backend's overwrite diff |
165
- | `directory-picker-wsl` | `wslPath` | `wsl.exe` | as above |
166
- | | `distroCacheMs` | `5000` | how long a resolved distro list stays fresh before `wsl.exe` is asked again |
167
- | `subprocess-wsl` | `wslPath` | `wsl.exe` | as above |
168
- | | `cwd` | `''` | fallback directory when a launch names none |
169
- | | `hostCwd` | unset → `%SystemRoot%` | as on `wsl-shell` |
170
- | | `forwardEnv` | `[]` | as on `wsl-shell`, for terminal launches |
171
-
172
- The shell row also inherits the shipped output budgets from `dsh-bash-local`,
173
- which documents them in full: `maxOutputBytes` (64000), `maxSpillBytes`
174
- (67108864) and `graceMs` (3000, the SIGTERM-to-SIGKILL grace). The spill keys
175
- belong to the one-shot path: with `agent: true` — the default — the capture is
176
- cut at `maxOutputBytes` per stream inside the distro, the result reports
177
- `truncated: true`, and no temporary file is written, so `maxSpillBytes` and
178
- `graceMs` have nothing to bound. They apply when `agent` is `false` or the agent
179
- is out of service and the call rides the one-shot `wsl.exe` path.
180
-
181
- ## Which distro is used
182
-
183
- `distro: ''` (or `distro: 'auto'`) follows the workspace: opening
184
- `\\wsl.localhost\Debian\...` runs there even when Ubuntu is WSL's default.
185
- `distro: 'default'` forces WSL's default instead, and a fixed distro (`default` or
186
- a name) pins every session to that distro — a foreign UNC then refuses with
187
- `FS_OUTSIDE_DISTRO` while `restrictToDistro` holds. A single-distro installation
188
- needs no configuration.
189
-
190
- ## The strict profile
191
-
192
- `maskWindowsDrive: true` with `sandbox: true` is the strict profile: every
193
- confined profile shadows `/mnt` with an empty tmpfs, so a confined command can
194
- neither read the Windows drive nor launch a Windows executable from it. Write it
195
- into a profile with the preset helper (the rows are nested inside `preset-wsl`):
237
+ Without `--install`, the helper performs a read-only check. On apt-based
238
+ distros, the direct install command is:
196
239
 
197
240
  ```bash
198
- pnpm run preset -- --sandbox true --mask-windows-drive true \
199
- --out "$DSH_HOME/profiles/<name>/cordis.patch.yml"
241
+ wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap
200
242
  ```
201
243
 
202
- The trade-offs are real: confined commands lose `/mnt/c` entirely — no reading
203
- Windows files, no running Windows tools, no dev server that expects a Windows
204
- path. It is also a narrowing, not a closure: a command can still write a Windows
205
- executable into the workspace and run it there, because WSL's `binfmt_misc`
206
- dispatches on content, not location. Enforcement therefore stays `partial`, and
207
- the result reports `windowsDrive: "masked"`. The only complete closure is
208
- distro-level: `[interop] enabled=false` in `/etc/wsl.conf`, then
209
- `wsl.exe --shutdown`, which removes interop from the distro entirely.
210
-
211
- | Interop state | How | `/mnt` | Windows exe from `/mnt` | Workspace-written `.exe` | Evidence |
212
- |---|---|---|---|---|---|
213
- | visible (default) | — | readable | runs, and escapes the Linux sandbox | runs | `pnpm run probe:sandbox` — INFO "interop escape is open" |
214
- | masked (strict) | `maskWindowsDrive: true` | empty tmpfs | unreachable | still runs (binfmt) | `pnpm run probe:sandbox` — the strict-profile checks |
215
- | disabled | `[interop] enabled=false`, `wsl --shutdown` | no Windows mounts | n/a | fails (no binfmt) | documented; not probed automatically (needs a distro-level change) |
216
-
217
- The masked and visible legs are asserted on the real machine by
218
- `test/probe/sandbox.sh`; the disabled leg needs a distro configuration change and
219
- a WSL shutdown, so it stays a manual check recorded here.
220
-
221
- ## Turning the sandbox off
222
-
223
- `sandbox: false` on `wsl-shell` and `wsl-fs` runs commands and writes without
224
- confinement. `sandboxMode` then reports `undefined`, and the tool layer tells the
225
- model that these operations have no sandbox. `fs-routing` takes the same key and
226
- drops the fence on the root-plane filesystem the same way, but that service reports
227
- no `sandboxMode` either way — it inherits the base class's empty getter — so the
228
- key decides the fence, not a capability fact the model reads. That is the documented
229
- opt-out; see [ARCHITECTURE.md](ARCHITECTURE.md#sandbox) for what the sandbox does
230
- and does not cover.
231
-
232
- The default posture needs `bubblewrap` **inside the distro** and fails closed: a
233
- distro without a usable `bwrap` refuses the command (`SANDBOX_UNAVAILABLE`) or the
234
- write (`FS_IO_ERROR`, the message carrying the same guidance) instead of
235
- running it unconfined. The refusal distinguishes the two causes — `bwrap`
236
- missing (install it) from `bwrap` present but failing (usually the kernel's
237
- unprivileged user namespaces; reinstalling will not help) — and the direct
238
- install command it prints follows the distro's own package family
239
- (apt/dnf/pacman/zypper). A failed probe is re-run,
240
- so installing `bubblewrap` takes effect on the next command without a restart.
241
- Install it with `scripts/bootstrap.sh <distro> --install`, or directly:
242
- `wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap`. Run without
243
- `--install`, `scripts/bootstrap.sh <distro>` is a read-only check that prints what
244
- is missing.
245
-
246
- The same probe also runs when a session opens: the `wsl-preflight` row warns
247
- with the full remedy if `bwrap` is unusable, so the news arrives with the
248
- session rather than with the first failed command. It also checks the session
249
- runtime's prerequisites — systemd as PID 1, the user manager, `systemd-run` and
250
- Linger — when `sessionSandbox` on the preflight row is `true` (the default,
251
- matching the shipped preset). Set `sessionSandbox: false` on that row when the
252
- profile opts out of the session runtime, so it does not warn about a
253
- prerequisite the profile does not use.
254
-
255
- The advisory is logged and also published as a `wsl-preflight` runtime-context
256
- entry, so it reaches the session transcript; DSH 0.2.1 has no toast/banner seam
257
- for a plugin (see [UPSTREAM-SESSION-NOTICE.md](UPSTREAM-SESSION-NOTICE.md) and
258
- [LIMITATIONS.md](LIMITATIONS.md#session-start-advisories)). It is advisory — it
259
- never installs anything and never changes the fail-closed semantics; drop the
260
- `wsl-preflight` row from your layer (or set `sandbox: false` on it) to silence
261
- the warning.
262
-
263
- ## Checking a change, and what refuses
264
-
265
- `dsh --profile <name> --dump-config` composes the profile and prints the result
266
- without booting. It is the fastest way to see whether a row you wrote landed —
267
- including the warning that says it did not — and for the rows inside `preset-wsl`
268
- the composed tree it prints carries the block to copy back into your layer.
269
- `scripts/bootstrap.sh <distro>` does the same for the distro-side prerequisites.
270
-
271
- When something refuses, the message names the row or the code:
272
-
273
- | Symptom | What you see | What clears it |
244
+ The `wsl-preflight` row runs these checks when a session opens. It also checks
245
+ the session-runtime prerequisites when `sessionSandbox: true`. The warning is
246
+ advisory: it does not install packages or change the providers' fail-closed
247
+ behavior. Set `sandbox: false` on this row or remove it to silence the
248
+ sandbox warning. Set `sessionSandbox: false` when the profile does not use a
249
+ session runtime.
250
+
251
+ Setting `sandbox: false` on `wsl-shell` or `wsl-fs` runs commands or writes
252
+ without confinement; their `sandboxMode` becomes `undefined`. The tool layer
253
+ reports that no sandbox is active. On `fs-routing`, the setting disables the
254
+ mutation fence; that row reports `sandboxMode: undefined` either way.
255
+
256
+ ## Diagnose configuration and refusals
257
+
258
+ Use `dsh --profile <name> --dump-config` to inspect the composed profile
259
+ without booting it. The output shows whether a row override applied. Use
260
+ `scripts/bootstrap.sh <distro>` to inspect distro prerequisites.
261
+
262
+ | Symptom | Result | Action |
274
263
  |---|---|---|
275
- | commands fail with no usable sandbox | `SANDBOX_UNAVAILABLE` — the text says whether `bwrap` is missing or present but failing, with the matching remedy | missing: install `bubblewrap` (`scripts/bootstrap.sh <distro> --install`, or the printed family command); present-but-failing: check `bwrap --version` and the kernel's unprivileged user namespaces, or set `sandbox: false` on `wsl-shell` |
276
- | writes fail with no usable sandbox | `FS_IO_ERROR`, with the same `bwrap` remedy the commands carry | the same install, or `sandbox: false` on `wsl-fs` (`fs-routing` for root-plane writes) |
277
- | file tools fail while the distro is down | `FS_IO_ERROR`, "the distro file substrate is unavailable" | start the distro (`wsl.exe -l -v`), restart the session if it stays out |
278
- | a command never starts | a `WSL_E_*` code naming the distro | start it with `wsl.exe -d <distro>`, or point `distro` at another one — wider permissions do not help |
279
- | `wsl.exe` itself cannot be started | the error names the executable and says to check `wslPath` on this row | fix `wslPath`, and check WSL is installed |
280
- | WSL wedges mid-command | no completion within the deadline | retry once, then `wsl.exe --shutdown` and reopen |
281
- | a watched path never fires | `FS_IO_ERROR` from `watch` | watch a path inside the selected distro |
282
- | a path in another distro is refused | `FS_OUTSIDE_DISTRO` | only when `distro` is pinned; set `distro: ''`/`auto` to follow the workspace, or open a session in that distro |
283
-
284
- The full code table — what each code means and what clears it — is
285
- [ARCHITECTURE.md § Error codes](ARCHITECTURE.md#error-codes); the design boundaries
286
- behind the fail-closed choices above are in [LIMITATIONS.md](LIMITATIONS.md).
287
-
288
- ## `wsl-shell-env`
289
-
290
- | Key | Default | Meaning |
291
- | --- | --- | --- |
292
- | `distro` | `''` | distro mode for the `DSH_WSL_*` facts: empty/`auto` follows each session's workspace, `default` forces WSL's default, and a name pins |
293
- | `portsRefreshMs` | `10000` | how often the `DSH_WSL_PORTS` listening-port snapshot refreshes through the resident agent. A failed scan keeps the last successful list (and a debug line), so a transient agent outage does not read as “no ports”; only a successful empty scan means none were listening |
264
+ | Command sandbox unavailable | `SANDBOX_UNAVAILABLE`; message distinguishes missing or unusable `bwrap`. | Install `bubblewrap`, repair user namespaces, or set `sandbox: false` on `wsl-shell`. |
265
+ | Confined write unavailable | `FS_IO_ERROR` with the `bwrap` remedy. | Install or repair `bubblewrap`; to disable the fence, set `sandbox: false` on `wsl-fs` or `fs-routing`. |
266
+ | File operations fail while the distro is stopped | `FS_IO_ERROR`: distro substrate unavailable. | Start the distro. If the agent remains unavailable, restart the session. |
267
+ | Command cannot start in the selected distro | `WSL_E_*` code naming the distro. | Start that distro or change `distro`. Wider permissions do not resolve a stopped distro. |
268
+ | `wsl.exe` cannot start | Error names the executable and the row's `wslPath`. | Correct `wslPath` and confirm WSL is installed. |
269
+ | WSL hangs during a command | Command reaches its deadline without completion. | Retry once, then run `wsl.exe --shutdown` and reopen the session. |
270
+ | A watched change does not notify | Polling can miss changes that preserve the observed metadata; no error is guaranteed. | Check the watcher limits in [LIMITATIONS.md](LIMITATIONS.md). |
271
+ | A path in another distro is refused | `FS_OUTSIDE_DISTRO`. | Use `distro: ''` or `auto` to follow the workspace, or open the session in the target distro. |
272
+
273
+ See [ARCHITECTURE.md — Error codes](ARCHITECTURE.md#error-codes) for the full
274
+ error reference and [LIMITATIONS.md](LIMITATIONS.md) for the design boundaries.