dsh-plugin-wsl-env 0.8.0 → 0.10.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 (54) hide show
  1. package/CHANGELOG.md +488 -12
  2. package/CONTRIBUTING.md +125 -62
  3. package/README.md +99 -68
  4. package/SECURITY.md +18 -2
  5. package/SUPPORT.md +26 -10
  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 +83 -16
  9. package/cordis.patch.yml +50 -35
  10. package/docs/ARCHITECTURE.md +203 -55
  11. package/docs/CONFIGURATION.md +185 -18
  12. package/docs/LIMITATIONS.md +125 -45
  13. package/docs/PARITY.md +68 -38
  14. package/docs/PEER-PARITY.md +103 -33
  15. package/docs/README.zh.md +105 -64
  16. package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
  17. package/docs/UPSTREAM-TERMINAL-TITLE.md +56 -14
  18. package/examples/preset-wsl-session-sandbox.cordis.patch.yml +178 -0
  19. package/lib/agent-protocol.d.ts +10 -4
  20. package/lib/agent-protocol.js +10 -3
  21. package/lib/agent.d.ts +84 -0
  22. package/lib/agent.js +167 -15
  23. package/lib/bwrap.d.ts +8 -0
  24. package/lib/bwrap.js +4 -1
  25. package/lib/fs-routing.d.ts +40 -4
  26. package/lib/fs-routing.js +80 -12
  27. package/lib/fsio-agent.js +1 -1
  28. package/lib/index.d.ts +134 -0
  29. package/lib/index.js +202 -9
  30. package/lib/native-agent.d.ts +124 -0
  31. package/lib/native-agent.js +198 -0
  32. package/lib/ports.d.ts +13 -2
  33. package/lib/ports.js +18 -5
  34. package/lib/preflight.d.ts +91 -0
  35. package/lib/preflight.js +117 -0
  36. package/lib/probe-cache.d.ts +12 -1
  37. package/lib/probe-cache.js +11 -3
  38. package/lib/sandbox-core.d.ts +70 -0
  39. package/lib/sandbox-core.js +187 -18
  40. package/lib/session-runtime.d.ts +161 -0
  41. package/lib/session-runtime.js +220 -0
  42. package/lib/session-scope.d.ts +124 -0
  43. package/lib/session-scope.js +191 -0
  44. package/lib/shell-env.js +10 -3
  45. package/lib/watcher.d.ts +25 -12
  46. package/lib/watcher.js +120 -32
  47. package/lib/workspace-files-route.d.ts +8 -4
  48. package/lib/workspace-files-route.js +42 -14
  49. package/lib/workspace-files-wsl.js +25 -8
  50. package/lib/wsl.js +10 -4
  51. package/locale/preflight/en.json +6 -0
  52. package/locale/preflight/zh.json +6 -0
  53. package/package.json +44 -27
  54. package/scripts/diagnose.sh +92 -0
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.
@@ -178,9 +187,20 @@
178
187
  # Because a preset's plugins mount under a REGISTRY-owned scope (not this row's
179
188
  # ctx), the realm has to be declared here, inside config.plugins.
180
189
  #
181
- # Generated from the shipped standard preset by build-preset-wsl.mjs so the
182
- # embedded plan-mode section and every nested group survive byte-for-byte.
183
- # Re-run it after an app update that changes the preset.
190
+ # Generated from the shipped standard preset so the embedded plan-mode
191
+ # section and every nested group survive byte-for-byte: this copy is
192
+ # `packages/bundle/web-app/presets/standard.patch.yml` of the
193
+ # `dsh-v0.2.1-rc.2` source tree, changed in exactly three ways —
194
+ # 1. every shipped row mounts inside `wsl-env`, so the preset's tools see
195
+ # the distro's shell and fs (the realm is declared on the group);
196
+ # 2. `tool-bash` is enabled and `tool-pwsh` disabled outright, where the
197
+ # shipped copy derives the choice from `process.platform`;
198
+ # 3. the identity: row `preset-wsl`, preset `wsl`, `order: 2`.
199
+ # Regenerate it when an app update changes the preset: take
200
+ # `packages/bundle/web-app/presets/standard.patch.yml` from the harness tree
201
+ # the peer pins track and run `node scripts/build-preset-wsl.mjs <that
202
+ # file>`, then replace this row to the end of the file. The script applies
203
+ # changes 1 and 2 itself, so only change 3 is yours.
184
204
  # ═════════════════════════════════════════════════════════════════════════════
185
205
  - id: preset-wsl
186
206
  name: '@deepseek-ai/dsh-agent-preset'
@@ -200,12 +220,15 @@
200
220
  # asked of the distro itself. Pin a directory here to override it —
201
221
  # a value that is right on one machine is wrong on every other.
202
222
  #
203
- # Each command is confined inside the distro by bubblewrap, in the
204
- # policy's mode, and the file tools are fenced by the same policy.
205
- # Install bubblewrap there (`sudo apt install bubblewrap`); without
206
- # it the providers fail closed rather than run unconfined. Add
207
- # `sandbox: false` to either provider for the old, unconfined
208
- # posture.
223
+ # Commands run in one persistent systemd user scope per session
224
+ # (ADR 0001): a background dev server survives the command that
225
+ # started it, `/tmp` is the distro's real `/tmp`, and `ps` sees the
226
+ # session. This needs systemd as PID 1 (`[boot] systemd=true` in
227
+ # /etc/wsl.conf) and a lingering user manager (`loginctl
228
+ # enable-linger <user>`); the preflight refuses with those remedies
229
+ # when one is missing. Set `sessionSandbox: false` for the old
230
+ # per-command shape. Bubblewrap is still required for the mount
231
+ # profile; without it the providers fail closed.
209
232
  - id: wsl-shell
210
233
  name: 'dsh-plugin-wsl-env'
211
234
  config:
@@ -213,6 +236,13 @@
213
236
  loginShell: true
214
237
  timeoutMs: 120000
215
238
  maxTimeoutMs: 600000
239
+ sessionSandbox: true
240
+ # auto: the native session-runtime agent when a verified
241
+ # source is available (packaged binary, or the
242
+ # DSH_WSL_AGENT_NATIVE development override); otherwise the
243
+ # shipped shell agent. native fails the first command instead
244
+ # of falling back; shell forces the shell agent.
245
+ agentBackend: auto
216
246
  - id: wsl-fs
217
247
  name: 'dsh-plugin-wsl-env/fs'
218
248
  config:
@@ -225,15 +255,19 @@
225
255
  # share: the opt-out — the Windows-side host fs stack over
226
256
  # the 9p share.
227
257
  substrate: agent
258
+ # Confined writes ride the session runtime (ADR 0001), so they
259
+ # share the command's real /tmp and workspace mounts.
260
+ sessionSandbox: true
228
261
  - id: persona
229
262
  name: '@deepseek-ai/dsh-persona'
230
263
  config:
231
- suffix: Your working directory is {{cwd}}.
232
264
  prefix: You are a coding agent powered by the {{model}} model.
233
265
  - id: agent-instructions
234
266
  name: '@deepseek-ai/dsh-agent-instructions'
235
267
  config:
236
268
  maxBytes: 65536
269
+ - id: time-context
270
+ name: '@deepseek-ai/dsh-time-context'
237
271
  - id: tool-bash
238
272
  name: '@deepseek-ai/dsh-tool-bash'
239
273
  disabled: false
@@ -248,6 +282,8 @@
248
282
  sampleOverCapGlobResults: false
249
283
  - id: tool-jobs
250
284
  name: '@deepseek-ai/dsh-tool-jobs'
285
+ - id: tool-schedule
286
+ name: '@deepseek-ai/dsh-tool-schedule'
251
287
  - id: skill-filesystem
252
288
  name: '@deepseek-ai/dsh-skill-filesystem'
253
289
  - id: tool-skill
@@ -266,7 +302,7 @@
266
302
  name: '@deepseek-ai/dsh-plan-mode'
267
303
  config:
268
304
  section: |
269
- You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
305
+ You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
270
306
 
271
307
  Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
272
308
 
@@ -310,41 +346,17 @@
310
346
  provider: spawn
311
347
  toolName: subagent
312
348
  modelSelectionSettings: true
313
- backgroundMode: continuable
314
349
  - id: tool-subagent-fork
315
350
  name: '@deepseek-ai/dsh-tool-subagent'
316
351
  config:
317
352
  provider: fork
318
353
  toolName: subagent_fork
319
- backgroundMode: continuable
320
- - id: tool-subagent-codex
321
- name: '@deepseek-ai/dsh-tool-subagent'
322
- disabled: true
323
- config:
324
- provider: codex
325
- toolName: subagent_codex
326
- backgroundMode: one-shot
327
- maxDepth: provider-managed
328
- - id: tool-subagent-claude-code
329
- name: '@deepseek-ai/dsh-tool-subagent'
330
- disabled: true
331
- config:
332
- provider: claude-code
333
- toolName: subagent_claude_code
334
- backgroundMode: one-shot
335
- maxDepth: provider-managed
336
354
  - id: workflow-ptc
337
355
  name: '@deepseek-ai/dsh-workflow-ptc'
338
356
  config:
339
357
  provider: spawn
340
358
  - id: tool-workflow
341
359
  name: '@deepseek-ai/dsh-tool-workflow'
342
- - id: tool-ralph
343
- name: '@deepseek-ai/dsh-tool-ralph'
344
- disabled: true
345
- config:
346
- subagentProvider: spawn
347
- maxRounds: 64
348
360
  - id: tool-ask-user
349
361
  name: '@deepseek-ai/dsh-tool-ask-user'
350
362
  - id: tool-todo
@@ -361,3 +373,6 @@
361
373
  - id: tool-plugin-manager
362
374
  name: '@deepseek-ai/dsh-plugin-manager/tools'
363
375
  disabled: true
376
+ - id: tool-plugin-market
377
+ name: '@deepseek-ai/dsh-plugin-market/tools'
378
+ disabled: true
@@ -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,24 +187,39 @@ 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
 
201
+ By default the profile wraps the session's resident agent ONCE, under
202
+ `systemd-run --user --scope`, and the command argv is the shell line itself:
203
+ `bwrap … --die-with-parent --bind /tmp /tmp --bind <workspace> <workspace> -- sh agent.sh`.
204
+ There is no `--unshare-pid` and no per-command tmpfs, so background processes
205
+ survive, `/tmp` is the distro's real `/tmp`, and `ps` sees the session. The
206
+ per-command argv above is what `sessionSandbox: false` (and the one-shot
207
+ fallback) still runs.
208
+
160
209
  These are the same arguments DSH's Linux runner uses (`dsh-sandbox-local`), so the
161
210
  behaviour and the error messages match a Linux host. The function
162
211
  `bwrapProfileArgs` in [`lib/bwrap.js`](../lib/bwrap.js) builds them — the
163
- peer-free builder every confinement site composes from.
212
+ peer-free builder every confinement site composes from. The trailing
213
+ `[--tmpfs /mnt]` is the `maskWindowsDrive` option: when it is on, the Windows
214
+ drive is shadowed by an empty tmpfs in every confined profile, so its files
215
+ cannot be read or exfiltrated and its executables cannot be launched through
216
+ interop. The option is off by default; [CONFIGURATION.md](CONFIGURATION.md) lists
217
+ the row keys, and `lib/sandbox-core.js` states its honest ceiling.
164
218
 
165
219
  | Mode | What a command inside the distro can do |
166
220
  |---|---|
167
221
  | `read-only` | read the whole distro; a fresh `/dev` is mounted writable, so `/dev/null` and `/dev/shm` work, and nothing else does |
168
- | `workspace-write` | the above, plus the session workspace is writable and `/tmp` is a temporary mount |
222
+ | `workspace-write` | the above, plus the session workspace is writable; `/tmp` is the distro's real `/tmp` in the session runtime, and a fresh tmpfs in the per-command profile (`sessionSandbox: false`) |
169
223
  | `danger-full-access` | no sandbox; used for an approved wider-permission request |
170
224
 
171
225
  `WslFileSystem` checks writes against the same policy and the same writable list,
@@ -185,6 +239,18 @@ selector read that value.
185
239
  distro can still run a Windows program through WSL interop, for example
186
240
  `/mnt/c/.../*.exe`. Bubblewrap does not govern that process.
187
241
 
242
+ `maskWindowsDrive` is the one mitigation the profile can offer, and it is a
243
+ narrowing rather than a closure: it takes the drive's data and its executables
244
+ off the table, but `binfmt` interop dispatches on file *content*, so a command
245
+ that can write the workspace can still write a PE there and have it launched as
246
+ a Windows process. Either way the provider reports `partial` — the complete
247
+ closure is distro-level (`[interop] enabled=false` in `wsl.conf`). The mask is
248
+ reported on the result's sandbox fact as `windowsDrive: "masked"` (or
249
+ `"visible"`), but that is this plugin's own extra field: the pinned harness's
250
+ sandbox type and its model-facing notes carry `mode`, `denied`, `enforcement` and
251
+ `runnerFailed` only, so the field reaches the result and the tests, not the
252
+ model.
253
+
188
254
  `pnpm run probe:sandbox` demonstrates the boundary on your machine and records it as
189
255
  `INFO`.
190
256
 
@@ -212,18 +278,37 @@ and `sandboxMode` returns `undefined`.
212
278
  ## Error codes
213
279
 
214
280
  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.
281
+ failures are named as well. Every `FsError` code the plugin itself raises appears
282
+ below — a model sees the code, and a reader asking what refused them finds it here.
283
+ Codes the shipped host stack raises behind a drive path pass through
284
+ `lib/fs-routing.js` unchanged and are the peer's, not this table's; the GUI file
285
+ tree's own wire refusals are a separate dialect, listed after the table.
217
286
 
218
287
  | Code | Raised by | Meaning | What clears it |
219
288
  |---|---|---|---|
220
289
  | `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 |
290
+ | `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
291
  | `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 | — |
292
+ | `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 |
293
+ | `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 |
294
+ | `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`) |
295
+ | `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 | — |
296
+ | `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 |
297
+ | `FS_NOT_DIRECTORY` | `lib/fsio-agent.js` (`listChildren`) | the target is not a directory, so there are no children to list | list a directory |
298
+ | `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 |
299
+ | `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 |
300
+ | `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 |
301
+ | `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 |
302
+ | `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` |
303
+
304
+ The GUI file tree speaks a second dialect. `WorkspaceFilesWsl` refuses through the
305
+ typert wire's `RemoteError`s, not `FsError`s, so its codes are not in the table
306
+ above: `workspace-file/not-found`, `workspace-file/outside-workspace`,
307
+ `workspace-file/not-directory`, `workspace-file/not-regular-file`,
308
+ `workspace-file/not-text`, and `workspace-file/too-large` (one code for both the
309
+ full-file cap and the byte-window cap), plus `gateway/bad-request` for a malformed
310
+ page request (a non-integer offset, or a limit above `maxLines`). A drive-path
311
+ workspace calls the shipped service and gets the peer's own answers instead.
227
312
 
228
313
  Two failures come from `wsl.exe` itself rather than from a code of ours, and both used
229
314
  to be invisible:
@@ -258,15 +343,35 @@ step with the checkout, and `pnpm run sync:windows` runs that script.
258
343
  |---|---|---|
259
344
  | Style and packaging checks | [`test/style.mjs`](../test/style.mjs) | no |
260
345
  | Syntax pass | [`test/syntax.mjs`](../test/syntax.mjs) | no |
346
+ | 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 |
347
+ | Type check | `tsc --noEmit` over `test/**` and `types/*.d.ts` (`src/` is checked by the build itself) | no |
261
348
  | Unit tests | `test/*.test.mjs` | no |
262
349
  | Sandbox probe | [`test/probe/sandbox.sh`](../test/probe/sandbox.sh) | no; it applies the profile arguments directly |
263
350
  | 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 |
351
+ | 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 |
352
+ | Host probe | [`test/probe/mode.sh`](../test/probe/mode.sh) | Windows Node, no harness |
265
353
  | Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
266
354
 
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.
355
+ `pnpm test` runs the first five rows in order: `lint:style`, `test:syntax`,
356
+ `lint:build`, `lint:types`, `test:unit`.
357
+ [`test/probe/run-all-when-closed.sh`](../test/probe/run-all-when-closed.sh)
358
+ waits for the app to exit and then runs the whole real-machine suite once —
359
+ `--include-fs` prepends `run.sh`.
360
+
361
+ The unit tests come in two kinds: most import only Node builtins, while
362
+ [`test/composition.test.mjs`](../test/composition.test.mjs),
363
+ [`test/fs.boot.test.mjs`](../test/fs.boot.test.mjs),
364
+ [`test/fsio-agent.test.mjs`](../test/fsio-agent.test.mjs),
365
+ [`test/provider.test.mjs`](../test/provider.test.mjs) and
366
+ [`test/workspace-files-wsl.test.mjs`](../test/workspace-files-wsl.test.mjs) import
367
+ pinned `@deepseek-ai/*` development dependencies (the composition and boot tests run
368
+ the loader's real patch algorithm). One `pnpm install` reproduces that tree before
369
+ the first run; the runtime package itself still ships zero dependencies. CI runs
370
+ `pnpm test` on Linux and Windows, on Node 22 and 24, and `pnpm run test:coverage` on
371
+ Node 24 only — the threshold flags need Node 22.8. Node 20 is absent on purpose:
372
+ `engines` requires `^22.19.0 || >=24.0.0`, the harness host's own floor. The harness
373
+ probes are a manual step, because a hosted runner has no WSL distro and no profile to
374
+ mount.
270
375
 
271
376
  The probe scripts share [`test/probe/env.sh`](../test/probe/env.sh), which derives
272
377
  the Windows user, the distro name and the UNC spelling of the checkout from the
@@ -276,33 +381,74 @@ placeholders that the scripts substitute into generated copies.
276
381
 
277
382
  ## File layout
278
383
 
384
+ `lib/` is generated output: every module's source is `src/<name>.ts`, and
385
+ `pnpm run build` emits `lib/<name>.js` plus its `lib/<name>.d.ts`. Never edit
386
+ `lib/` by hand — `lint:build` fails when the committed bytes drift from a real
387
+ build, and it also fails on an orphan in either direction.
388
+
389
+ ```text
390
+ src/<name>.ts the sources — edit THESE; each builds to lib/<name>.{js,d.ts}
391
+ agent/wsl-agent.sh the resident in-distro agent (POSIX sh, coreutils only)
392
+ cordis.patch.yml the bundle configuration layer (dsh.bundle), commented
393
+ examples/ one machine-local profile layer, for comparison
394
+ locale/ the Plugins-page display metadata (title/description)
395
+ scripts/ the build (build.mjs), the release helpers
396
+ (check-release-tag.mjs, changelog-section.mjs), and the
397
+ distro-side bootstrap installer (bootstrap.sh)
398
+ test/ unit tests, the style/build/type gates, and the probes
399
+ docs/ this file, the configuration and limitation references,
400
+ the release checklist, and the archived designs
401
+ ```
402
+
403
+ The modules, in alphabetical order:
404
+
279
405
  ```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
406
+ src/agent-confined.ts the confined residents' factory (one per policy)
407
+ src/agent-errors.ts the error a resident outage rejects with
408
+ src/agent-exec.ts the agent-backed shell execution handle
409
+ src/agent-protocol.ts the wire protocol codecs (pure)
410
+ src/agent-shared.ts the per-distro resident shared by every provider
411
+ src/agent.ts the resident in-distro agent's host side
412
+ src/auto-preset.ts per-session environment selection
413
+ src/bwrap.ts the bwrap command line every confinement site
414
+ assembles (no imports, so a bare checkout can
415
+ assert it)
416
+ src/file-reference-wsl.ts the distro-side `@`-completion traversal — built,
417
+ tested, not yet wired (see above)
418
+ src/fs.ts the ctx.fs subpath entry point
419
+ src/fs-decisions.ts the filesystem provider's policy decisions (pure,
420
+ unit-testable in a bare checkout)
421
+ src/fs-routing.ts WslRoutingFileSystem, the root-plane ctx.fs
422
+ src/fs-substrate.ts the agent substrate's provider-shaped operations
423
+ src/fsio-agent.ts fsio's mechanics over the agent's FS frames
424
+ src/fsio-text.ts the peer's pure text mechanics, replicated pending
425
+ the upstream fsio export (UPSTREAM-FSIO-EXPORT.md)
426
+ src/git-route.ts which git spawns belong inside the distro (pure)
427
+ src/index.ts WslShellExecutor (ctx.shell) and WslFileSystem (ctx.fs)
428
+ src/listing.ts pure directory listing and breadcrumb helpers
429
+ src/paths.ts pure conversion between the three path formats
430
+ src/picker.ts WslDirectoryPicker (ctx.directoryPicker)
431
+ src/ports.ts the listening-port snapshot behind DSH_WSL_PORTS
432
+ src/preset-choice.ts whether a new session adopts the WSL preset (pure)
433
+ src/probe-cache.ts which probe verdicts may be remembered (pure)
434
+ src/sandbox.ts the distro-side bwrap confinement shared by both
435
+ providers, bound to the peer's failure type
436
+ src/sandbox-core.ts the probe-and-confinement core, free of DSH peers
437
+ src/search-exec.ts the agent-backed execution handle for a search
438
+ src/search-route.ts which execution world a search spawn belongs to (pure)
439
+ src/shell.ts the ctx.shell subpath entry point
440
+ src/shell-env.ts registers the DSH_WSL_* environment variables
441
+ src/subprocess.ts WslSubprocessRuntime (ctx.subprocess), the GUI
442
+ terminal window
443
+ src/terminal-activity.ts the distro-side activity probe for idle terminal
444
+ reclamation
445
+ src/terminal-route.ts which execution world a terminal launch belongs to (pure)
446
+ src/watcher.ts in-distro file watching for WslFileSystem.watch
447
+ src/workspace-files-route.ts pure builders and parsers for the routed
448
+ workspace-files service
449
+ src/workspace-files-wsl.ts WorkspaceFilesWsl (ctx.workspaceFiles), the GUI
450
+ file tree and previews
451
+ src/wsl.ts wsl.exe interop primitives (no DSH imports)
306
452
  ```
307
453
 
308
454
  `package.json` publishes a subset of this tree: the `files` list is the contract, and
@@ -318,6 +464,8 @@ docs/ this file, the configuration and limitation references, the
318
464
  - [`archive/engineering-record.zh.md`](archive/engineering-record.zh.md) is the
319
465
  full engineering record, in Chinese: measured contracts, debugging rounds, and
320
466
  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.
467
+ - [`archive/README.md`](archive/README.md) and
468
+ [`archive/README.en.md`](archive/README.en.md) are two English indexes of the
469
+ same three archived files, the second so a reader who does not read Chinese can
470
+ still tell what is there; the records themselves are Chinese.
323
471
  - [`README.md`](../README.md) is the only statement of current state.