dsh-plugin-wsl-env 0.11.0 → 0.13.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/CHANGELOG.md +181 -0
- package/CONTRIBUTING.md +29 -7
- package/README.md +124 -138
- package/SECURITY.md +11 -1
- package/SUPPORT.md +29 -2
- package/cordis.patch.yml +17 -19
- package/docs/ARCHITECTURE.md +30 -29
- package/docs/CONFIGURATION.md +81 -21
- package/docs/LIMITATIONS.md +71 -6
- package/docs/MIGRATION.md +98 -0
- package/docs/PARITY.md +1 -1
- package/docs/PERFORMANCE.md +60 -0
- package/docs/QUICKSTART.md +125 -0
- package/docs/README.zh.md +117 -137
- package/docs/ROADMAP-1.0.md +780 -0
- package/docs/ROADMAP.md +4 -0
- package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +80 -0
- package/docs/UPSTREAM-LSP-SEAM.md +90 -0
- package/docs/UPSTREAM-SESSION-NOTICE.md +95 -0
- package/lib/agent-errors.d.ts +19 -0
- package/lib/agent-errors.js +22 -0
- package/lib/agent-exec.js +2 -2
- package/lib/agent.d.ts +68 -5
- package/lib/agent.js +129 -22
- package/lib/distro-route.d.ts +75 -0
- package/lib/distro-route.js +86 -0
- package/lib/fs-routing.d.ts +4 -1
- package/lib/index.d.ts +166 -37
- package/lib/index.js +353 -75
- package/lib/paths.d.ts +15 -0
- package/lib/paths.js +28 -0
- package/lib/preflight.d.ts +46 -3
- package/lib/preflight.js +83 -14
- package/lib/shell-env.d.ts +9 -3
- package/lib/shell-env.js +121 -43
- package/lib/subprocess.d.ts +12 -3
- package/lib/subprocess.js +21 -3
- package/package.json +23 -15
- package/scripts/diagnose.sh +9 -88
- package/scripts/doctor.mjs +548 -0
- package/scripts/profile-preset.mjs +254 -0
- package/lib/workspace-files-route.d.ts +0 -136
- package/lib/workspace-files-route.js +0 -225
- package/lib/workspace-files-wsl.d.ts +0 -257
- package/lib/workspace-files-wsl.js +0 -418
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -26,19 +26,25 @@ root-plane services, and adds three smaller integrations.
|
|
|
26
26
|
extends the shipped `LocalFileSystem`. Only the parts that need a distro are
|
|
27
27
|
overridden.
|
|
28
28
|
|
|
29
|
-
## The
|
|
29
|
+
## The root-plane filesystem takeover
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
One of the shipped services is session-less — the root `ctx.fs` — so it cannot
|
|
32
|
+
be served from an agent preset. The layer replaces its shipped row outright with
|
|
33
|
+
a subclass that routes by coordinate:
|
|
34
34
|
|
|
35
35
|
| Service | Class | File | What it does |
|
|
36
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
37
|
| `ctx.fs` (root plane) | `WslRoutingFileSystem` | [`lib/fs-routing.js`](../lib/fs-routing.js) | The root filesystem, routed by coordinate: a distro UNC identity is served by the resident agent (real watch events, ext4 reads), a drive path stays host-native. Replaces the shipped `fs-sandbox` row, whose write fence has no root-plane consumers — the model's writes ride the preset filesystems. [`root-fs-routing.md`](root-fs-routing.md) is the design. |
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
The GUI file tree and previews keep the shipped `workspace-files` row. Its
|
|
40
|
+
client half is the browser `file` resource provider the Sidebar previews need,
|
|
41
|
+
so replacing the row with a server-only subclass would drop it; the shipped
|
|
42
|
+
service reads WSL paths through the routed root filesystem instead. The cost is
|
|
43
|
+
extra `ctx.fs` round trips per listing, `stat` or page read (see
|
|
44
|
+
[LIMITATIONS](LIMITATIONS.md)).
|
|
45
|
+
|
|
46
|
+
The takeover is composition-level for the mirror-image reason the terminal is
|
|
47
|
+
(see below): no session identity reaches the root filesystem. It is a top-level
|
|
42
48
|
`disabled:` entry targeting the shipped row plus a bare `insert:` list holding
|
|
43
49
|
the replacement — because a `disabled` written *inside* an insert list is not a
|
|
44
50
|
patch but a duplicate-id data row, and a nested `insert:` is never interpreted at
|
|
@@ -60,7 +66,7 @@ functions.
|
|
|
60
66
|
|---|---|---|---|
|
|
61
67
|
| `dsh-plugin-wsl-env/picker` | `WslDirectoryPicker` | [`lib/picker.js`](../lib/picker.js) | Adds the installed distros to the folder picker, next to the Windows home directory, and lists distro levels from inside the distro (the resident's `ls`, one-shot `wsl.exe` when it is out). |
|
|
62
68
|
| `dsh-plugin-wsl-env/auto-preset` | `auto-preset` | [`lib/auto-preset.js`](../lib/auto-preset.js) | Binds the `wsl` agent preset when a new session's workspace is inside a distro. |
|
|
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. |
|
|
69
|
+
| `dsh-plugin-wsl-env/shell-env` | `wsl-shell-env` | [`lib/shell-env.js`](../lib/shell-env.js) | Contributes `DSH_WSL_DISTRO`, `DSH_WSL_SHELL`, `DSH_WSL_HOME` and `DSH_WSL_PORTS` to the managed `DSH_*` namespace, choosing the session workspace's distro (default/pinned facts are probed at mount; other distros are warmed when their session appears). The port list is a snapshot per active distro, refreshed every `portsRefreshMs` (10000 by default), so a server started moments ago may not be listed yet. |
|
|
64
70
|
|
|
65
71
|
Three modules support them and import no DSH package.
|
|
66
72
|
[`lib/paths.js`](../lib/paths.js) translates paths.
|
|
@@ -79,9 +85,9 @@ The environment is per session, not per process:
|
|
|
79
85
|
|---|---|---|
|
|
80
86
|
| `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 |
|
|
81
87
|
| `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
|
|
88
|
+
| `workspace-files` | shipped, not replaced | its client half is the browser `file` resource provider the Sidebar previews need; it reads WSL paths through the routed root filesystem |
|
|
83
89
|
| `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 |
|
|
84
|
-
| `directory-picker-wsl`, `wsl-shell-env`, `auto-preset` | at the app level | they answer questions that are not scoped to a session |
|
|
90
|
+
| `directory-picker-wsl`, `wsl-shell-env`, `auto-preset` | at the app level | they answer questions that are not scoped to a single preset; `shell-env` resolves its facts per session workspace |
|
|
85
91
|
|
|
86
92
|
The preset mount requires the isolate realms. Without them the registry rejects the
|
|
87
93
|
providers with `Preset services require isolate realms: shell, fs`.
|
|
@@ -282,16 +288,17 @@ failures are named as well. Every `FsError` code the plugin itself raises appear
|
|
|
282
288
|
below — a model sees the code, and a reader asking what refused them finds it here.
|
|
283
289
|
Codes the shipped host stack raises behind a drive path pass through
|
|
284
290
|
`lib/fs-routing.js` unchanged and are the peer's, not this table's; the GUI file
|
|
285
|
-
|
|
291
|
+
service is the shipped `workspace-files` row, so its wire refusals are the peer's
|
|
292
|
+
own dialect, described after the table.
|
|
286
293
|
|
|
287
294
|
| Code | Raised by | Meaning | What clears it |
|
|
288
295
|
|---|---|---|---|
|
|
289
296
|
| `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` |
|
|
290
297
|
| `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 |
|
|
291
|
-
| `FS_OUTSIDE_DISTRO` | `WslFileSystem.worldPath` | `restrictToDistro` is on and the path names **another distro**'s share — a configuration fence, not a sandbox decision
|
|
298
|
+
| `FS_OUTSIDE_DISTRO` | `WslFileSystem.worldPath` | `distro` is fixed (`default` or a name), `restrictToDistro` is on, and the path names **another distro**'s share — a configuration fence, not a sandbox decision. With `distro: ''`/`auto` a UNC target is served by its own distro, so the fence has nothing foreign to refuse | set `distro` to `''`/`auto` (the default), or open a session in that distro. A wider permission does **not** lift it |
|
|
292
299
|
| `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
300
|
| `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
|
|
301
|
+
| `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 selected 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
302
|
| `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
303
|
| `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
304
|
| `FS_NOT_DIRECTORY` | `lib/fsio-agent.js` (`listChildren`) | the target is not a directory, so there are no children to list | list a directory |
|
|
@@ -301,14 +308,11 @@ tree's own wire refusals are a separate dialect, listed after the table.
|
|
|
301
308
|
| `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
309
|
| `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
310
|
|
|
304
|
-
The GUI file
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
`
|
|
308
|
-
`
|
|
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.
|
|
311
|
+
The GUI file service keeps the shipped `workspace-files` row, so its wire
|
|
312
|
+
refusals are the peer's `workspace-file/*` dialect (plus `gateway/bad-request`
|
|
313
|
+
for a malformed page request), not this plugin's `FsError` codes. The plugin
|
|
314
|
+
supplies the routed `ctx.fs`; an agent outage reaches the service as
|
|
315
|
+
`FS_IO_ERROR` and the peer maps it.
|
|
312
316
|
|
|
313
317
|
Two failures come from `wsl.exe` itself rather than from a code of ours, and both used
|
|
314
318
|
to be invisible:
|
|
@@ -347,7 +351,7 @@ step with the checkout, and `pnpm run sync:windows` runs that script.
|
|
|
347
351
|
| Type check | `tsc --noEmit` over `test/**` and `types/*.d.ts` (`src/` is checked by the build itself) | no |
|
|
348
352
|
| Unit tests | `test/*.test.mjs` | no |
|
|
349
353
|
| Sandbox probe | [`test/probe/sandbox.sh`](../test/probe/sandbox.sh) | no; it applies the profile arguments directly |
|
|
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 |
|
|
354
|
+
| Harness probes | [`test/probe/run.sh`](../test/probe/run.sh), `terminal.sh`, `sandbox-shell.sh`, `picker.sh`, `fs-routing.sh`, `missing-wsl.sh`, `sandbox-off.sh` | yes: Windows, WSL2, a mounted profile, and a linked checkout |
|
|
351
355
|
| Distro probes | [`test/probe/substrate.sh`](../test/probe/substrate.sh), `watch.sh`, `agent.sh`, `exec.sh` | Windows and WSL2, but no harness boot and no profile: they run from a distro terminal with Node available, reaching `wsl.exe` through interop |
|
|
352
356
|
| Host probe | [`test/probe/mode.sh`](../test/probe/mode.sh) | Windows Node, no harness |
|
|
353
357
|
| Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
|
|
@@ -361,9 +365,8 @@ waits for the app to exit and then runs the whole real-machine suite once —
|
|
|
361
365
|
The unit tests come in two kinds: most import only Node builtins, while
|
|
362
366
|
[`test/composition.test.mjs`](../test/composition.test.mjs),
|
|
363
367
|
[`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)
|
|
366
|
-
[`test/workspace-files-wsl.test.mjs`](../test/workspace-files-wsl.test.mjs) import
|
|
368
|
+
[`test/fsio-agent.test.mjs`](../test/fsio-agent.test.mjs) and
|
|
369
|
+
[`test/provider.test.mjs`](../test/provider.test.mjs) import
|
|
367
370
|
pinned `@deepseek-ai/*` development dependencies (the composition and boot tests run
|
|
368
371
|
the loader's real patch algorithm). One `pnpm install` reproduces that tree before
|
|
369
372
|
the first run; the runtime package itself still ships zero dependencies. CI runs
|
|
@@ -413,6 +416,8 @@ src/auto-preset.ts per-session environment selection
|
|
|
413
416
|
src/bwrap.ts the bwrap command line every confinement site
|
|
414
417
|
assembles (no imports, so a bare checkout can
|
|
415
418
|
assert it)
|
|
419
|
+
src/distro-route.ts the pure distro-mode selector (pinned / workspace /
|
|
420
|
+
default) shared by shell, fs, preflight and env
|
|
416
421
|
src/file-reference-wsl.ts the distro-side `@`-completion traversal — built,
|
|
417
422
|
tested, not yet wired (see above)
|
|
418
423
|
src/fs.ts the ctx.fs subpath entry point
|
|
@@ -444,10 +449,6 @@ src/terminal-activity.ts the distro-side activity probe for idle terminal
|
|
|
444
449
|
reclamation
|
|
445
450
|
src/terminal-route.ts which execution world a terminal launch belongs to (pure)
|
|
446
451
|
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
452
|
src/wsl.ts wsl.exe interop primitives (no DSH imports)
|
|
452
453
|
```
|
|
453
454
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -7,7 +7,7 @@ profile layer: `$DSH_HOME/profiles/<name>/cordis.patch.yml`. Start from
|
|
|
7
7
|
reference for every value the plugin ships.
|
|
8
8
|
|
|
9
9
|
Which rows a patch can reach depends on where they sit. `fs-routing`,
|
|
10
|
-
`
|
|
10
|
+
`directory-picker-wsl`, `subprocess-wsl`, `wsl-shell-env`
|
|
11
11
|
and `preset-wsl` are top-level rows, so `- id: <row>` in your layer patches them.
|
|
12
12
|
`wsl-shell` and `wsl-fs` are not top-level: they are plugins of the `wsl` agent
|
|
13
13
|
preset, nested inside the `preset-wsl` row's `config.plugins`. A patch that
|
|
@@ -22,44 +22,45 @@ has its own table at the end of this page.
|
|
|
22
22
|
|
|
23
23
|
| Row | Key | Default | Meaning |
|
|
24
24
|
|---|---|---|---|
|
|
25
|
-
| `wsl-shell` | `distro` | `''` *(shipped)* | distro
|
|
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
26
|
| | `shell` | `''` | pin a shell inside the distro; empty means the user's login shell |
|
|
27
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
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)* | 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).
|
|
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
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
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
32
|
| | `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` |
|
|
33
33
|
| | `cwd` | `''` | default working directory; empty means the distro user's home |
|
|
34
34
|
| | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000` *(shipped)* | per-call time limit, and the ceiling a call may ask for |
|
|
35
|
-
| `wsl-fs` | `distro` | `''` *(shipped)* |
|
|
36
|
-
| | `restrictToDistro` | `true` *(shipped)* | refuse a path in **another distro**'s share.
|
|
35
|
+
| `wsl-fs` | `distro` | `''` *(shipped)* | distro mode: empty/`auto` follows each target path, `default` forces WSL's default, and a name pins the filesystem |
|
|
36
|
+
| | `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 |
|
|
37
37
|
| | `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 |
|
|
38
38
|
| | `maskWindowsDrive` | `false` | as on `wsl-shell`: confined fs mutations publish through a resident whose profile shadows `/mnt` (same honest limits) |
|
|
39
|
-
| | `sessionSandbox` | `true` *(shipped)* | 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. Set `false` for the per-policy confined resident. The app-level `fs-routing` backend is not session-owned and keeps this off |
|
|
39
|
+
| | `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 |
|
|
40
40
|
| | `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 |
|
|
41
41
|
| | `cwd` | `''` | base directory for relative paths; empty means the distro user's home |
|
|
42
42
|
| | `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 |
|
|
43
|
-
| `fs-routing` | `distro` | `''` *(shipped)* |
|
|
43
|
+
| `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 |
|
|
44
44
|
| | `wslPath` | `wsl.exe` *(shipped)* | path to `wsl.exe`; used here only to resolve the default distro |
|
|
45
45
|
| | `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 |
|
|
46
46
|
| | `maskWindowsDrive` | `false` *(shipped)* | as on `wsl-shell`, for routed mutations |
|
|
47
|
-
| | `restrictToDistro` | `true` *(shipped)* | refuse a path in another distro's share (`FS_OUTSIDE_DISTRO`) |
|
|
47
|
+
| | `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 |
|
|
48
48
|
| | `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 |
|
|
49
49
|
| | `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 |
|
|
50
|
-
| `workspace-files-wsl` | `maxBytes` | `2097152` *(shipped)* | byte cap on one page of a text read, and the default byte window of a byte read |
|
|
51
|
-
| | `maxFileBytes` | `33554432` *(shipped)* | full-file byte cap for the byte reader |
|
|
52
|
-
| | `maxLines` | `5000` *(shipped)* | line cap on one text read |
|
|
53
|
-
| | `maxEntries` | `2000` *(shipped)* | directory entries listed per level before the listing is flagged truncated |
|
|
54
50
|
| `directory-picker-wsl` | `preferredDistro` | `''` *(shipped)* | the distro listed first in the picker |
|
|
55
51
|
| | `includeHostHome` | `true` *(shipped)* | also list the Windows home directory |
|
|
56
52
|
| | `maxEntries` | `1000` *(shipped)* | maximum entries listed per directory |
|
|
57
|
-
| `subprocess-wsl` | `distro` | `''` *(shipped)* |
|
|
53
|
+
| `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 |
|
|
58
54
|
| | `shell` | `''` | pin a shell; empty lets `wsl.exe` decide |
|
|
59
55
|
| | `loginShell` | `true` | whether a pinned shell uses login semantics |
|
|
60
56
|
| | `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 |
|
|
61
57
|
| | `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 |
|
|
62
58
|
|
|
59
|
+
The GUI file tree and previews use the shipped `workspace-files` row, not a plugin
|
|
60
|
+
row. Its caps are the peer defaults (`maxBytes` 2 MiB, `maxFileBytes` 32 MiB,
|
|
61
|
+
`maxLines` 5000, `maxEntries` 2000); override them with `- id: workspace-files`
|
|
62
|
+
in your own layer.
|
|
63
|
+
|
|
63
64
|
## Overriding the rows inside `preset-wsl`
|
|
64
65
|
|
|
65
66
|
`wsl-shell` and `wsl-fs` are mounted as plugins of the `wsl` agent preset, inside
|
|
@@ -82,6 +83,21 @@ changes nothing and does not fail: the log carries
|
|
|
82
83
|
`patch: entry "wsl-shell" not found` and `patch: entry "wsl-fs" not found`, and the
|
|
83
84
|
composed config keeps the shipped values.
|
|
84
85
|
|
|
86
|
+
The helper does the extraction and the common edits for you:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pnpm run preset -- --distro Ubuntu --session-sandbox false \
|
|
90
|
+
--out "$DSH_HOME/profiles/<name>/cordis.patch.yml"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`--distro`, `--session-sandbox`, `--sandbox`, `--mask-windows-drive` and
|
|
94
|
+
`--agent-backend` are the supported edits; without `--out` the helper prints the
|
|
95
|
+
complete block instead of writing it. `pnpm run preset -- --check <file>` reads an
|
|
96
|
+
existing layer and **fails with the exact block to paste** when it finds a
|
|
97
|
+
top-level `- id: wsl-shell`/`wsl-fs` entry — the silent no-op is caught before
|
|
98
|
+
boot. The generated block is byte-for-byte the shipped row plus your edits, so a
|
|
99
|
+
plugin upgrade only needs a re-run.
|
|
100
|
+
|
|
85
101
|
The route that works is to override the `preset-wsl` row as a whole. A row's
|
|
86
102
|
`config` is replaced whole, never deep-merged, so the replacement has to carry
|
|
87
103
|
`id`, `order` and the entire `plugins` list — every plugin, the `wsl-env` group
|
|
@@ -163,9 +179,43 @@ is out of service and the call rides the one-shot `wsl.exe` path.
|
|
|
163
179
|
|
|
164
180
|
## Which distro is used
|
|
165
181
|
|
|
166
|
-
`distro: ''`
|
|
167
|
-
|
|
168
|
-
default
|
|
182
|
+
`distro: ''` (or `distro: 'auto'`) follows the workspace: opening
|
|
183
|
+
`\\wsl.localhost\Debian\...` runs there even when Ubuntu is WSL's default.
|
|
184
|
+
`distro: 'default'` forces WSL's default instead, and a fixed distro (`default` or
|
|
185
|
+
a name) pins every session to that distro — a foreign UNC then refuses with
|
|
186
|
+
`FS_OUTSIDE_DISTRO` while `restrictToDistro` holds. A single-distro installation
|
|
187
|
+
needs no configuration.
|
|
188
|
+
|
|
189
|
+
## The strict profile
|
|
190
|
+
|
|
191
|
+
`maskWindowsDrive: true` with `sandbox: true` is the strict profile: every
|
|
192
|
+
confined profile shadows `/mnt` with an empty tmpfs, so a confined command can
|
|
193
|
+
neither read the Windows drive nor launch a Windows executable from it. Write it
|
|
194
|
+
into a profile with the preset helper (the rows are nested inside `preset-wsl`):
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pnpm run preset -- --sandbox true --mask-windows-drive true \
|
|
198
|
+
--out "$DSH_HOME/profiles/<name>/cordis.patch.yml"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The trade-offs are real: confined commands lose `/mnt/c` entirely — no reading
|
|
202
|
+
Windows files, no running Windows tools, no dev server that expects a Windows
|
|
203
|
+
path. It is also a narrowing, not a closure: a command can still write a Windows
|
|
204
|
+
executable into the workspace and run it there, because WSL's `binfmt_misc`
|
|
205
|
+
dispatches on content, not location. Enforcement therefore stays `partial`, and
|
|
206
|
+
the result reports `windowsDrive: "masked"`. The only complete closure is
|
|
207
|
+
distro-level: `[interop] enabled=false` in `/etc/wsl.conf`, then
|
|
208
|
+
`wsl.exe --shutdown`, which removes interop from the distro entirely.
|
|
209
|
+
|
|
210
|
+
| Interop state | How | `/mnt` | Windows exe from `/mnt` | Workspace-written `.exe` | Evidence |
|
|
211
|
+
|---|---|---|---|---|---|
|
|
212
|
+
| visible (default) | — | readable | runs, and escapes the Linux sandbox | runs | `pnpm run probe:sandbox` — INFO "interop escape is open" |
|
|
213
|
+
| masked (strict) | `maskWindowsDrive: true` | empty tmpfs | unreachable | still runs (binfmt) | `pnpm run probe:sandbox` — the strict-profile checks |
|
|
214
|
+
| disabled | `[interop] enabled=false`, `wsl --shutdown` | no Windows mounts | n/a | fails (no binfmt) | documented; not probed automatically (needs a distro-level change) |
|
|
215
|
+
|
|
216
|
+
The masked and visible legs are asserted on the real machine by
|
|
217
|
+
`test/probe/sandbox.sh`; the disabled leg needs a distro configuration change and
|
|
218
|
+
a WSL shutdown, so it stays a manual check recorded here.
|
|
169
219
|
|
|
170
220
|
## Turning the sandbox off
|
|
171
221
|
|
|
@@ -194,8 +244,18 @@ is missing.
|
|
|
194
244
|
|
|
195
245
|
The same probe also runs when a session opens: the `wsl-preflight` row warns
|
|
196
246
|
with the full remedy if `bwrap` is unusable, so the news arrives with the
|
|
197
|
-
session rather than with the first failed command. It
|
|
198
|
-
|
|
247
|
+
session rather than with the first failed command. It also checks the session
|
|
248
|
+
runtime's prerequisites — systemd as PID 1, the user manager, `systemd-run` and
|
|
249
|
+
Linger — when `sessionSandbox` on the preflight row is `true` (the default,
|
|
250
|
+
matching the shipped preset). Set `sessionSandbox: false` on that row when the
|
|
251
|
+
profile opts out of the session runtime, so it does not warn about a
|
|
252
|
+
prerequisite the profile does not use.
|
|
253
|
+
|
|
254
|
+
The advisory is logged and also published as a `wsl-preflight` runtime-context
|
|
255
|
+
entry, so it reaches the session transcript; DSH 0.2.1 has no toast/banner seam
|
|
256
|
+
for a plugin (see [UPSTREAM-SESSION-NOTICE.md](UPSTREAM-SESSION-NOTICE.md) and
|
|
257
|
+
[LIMITATIONS.md](LIMITATIONS.md#session-start-advisories)). It is advisory — it
|
|
258
|
+
never installs anything and never changes the fail-closed semantics; drop the
|
|
199
259
|
`wsl-preflight` row from your layer (or set `sandbox: false` on it) to silence
|
|
200
260
|
the warning.
|
|
201
261
|
|
|
@@ -217,8 +277,8 @@ When something refuses, the message names the row or the code:
|
|
|
217
277
|
| 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 |
|
|
218
278
|
| `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 |
|
|
219
279
|
| WSL wedges mid-command | no completion within the deadline | retry once, then `wsl.exe --shutdown` and reopen |
|
|
220
|
-
| a watched path never fires | `FS_IO_ERROR` from `watch` | watch a path inside the
|
|
221
|
-
| a path in another distro is refused | `FS_OUTSIDE_DISTRO` |
|
|
280
|
+
| a watched path never fires | `FS_IO_ERROR` from `watch` | watch a path inside the selected distro |
|
|
281
|
+
| 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 |
|
|
222
282
|
|
|
223
283
|
The full code table — what each code means and what clears it — is
|
|
224
284
|
[ARCHITECTURE.md § Error codes](ARCHITECTURE.md#error-codes); the design boundaries
|
|
@@ -228,5 +288,5 @@ behind the fail-closed choices above are in [LIMITATIONS.md](LIMITATIONS.md).
|
|
|
228
288
|
|
|
229
289
|
| Key | Default | Meaning |
|
|
230
290
|
| --- | --- | --- |
|
|
231
|
-
| `distro` | `''` |
|
|
291
|
+
| `distro` | `''` | distro mode for the `DSH_WSL_*` facts: empty/`auto` follows each session's workspace, `default` forces WSL's default, and a name pins |
|
|
232
292
|
| `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 |
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -18,8 +18,10 @@ crosses the 9p share. What to know about the one substrate there is:
|
|
|
18
18
|
Overwrites and edits keep the original bits. Run `chmod` (or ask the model to)
|
|
19
19
|
when a new file must be executable.
|
|
20
20
|
- **When the agent is out, file operations fail closed.** The distro stopped, or
|
|
21
|
-
the resident crashed
|
|
22
|
-
with the recovery path in the message.
|
|
21
|
+
the resident crashed through its revive budget: `FS_IO_ERROR` naming the
|
|
22
|
+
substrate, with the recovery path in the message. A single crash rides the
|
|
23
|
+
one rebuild (read-only ops only); a spent rebuild enters the bounded revival
|
|
24
|
+
(2/4/8 s), and three deaths make the state permanent with the restart remedy. There is no fallback I/O to degrade
|
|
23
25
|
into — deliberately; the old failure message pointed at the share, and the
|
|
24
26
|
share is what this plugin exists to keep the model off.
|
|
25
27
|
- **The mutation guard survives to the write.** `createIfAbsent` publishes with
|
|
@@ -110,6 +112,37 @@ crosses the 9p share. What to know about the one substrate there is:
|
|
|
110
112
|
kernel can actually report change.
|
|
111
113
|
- `editText` reads the whole file into memory before writing it back.
|
|
112
114
|
|
|
115
|
+
## The GUI workspace-files service
|
|
116
|
+
|
|
117
|
+
- **The GUI file tree and previews use the shipped `workspace-files` row.** The
|
|
118
|
+
plugin deliberately does not replace it: its client half is the browser `file`
|
|
119
|
+
resource provider the Sidebar previews need, and a server-only replacement
|
|
120
|
+
drops it (the earlier `workspace-files-wsl` takeover did exactly that). The
|
|
121
|
+
shipped service reads WSL paths through the routed root filesystem
|
|
122
|
+
(`fs-routing`), so the file tree and previews still run inside the distro.
|
|
123
|
+
|
|
124
|
+
- **That path costs extra agent round trips.** A listing, `stat` or page read
|
|
125
|
+
goes through `ctx.fs` before reaching the resident agent: roughly four to six
|
|
126
|
+
round trips instead of one, and a deep page read streams the preceding lines
|
|
127
|
+
through the pipe before cutting. Expect the GUI file tree and previews to be
|
|
128
|
+
slower than a direct agent call, especially on large files; the model's own
|
|
129
|
+
file operations are unaffected — they use the preset filesystem and the
|
|
130
|
+
resident agent directly. Measured 2026-10-19 with
|
|
131
|
+
[`test/probe/gui-preview.sh`](../test/probe/gui-preview.sh) (20 iterations on
|
|
132
|
+
this machine): the routed backend's `stat` p95 2.7 ms, `listDir` p95 5.1 ms,
|
|
133
|
+
and a 1 MiB `readByteRange` p95 **63.5 ms/MiB** — the service's own round
|
|
134
|
+
trips sit on top of those, and the 1 s/MiB budget has a wide margin. No
|
|
135
|
+
optimization is justified by these numbers; revisit only with a measured
|
|
136
|
+
regression.
|
|
137
|
+
|
|
138
|
+
- **`fs-routing` is the seam.** The shipped service depends on the routed root
|
|
139
|
+
filesystem staying enabled and faithful to the `ctx.fs` contract (`resolve`,
|
|
140
|
+
`lstat`, `stat`, `listDir`, `streamText`, `readBytes`, `watch`, `contains`,
|
|
141
|
+
`processPath`). Disabling `fs-routing` restores the host 9p-backed root
|
|
142
|
+
filesystem, and the GUI file service then crosses 9p for WSL workspaces. The
|
|
143
|
+
shipped row's own caps (`maxBytes`, `maxFileBytes`, `maxLines`, `maxEntries`)
|
|
144
|
+
can be overridden on the `workspace-files` row.
|
|
145
|
+
|
|
113
146
|
## The sandbox
|
|
114
147
|
|
|
115
148
|
- **The one-shot fallback has only the host deadline.** With `agent: false`, or
|
|
@@ -168,15 +201,47 @@ crosses the 9p share. What to know about the one substrate there is:
|
|
|
168
201
|
a separate case and is now reported as an error naming that directory, because
|
|
169
202
|
`wsl.exe` would otherwise run the command in `/` and exit 0.
|
|
170
203
|
|
|
204
|
+
## Agent diagnostics
|
|
205
|
+
|
|
206
|
+
- **The doctor's `agent.handshake` check probes a fresh resident, not the live
|
|
207
|
+
one.** It starts `agent/wsl-agent.sh` in the distro, reads its HELLO (pid,
|
|
208
|
+
protocol, script digest) and stops it, so it proves that the agent path can
|
|
209
|
+
start and that the file the distro reads is the shipped one. It cannot read
|
|
210
|
+
the running app resident's last error — there is no IPC channel from a plugin
|
|
211
|
+
script to the app process — so that error stays in the app log. In-process,
|
|
212
|
+
`WslAgent.status()` exposes state, pid, lastError, deaths and pending.
|
|
213
|
+
- **`dsh-wsl-*.scope` counts are point-in-time.** The doctor lists and inspects
|
|
214
|
+
the user scopes once; a scope created or stopped a moment later is not
|
|
215
|
+
reflected. `--fix` runs the same sweep the session preflight uses.
|
|
216
|
+
|
|
217
|
+
## Session-start advisories
|
|
218
|
+
|
|
219
|
+
- **There is no session-UI notice seam in DSH 0.2.1.** When a distro session
|
|
220
|
+
opens, `wsl-preflight` checks `bubblewrap`, systemd as PID 1, the user
|
|
221
|
+
manager, `systemd-run` and Linger. A failure is advisory: it never installs
|
|
222
|
+
anything, never cancels the session and never changes the fail-closed
|
|
223
|
+
behavior of the first command. The finding goes to the host log
|
|
224
|
+
(`logger.warn`) and into the session's runtime context as a `wsl-preflight`
|
|
225
|
+
entry — model-visible on the next assembly — but the GUI today has no
|
|
226
|
+
toast/banner service a plugin can publish to. `pnpm run doctor` is the
|
|
227
|
+
deterministic human-facing check; the exact upstream ask is
|
|
228
|
+
[UPSTREAM-SESSION-NOTICE.md](UPSTREAM-SESSION-NOTICE.md).
|
|
229
|
+
- **The runtime-context entry is a repeated snapshot, not a dismissible
|
|
230
|
+
notification.** It appears in every assembly until the environment is fixed;
|
|
231
|
+
it has no severity decoration and no dismiss control. Removing the
|
|
232
|
+
`wsl-preflight` row (or setting `sandbox: false` on it) silences the probe
|
|
233
|
+
entirely.
|
|
234
|
+
|
|
171
235
|
## Ports
|
|
172
236
|
|
|
173
237
|
- **The port poller keeps the resident agent permanently warm.** The
|
|
174
238
|
`DSH_WSL_PORTS` snapshot refreshes through the shared resident agent every
|
|
175
239
|
`portsRefreshMs` (10 s by default, well inside the agent's idle timeout), so
|
|
176
|
-
while
|
|
177
|
-
in-distro agent process stay resident per
|
|
178
|
-
in flight.
|
|
179
|
-
|
|
240
|
+
while a distro's session is active the agent never idles out: one `wsl.exe`
|
|
241
|
+
and one in-distro agent process stay resident per active distro even with no
|
|
242
|
+
command in flight. The poller starts only for a distro a session actually
|
|
243
|
+
uses; raising `portsRefreshMs` past the agent's idle timeout restores idle
|
|
244
|
+
shutdown, at the cost of staler port facts.
|
|
180
245
|
|
|
181
246
|
## The directory picker
|
|
182
247
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Configuration stability and migration
|
|
2
|
+
|
|
3
|
+
What a release may change, how a breaking configuration change is announced and
|
|
4
|
+
carried, and which gates a release must clear. The version table in
|
|
5
|
+
[RELEASING.md](RELEASING.md#1-decide-the-version) is the source for the bump; this
|
|
6
|
+
page is the contract those bumps follow.
|
|
7
|
+
|
|
8
|
+
## The freeze below 1.0
|
|
9
|
+
|
|
10
|
+
The package is below `1.0.0`, so the on-disk profile format and the peer pins may
|
|
11
|
+
still move in a **minor**. Within that freedom:
|
|
12
|
+
|
|
13
|
+
- **Adding a configuration key is a minor.** The key must have a schema default
|
|
14
|
+
that preserves the previous behavior; a key that changes behavior by existing
|
|
15
|
+
is a major-shaped change and waits.
|
|
16
|
+
- **Removing or renaming a key is a major** *or* a minor with a boot-time refusal
|
|
17
|
+
that names the migration (see below). Silent acceptance of a retired key is
|
|
18
|
+
never allowed: it is the failure the `preset-wsl` trap and the `substrate`
|
|
19
|
+
refusal exist to prevent.
|
|
20
|
+
- **Changing a key's meaning is a minor with a migration note**, because a profile
|
|
21
|
+
written for the old meaning keeps booting. The note must say what changed, how
|
|
22
|
+
to keep the old posture, and where the behavior differs.
|
|
23
|
+
- **Peer pins are exact.** A new upstream peer line is tested by the
|
|
24
|
+
[peer canary](../.github/workflows/peer-canary.yml) and lands as a minor with
|
|
25
|
+
the delta in the release note (or as a patch if the plugin code needs no
|
|
26
|
+
change).
|
|
27
|
+
- **Tool names and error codes are API.** `FS_OUTSIDE_DISTRO`,
|
|
28
|
+
`SANDBOX_UNAVAILABLE`, `FS_STALE_VERSION` and the diagnostic scripts
|
|
29
|
+
(`doctor`, `diagnose`, `preset`, `bootstrap`) keep their names and contracts;
|
|
30
|
+
a rename follows the deprecation path below.
|
|
31
|
+
|
|
32
|
+
## Deprecating a configuration value
|
|
33
|
+
|
|
34
|
+
1. **Announce it in the release that introduces the deprecation**: a CHANGELOG
|
|
35
|
+
entry and the row in [CONFIGURATION.md](CONFIGURATION.md). The old value keeps
|
|
36
|
+
working for that release.
|
|
37
|
+
2. **Keep the old spelling for one minor** when it is unambiguous. When keeping
|
|
38
|
+
it would silently change behavior, refuse it at boot instead and print the
|
|
39
|
+
migration — the `substrate: "share"` case: the value is refused with the
|
|
40
|
+
message that says to delete the line, because serving the share is exactly
|
|
41
|
+
what the plugin must not do.
|
|
42
|
+
3. **Remove it in the next minor** with another CHANGELOG entry. The release note
|
|
43
|
+
names the version where the refusal appeared.
|
|
44
|
+
|
|
45
|
+
### Case study: `distro: ''` (0.12.0)
|
|
46
|
+
|
|
47
|
+
`distro: ''` used to mean "WSL's default distro". 0.12.0 made the empty value
|
|
48
|
+
mean **auto**: follow the request workdir's UNC distro. A profile that relied on
|
|
49
|
+
the old meaning keeps booting, but now behaves differently. The migration:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
# keep the old posture: every command in WSL's default distro
|
|
53
|
+
- id: preset-wsl
|
|
54
|
+
config:
|
|
55
|
+
plugins:
|
|
56
|
+
# … full block; set on both rows …
|
|
57
|
+
- id: wsl-shell
|
|
58
|
+
config:
|
|
59
|
+
distro: default
|
|
60
|
+
- id: wsl-fs
|
|
61
|
+
config:
|
|
62
|
+
distro: default
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`pnpm run preset -- --distro default --out …` writes that block. The CHANGELOG
|
|
66
|
+
entry for 0.12.0 states the change; this page is the durable copy. `doctor`
|
|
67
|
+
reports the effective distro mode in its host/distro rows, and `--dump-config`
|
|
68
|
+
shows the composed value.
|
|
69
|
+
|
|
70
|
+
## Gates a release must clear
|
|
71
|
+
|
|
72
|
+
| Gate | How | Runs where |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| Config docs and CHANGELOG current | review the diff; `lint:links` covers cross-references | CI |
|
|
75
|
+
| `pnpm test` | style, links, syntax, build freshness, tsc, 40 unit files | CI (Node 22/24, Ubuntu + Windows) |
|
|
76
|
+
| `pnpm run test:coverage` | lines 85 / branches 84 / functions 70 | CI (Node 24, Ubuntu) |
|
|
77
|
+
| Peer line tested | `peer-canary` workflow resolves `next` and runs the scratch upgrade | CI schedule/manual |
|
|
78
|
+
| Packed install/upgrade/uninstall | `pnpm run e2e:install` (temp `DSH_HOME`, 0.11.0 → current, marker) | manual, this machine (10/10 on 2026-10-19) |
|
|
79
|
+
| Real-machine probe suite | `test/probe/run-all-when-closed.sh` (needs the app closed) | manual; **not run in this environment** since the last batch's targeted probes |
|
|
80
|
+
| Clean Win11 + Ubuntu first run ≤ 5 min | a clean machine, a stopwatch | **not run** (the project's machine is already configured) |
|
|
81
|
+
| aarch64 executed | the native suite on aarch64 WSL or qemu | **not run** (CI builds the binary only) |
|
|
82
|
+
| No open P0 bug for 30 days | issue tracker | time-based; cannot be asserted from a checkout |
|
|
83
|
+
|
|
84
|
+
The last four rows are deliberately marked as unrun rather than assumed: a
|
|
85
|
+
release note must not claim them, and [ROADMAP-1.0.md](ROADMAP-1.0.md#wsl-304--10-readiness)
|
|
86
|
+
tracks them. A release that skips the manual rows still ships through the tag
|
|
87
|
+
workflow, but its notes say which gates were executed.
|
|
88
|
+
|
|
89
|
+
## Verifying a migration locally
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pnpm run preset -- --distro default --check <profile>/cordis.patch.yml # no nested-row trap
|
|
93
|
+
pnpm run doctor -- --json # effective environment facts
|
|
94
|
+
dsh --profile <name> --dump-config # composed rows
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A profile that fails to boot after a migration should name the remedy in the
|
|
98
|
+
error; if it does not, that is a bug — report it with the `doctor --json` output.
|
package/docs/PARITY.md
CHANGED
|
@@ -83,7 +83,7 @@ shaped a phase of the plan.
|
|
|
83
83
|
| --- | --- | --- | --- |
|
|
84
84
|
| 0 | landed | This document; contract audit above | baseline recorded |
|
|
85
85
|
| 1 | landed | `lib/agent.js` (with `lib/agent-protocol.js`, `lib/agent-shared.js`) and `agent/wsl-agent.sh`: resident process speaking a line protocol (pipe-separated fields, single-line base64 payloads), version + script-digest handshake, lazy start, idle shutdown, crash-rebuild-once, one-shot `wsl.exe` fallback for execution | resident server |
|
|
86
|
-
| 2 | landed | real `watch()` shipped (
|
|
86
|
+
| 2 | landed | real `watch()` shipped: inotify for an unbounded directory since 0.10.0 (polling fallback for a file target, a bounded depth or a distro without `inotify-tools`); the native-ext4 migration of the remaining fs operations finished with the share's retirement — every model-facing file operation now runs on the agent, and the share-era workarounds went with it | native file semantics, change invalidation — inotify primary, polling fallback; blind spots in LIMITATIONS and CONFIGURATION |
|
|
87
87
|
| 3 | landed (deviation: no streaming, no spill files) | `WslShellExecutor` exec via agent; in-distro TERM→KILL; handle shape preserved | native exec latency |
|
|
88
88
|
| 4 | landed | `npm run bootstrap`, family-aware install command, `SANDBOX_UNAVAILABLE` names it; the handshake ships the protocol version plus the script digest | open-and-it-works |
|
|
89
89
|
| 5 | landed | per-session routing by workspace coordinates (`terminal-route.js`, `hostSessions`); idle distro terminals are reclaimed by a distro-side scan (`lib/terminal-activity.js`) — the host-side shell-activity integration still does not arm for `wsl.exe`, which is why the observation is a scan | terminal per window/session |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Performance: ext4 agent vs the 9p share
|
|
2
|
+
|
|
3
|
+
The plugin's reason to exist is not only correctness: a WSL workspace served
|
|
4
|
+
through the Windows 9p share is measurably slower than the same files read on
|
|
5
|
+
the distro's ext4 through the resident agent. This page records the measurements
|
|
6
|
+
and how to reproduce them; it does not extrapolate beyond them.
|
|
7
|
+
|
|
8
|
+
## The two substrates
|
|
9
|
+
|
|
10
|
+
| | 9p share (`\\wsl.localhost\...`) | This plugin |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| File I/O | host stack crosses the VM boundary per operation | resident agent serves reads/writes on ext4 inside the distro |
|
|
13
|
+
| Change feed | polling the share | inotify inside the distro (`watchMaxDepth: 0`) |
|
|
14
|
+
| Commands | host process | distro login shell through `wsl.exe` |
|
|
15
|
+
|
|
16
|
+
The plugin's root filesystem routes every WSL path to the distro backend
|
|
17
|
+
(`fs-routing`), so the GUI file tree, previews and the model's file tools all
|
|
18
|
+
use the agent path. The design and its trade-offs are in
|
|
19
|
+
[root-fs-routing.md](root-fs-routing.md); the known limits are in
|
|
20
|
+
[LIMITATIONS.md](LIMITATIONS.md).
|
|
21
|
+
|
|
22
|
+
## Measured numbers
|
|
23
|
+
|
|
24
|
+
Machine: Windows 11 + WSL 2, Ubuntu, x86_64, the project's development machine.
|
|
25
|
+
Absolute numbers are machine-dependent; the ratio and the budgets are the point.
|
|
26
|
+
|
|
27
|
+
| Scenario | 9p share | Agent (ext4) | Source |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| 16 concurrent shared reads | 342–614 ms (queue up to 583 ms) | **2.4–4.1 ms** after A7, first start ~70 ms once | [ADR 0002 fs queue](adr/0002-native-agent.md#fs-queue-measurement-a6-2026-10-18) |
|
|
30
|
+
| Single FS op (host round trip) | — | p95 ≈ 22 ms | [ROADMAP-1.0.md](ROADMAP-1.0.md#7-metrics) |
|
|
31
|
+
| GUI-preview backend: `stat` | — | p95 **2.7 ms** | `pnpm run probe:gui-preview` (20 iterations, 2026-10-19) |
|
|
32
|
+
| GUI-preview backend: listing | — | p95 **5.1 ms** | same |
|
|
33
|
+
| GUI-preview backend: 1 MiB page | — | p95 **63.5 ms/MiB** | same |
|
|
34
|
+
|
|
35
|
+
The GUI-preview number covers the routed backend operations a preview pays.
|
|
36
|
+
The shipped `workspace-files` service adds its own calls on top; that service
|
|
37
|
+
shape stays qualitative in [LIMITATIONS.md](LIMITATIONS.md). The 1 s/MiB budget
|
|
38
|
+
has a wide margin, so no further optimization (the deferred v6 FS concurrency)
|
|
39
|
+
is justified by these measurements.
|
|
40
|
+
|
|
41
|
+
## Reproduce
|
|
42
|
+
|
|
43
|
+
Inside the distro, with the checkout:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm run probe:gui-preview # stat / listDir / 1 MiB page read, p50/p95
|
|
47
|
+
node test/probe/fs-queue.mjs # the agent queue, sequential vs concurrent, with per-op latency
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Both need a working distro and Node; neither boots the harness. The probes print
|
|
51
|
+
the raw samples' p50/p95/max and fail when a budget is exceeded, so a regression
|
|
52
|
+
is a red probe rather than a claim in prose.
|
|
53
|
+
|
|
54
|
+
## What is not measured
|
|
55
|
+
|
|
56
|
+
- End-to-end GUI paint time (the browser and the service's own work are out of
|
|
57
|
+
scope for these probes).
|
|
58
|
+
- 9p numbers for large files or many small files beyond the A6 sample above;
|
|
59
|
+
the A6 measurement is the recorded baseline, not a re-runnable benchmark.
|
|
60
|
+
- aarch64; the binary is built but not executed in CI.
|