dsh-plugin-wsl-env 0.7.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +345 -8
- package/CONTRIBUTING.md +221 -68
- package/README.md +50 -35
- package/SECURITY.md +18 -2
- package/SUPPORT.md +23 -7
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +200 -57
- package/docs/CONFIGURATION.md +180 -16
- package/docs/LIMITATIONS.md +91 -33
- package/docs/PARITY.md +66 -38
- package/docs/PEER-PARITY.md +95 -32
- package/docs/README.zh.md +49 -31
- package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
- package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1106 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preflight.d.ts +91 -0
- package/lib/preflight.js +94 -0
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +51 -0
- package/lib/probe-cache.js +43 -29
- package/lib/sandbox-core.d.ts +149 -0
- package/lib/sandbox-core.js +239 -87
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +150 -155
- package/locale/preflight/en.json +6 -0
- package/locale/preflight/zh.json +6 -0
- package/package.json +78 -22
- package/scripts/diagnose.sh +92 -0
package/docs/CONFIGURATION.md
CHANGED
|
@@ -6,26 +6,48 @@ profile layer: `$DSH_HOME/profiles/<name>/cordis.patch.yml`. Start from
|
|
|
6
6
|
[`cordis.patch.yml`](../cordis.patch.yml) open beside it — that file is the commented
|
|
7
7
|
reference for every value the plugin ships.
|
|
8
8
|
|
|
9
|
+
Which rows a patch can reach depends on where they sit. `fs-routing`,
|
|
10
|
+
`workspace-files-wsl`, `directory-picker-wsl`, `subprocess-wsl`, `wsl-shell-env`
|
|
11
|
+
and `preset-wsl` are top-level rows, so `- id: <row>` in your layer patches them.
|
|
12
|
+
`wsl-shell` and `wsl-fs` are not top-level: they are plugins of the `wsl` agent
|
|
13
|
+
preset, nested inside the `preset-wsl` row's `config.plugins`. A patch that
|
|
14
|
+
targets them by id matches nothing — the loader logs `patch: entry "wsl-shell"
|
|
15
|
+
not found` (a warning, not an error), the profile boots anyway, and the value you
|
|
16
|
+
wrote is not in force. See
|
|
17
|
+
[Overriding the rows inside `preset-wsl`](#overriding-the-rows-inside-preset-wsl).
|
|
18
|
+
|
|
9
19
|
In the table below, *(shipped)* marks the values set by `cordis.patch.yml`. The rest
|
|
10
|
-
are schema defaults, listed because they are the ones worth knowing.
|
|
20
|
+
are schema defaults, listed because they are the ones worth knowing. `wsl-shell-env`
|
|
21
|
+
has its own table at the end of this page.
|
|
11
22
|
|
|
12
23
|
| Row | Key | Default | Meaning |
|
|
13
24
|
|---|---|---|---|
|
|
14
25
|
| `wsl-shell` | `distro` | `''` *(shipped)* | distro name; empty means WSL's default distro |
|
|
15
26
|
| | `shell` | `''` | pin a shell inside the distro; empty means the user's login shell |
|
|
16
|
-
| | `loginShell` | `true` *(shipped)* | use `<shell> -lc`, which sources your profile, instead of a bare `-c` |
|
|
17
|
-
| | `agent` | `true` | run commands over the resident in-distro agent (one long-lived process; in-distro timeout);
|
|
18
|
-
| | `sandbox` | `true` | confine commands with `bubblewrap`; `false` disables the sandbox |
|
|
27
|
+
| | `loginShell` | `true` *(shipped)* | use `<shell> -lc`, which sources your profile, instead of a bare `-c`. Applies only to the POSIX family; a non-POSIX shell (fish, csh) still gets a bare `-c` |
|
|
28
|
+
| | `agent` | `true` | run commands over the resident in-distro agent (one long-lived process; in-distro timeout); the one-shot `wsl.exe` path is fallen back to only when the agent is out of service. Anything else — a cwd refusal, a sandbox refusal — is the command's own verdict and is raised, not retried on the other path |
|
|
29
|
+
| | `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) |
|
|
19
30
|
| | `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` |
|
|
20
31
|
| | `cwd` | `''` | default working directory; empty means the distro user's home |
|
|
21
32
|
| | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000` *(shipped)* | per-call time limit, and the ceiling a call may ask for |
|
|
22
33
|
| `wsl-fs` | `distro` | `''` *(shipped)* | as above |
|
|
23
34
|
| | `restrictToDistro` | `true` *(shipped)* | refuse a path in **another distro**'s share. `/mnt/c` is a directory *inside* the pinned distro, so it is not affected. The refusal carries `FS_OUTSIDE_DISTRO`; that is not a sandbox decision, so the file tools do not offer a wider permission for it. Set `false` to allow another distro's share |
|
|
24
|
-
| | `sandbox` | `true` | check `writeText` and `editText` against the policy |
|
|
35
|
+
| | `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 |
|
|
25
36
|
| | `maskWindowsDrive` | `false` | as on `wsl-shell`: confined fs mutations publish through a resident whose profile shadows `/mnt` (same honest limits) |
|
|
26
|
-
| | `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 |
|
|
37
|
+
| | `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 |
|
|
27
38
|
| | `cwd` | `''` | base directory for relative paths; empty means the distro user's home |
|
|
28
39
|
| | `watchMaxDepth` | `0` | how deep the in-distro watch scan walks; `0` walks the whole tree. Bound it (e.g. `4`) when the watched tree holds a `node_modules` — a change below the bound is seen only when another change above it fires the same callback |
|
|
40
|
+
| `fs-routing` | `distro` | `''` *(shipped)* | which distro the root-plane filesystem routes to; empty resolves WSL's default. **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 |
|
|
41
|
+
| | `wslPath` | `wsl.exe` *(shipped)* | path to `wsl.exe`; used here only to resolve the default distro |
|
|
42
|
+
| | `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 |
|
|
43
|
+
| | `maskWindowsDrive` | `false` *(shipped)* | as on `wsl-shell`, for routed mutations |
|
|
44
|
+
| | `restrictToDistro` | `true` *(shipped)* | refuse a path in another distro's share (`FS_OUTSIDE_DISTRO`) |
|
|
45
|
+
| | `watchMaxDepth` | `0` *(shipped)* | how deep the routed watch scan walks; `0` walks the whole tree |
|
|
46
|
+
| | `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 |
|
|
47
|
+
| `workspace-files-wsl` | `maxBytes` | `2097152` *(shipped)* | byte cap on one page of a text read, and the default byte window of a byte read |
|
|
48
|
+
| | `maxFileBytes` | `33554432` *(shipped)* | full-file byte cap for the byte reader |
|
|
49
|
+
| | `maxLines` | `5000` *(shipped)* | line cap on one text read |
|
|
50
|
+
| | `maxEntries` | `2000` *(shipped)* | directory entries listed per level before the listing is flagged truncated |
|
|
29
51
|
| `directory-picker-wsl` | `preferredDistro` | `''` *(shipped)* | the distro listed first in the picker |
|
|
30
52
|
| | `includeHostHome` | `true` *(shipped)* | also list the Windows home directory |
|
|
31
53
|
| | `maxEntries` | `1000` *(shipped)* | maximum entries listed per directory |
|
|
@@ -35,13 +57,106 @@ are schema defaults, listed because they are the ones worth knowing.
|
|
|
35
57
|
| | `hostSessions` | `true` | give a session opened on a Windows folder a host `powershell.exe` in its own directory instead of the distro shell at `/mnt/<drive>/…`; `false` restores the composition-owned distro terminal for every session |
|
|
36
58
|
| | `terminalIdleReclaim` | `true` | let the controller reclaim an idle distro terminal: the launch is marked with a per-terminal `DSH_TERMINAL_ID`, and the handle's `inspectActivity` counts the marked processes through the resident agent — one (the shell at its prompt) is idle, more than one (a running command or a nested shell) is busy, `unknown` (agent out) pauses reclamation. `false` restores close-by-hand |
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
## Overriding the rows inside `preset-wsl`
|
|
61
|
+
|
|
62
|
+
`wsl-shell` and `wsl-fs` are mounted as plugins of the `wsl` agent preset, inside
|
|
63
|
+
the `preset-wsl` row's `config.plugins`. The loader indexes top-level entry ids —
|
|
64
|
+
plus the plugin lists of rows that are themselves groups — and `preset-wsl` is a
|
|
65
|
+
plain entry whose plugins sit inside its `config`, so those two ids are not
|
|
66
|
+
addressable from a patch layer at all. A layer of
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
- id: wsl-shell
|
|
70
|
+
config:
|
|
71
|
+
distro: Ubuntu
|
|
72
|
+
|
|
73
|
+
- id: wsl-fs
|
|
74
|
+
config:
|
|
75
|
+
restrictToDistro: false
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
changes nothing and does not fail: the log carries
|
|
79
|
+
`patch: entry "wsl-shell" not found` and `patch: entry "wsl-fs" not found`, and the
|
|
80
|
+
composed config keeps the shipped values.
|
|
81
|
+
|
|
82
|
+
The route that works is to override the `preset-wsl` row as a whole. A row's
|
|
83
|
+
`config` is replaced whole, never deep-merged, so the replacement has to carry
|
|
84
|
+
`id`, `order` and the entire `plugins` list — every plugin, the `wsl-env` group
|
|
85
|
+
and everything after it. Copy the whole `- id: preset-wsl` block out of
|
|
86
|
+
[`cordis.patch.yml`](../cordis.patch.yml) (or out of
|
|
87
|
+
`dsh --profile <name> --dump-config`), paste it into your layer, and edit only the
|
|
88
|
+
values you want:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
- id: preset-wsl
|
|
92
|
+
config:
|
|
93
|
+
id: wsl
|
|
94
|
+
order: 2
|
|
95
|
+
plugins:
|
|
96
|
+
- id: wsl-env
|
|
97
|
+
name: cordis:group
|
|
98
|
+
group: true
|
|
99
|
+
isolate:
|
|
100
|
+
shell: true
|
|
101
|
+
fs: true
|
|
102
|
+
config:
|
|
103
|
+
- id: wsl-shell
|
|
104
|
+
name: 'dsh-plugin-wsl-env'
|
|
105
|
+
config:
|
|
106
|
+
distro: Ubuntu # ← the edit
|
|
107
|
+
loginShell: true
|
|
108
|
+
timeoutMs: 120000
|
|
109
|
+
maxTimeoutMs: 600000
|
|
110
|
+
- id: wsl-fs
|
|
111
|
+
name: 'dsh-plugin-wsl-env/fs'
|
|
112
|
+
config:
|
|
113
|
+
distro: ''
|
|
114
|
+
restrictToDistro: false # ← the edit
|
|
115
|
+
substrate: agent
|
|
116
|
+
# … and the rest of the group, verbatim — do not leave a plugin out …
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Leaving a plugin out of the replacement drops it: the preset would mount only the
|
|
120
|
+
plugins you listed. The keys described above are the only reason to rewrite this
|
|
121
|
+
row, so treat the `preset-wsl` block as one unit and re-copy it after a plugin
|
|
122
|
+
upgrade that changes it.
|
|
123
|
+
|
|
124
|
+
For the root-plane filesystem — the row that serves `ctx.fs` outside any session,
|
|
125
|
+
`fs-routing` — no rewrite is needed: it is a top-level row, and
|
|
126
|
+
`restrictToDistro`, `sandbox` and `maskWindowsDrive` can be set on it directly.
|
|
127
|
+
That governs different traffic from `wsl-fs`; see
|
|
128
|
+
[`docs/root-fs-routing.md`](root-fs-routing.md).
|
|
129
|
+
|
|
130
|
+
## Other keys
|
|
131
|
+
|
|
132
|
+
You can override the other keys in the same way, with one split: every row in the
|
|
133
|
+
table below except `wsl-shell` and `wsl-fs` is top-level, so `- id: <row>` in your
|
|
134
|
+
own layer patches it — those two are inside `preset-wsl` and take the rewrite
|
|
135
|
+
above. They all keep their defaults:
|
|
136
|
+
|
|
137
|
+
| Row | Key | Default | Meaning |
|
|
138
|
+
|---|---|---|---|
|
|
139
|
+
| `wsl-shell` (in `preset-wsl`) | `wslPath` | `wsl.exe` | path to `wsl.exe`, for an installation that is not on `PATH` |
|
|
140
|
+
| | `hostCwd` | unset → `%SystemRoot%` | the Windows directory the `wsl.exe` process itself starts in |
|
|
141
|
+
| | `forwardEnv` | `[]` | extra host environment variable names to forward into the distro through `WSLENV`. This is the one key here that widens what crosses the boundary: `NO_COLOR`, `TERM`, `PAGER`, `GIT_PAGER`, `LANG`, `LC_ALL` and every managed `DSH_*` name ride along without it, and `PATH` is deliberately never forwarded |
|
|
142
|
+
| `wsl-fs` (in `preset-wsl`) | `wslPath` | `wsl.exe` | as above; used only to resolve the default distro |
|
|
143
|
+
| | `diffBasisMaxBytes` | `10485760` | UTF-8 byte limit per side of an overwrite diff; a larger file publishes without a diff |
|
|
144
|
+
| `fs-routing` | `cwd`, `diffBasisMaxBytes` | `process.cwd()`, `10485760` | `cwd` configures the host side only (the distro side reads `distroCwd`); `diffBasisMaxBytes` is shared by both — it also bounds the routed backend's overwrite diff |
|
|
145
|
+
| `directory-picker-wsl` | `wslPath` | `wsl.exe` | as above |
|
|
146
|
+
| | `distroCacheMs` | `5000` | how long a resolved distro list stays fresh before `wsl.exe` is asked again |
|
|
147
|
+
| `subprocess-wsl` | `wslPath` | `wsl.exe` | as above |
|
|
148
|
+
| | `cwd` | `''` | fallback directory when a launch names none |
|
|
149
|
+
| | `hostCwd` | unset → `%SystemRoot%` | as on `wsl-shell` |
|
|
150
|
+
| | `forwardEnv` | `[]` | as on `wsl-shell`, for terminal launches |
|
|
151
|
+
|
|
152
|
+
The shell row also inherits the shipped output budgets from `dsh-bash-local`,
|
|
153
|
+
which documents them in full: `maxOutputBytes` (64000), `maxSpillBytes`
|
|
154
|
+
(67108864) and `graceMs` (3000, the SIGTERM-to-SIGKILL grace). The spill keys
|
|
155
|
+
belong to the one-shot path: with `agent: true` — the default — the capture is
|
|
156
|
+
cut at `maxOutputBytes` per stream inside the distro, the result reports
|
|
157
|
+
`truncated: true`, and no temporary file is written, so `maxSpillBytes` and
|
|
158
|
+
`graceMs` have nothing to bound. They apply when `agent` is `false` or the agent
|
|
159
|
+
is out of service and the call rides the one-shot `wsl.exe` path.
|
|
45
160
|
|
|
46
161
|
## Which distro is used
|
|
47
162
|
|
|
@@ -53,9 +168,58 @@ default is not the one you want sessions in.
|
|
|
53
168
|
|
|
54
169
|
`sandbox: false` on `wsl-shell` and `wsl-fs` runs commands and writes without
|
|
55
170
|
confinement. `sandboxMode` then reports `undefined`, and the tool layer tells the
|
|
56
|
-
model that these operations have no sandbox.
|
|
57
|
-
|
|
58
|
-
|
|
171
|
+
model that these operations have no sandbox. `fs-routing` takes the same key and
|
|
172
|
+
drops the fence on the root-plane filesystem the same way, but that service reports
|
|
173
|
+
no `sandboxMode` either way — it inherits the base class's empty getter — so the
|
|
174
|
+
key decides the fence, not a capability fact the model reads. That is the documented
|
|
175
|
+
opt-out; see [ARCHITECTURE.md](ARCHITECTURE.md#sandbox) for what the sandbox does
|
|
176
|
+
and does not cover.
|
|
177
|
+
|
|
178
|
+
The default posture needs `bubblewrap` **inside the distro** and fails closed: a
|
|
179
|
+
distro without a usable `bwrap` refuses the command (`SANDBOX_UNAVAILABLE`) or the
|
|
180
|
+
write (`FS_IO_ERROR`, the message carrying the same guidance) instead of
|
|
181
|
+
running it unconfined. The refusal distinguishes the two causes — `bwrap`
|
|
182
|
+
missing (install it) from `bwrap` present but failing (usually the kernel's
|
|
183
|
+
unprivileged user namespaces; reinstalling will not help) — and the direct
|
|
184
|
+
install command it prints follows the distro's own package family
|
|
185
|
+
(apt/dnf/pacman/zypper). A failed probe is re-run,
|
|
186
|
+
so installing `bubblewrap` takes effect on the next command without a restart.
|
|
187
|
+
Install it with `scripts/bootstrap.sh <distro> --install`, or directly:
|
|
188
|
+
`wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap`. Run without
|
|
189
|
+
`--install`, `scripts/bootstrap.sh <distro>` is a read-only check that prints what
|
|
190
|
+
is missing.
|
|
191
|
+
|
|
192
|
+
The same probe also runs when a session opens: the `wsl-preflight` row warns
|
|
193
|
+
with the full remedy if `bwrap` is unusable, so the news arrives with the
|
|
194
|
+
session rather than with the first failed command. It is advisory — it never
|
|
195
|
+
installs anything and never changes the fail-closed semantics; drop the
|
|
196
|
+
`wsl-preflight` row from your layer (or set `sandbox: false` on it) to silence
|
|
197
|
+
the warning.
|
|
198
|
+
|
|
199
|
+
## Checking a change, and what refuses
|
|
200
|
+
|
|
201
|
+
`dsh --profile <name> --dump-config` composes the profile and prints the result
|
|
202
|
+
without booting. It is the fastest way to see whether a row you wrote landed —
|
|
203
|
+
including the warning that says it did not — and for the rows inside `preset-wsl`
|
|
204
|
+
the composed tree it prints carries the block to copy back into your layer.
|
|
205
|
+
`scripts/bootstrap.sh <distro>` does the same for the distro-side prerequisites.
|
|
206
|
+
|
|
207
|
+
When something refuses, the message names the row or the code:
|
|
208
|
+
|
|
209
|
+
| Symptom | What you see | What clears it |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| commands fail with no usable sandbox | `SANDBOX_UNAVAILABLE` — the text says whether `bwrap` is missing or present but failing, with the matching remedy | missing: install `bubblewrap` (`scripts/bootstrap.sh <distro> --install`, or the printed family command); present-but-failing: check `bwrap --version` and the kernel's unprivileged user namespaces, or set `sandbox: false` on `wsl-shell` |
|
|
212
|
+
| writes fail with no usable sandbox | `FS_IO_ERROR`, with the same `bwrap` remedy the commands carry | the same install, or `sandbox: false` on `wsl-fs` (`fs-routing` for root-plane writes) |
|
|
213
|
+
| file tools fail while the distro is down | `FS_IO_ERROR`, "the distro file substrate is unavailable" | start the distro (`wsl.exe -l -v`), restart the session if it stays out |
|
|
214
|
+
| 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 |
|
|
215
|
+
| `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 |
|
|
216
|
+
| WSL wedges mid-command | no completion within the deadline | retry once, then `wsl.exe --shutdown` and reopen |
|
|
217
|
+
| a watched path never fires | `FS_IO_ERROR` from `watch` | watch a path inside the pinned distro |
|
|
218
|
+
| a path in another distro is refused | `FS_OUTSIDE_DISTRO` | open a session in that distro, or set `restrictToDistro: false` |
|
|
219
|
+
|
|
220
|
+
The full code table — what each code means and what clears it — is
|
|
221
|
+
[ARCHITECTURE.md § Error codes](ARCHITECTURE.md#error-codes); the design boundaries
|
|
222
|
+
behind the fail-closed choices above are in [LIMITATIONS.md](LIMITATIONS.md).
|
|
59
223
|
|
|
60
224
|
## `wsl-shell-env`
|
|
61
225
|
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -29,13 +29,21 @@ crosses the 9p share. What to know about the one substrate there is:
|
|
|
29
29
|
syscall before the rename: a concurrent writer wins and the stale write
|
|
30
30
|
refuses with `FS_STALE_VERSION`. The residual stat-then-rename pair is the
|
|
31
31
|
same sliver a Linux-host session's own rename has.
|
|
32
|
+
- **A write is durable against process death, not machine death.** The
|
|
33
|
+
publication fsyncs the staged file before its atomic rename, but the
|
|
34
|
+
containing directory is not fsynced afterwards: a kernel panic or power loss
|
|
35
|
+
immediately after a successful write can lose the rename, and the file
|
|
36
|
+
reverts to its pre-write content. The peer backend fsyncs its staged file the
|
|
37
|
+
same way, but neither fsyncs the containing directory — the exposure is the
|
|
38
|
+
peer's too, and it stays short of a crash-consistent store.
|
|
32
39
|
- **A Linux filename containing a backslash cannot be addressed.** The UNC
|
|
33
40
|
display form (`\\wsl.localhost\<distro>\...`) is the file's identity
|
|
34
41
|
everywhere - the session header, the GUI, the caches - and the backslash is
|
|
35
42
|
the UNC's separator, so `a\b.ts` in the distro round-trips to `a/b.ts`: the
|
|
36
43
|
tools would silently address a different path. This is the coordinate
|
|
37
|
-
system's own limit
|
|
38
|
-
the substrate; a rename away from
|
|
44
|
+
system's own limit — a backslash-delimited identity cannot spell a name
|
|
45
|
+
containing a backslash — not a defect of the substrate; a rename away from
|
|
46
|
+
backslash names is the workaround.
|
|
39
47
|
- **Stage two routes mutations through a confined resident.** A write or edit
|
|
40
48
|
under a confined policy runs on its own long-lived agent, spawned inside the
|
|
41
49
|
bwrap profile that binds exactly what the mode grants — so the kernel, not
|
|
@@ -45,32 +53,27 @@ crosses the 9p share. What to know about the one substrate there is:
|
|
|
45
53
|
refuses them with the bootstrap command, rather than downgrading); a policy
|
|
46
54
|
change addresses a different resident, so the first write after switching the
|
|
47
55
|
Permissions selector pays one agent startup; and a write the policy grants to
|
|
48
|
-
the distro's `/tmp` lands in the
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
the distro's `/tmp` lands in the distro's real `/tmp`, because the
|
|
57
|
+
file-writes resident binds it read-write — where the read tools, the plain
|
|
58
|
+
resident and the user's own shell all see it. The ephemeral tmpfs belongs to
|
|
59
|
+
the command side alone, so a file handed over from a command still goes
|
|
60
|
+
through the session workspace.
|
|
51
61
|
- **`glob` and `grep` run the distro's own rg** (`lib/search-route.js`): the
|
|
52
62
|
tool's arguments are forwarded verbatim, so results, output format and exit
|
|
53
63
|
codes are rg's own — at ext4 speed, with the distro's `.gitignore` rules.
|
|
54
|
-
A distro without rg
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
A distro without rg fails the search outright: exit 127, with the message
|
|
65
|
+
from the exec launcher and never from rg itself (which is the missing
|
|
66
|
+
program) — `setsid: failed to execute rg: No such file or directory` where
|
|
67
|
+
the distro has `setsid`, the script shell's `rg: not found` where it does
|
|
68
|
+
not. The install command `scripts/bootstrap.sh` prints — `wsl.exe -d <distro>
|
|
69
|
+
-u root -- apt-get install -y bubblewrap git ripgrep inotify-tools`, or the
|
|
70
|
+
detected family's spelling — installs it. `--install` runs that command only
|
|
71
|
+
when `bubblewrap` itself is missing, so a distro that has bwrap but not rg
|
|
72
|
+
needs the printed line run by hand. A search whose directory is a Windows
|
|
73
|
+
folder keeps the host binary, which is native there.
|
|
57
74
|
- **`watch()` is one poll loop** (a `find -newer` scan inside the distro), so
|
|
58
75
|
the coarse-invalidation caveats below apply. An inotify backend is planned,
|
|
59
76
|
not shipped.
|
|
60
|
-
|
|
61
|
-
- **The sandbox does not govern WSL interop.** A confined command can still run a
|
|
62
|
-
Windows program, which escapes the Linux-side boundary. The plugin reports this as
|
|
63
|
-
`enforcement: partial`. See [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement).
|
|
64
|
-
The `maskWindowsDrive` key on `wsl-shell` and `wsl-fs` narrows the hole: the
|
|
65
|
-
confined profile shadows `/mnt` with an empty tmpfs, so the drive's files cannot be
|
|
66
|
-
read or exfiltrated and its executables cannot be launched. It is not a closure — a
|
|
67
|
-
command that can write the workspace can write an executable there and run it
|
|
68
|
-
(binfmt interop dispatches on file content, not location) — so enforcement stays
|
|
69
|
-
`partial`, the result's sandbox fact reports `windowsDrive: "masked"`, and the only
|
|
70
|
-
complete closure remains distro-level (`[interop] enabled=false` in `wsl.conf`).
|
|
71
|
-
- **`bubblewrap` must be installed**, or both providers fail immediately. The
|
|
72
|
-
usability probe re-runs after a failure, so installing bubblewrap while the app
|
|
73
|
-
runs is believed on the next command; only a success is cached.
|
|
74
77
|
- **A confined command's `/tmp` is not the write tool's `/tmp`.** `workspace-write`
|
|
75
78
|
mounts a fresh tmpfs at `/tmp` for the command — that is what makes it ephemeral —
|
|
76
79
|
while the file tools' fence grants the distro's *real* `/tmp`. Both are writable, so
|
|
@@ -78,25 +81,77 @@ crosses the 9p share. What to know about the one substrate there is:
|
|
|
78
81
|
`bash` and vice versa. Hand a file between them through the session workspace.
|
|
79
82
|
- A distro that is stopped or unregistered while the app runs takes the file
|
|
80
83
|
tools' substrate with it: reads and writes report the substrate-unavailable
|
|
81
|
-
error naming the cause
|
|
84
|
+
error naming the cause. A death with nothing in flight costs nothing — the
|
|
85
|
+
next call starts a fresh transport, so a distro that comes back is served
|
|
86
|
+
again. A death with a request in flight spends the resident's one permitted
|
|
87
|
+
rebuild, and when that rebuild cannot complete — the distro is still down, or
|
|
88
|
+
gone — the resident is permanently out: every later file operation fails the
|
|
89
|
+
same way until the app restarts.
|
|
90
|
+
- `watch()` observes from inside the distro (a `find -newer` poll loop, see
|
|
91
|
+
`lib/watcher.js`), so events are coarse invalidation, not per-file notifications,
|
|
92
|
+
and they arrive on the poll cadence, not instantly. A creation is seen even when
|
|
93
|
+
the new file's own mtime is old: creating it bumps the containing directory's
|
|
94
|
+
mtime, and the scan includes the watched directory itself (deletions are seen the
|
|
95
|
+
same way). What escapes the stamp is a change that leaves every scanned mtime
|
|
96
|
+
older than it — an in-place `cp -p` onto an existing file (new content, a
|
|
97
|
+
copied-back mtime), or a `tar -x` whose archive restores the containing
|
|
98
|
+
directory's own mtime. Watching a 9p share from the Windows side remains
|
|
99
|
+
unsupported — the loop runs where the kernel can actually report change.
|
|
100
|
+
- `editText` reads the whole file into memory before writing it back.
|
|
101
|
+
|
|
102
|
+
## The sandbox
|
|
103
|
+
|
|
104
|
+
- **The sandbox does not govern WSL interop.** A confined command can still run a
|
|
105
|
+
Windows program, which escapes the Linux-side boundary. The plugin reports this as
|
|
106
|
+
`enforcement: partial`. See [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement).
|
|
107
|
+
The `maskWindowsDrive` key on `wsl-shell`, `wsl-fs` and `fs-routing` narrows the
|
|
108
|
+
hole: the confined profile shadows `/mnt` with an empty tmpfs, so the drive's files
|
|
109
|
+
cannot be read or exfiltrated and its executables cannot be launched. It is not a
|
|
110
|
+
closure — a command that can write the workspace can write an executable there and
|
|
111
|
+
run it (binfmt interop dispatches on file content, not location) — so enforcement
|
|
112
|
+
stays `partial`, the result's sandbox fact reports `windowsDrive: "masked"`, and
|
|
113
|
+
the only complete closure remains distro-level (`[interop] enabled=false` in
|
|
114
|
+
`wsl.conf`).
|
|
115
|
+
- **`bubblewrap` must be installed**, or both providers fail immediately. The
|
|
116
|
+
usability probe re-runs after a failure, so installing bubblewrap while the app
|
|
117
|
+
runs is believed on the next command; only a success is cached. The refusal
|
|
118
|
+
classifies the probe's failure — missing versus present-but-failing — and the
|
|
119
|
+
direct install command follows the distro's package family; a probe failure
|
|
120
|
+
with no recognisable shape falls back to the install remedy with the raw
|
|
121
|
+
output quoted. A session-start preflight (`wsl-preflight`) surfaces the same
|
|
122
|
+
text as a warning; it is advisory and never installs.
|
|
82
123
|
- `workspace-write` binds the workspace root as writable, and bubblewrap refuses a
|
|
83
124
|
bind whose source directory does not exist, so a session whose workspace directory was
|
|
84
125
|
deleted fails with a runner error; the plugin does not recreate it. A workdir that
|
|
85
126
|
does not exist — a configured `cwd` pointing at a deleted directory, for instance — is
|
|
86
127
|
a separate case and is now reported as an error naming that directory, because
|
|
87
128
|
`wsl.exe` would otherwise run the command in `/` and exit 0.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
129
|
+
|
|
130
|
+
## Ports
|
|
131
|
+
|
|
132
|
+
- **The port poller keeps the resident agent permanently warm.** The
|
|
133
|
+
`DSH_WSL_PORTS` snapshot refreshes through the shared resident agent every
|
|
134
|
+
`portsRefreshMs` (10 s by default, well inside the agent's idle timeout), so
|
|
135
|
+
while the app runs the agent never idles out: one `wsl.exe` and one
|
|
136
|
+
in-distro agent process stay resident per pinned distro even with no command
|
|
137
|
+
in flight. Raising `portsRefreshMs` past the agent's idle timeout restores
|
|
138
|
+
idle shutdown, at the cost of staler port facts.
|
|
139
|
+
|
|
140
|
+
## The directory picker
|
|
141
|
+
|
|
95
142
|
- The directory picker lists distro levels from inside the distro — the
|
|
96
143
|
resident's `ls -1ALp` in one round trip, a one-shot `wsl.exe --exec ls` when
|
|
97
144
|
it is out, never the 9p walk the host used to pay per entry. The listing is
|
|
98
|
-
line-parsed
|
|
99
|
-
|
|
145
|
+
line-parsed and only a line that ends in `/` becomes a row, so a directory
|
|
146
|
+
name containing a newline cannot be listed correctly: `ls` emits `a\nb` as
|
|
147
|
+
the two lines `a` and `b/`, and the picker offers the single row `b` at
|
|
148
|
+
`<parent>/b` — the part after the newline, a different directory or none at
|
|
149
|
+
all. A name that ends in a newline yields no row at all: the split leaves one
|
|
150
|
+
segment without a trailing `/` and one empty name, and the parser keeps
|
|
151
|
+
neither. `/` cannot appear in a name, so nothing else can split one.
|
|
152
|
+
|
|
153
|
+
## Terminals
|
|
154
|
+
|
|
100
155
|
- Terminal routing is per-session by workspace coordinates (`subprocess-wsl.hostSessions`,
|
|
101
156
|
default on): a WSL-folder session gets the distro shell, a Windows-folder session gets
|
|
102
157
|
`powershell.exe` in its own directory. The shell MENU remains the single configured
|
|
@@ -124,6 +179,9 @@ crosses the 9p share. What to know about the one substrate there is:
|
|
|
124
179
|
the idle threshold is the controller's `unattendedTimeoutMs` (2 h by
|
|
125
180
|
default); and the host-side shell-activity integration is still not armed
|
|
126
181
|
for these launches — `wsl.exe` on win32 fails its gate — which is why the
|
|
127
|
-
observation is a scan rather than shell cooperation.
|
|
182
|
+
observation is a scan rather than shell cooperation. `terminalIdleReclaim:
|
|
183
|
+
false` on the `subprocess-wsl` row restores close-by-hand: the launch then
|
|
184
|
+
carries no `DSH_TERMINAL_ID` marker, and the handle keeps the shipped
|
|
185
|
+
`unknown` activity, which never accumulates idle.
|
|
128
186
|
- Windows-folder host terminals (the `powershell.exe` branch) keep the shipped
|
|
129
187
|
behaviour: `unknown` activity, no reclamation.
|
package/docs/PARITY.md
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
# Parity with VS Code Remote-WSL
|
|
2
2
|
|
|
3
3
|
What parity means for this plugin, the seam contracts the plan was audited
|
|
4
|
-
against, and the phase checklist
|
|
5
|
-
|
|
4
|
+
against, and the phase checklist with each phase's current status. The plan
|
|
5
|
+
has landed; the rows below record where each phase ended up and what
|
|
6
|
+
deviation, if any, still stands.
|
|
7
|
+
|
|
8
|
+
Verified against: `dsh-plugin-wsl-env@0.8.0` / peer `0.2.0-rc.2` / agent
|
|
9
|
+
protocol v4, 2026-10-06. Update this line when the peer pin or the agent
|
|
10
|
+
protocol moves.
|
|
6
11
|
|
|
7
12
|
## What parity means here
|
|
8
13
|
|
|
9
14
|
Remote-WSL's experience rests on one thing: a resident server inside the
|
|
10
15
|
distro. Everything users love — native ext4 semantics, inotify, native
|
|
11
16
|
`rg`, low latency — is a derivative. This plugin's 9p-UNC plus one-shot
|
|
12
|
-
`wsl.exe` model can only approach it
|
|
13
|
-
in-distro agent and
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
`wsl.exe` model can only approach it, so the plan landed a resident
|
|
18
|
+
in-distro agent and moved the `fs` and `shell` seams onto it. The agent is
|
|
19
|
+
now the only substrate for the file tools; the fallbacks left are at the
|
|
20
|
+
transport level — the one-shot `wsl.exe` path that `wsl-shell.agent: false`
|
|
21
|
+
pins for command execution, and that a search spawn drops to when the agent
|
|
22
|
+
is out, never a share.
|
|
16
23
|
|
|
17
24
|
Not applicable, recorded so the gap is a decision and not an omission:
|
|
18
25
|
|
|
@@ -21,12 +28,12 @@ Not applicable, recorded so the gap is a decision and not an omission:
|
|
|
21
28
|
- Settings Sync, Remote-SSH tunnel chaining, Codespaces: composition-layer
|
|
22
29
|
responsibilities, not this plugin's.
|
|
23
30
|
|
|
24
|
-
> **Update (2026-10-04).** The plan's transitional invariant —
|
|
25
|
-
> false` must equal the pre-agent share-backed behaviour —
|
|
26
|
-
> share substrate is refused at construction, and the
|
|
27
|
-
> distro's rg. The share is no longer a fallback path
|
|
28
|
-
> only fallbacks left are transport-level (one-shot
|
|
29
|
-
> resident agent is out), never share I/O.
|
|
31
|
+
> **Update (2026-10-04).** The plan's transitional invariant — that
|
|
32
|
+
> `wsl-shell.agent: false` must equal the pre-agent share-backed behaviour —
|
|
33
|
+
> is superseded: the share substrate is refused at construction, and the
|
|
34
|
+
> search spawn rides the distro's rg. The share is no longer a fallback path
|
|
35
|
+
> of this plugin; the only fallbacks left are transport-level (one-shot
|
|
36
|
+
> `wsl.exe` where the resident agent is out), never share I/O.
|
|
30
37
|
|
|
31
38
|
## Contract audit (Phase 0)
|
|
32
39
|
|
|
@@ -47,17 +54,18 @@ shaped a phase of the plan.
|
|
|
47
54
|
distro's share, rewrite the spawn to run a distro-side `rg` (falling back
|
|
48
55
|
to `grep -rE` + `find` when absent) with identical stdout conventions.
|
|
49
56
|
Under the agent, that exec rides the agent's `exec` capability.
|
|
50
|
-
**LANDED (2026-10-04), as the design above has it:**
|
|
51
|
-
|
|
52
|
-
and the exec rides the agent's `exec` capability —
|
|
53
|
-
through the minimal handle facade of
|
|
54
|
-
returns its handle synchronously, so
|
|
55
|
-
with the one-shot `wsl.exe --exec`
|
|
56
|
-
out. The `grep -rE` fallback is
|
|
57
|
-
`.gitignore`, so it is not an rg
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
share
|
|
57
|
+
**LANDED (2026-10-04), as the design above has it:**
|
|
58
|
+
`WslSubprocessRuntime.spawn` performs the rewrite, the decision living in
|
|
59
|
+
`lib/search-route.js`, and the exec rides the agent's `exec` capability —
|
|
60
|
+
Phase 1's resident — through the minimal handle facade of
|
|
61
|
+
`lib/search-exec.js` (the spawn seam returns its handle synchronously, so
|
|
62
|
+
the facade degrades inside `done`), with the one-shot `wsl.exe --exec`
|
|
63
|
+
handle as the fallback when the agent is out. The `grep -rE` fallback is
|
|
64
|
+
NOT implemented — grep does not read `.gitignore`, so it is not an rg
|
|
65
|
+
substitute; a distro without one surfaces rg's own "command not found",
|
|
66
|
+
and `scripts/bootstrap.sh` reports whether the binary is installed. With
|
|
67
|
+
the share substrate retired the same day, no model-facing search crosses
|
|
68
|
+
the 9p share at all.
|
|
61
69
|
3. **`dsh-fs-local` watches with chokidar over the share** — the behaviour
|
|
62
70
|
this plugin replaces: chokidar on 9p is the unreliability that made
|
|
63
71
|
`WslFileSystem.watch` refuse. The in-distro watcher replaces it wholesale.
|
|
@@ -69,21 +77,41 @@ shaped a phase of the plan.
|
|
|
69
77
|
|
|
70
78
|
## Phase checklist
|
|
71
79
|
|
|
72
|
-
| Phase | Deliverable | Parity row |
|
|
73
|
-
| --- | --- | --- |
|
|
74
|
-
| 0 | This document; contract audit above | baseline recorded |
|
|
75
|
-
| 1 | `lib/agent
|
|
76
|
-
| 2 | real `watch()`
|
|
77
|
-
| 3 |
|
|
78
|
-
| 4 |
|
|
79
|
-
| 5 |
|
|
80
|
-
| 6 |
|
|
81
|
-
| 7 |
|
|
80
|
+
| Phase | Status | Deliverable | Parity row |
|
|
81
|
+
| --- | --- | --- | --- |
|
|
82
|
+
| 0 | landed | This document; contract audit above | baseline recorded |
|
|
83
|
+
| 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 |
|
|
84
|
+
| 2 | landed | real `watch()` shipped (in-distro mtime poll, `lib/watcher.js`); 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 — watch + publication done (a poll, not inotify; blind spots in LIMITATIONS and CONFIGURATION) |
|
|
85
|
+
| 3 | landed (deviation: no streaming, no spill files) | `WslShellExecutor` exec via agent; in-distro TERM→KILL; handle shape preserved | native exec latency |
|
|
86
|
+
| 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 |
|
|
87
|
+
| 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 |
|
|
88
|
+
| 6 | landed | `DSH_WSL_PORTS` (agent-scanned `/proc/net/tcp{,6}`, timer-refreshed); localhost reachability (WSL2 localhost forwarding is platform behaviour) documented in README | port forwarding visibility |
|
|
89
|
+
| 7 | landed | recipes section in both READMEs: credential sharing, `/mnt/c` guidance, `WSLENV` policy | developer-loop odds and ends |
|
|
82
90
|
|
|
83
91
|
## Fallback invariant
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
The plan's rule — every phase keeps the pre-phase behaviour byte for byte —
|
|
94
|
+
is superseded by the Update above. What is still load-bearing is narrower,
|
|
95
|
+
and no longer involves the share:
|
|
96
|
+
|
|
97
|
+
- **Command execution.** `wsl-shell.agent: false`, and any agent failure at
|
|
98
|
+
runtime, run the command through the one-shot `wsl.exe` path, which is the
|
|
99
|
+
pre-agent `executeOneShot` verbatim; confinement and result decoration are
|
|
100
|
+
identical on both, and the budgets keep their shapes — the agent path cuts
|
|
101
|
+
each stream at `maxOutputBytes` inside the distro and reports
|
|
102
|
+
`truncated: true` with no spill file, where the one-shot path can spill
|
|
103
|
+
(`maxSpillBytes`, `graceMs`). The probe asserts this:
|
|
104
|
+
`pnpm run probe:agent` (`bash test/probe/agent.sh`, Windows + WSL only)
|
|
105
|
+
runs the same operations through the resident and through the one-shot path
|
|
106
|
+
and compares outcomes — exit code, stdout bytes, stderr bytes, working
|
|
107
|
+
directory — not internals.
|
|
108
|
+
- **Search has no share fallback either.** The distro rewrite runs on the
|
|
109
|
+
resident agent — a distro-side `rg` must exist — and an agent that is out
|
|
110
|
+
drops the same spawn to the one-shot `wsl.exe --exec` handle
|
|
111
|
+
(`lib/search-exec.js`): a transport fallback, never the 9p share.
|
|
112
|
+
- **File tools fail closed.** `wsl-fs.substrate` has one working value,
|
|
113
|
+
`agent`; a profile still carrying the retired `share` is refused at
|
|
114
|
+
construction. When the agent is out these operations fail closed with
|
|
115
|
+
`FS_IO_ERROR` naming the substrate — deliberately, there is no I/O to
|
|
116
|
+
degrade into, and with the share retired no model-facing file path crosses
|
|
117
|
+
9p.
|