dsh-plugin-wsl-env 0.8.0 → 0.9.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.
package/SECURITY.md CHANGED
@@ -45,6 +45,21 @@ These are targets, not a contract.
45
45
 
46
46
  If a report is out of scope, we will say so and explain why.
47
47
 
48
+ ## Disclosure
49
+
50
+ A confirmed vulnerability is fixed and disclosed through the ordinary release
51
+ process, not through a separate security track.
52
+
53
+ - The fix ships in the next release of the package, which is the only version the
54
+ Supported versions table above covers.
55
+ - That release records the fix in [`CHANGELOG.md`](CHANGELOG.md), and the GitHub
56
+ Release notes are taken from that section.
57
+ - The advisory is published on this repository's Security tab, where the private
58
+ report was filed, once the release that carries the fix is out.
59
+
60
+ A report that is out of scope, or that we cannot confirm as a vulnerability, is
61
+ closed without a published advisory.
62
+
48
63
  ## Scope
49
64
 
50
65
  In scope:
@@ -69,8 +84,9 @@ process inside the distro can still start a Windows program through WSL interop,
69
84
  for example `/mnt/c/.../*.exe`. That program runs outside the Linux sandbox.
70
85
 
71
86
  This is a documented design boundary, not a vulnerability by itself. The command
72
- `npm run probe:sandbox` demonstrates it on your machine, and the README states it
73
- in its Sandbox section.
87
+ `npm run probe:sandbox` demonstrates it on your machine when run from inside the
88
+ distro, where `bubblewrap` and `node` must be installed; the README states it in
89
+ its Sandbox section.
74
90
 
75
91
  A report is still welcome if you find an escape that does not depend on interop,
76
92
  or if the plugin ever reports `full` while interop is reachable.
package/SUPPORT.md CHANGED
@@ -8,7 +8,11 @@
8
8
  | Linux side | WSL2 with at least one distro |
9
9
  | DSH packages | the `0.2.0-rc.2`-era packages listed in `peerDependencies` |
10
10
  | Plugin version | the latest published version only |
11
- | Node | 20, 22, or 24 |
11
+ | Node | 22.19+ (`^22.19.0`), or 24 and later (`>=24.0.0`) |
12
+
13
+ Node 20 is outside the range on purpose: the floor is the harness host's own, and
14
+ CI covers 22 and 24. `engines` in `package.json` is the authoritative statement,
15
+ not this table.
12
16
 
13
17
  Outside Windows there is nothing for the plugin to attach to. It runs `ctx.shell`
14
18
  inside the distro through `wsl.exe`. A Linux host, a macOS host, or WSL1 is not a
@@ -21,7 +25,10 @@ Open an issue in the repository:
21
25
  https://github.com/Big-Dao/dsh-plugin-wsl-env/issues
22
26
 
23
27
  Use it for bug reports and for questions about configuration. Search the open and
24
- the closed issues first.
28
+ the closed issues first, and check whether the answer is already written down:
29
+ [docs/LIMITATIONS.md](docs/LIMITATIONS.md) lists the known design boundaries, and
30
+ the Troubleshooting table in [README.md](README.md#troubleshooting) covers the
31
+ common failures (`SANDBOX_UNAVAILABLE`, a missing `rg`, and the rest).
25
32
 
26
33
  For a security issue, follow [SECURITY.md](SECURITY.md). Do not open a public
27
34
  issue for one.
@@ -31,8 +38,12 @@ issue for one.
31
38
  - macOS, and Linux hosts that are not WSL2.
32
39
  - Windows without WSL2.
33
40
  - A modified DSH build, or a fork of the harness packages.
34
- - A distro the plugin cannot recognize. Examples are a replaced `/bin/sh`, a
35
- container-style root filesystem, and a missing `bwrap`.
41
+ - A distro the plugin cannot recognize, which is what its probes report: no
42
+ distro from `wsl.exe -l -q`, a `$HOME` that is empty or not an absolute path,
43
+ or no absolute shell from the `getent passwd` / `$SHELL` probes. The detection
44
+ lives in `src/wsl.ts`.
45
+ - A distro without `bwrap`. Commands there fail closed rather than run
46
+ unconfined.
36
47
  - The upstream `@deepseek-ai/*` packages. Report those to their own projects.
37
48
  - Third-party npm mirrors. Install from the registry the package was published to.
38
49
 
@@ -47,14 +58,19 @@ There is no long-term support branch. Fixes go to the latest version.
47
58
 
48
59
  ## What a good bug report contains
49
60
 
61
+ The one-command path: `pnpm run diagnose` (from a checkout of the plugin, or
62
+ `bash scripts/diagnose.sh`) collects most of the table below — Windows build,
63
+ WSL status, distro, kernel, package family, the four tools with their versions,
64
+ and a bwrap functional probe — as a paste-ready report. Run it first, then add
65
+ what it cannot see:
66
+
50
67
  | Item | How to get it |
51
68
  |---|---|
69
+ | The diagnose report | `pnpm run diagnose -- <distro>` (read-only; safe to paste) |
52
70
  | Plugin version | `npm ls dsh-plugin-wsl-env`, or the git tag you built from |
53
71
  | DSH version | the version of the harness you run |
54
- | Distro and kernel | `wsl.exe -l -v`, and `uname -r` inside the distro |
55
- | Windows build | `winver` |
56
72
  | The composed configuration | `dsh --profile <name> --dump-config`, in particular the `dsh-plugin-wsl-env` layer and the rows it patches |
57
- | Sandbox behaviour | the output of `npm run probe:sandbox`, when the sandbox is involved |
73
+ | Sandbox behaviour, in depth | the output of `npm run probe:sandbox`, run from inside the distro (`wsl.exe -d <distro>` first) — the probe calls `bwrap` and reads `lib/bwrap.js` there, so it needs both in the distro |
58
74
  | What you expected and what happened | one paragraph each |
59
75
  | A reproduction | the exact command, or the exact sequence of clicks |
60
76
 
package/cordis.patch.yml CHANGED
@@ -162,6 +162,15 @@
162
162
  - id: auto-preset
163
163
  name: 'dsh-plugin-wsl-env/auto-preset'
164
164
 
165
+ # Session-start sandbox preflight: when a distro session opens, run the
166
+ # providers' own bwrap usability probe (shared cache, so the first confined
167
+ # command pays nothing) and surface a missing or broken bubblewrap as a
168
+ # warning with the full remedy — instead of the first command's fail-closed
169
+ # error being the discovery moment. Advisory only: it never installs and
170
+ # never changes the providers' fail-closed semantics.
171
+ - id: wsl-preflight
172
+ name: 'dsh-plugin-wsl-env/preflight'
173
+
165
174
  - insert:
166
175
  # ═════════════════════════════════════════════════════════════════════════════
167
176
  # preset-wsl — the WSL environment, scoped to agents that select this preset.
@@ -11,28 +11,56 @@ DSH exposes its capabilities as services. The model-facing tools (`bash`, `read`
11
11
  `write`, `edit`, `glob`, `grep`) consume those services. They never touch a
12
12
  filesystem or a shell directly.
13
13
 
14
- This plugin supplies WSL-backed implementations of three of them, plus three
15
- smaller integrations.
14
+ This plugin supplies WSL-backed implementations of three of them, takes over two
15
+ root-plane services, and adds three smaller integrations.
16
16
 
17
17
  ## The three service providers
18
18
 
19
19
  | Service | Class | File | What it does |
20
20
  |---|---|---|---|
21
- | `ctx.shell` | `WslShellExecutor` | [`lib/index.js`](../lib/index.js) | Runs each command as `wsl.exe -d <distro> --cd <linux dir> --exec <login shell> -lc <cmd>`, wrapped in a distro-side `bwrap` sandbox. |
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
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). |
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 spawn into the distro's own rg (see below). |
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`
26
26
  extends the shipped `LocalFileSystem`. Only the parts that need a distro are
27
27
  overridden.
28
28
 
29
+ ## The two root-plane takeovers
30
+
31
+ Two of the shipped services are session-less — the GUI file tree and the root
32
+ `ctx.fs` — so they cannot be served from an agent preset. The layer replaces
33
+ their shipped rows outright, with subclasses that route by coordinate:
34
+
35
+ | Service | Class | File | What it does |
36
+ |---|---|---|---|
37
+ | `ctx.workspaceFiles` | `WorkspaceFilesWsl` | [`lib/workspace-files-wsl.js`](../lib/workspace-files-wsl.js) | The GUI file tree and previews for a `\\wsl.localhost\<distro>` workspace, served from inside the distro over the resident agent instead of Windows-side 9P walks. Extends the shipped service: a distro UNC routes distro-side, a drive path calls `super`, and the watch stream delegates unchanged — so replacing the row is safe for Windows-folder sessions too. Replaces the shipped `workspace-files` row. |
38
+ | `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. |
39
+
40
+ Both routes are composition-level for the mirror-image reason the terminal is
41
+ (see below): no session identity reaches them. Each takeover is a top-level
42
+ `disabled:` entry targeting the shipped row plus a bare `insert:` list holding
43
+ the replacement — because a `disabled` written *inside* an insert list is not a
44
+ patch but a duplicate-id data row, and a nested `insert:` is never interpreted at
45
+ all. [`cordis.patch.yml`](../cordis.patch.yml) carries the full row list, and
46
+ `test/composition.test.mjs` runs the file through the real patch algorithm to
47
+ keep the shape honest.
48
+
49
+ One further module ships built but deliberately unwired:
50
+ [`lib/file-reference-wsl.js`](../lib/file-reference-wsl.js), the distro-side
51
+ traversal strategy for `@` completion. It is built and tested, but **not** wired
52
+ into `cordis.patch.yml` — the seam it consumes is an upstream proposal
53
+ ([`upstream/rfc-wsl-workspaces.md`](upstream/rfc-wsl-workspaces.md)), and
54
+ reaching the funnels without it would mean subclassing past module-internal
55
+ functions.
56
+
29
57
  ## The three smaller integrations
30
58
 
31
59
  | Subpath | Class or name | File | What it does |
32
60
  |---|---|---|---|
33
61
  | `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). |
34
62
  | `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. |
35
- | `dsh-plugin-wsl-env/shell-env` | `wsl-shell-env` | [`lib/shell-env.js`](../lib/shell-env.js) | Contributes `DSH_WSL_DISTRO`, `DSH_WSL_SHELL`, and `DSH_WSL_HOME` to the managed `DSH_*` namespace. |
63
+ | `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. The port list is a snapshot, refreshed every `portsRefreshMs` (10000 by default), so a server started moments ago may not be listed yet. |
36
64
 
37
65
  Three modules support them and import no DSH package.
38
66
  [`lib/paths.js`](../lib/paths.js) translates paths.
@@ -50,7 +78,9 @@ The environment is per session, not per process:
50
78
  | Row | Mounted | Why there |
51
79
  |---|---|---|
52
80
  | `wsl-shell`, `wsl-fs` | inside `preset-wsl`, an agent preset with isolate realms | so one process can serve a Windows workspace and a distro workspace at the same time |
53
- | `subprocess-wsl` | at the app level, called the composition | because no session identity reaches the terminal controller, see below |
81
+ | `subprocess-wsl` | at the app level, called the composition; the shipped `subprocess` row is disabled | because no session identity reaches the terminal controller, see below |
82
+ | `workspace-files-wsl` | at the app level; the shipped `workspace-files` row is disabled | the GUI file tree is session-less, so it cannot be served from a preset |
83
+ | `fs-routing` | at the app level; the shipped `fs-sandbox` row is disabled | the root `ctx.fs` is session-less, and its write fence has no root-plane consumers |
54
84
  | `directory-picker-wsl`, `wsl-shell-env`, `auto-preset` | at the app level | they answer questions that are not scoped to a session |
55
85
 
56
86
  The preset mount requires the isolate realms. Without them the registry rejects the
@@ -71,8 +101,15 @@ does not use it.
71
101
 
72
102
  So the layer disables the shipped `subprocess` row and inserts
73
103
  `dsh-plugin-wsl-env/subprocess` in its place at the app level. The subclass
74
- overrides `spawnTerminal` only. Ordinary `spawn()`, the host ripgrep search, the
75
- pwsh executor, and the LSP host reach the shipped implementation unchanged.
104
+ overrides two methods: `spawnTerminal` rewrites a `wsl.exe` launch into
105
+ `wsl.exe -d <distro> --cd <linux dir>`, and `spawn()` rewrites two kinds of
106
+ launches into the distro — the file-search tool's packaged ripgrep (agent-first,
107
+ with the one-shot `wsl.exe --exec` handle as the fallback when the agent is out)
108
+ and git snapshot commands. Both spawn decisions are pure coordinate functions
109
+ (`lib/search-route.js`, `lib/git-route.js`); every other `spawn()` — including
110
+ the one-shot fallback's own `wsl.exe -lc <command>` (the default agent path
111
+ carries the command over the resident instead of spawning) — reaches the shipped
112
+ implementation untouched, as do the pwsh executor and the LSP host.
76
113
 
77
114
  The terminal's execution world follows the session (`hostSessions`, default on):
78
115
  a WSL-folder session runs the distro shell, a Windows-folder session runs
@@ -124,9 +161,11 @@ profile carrying the value is refused at boot, and no file tool crosses the
124
161
  refuses confined mutations with the bootstrap command, the same closed
125
162
  failure the command path has.
126
163
 
127
- Writes fence with the same host-side policy check (`checkedTarget`) the
128
- command path uses, and the substrate does not change what `sandboxMode`
129
- reports. The wire protocol the substrate speaks is documented in
164
+ Writes fence with the same host-side policy the command path's sandbox
165
+ describes — `WslFileSystem.checkedTarget`, over the same writable list; the
166
+ command path itself is confined by the distro-side profile and never calls it —
167
+ and the substrate does not change what `sandboxMode` reports. The wire protocol
168
+ the substrate speaks is documented in
130
169
  [`agent/wsl-agent.sh`](../agent/wsl-agent.sh); the orchestration layers are
131
170
  [`lib/fsio-agent.js`](../lib/fsio-agent.js) (fsio's mechanics over the FS
132
171
  frames), [`lib/fs-substrate.js`](../lib/fs-substrate.js) (the provider-shaped
@@ -148,19 +187,26 @@ sandbox.
148
187
 
149
188
  ### What the plugin does instead
150
189
 
151
- The plugin builds a `bubblewrap` profile on the Windows side and passes it to
152
- `wsl.exe --exec`. The confinement is created inside the distro:
190
+ The plugin builds a `bubblewrap` profile on the Windows side and hands the
191
+ resulting argv to the distro — over the resident agent by default, or to
192
+ `wsl.exe --exec` on the one-shot fallback. The confinement is created inside the
193
+ distro, and the argv is the same either way:
153
194
 
154
195
  ```text
155
196
  wsl.exe -d <distro> --cd <linux dir> --exec bwrap \
156
197
  --ro-bind / / --dev /dev --unshare-pid --proc /proc --die-with-parent \
157
- [--tmpfs /tmp --bind <workspace> <workspace>] -- <shell> -lc <cmd>
198
+ [--tmpfs /tmp --bind <workspace> <workspace>] [--tmpfs /mnt] -- <shell> -lc <cmd>
158
199
  ```
159
200
 
160
201
  These are the same arguments DSH's Linux runner uses (`dsh-sandbox-local`), so the
161
202
  behaviour and the error messages match a Linux host. The function
162
203
  `bwrapProfileArgs` in [`lib/bwrap.js`](../lib/bwrap.js) builds them — the
163
- peer-free builder every confinement site composes from.
204
+ peer-free builder every confinement site composes from. The trailing
205
+ `[--tmpfs /mnt]` is the `maskWindowsDrive` option: when it is on, the Windows
206
+ drive is shadowed by an empty tmpfs in every confined profile, so its files
207
+ cannot be read or exfiltrated and its executables cannot be launched through
208
+ interop. The option is off by default; [CONFIGURATION.md](CONFIGURATION.md) lists
209
+ the row keys, and `lib/sandbox-core.js` states its honest ceiling.
164
210
 
165
211
  | Mode | What a command inside the distro can do |
166
212
  |---|---|
@@ -185,6 +231,18 @@ selector read that value.
185
231
  distro can still run a Windows program through WSL interop, for example
186
232
  `/mnt/c/.../*.exe`. Bubblewrap does not govern that process.
187
233
 
234
+ `maskWindowsDrive` is the one mitigation the profile can offer, and it is a
235
+ narrowing rather than a closure: it takes the drive's data and its executables
236
+ off the table, but `binfmt` interop dispatches on file *content*, so a command
237
+ that can write the workspace can still write a PE there and have it launched as
238
+ a Windows process. Either way the provider reports `partial` — the complete
239
+ closure is distro-level (`[interop] enabled=false` in `wsl.conf`). The mask is
240
+ reported on the result's sandbox fact as `windowsDrive: "masked"` (or
241
+ `"visible"`), but that is this plugin's own extra field: the pinned harness's
242
+ sandbox type and its model-facing notes carry `mode`, `denied`, `enforcement` and
243
+ `runnerFailed` only, so the field reaches the result and the tests, not the
244
+ model.
245
+
188
246
  `pnpm run probe:sandbox` demonstrates the boundary on your machine and records it as
189
247
  `INFO`.
190
248
 
@@ -212,18 +270,37 @@ and `sandboxMode` returns `undefined`.
212
270
  ## Error codes
213
271
 
214
272
  The plugin raises `FsError`s with a code the tool layer maps, and `wsl.exe`'s own
215
- failures are named as well. A model sees the code; a reader asking what refused them
216
- finds it here.
273
+ failures are named as well. Every `FsError` code the plugin itself raises appears
274
+ below — a model sees the code, and a reader asking what refused them finds it here.
275
+ Codes the shipped host stack raises behind a drive path pass through
276
+ `lib/fs-routing.js` unchanged and are the peer's, not this table's; the GUI file
277
+ tree's own wire refusals are a separate dialect, listed after the table.
217
278
 
218
279
  | Code | Raised by | Meaning | What clears it |
219
280
  |---|---|---|---|
220
281
  | `SANDBOX_UNAVAILABLE` | `WslSandbox.confine` | the requested mode cannot be enforced: no usable `bwrap` in the distro | install `bubblewrap` there (a failed probe is re-run, so no restart is needed), or set `sandbox: false` |
221
- | `FS_SANDBOX_DENIED` | `WslFileSystem.checkedTarget` | the file-effect policy refused the write: `read-only`, or a target outside the writable roots | a wider permission for that one call, or a session workspace that contains the target |
282
+ | `FS_SANDBOX_DENIED` | `WslFileSystem.checkedTarget`, and `lib/fsio-agent.js` (a write the confined resident's mount table refused, classified from the kernel's `read-only file system`) | the file-effect policy refused the write: `read-only`, or a target outside the writable roots — or the kernel refused it at the resident's profile | a wider permission for that one call, or a session workspace that contains the target |
222
283
  | `FS_OUTSIDE_DISTRO` | `WslFileSystem.worldPath` | `restrictToDistro` is on and the path names **another distro**'s share — a configuration fence, not a sandbox decision | open a session in that distro, or set `restrictToDistro: false`. A wider permission does **not** lift it |
223
- | `FS_NOT_OBSERVED` | `WslFileSystem.assertGuard` | a guarded create (`createIfAbsent`) targeted a file that exists and had not been read first | read the file, then overwrite with the version guard or edit it |
224
- | `FS_STALE_VERSION` | `WslFileSystem.assertGuard` | the version the caller holds no longer matches: the file changed, or is gone | read it again and retry with the fresh version |
225
- | `FS_IO_ERROR` | `WslFileSystem.watch` | the watch target is not a path inside the pinned distro, so there is nothing the in-distro poll loop can observe | watch a distro path; the watch itself runs where the files live (`lib/watcher.js`) |
226
- | `FS_NOT_FOUND` | the host filesystem stack | the path does not exist — the ordinary answer, passed through | — |
284
+ | `FS_NOT_OBSERVED` | `lib/fs-substrate.js` (`writeText`; the agent's `exists` conclusion, mapped in `lib/fsio-agent.js`) | a guarded create (`createIfAbsent`) targeted a file that exists and had not been read first | read the file, then overwrite with the version guard or edit it |
285
+ | `FS_STALE_VERSION` | `lib/fs-substrate.js` (`writeText`, `editText`; the agent's `stale` conclusion, mapped in `lib/fsio-agent.js`) | the version the caller holds no longer matches: the file changed, or is gone | read it again and retry with the fresh version |
286
+ | `FS_IO_ERROR` | the filesystem stack: `lib/fs-decisions.js` (the substrate is unavailable), `lib/fsio-agent.js` (a `perm`/`loop`/`io` reason or a malformed record), `lib/index.js` (a path that cannot be mapped, or a `SandboxUnavailableError`), and `WslFileSystem.watch` | the catch-all I/O failure — most often the resident agent is out or the distro returned an I/O error, the "agent outage fails closed" case above. A watch target outside the pinned distro is the narrow one | get the resident back (check `wsl.exe -l -v`, then restart the session); for a watch, aim it at a distro path — the watch itself runs where the files live (`lib/watcher.js`) |
287
+ | `FS_NOT_FOUND` | `lib/fsio-agent.js` and `lib/fs-substrate.js` (the agent's `notfound`/`notdir` conclusion, an empty path, or a missing edit target), and the host filesystem stack behind a drive path | the path does not exist — the ordinary answer | — |
288
+ | `FS_NOT_REGULAR_FILE` | `lib/fsio-agent.js`, `lib/fs-substrate.js` | the target is not a regular file where one is required: a directory, symlink or device on a read, or a non-file target on a write | point the call at a regular file |
289
+ | `FS_NOT_DIRECTORY` | `lib/fsio-agent.js` (`listChildren`) | the target is not a directory, so there are no children to list | list a directory |
290
+ | `FS_NOT_TEXT` | `lib/fsio-agent.js`, `lib/fsio-text.js` | the content is not UTF-8 text: a NUL sample (binary) or a decode failure | not a policy refusal — read text, or use a command for binary content |
291
+ | `FS_TOO_LARGE` | `lib/fsio-agent.js` | the content exceeds the byte cap the caller's operation carries | read a byte range (`WslFileSystem.readByteRange`) instead of the whole file, or raise the cap |
292
+ | `FS_ABORTED` | `lib/fsio-agent.js`, `lib/fsio-text.js` | the caller's signal was aborted before or during the operation | not a refusal — the caller cancelled; a fresh call clears it |
293
+ | `FS_EDIT_NOT_FOUND` | `lib/fsio-text.js` (`applyLiteralEdit`) | `old_string` was empty, or matched nothing in the file | give an `old_string` the file actually contains |
294
+ | `FS_AMBIGUOUS_EDIT` | `lib/fsio-text.js` (`applyLiteralEdit`) | `old_string` matched more than once and `replace_all` was not set | make `old_string` unique, or set `replace_all` |
295
+
296
+ The GUI file tree speaks a second dialect. `WorkspaceFilesWsl` refuses through the
297
+ typert wire's `RemoteError`s, not `FsError`s, so its codes are not in the table
298
+ above: `workspace-file/not-found`, `workspace-file/outside-workspace`,
299
+ `workspace-file/not-directory`, `workspace-file/not-regular-file`,
300
+ `workspace-file/not-text`, and `workspace-file/too-large` (one code for both the
301
+ full-file cap and the byte-window cap), plus `gateway/bad-request` for a malformed
302
+ page request (a non-integer offset, or a limit above `maxLines`). A drive-path
303
+ workspace calls the shipped service and gets the peer's own answers instead.
227
304
 
228
305
  Two failures come from `wsl.exe` itself rather than from a code of ours, and both used
229
306
  to be invisible:
@@ -258,15 +335,35 @@ step with the checkout, and `pnpm run sync:windows` runs that script.
258
335
  |---|---|---|
259
336
  | Style and packaging checks | [`test/style.mjs`](../test/style.mjs) | no |
260
337
  | Syntax pass | [`test/syntax.mjs`](../test/syntax.mjs) | no |
338
+ | 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 |
339
+ | Type check | `tsc --noEmit` over `test/**` and `types/*.d.ts` (`src/` is checked by the build itself) | no |
261
340
  | Unit tests | `test/*.test.mjs` | no |
262
341
  | Sandbox probe | [`test/probe/sandbox.sh`](../test/probe/sandbox.sh) | no; it applies the profile arguments directly |
263
342
  | Harness probes | [`test/probe/run.sh`](../test/probe/run.sh), `terminal.sh`, `sandbox-shell.sh`, `picker.sh`, `missing-wsl.sh`, `sandbox-off.sh` | yes: Windows, WSL2, a mounted profile, and a linked checkout |
264
- | Host probes | [`test/probe/mode.sh`](../test/probe/mode.sh) | Windows Node, no harness |
343
+ | 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 |
344
+ | Host probe | [`test/probe/mode.sh`](../test/probe/mode.sh) | Windows Node, no harness |
265
345
  | Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
266
346
 
267
- The unit tests import only Node builtins, so they run on any platform. CI runs them
268
- on Linux and Windows, on Node 20, 22 and 24. The harness probes are a manual step,
269
- because a hosted runner has no WSL distro and no profile to mount.
347
+ `pnpm test` runs the first five rows in order: `lint:style`, `test:syntax`,
348
+ `lint:build`, `lint:types`, `test:unit`.
349
+ [`test/probe/run-all-when-closed.sh`](../test/probe/run-all-when-closed.sh)
350
+ waits for the app to exit and then runs the whole real-machine suite once —
351
+ `--include-fs` prepends `run.sh`.
352
+
353
+ The unit tests come in two kinds: most import only Node builtins, while
354
+ [`test/composition.test.mjs`](../test/composition.test.mjs),
355
+ [`test/fs.boot.test.mjs`](../test/fs.boot.test.mjs),
356
+ [`test/fsio-agent.test.mjs`](../test/fsio-agent.test.mjs),
357
+ [`test/provider.test.mjs`](../test/provider.test.mjs) and
358
+ [`test/workspace-files-wsl.test.mjs`](../test/workspace-files-wsl.test.mjs) import
359
+ pinned `@deepseek-ai/*` development dependencies (the composition and boot tests run
360
+ the loader's real patch algorithm). One `pnpm install` reproduces that tree before
361
+ the first run; the runtime package itself still ships zero dependencies. CI runs
362
+ `pnpm test` on Linux and Windows, on Node 22 and 24, and `pnpm run test:coverage` on
363
+ Node 24 only — the threshold flags need Node 22.8. Node 20 is absent on purpose:
364
+ `engines` requires `^22.19.0 || >=24.0.0`, the harness host's own floor. The harness
365
+ probes are a manual step, because a hosted runner has no WSL distro and no profile to
366
+ mount.
270
367
 
271
368
  The probe scripts share [`test/probe/env.sh`](../test/probe/env.sh), which derives
272
369
  the Windows user, the distro name and the UNC spelling of the checkout from the
@@ -276,33 +373,74 @@ placeholders that the scripts substitute into generated copies.
276
373
 
277
374
  ## File layout
278
375
 
376
+ `lib/` is generated output: every module's source is `src/<name>.ts`, and
377
+ `pnpm run build` emits `lib/<name>.js` plus its `lib/<name>.d.ts`. Never edit
378
+ `lib/` by hand — `lint:build` fails when the committed bytes drift from a real
379
+ build, and it also fails on an orphan in either direction.
380
+
381
+ ```text
382
+ src/<name>.ts the sources — edit THESE; each builds to lib/<name>.{js,d.ts}
383
+ agent/wsl-agent.sh the resident in-distro agent (POSIX sh, coreutils only)
384
+ cordis.patch.yml the bundle configuration layer (dsh.bundle), commented
385
+ examples/ one machine-local profile layer, for comparison
386
+ locale/ the Plugins-page display metadata (title/description)
387
+ scripts/ the build (build.mjs), the release helpers
388
+ (check-release-tag.mjs, changelog-section.mjs), and the
389
+ distro-side bootstrap installer (bootstrap.sh)
390
+ test/ unit tests, the style/build/type gates, and the probes
391
+ docs/ this file, the configuration and limitation references,
392
+ the release checklist, and the archived designs
393
+ ```
394
+
395
+ The modules, in alphabetical order:
396
+
279
397
  ```text
280
- lib/paths.js pure conversion between the three path formats
281
- lib/wsl.js wsl.exe interop primitives (no DSH imports)
282
- lib/listing.js pure directory listing and breadcrumb helpers (no DSH imports)
283
- lib/index.js WslShellExecutor (ctx.shell) and WslFileSystem (ctx.fs)
284
- lib/sandbox.js the distro-side bwrap confinement shared by both providers
285
- lib/bwrap.js the bwrap command line both confinement sites assemble
286
- (no imports, so a bare checkout can assert it)
287
- lib/fsio-text.js the peer's pure text mechanics, replicated pending the
288
- upstream fsio export (see UPSTREAM-FSIO-EXPORT.md)
289
- lib/fsio-agent.js fsio's mechanics over the agent's FS frames
290
- lib/fs-substrate.js the agent substrate's provider-shaped operations
291
- lib/agent.js the resident in-distro agent's host side
292
- lib/agent-confined.js the confined residents' factory (one per policy)
293
- lib/agent-protocol.js the wire protocol codecs (pure)
294
- agent/wsl-agent.sh the resident in-distro agent (POSIX sh, coreutils only)
295
- lib/picker.js WslDirectoryPicker (ctx.directoryPicker)
296
- lib/subprocess.js WslSubprocessRuntime (ctx.subprocess), the terminal window
297
- lib/auto-preset.js per-session environment selection
298
- lib/shell-env.js registers the DSH_WSL_* environment variables
299
- lib/{shell,fs}.js one-line subpath entry points
300
- cordis.patch.yml the bundle configuration layer (dsh.bundle), commented
301
- examples/ one machine-local profile layer, for comparison
302
- scripts/ release helpers, used by the release workflow
303
- test/ unit tests, the style gate, and the behaviour probes
304
- docs/ this file, the configuration and limitation references, the
305
- release checklist, and the archived designs
398
+ src/agent-confined.ts the confined residents' factory (one per policy)
399
+ src/agent-errors.ts the error a resident outage rejects with
400
+ src/agent-exec.ts the agent-backed shell execution handle
401
+ src/agent-protocol.ts the wire protocol codecs (pure)
402
+ src/agent-shared.ts the per-distro resident shared by every provider
403
+ src/agent.ts the resident in-distro agent's host side
404
+ src/auto-preset.ts per-session environment selection
405
+ src/bwrap.ts the bwrap command line every confinement site
406
+ assembles (no imports, so a bare checkout can
407
+ assert it)
408
+ src/file-reference-wsl.ts the distro-side `@`-completion traversal — built,
409
+ tested, not yet wired (see above)
410
+ src/fs.ts the ctx.fs subpath entry point
411
+ src/fs-decisions.ts the filesystem provider's policy decisions (pure,
412
+ unit-testable in a bare checkout)
413
+ src/fs-routing.ts WslRoutingFileSystem, the root-plane ctx.fs
414
+ src/fs-substrate.ts the agent substrate's provider-shaped operations
415
+ src/fsio-agent.ts fsio's mechanics over the agent's FS frames
416
+ src/fsio-text.ts the peer's pure text mechanics, replicated pending
417
+ the upstream fsio export (UPSTREAM-FSIO-EXPORT.md)
418
+ src/git-route.ts which git spawns belong inside the distro (pure)
419
+ src/index.ts WslShellExecutor (ctx.shell) and WslFileSystem (ctx.fs)
420
+ src/listing.ts pure directory listing and breadcrumb helpers
421
+ src/paths.ts pure conversion between the three path formats
422
+ src/picker.ts WslDirectoryPicker (ctx.directoryPicker)
423
+ src/ports.ts the listening-port snapshot behind DSH_WSL_PORTS
424
+ src/preset-choice.ts whether a new session adopts the WSL preset (pure)
425
+ src/probe-cache.ts which probe verdicts may be remembered (pure)
426
+ src/sandbox.ts the distro-side bwrap confinement shared by both
427
+ providers, bound to the peer's failure type
428
+ src/sandbox-core.ts the probe-and-confinement core, free of DSH peers
429
+ src/search-exec.ts the agent-backed execution handle for a search
430
+ src/search-route.ts which execution world a search spawn belongs to (pure)
431
+ src/shell.ts the ctx.shell subpath entry point
432
+ src/shell-env.ts registers the DSH_WSL_* environment variables
433
+ src/subprocess.ts WslSubprocessRuntime (ctx.subprocess), the GUI
434
+ terminal window
435
+ src/terminal-activity.ts the distro-side activity probe for idle terminal
436
+ reclamation
437
+ src/terminal-route.ts which execution world a terminal launch belongs to (pure)
438
+ src/watcher.ts in-distro file watching for WslFileSystem.watch
439
+ src/workspace-files-route.ts pure builders and parsers for the routed
440
+ workspace-files service
441
+ src/workspace-files-wsl.ts WorkspaceFilesWsl (ctx.workspaceFiles), the GUI
442
+ file tree and previews
443
+ src/wsl.ts wsl.exe interop primitives (no DSH imports)
306
444
  ```
307
445
 
308
446
  `package.json` publishes a subset of this tree: the `files` list is the contract, and
@@ -318,6 +456,8 @@ docs/ this file, the configuration and limitation references, the
318
456
  - [`archive/engineering-record.zh.md`](archive/engineering-record.zh.md) is the
319
457
  full engineering record, in Chinese: measured contracts, debugging rounds, and
320
458
  the conclusions as they stood then.
321
- - [`archive/README.md`](archive/README.md) indexes the archived material in
322
- Chinese, and [`archive/README.en.md`](archive/README.en.md) is the English index.
459
+ - [`archive/README.md`](archive/README.md) and
460
+ [`archive/README.en.md`](archive/README.en.md) are two English indexes of the
461
+ same three archived files, the second so a reader who does not read Chinese can
462
+ still tell what is there; the records themselves are Chinese.
323
463
  - [`README.md`](../README.md) is the only statement of current state.