dsh-plugin-wsl-env 0.1.4 → 0.1.6
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 +104 -0
- package/CONTRIBUTING.md +14 -5
- package/README.md +5 -2
- package/docs/ARCHITECTURE.md +23 -6
- package/docs/CONFIGURATION.md +6 -2
- package/docs/LIMITATIONS.md +12 -1
- package/docs/README.zh.md +5 -2
- package/lib/auto-preset.js +2 -2
- package/lib/index.js +75 -14
- package/lib/paths.js +9 -4
- package/lib/preset-choice.js +28 -0
- package/lib/probe-cache.js +48 -0
- package/lib/sandbox.js +19 -18
- package/lib/wsl.js +24 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,110 @@ reference below points at that record's numbering.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.1.6] - 2026-10-02
|
|
16
|
+
|
|
17
|
+
Three probes that close coverage gaps a closure review named, the documentation of a
|
|
18
|
+
`/tmp` asymmetry, and one clearer error message. No behaviour changed otherwise.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `npm run probe:picker` drives the directory picker in a throwaway boot: the root
|
|
23
|
+
level's crumbs, the Windows home, every installed distro, the documented
|
|
24
|
+
`directory-unreadable` refusal for a relative path and for a directory that does not
|
|
25
|
+
exist, and `maxEntries` with its `truncated` flag. Nothing ran `lib/picker.js` before
|
|
26
|
+
this — the unit tests import only `lib/listing.js`.
|
|
27
|
+
- `npm run probe:missing-wsl` boots a profile whose `wslPath` cannot start and pins how
|
|
28
|
+
that is reported: the executable is named, `wslPath` is suggested, the failure is not
|
|
29
|
+
dressed up as a sandbox refusal, and no sandbox facts are claimed for a command that
|
|
30
|
+
never ran. The other fail-closed state — `wsl.exe` works but the distro has no
|
|
31
|
+
`bubblewrap` — is asserted in `test/sandbox.test.mjs`, which drives
|
|
32
|
+
`WslSandbox.confine` against a failing probe, because driving that end to end needs a
|
|
33
|
+
distro without `bubblewrap`.
|
|
34
|
+
- `npm run probe:mode` runs `test/probe/mode-probe.mjs`, which measures which POSIX-mode
|
|
35
|
+
facts survive the share. The file was cited by `docs/LIMITATIONS.md` and wired into
|
|
36
|
+
no script at all.
|
|
37
|
+
- `test/preset-choice.test.mjs` covers the session-preset decision, now that the rule
|
|
38
|
+
is the pure `lib/preset-choice.js`. The frame timing that applies the decision still
|
|
39
|
+
needs the harness, and the test says so.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- `runCapture` names the cause when the executable itself cannot start: the message now
|
|
44
|
+
points at `wslPath` and at WSL being installed, the same way it already did for a
|
|
45
|
+
token denied by the Windows sandbox. `npm run probe:missing-wsl` asserts it.
|
|
46
|
+
|
|
47
|
+
### Documentation
|
|
48
|
+
|
|
49
|
+
- `docs/LIMITATIONS.md` states the `/tmp` split: a confined command's `/tmp` is a fresh
|
|
50
|
+
tmpfs while the file tools' fence grants the distro's *real* `/tmp`, so both sides can
|
|
51
|
+
write but neither sees the other's files. Hand a file between them through the session
|
|
52
|
+
workspace. `docs/ARCHITECTURE.md` describes the same distinction where it explains the
|
|
53
|
+
fence, including why the Windows temp directory is deliberately not granted to a
|
|
54
|
+
distro session.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## [0.1.5] - 2026-10-02
|
|
58
|
+
|
|
59
|
+
Three defects from a design-closure review. Two are cases where a documented remedy
|
|
60
|
+
could not work, and one where the cause of a failure was invisible.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- A failed sandbox probe was remembered for the process lifetime, so installing
|
|
65
|
+
`bubblewrap` after the app had started changed nothing until a restart — while the
|
|
66
|
+
error told the user to install exactly that. Only a success is cached now; a failure
|
|
67
|
+
is re-probed on the next command, and concurrent first commands share one probe. The
|
|
68
|
+
policy lives in [`lib/probe-cache.js`](lib/probe-cache.js), which is pure, so the
|
|
69
|
+
regression is pinned on every platform including CI
|
|
70
|
+
([`test/probe-cache.test.mjs`](test/probe-cache.test.mjs)).
|
|
71
|
+
- `restrictToDistro` refused another distro's share with `FS_SANDBOX_DENIED`. The
|
|
72
|
+
file-tool layer turns that code into a denial marker plus an escalation offer
|
|
73
|
+
(`mapError` in `dsh-tool-fs`), and no wider permission can lift this fence: it is a
|
|
74
|
+
configuration choice, not a sandbox decision. It now reports `FS_OUTSIDE_DISTRO`,
|
|
75
|
+
which the tool layer passes through unchanged, and the message says what to do.
|
|
76
|
+
- A command that never started because the distro was missing, stopped or
|
|
77
|
+
unregistered reached the model as a bare non-zero exit. `wsl.exe` writes
|
|
78
|
+
`Wsl/Service/WSL_E_DISTRO_NOT_FOUND` to **stdout** — UTF-16LE on some builds — with
|
|
79
|
+
an empty stderr, so every signature in the plugin missed it. `WslShellExecutor` now
|
|
80
|
+
reads that code (NUL-stripped, so either encoding is found), pins `WSL_UTF8` on its
|
|
81
|
+
own spawn so the text is readable, and fails with the distro name and the remedy.
|
|
82
|
+
Deliberately not a sandbox code: a wider permission cannot create a distro.
|
|
83
|
+
|
|
84
|
+
### Documentation
|
|
85
|
+
|
|
86
|
+
- Two statements were wrong and are corrected against measurement. `read-only` does
|
|
87
|
+
not leave `/dev/null` as the only writable path: `bwrap --dev /dev` mounts a fresh
|
|
88
|
+
`/dev`, so `/dev/shm` is writable scratch and only `/tmp` stays read-only. And
|
|
89
|
+
`restrictToDistro` does not refuse `/mnt/c`, which is a directory *inside* the
|
|
90
|
+
distro — it refuses another distro's share.
|
|
91
|
+
- The shell row's inherited output budgets (`maxOutputBytes`, `maxSpillBytes`,
|
|
92
|
+
`graceMs`) are listed in [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md); the
|
|
93
|
+
fail-closed bullet in [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md) matches the
|
|
94
|
+
re-probing behaviour, and what the file tools do when a distro stops mid-session is
|
|
95
|
+
stated there too.
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
|
|
99
|
+
- `test:unit` gained `test/probe-cache.test.mjs` and `test/sandbox.test.mjs`;
|
|
100
|
+
`test:coverage` gained the pure one. The sandbox test skips itself where the
|
|
101
|
+
`dsh-sandbox` peer is absent, which is the CI case, and
|
|
102
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md) explains when a test belongs in which list.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
- A shell request that names its workdir only **relatively** now lands on the
|
|
108
|
+
distro user's home, joined under it, when no `cwd` is configured — instead of
|
|
109
|
+
riding on the parent's `process.cwd()` fallback, a Windows directory no
|
|
110
|
+
`wsl.exe --cd` accepts. The empty-workdir case already defaulted to the home;
|
|
111
|
+
the relative tail was the one shape the marker missed. Absolute workdirs in
|
|
112
|
+
any coordinate system are unchanged.
|
|
113
|
+
- A Windows UNC path outside the WSL share (`\\server\share\...`) is now
|
|
114
|
+
treated as the world path it already is, instead of being joined onto the
|
|
115
|
+
default workdir as if it were relative — which named a file that existed
|
|
116
|
+
nowhere. It also no longer triggers a `wsl.exe` home query it can never need:
|
|
117
|
+
`isRelativeWorldPath` now reports every UNC form, WSL or not, as absolute.
|
|
118
|
+
|
|
15
119
|
## [0.1.4] - 2026-10-02
|
|
16
120
|
|
|
17
121
|
Documentation only: a shorter README, and the reference documents it links to.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -69,6 +69,9 @@ terminal if you would rather not see the prompt.
|
|
|
69
69
|
| `npm run probe:sandbox` | what bubblewrap confines and what it does not | no |
|
|
70
70
|
| `npm run probe:sandbox-shell` | the confined executor through a real boot | yes |
|
|
71
71
|
| `npm run probe:terminal` | the terminal provider | yes |
|
|
72
|
+
| `npm run probe:missing-wsl` | how a `wslPath` that cannot start is reported | yes |
|
|
73
|
+
| `npm run probe:picker` | the picker's root level, its refusals and its cap | yes |
|
|
74
|
+
| `npm run probe:mode` | which POSIX-mode facts survive the share | no; Windows Node |
|
|
72
75
|
|
|
73
76
|
Every script above is wired in `package.json`. `npm test` runs `lint:style`,
|
|
74
77
|
`test:syntax` and `test:unit` in that order. `prepublishOnly` runs `npm test` again
|
|
@@ -92,9 +95,12 @@ Choose the layer by what the test needs.
|
|
|
92
95
|
| Unit test | `test/*.test.mjs` | pure logic: path translation, listing, shell argument building, terminal helpers |
|
|
93
96
|
| Probe | `test/probe/` | behaviour that needs a real distro, a real profile, or bubblewrap |
|
|
94
97
|
|
|
95
|
-
Unit tests run under `node --test
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
Unit tests run under `node --test`. A test that imports only this repository goes in
|
|
99
|
+
both the `test:unit` and the `test:coverage` lists in `package.json`, which name
|
|
100
|
+
their files explicitly. A test that needs a DSH peer — `test/sandbox.test.mjs`
|
|
101
|
+
imports `lib/sandbox.js`, which imports `@deepseek-ai/dsh-sandbox` — goes in
|
|
102
|
+
`test:unit` only and must skip itself when the peer is missing, because CI installs
|
|
103
|
+
nothing: try the import, print a `SKIP` line, exit 0.
|
|
98
104
|
|
|
99
105
|
Probes are shell scripts that mount a throwaway profile, plus the small plugin
|
|
100
106
|
files they load. Those plugin files are the `*-probe.mjs` and `*-probe.yml` files
|
|
@@ -141,8 +147,11 @@ On Windows 11 + WSL2 (Ubuntu 26.04) the following has been verified:
|
|
|
141
147
|
outside the session workspace is refused inside the distro and the following
|
|
142
148
|
wider-permission request succeeds.
|
|
143
149
|
|
|
144
|
-
Counts:
|
|
145
|
-
sandbox
|
|
150
|
+
Counts: 55 unit assertions on a bare checkout (50 plus the 5 preset-decision
|
|
151
|
+
checks), plus 5 in `test/sandbox.test.mjs` which need the `dsh-sandbox` peer and skip
|
|
152
|
+
themselves without it; 19 filesystem-probe assertions; 12 shell-probe checks; 9 picker
|
|
153
|
+
checks; 6 missing-executable checks; 5 mode checks and 10 sandbox expectations plus the
|
|
154
|
+
recorded escape; and 3 terminal assertions.
|
|
146
155
|
|
|
147
156
|
## Commit and pull request conventions
|
|
148
157
|
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Override a row by id in `$DSH_HOME/profiles/<name>/cordis.patch.yml`. The keys w
|
|
|
54
54
|
| `wsl-shell` | `distro` | `''` | distro name; empty means WSL's default distro |
|
|
55
55
|
| | `sandbox` | `true` | confine commands with `bubblewrap`; `false` disables the sandbox |
|
|
56
56
|
| `wsl-fs` | `distro` | `''` | as above |
|
|
57
|
-
| | `restrictToDistro` | `true` | refuse
|
|
57
|
+
| | `restrictToDistro` | `true` | refuse a path in **another distro**'s share; `/mnt/c` is inside this distro and is not affected. Refused with `FS_OUTSIDE_DISTRO`, which is not a sandbox denial and cannot be lifted by wider permissions |
|
|
58
58
|
| | `sandbox` | `true` | check `writeText` and `editText` against the policy |
|
|
59
59
|
| `directory-picker-wsl` | `includeHostHome` | `true` | also list the Windows home directory |
|
|
60
60
|
| `subprocess-wsl` | `distro` | `''` | which distro the GUI terminal opens in |
|
|
@@ -67,7 +67,7 @@ Commands are confined by `bubblewrap` inside the distro, and file writes are che
|
|
|
67
67
|
|
|
68
68
|
| Mode | What a command inside the distro can do |
|
|
69
69
|
|---|---|
|
|
70
|
-
| `read-only` | read the whole distro;
|
|
70
|
+
| `read-only` | read the whole distro; a fresh `/dev` is mounted writable, so `/dev/null` and `/dev/shm` work, and nothing else does |
|
|
71
71
|
| `workspace-write` | the above, plus the session workspace is writable and `/tmp` is a temporary mount |
|
|
72
72
|
| `danger-full-access` | no sandbox; used for an approved wider-permission request |
|
|
73
73
|
|
|
@@ -100,6 +100,9 @@ npm run probe:sandbox # measure inside the distro what bubblewrap does an
|
|
|
100
100
|
npm run probe # filesystem probe against a real distro (Windows + WSL only)
|
|
101
101
|
npm run probe:sandbox-shell # boot a real harness and drive the confined executor
|
|
102
102
|
npm run probe:terminal # open a PTY through the terminal provider
|
|
103
|
+
npm run probe:missing-wsl # boot a profile whose wslPath cannot start (Windows + WSL only)
|
|
104
|
+
npm run probe:picker # list the picker's root level, refusals and its cap (Windows + WSL only)
|
|
105
|
+
npm run probe:mode # which POSIX-mode facts survive the share (Windows node only)
|
|
103
106
|
```
|
|
104
107
|
|
|
105
108
|
There is nothing to install first: the package has no dependencies, and every module under test imports only Node builtins. CI runs `npm test` on Node 20, 22 and 24, on Linux and Windows, and `npm run test:coverage` on Node 24.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -120,14 +120,20 @@ behaviour and the error messages match a Linux host. The function
|
|
|
120
120
|
|
|
121
121
|
| Mode | What a command inside the distro can do |
|
|
122
122
|
|---|---|
|
|
123
|
-
| `read-only` | read the whole distro;
|
|
123
|
+
| `read-only` | read the whole distro; a fresh `/dev` is mounted writable, so `/dev/null` and `/dev/shm` work, and nothing else does |
|
|
124
124
|
| `workspace-write` | the above, plus the session workspace is writable and `/tmp` is a temporary mount |
|
|
125
125
|
| `danger-full-access` | no sandbox; used for an approved wider-permission request |
|
|
126
126
|
|
|
127
|
-
`WslFileSystem` checks writes against the same policy and the same writable list
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
`WslFileSystem` checks writes against the same policy and the same writable list,
|
|
128
|
+
with one deliberate translation: for a session inside a distro the temp root is the
|
|
129
|
+
distro's own `/tmp`, and the Windows temp directory is *not* granted, because it lies
|
|
130
|
+
outside the execution world the policy describes. That avoids a mismatch where `bash`
|
|
131
|
+
could write `/tmp` but the write tool could not. The two `/tmp`s are still different
|
|
132
|
+
directories — the command's is a fresh tmpfs — see [LIMITATIONS.md](LIMITATIONS.md). The fence is complete for the model's reach: `writeText` and `editText` are
|
|
133
|
+
the only mutation calls the model-facing file tools make on the filesystem seam,
|
|
134
|
+
so checking those two checks everything the model can mutate. Both providers
|
|
135
|
+
report their mode through `sandboxMode`. The tool layer and the Permissions
|
|
136
|
+
selector read that value.
|
|
131
137
|
|
|
132
138
|
### Reported enforcement
|
|
133
139
|
|
|
@@ -146,6 +152,16 @@ throws `SandboxUnavailableError`, and every confined command reports
|
|
|
146
152
|
`read-only file system` is classified as a denial, so the tool layer can offer the
|
|
147
153
|
wider-permission path.
|
|
148
154
|
|
|
155
|
+
A failed probe is **not** remembered: only a success is cached for the process
|
|
156
|
+
lifetime, so a `bubblewrap` installed while the app runs is believed on the next
|
|
157
|
+
command. See [`lib/probe-cache.js`](../lib/probe-cache.js).
|
|
158
|
+
|
|
159
|
+
A command that never started because the distro itself is missing, stopped or
|
|
160
|
+
unregistered is reported separately: `wsl.exe` puts a `WSL_E_*` code on stdout with
|
|
161
|
+
an empty stderr, and `WslShellExecutor` turns that into an error naming the distro
|
|
162
|
+
and the remedy instead of a bare non-zero exit. It is deliberately not a sandbox
|
|
163
|
+
code, because a wider permission cannot create a distro.
|
|
164
|
+
|
|
149
165
|
Set `sandbox: false` on either provider to opt out. Commands then run unconfined,
|
|
150
166
|
and `sandboxMode` returns `undefined`.
|
|
151
167
|
|
|
@@ -168,7 +184,8 @@ step with the checkout, and `npm run sync:windows` runs that script.
|
|
|
168
184
|
| Syntax pass | [`test/syntax.mjs`](../test/syntax.mjs) | no |
|
|
169
185
|
| Unit tests | `test/*.test.mjs` | no |
|
|
170
186
|
| Sandbox probe | [`test/probe/sandbox.sh`](../test/probe/sandbox.sh) | no; it applies the profile arguments directly |
|
|
171
|
-
| Harness probes | [`test/probe/run.sh`](../test/probe/run.sh), `terminal.sh`, `sandbox-shell.sh` | yes: Windows, WSL2, a mounted profile, and a linked checkout |
|
|
187
|
+
| Harness probes | [`test/probe/run.sh`](../test/probe/run.sh), `terminal.sh`, `sandbox-shell.sh`, `picker.sh`, `missing-wsl.sh` | yes: Windows, WSL2, a mounted profile, and a linked checkout |
|
|
188
|
+
| Host probes | [`test/probe/mode.sh`](../test/probe/mode.sh) | Windows Node, no harness |
|
|
172
189
|
| Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
|
|
173
190
|
|
|
174
191
|
The unit tests import only Node builtins, so they run on any platform. CI runs them
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -18,7 +18,7 @@ are schema defaults, listed because they are the ones worth knowing.
|
|
|
18
18
|
| | `cwd` | `''` | default working directory; empty means the distro user's home |
|
|
19
19
|
| | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000` *(shipped)* | per-call time limit, and the ceiling a call may ask for |
|
|
20
20
|
| `wsl-fs` | `distro` | `''` *(shipped)* | as above |
|
|
21
|
-
| | `restrictToDistro` | `true` *(shipped)* | refuse
|
|
21
|
+
| | `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 |
|
|
22
22
|
| | `sandbox` | `true` | check `writeText` and `editText` against the policy |
|
|
23
23
|
| | `resolveSymlinks` | `true` | follow Linux symlinks that the share cannot traverse, such as `/etc/os-release` and `/bin` |
|
|
24
24
|
| | `cwd` | `''` | base directory for relative paths; empty means the distro user's home |
|
|
@@ -31,7 +31,11 @@ are schema defaults, listed because they are the ones worth knowing.
|
|
|
31
31
|
|
|
32
32
|
You can override the other keys in the same way; they keep their defaults. Those are
|
|
33
33
|
`wslPath`, `hostCwd` and `forwardEnv` on the shell and terminal rows,
|
|
34
|
-
`diffBasisMaxBytes` on `wsl-fs`, and `distroCacheMs` on the picker.
|
|
34
|
+
`diffBasisMaxBytes` on `wsl-fs`, and `distroCacheMs` on the picker. The shell row
|
|
35
|
+
also inherits the shipped output budgets — `maxOutputBytes` (64000 bytes per
|
|
36
|
+
stream before the rest spills to a temporary file), `maxSpillBytes` (67108864) and
|
|
37
|
+
`graceMs` (3000, the SIGTERM-to-SIGKILL grace) — from `dsh-bash-local`, which
|
|
38
|
+
documents them in full.
|
|
35
39
|
|
|
36
40
|
## Which distro is used
|
|
37
41
|
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -6,7 +6,18 @@ boundary; one that is not is a bug report waiting to happen.
|
|
|
6
6
|
- **The sandbox does not govern WSL interop.** A confined command can still run a
|
|
7
7
|
Windows program, which escapes the Linux-side boundary. The plugin reports this as
|
|
8
8
|
`enforcement: partial`. See [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement).
|
|
9
|
-
- **`bubblewrap` must be installed**, or both providers fail immediately.
|
|
9
|
+
- **`bubblewrap` must be installed**, or both providers fail immediately. The
|
|
10
|
+
usability probe re-runs after a failure, so installing bubblewrap while the app
|
|
11
|
+
runs is believed on the next command; only a success is cached.
|
|
12
|
+
- **A confined command's `/tmp` is not the write tool's `/tmp`.** `workspace-write`
|
|
13
|
+
mounts a fresh tmpfs at `/tmp` for the command — that is what makes it ephemeral —
|
|
14
|
+
while the file tools' fence grants the distro's *real* `/tmp`. Both are writable, so
|
|
15
|
+
neither side is refused, but a file the write tool puts in `/tmp` is invisible to
|
|
16
|
+
`bash` and vice versa. Hand a file between them through the session workspace.
|
|
17
|
+
- A distro that is stopped or unregistered while the app runs leaves the UNC share
|
|
18
|
+
unreachable: the shell tools report `wsl.exe`'s `WSL_E_*` code with the reason, but
|
|
19
|
+
the file tools can only report a file-level error, because the share itself is
|
|
20
|
+
gone.
|
|
10
21
|
- `workspace-write` binds the workspace root as writable, and bubblewrap refuses a
|
|
11
22
|
bind whose source directory does not exist. If a session's workspace directory is
|
|
12
23
|
deleted, commands fail with a runner error; the plugin does not recreate the
|
package/docs/README.zh.md
CHANGED
|
@@ -54,7 +54,7 @@ dsh --profile wsl # 4. 启动
|
|
|
54
54
|
| `wsl-shell` | `distro` | `''` | 发行版名;空表示用 WSL 的默认发行版 |
|
|
55
55
|
| | `sandbox` | `true` | 用 `bubblewrap` 约束命令;`false` 表示不用沙箱 |
|
|
56
56
|
| `wsl-fs` | `distro` | `''` | 同上 |
|
|
57
|
-
| | `restrictToDistro` | `true` |
|
|
57
|
+
| | `restrictToDistro` | `true` | 拒绝指向**其它发行版**共享的路径;`/mnt/c` 属于本发行版内部,不受影响。拒绝码为 `FS_OUTSIDE_DISTRO`,它不是沙箱拒绝,放宽权限也无法解除 |
|
|
58
58
|
| | `sandbox` | `true` | 写入时按策略检查 `writeText` 和 `editText` |
|
|
59
59
|
| `directory-picker-wsl` | `includeHostHome` | `true` | 同时列出 Windows 家目录 |
|
|
60
60
|
| `subprocess-wsl` | `distro` | `''` | GUI 终端开在哪个发行版 |
|
|
@@ -67,7 +67,7 @@ dsh --profile wsl # 4. 启动
|
|
|
67
67
|
|
|
68
68
|
| 模式 | 发行版里的命令可以做什么 |
|
|
69
69
|
|---|---|
|
|
70
|
-
| `read-only` |
|
|
70
|
+
| `read-only` | 整个发行版只读;`/dev` 是新建的可写挂载(`/dev/null` 与 `/dev/shm` 可用),其余不可写 |
|
|
71
71
|
| `workspace-write` | 在上一行基础上,把会话工作区绑定为可写,并把 `/tmp` 挂成临时目录 |
|
|
72
72
|
| `danger-full-access` | 不加沙箱;用于批准后的放宽权限请求 |
|
|
73
73
|
|
|
@@ -100,6 +100,9 @@ npm run probe:sandbox # 在发行版里实测 bubblewrap 能约束什么
|
|
|
100
100
|
npm run probe # 文件系统探针,需要真实发行版(仅 Windows + WSL)
|
|
101
101
|
npm run probe:sandbox-shell # 启动真实 harness,驱动受限执行器
|
|
102
102
|
npm run probe:terminal # 通过终端 provider 打开一个 PTY
|
|
103
|
+
npm run probe:missing-wsl # 启动一个 wslPath 无法启动的 profile(仅 Windows + WSL)
|
|
104
|
+
npm run probe:picker # 列出选择器的根级、拒绝路径与上限(仅 Windows + WSL)
|
|
105
|
+
npm run probe:mode # 哪些 POSIX 权限事实能穿过共享层(仅需 Windows node)
|
|
103
106
|
```
|
|
104
107
|
|
|
105
108
|
这个包没有依赖,被测模块只 import Node 内置模块,所以不需要先安装任何东西。CI 在 Linux 和 Windows 上用 Node 20、22、24 运行 `npm test`,并在 Node 24 上运行 `npm run test:coverage`。
|
package/lib/auto-preset.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { appendFileSync } from "node:fs";
|
|
34
34
|
import z from "@deepseek-ai/schemastery";
|
|
35
35
|
import { isWslUnc } from "./paths.js";
|
|
36
|
+
import { shouldAdoptWslPreset } from "./preset-choice.js";
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Set `DSH_WSL_TRACE` to a file path to record every decision this plugin makes.
|
|
@@ -106,14 +107,13 @@ export async function apply(ctx, config = {}) {
|
|
|
106
107
|
// registry default (i.e. the operator did not ask for one). `undefined` means
|
|
107
108
|
// the creating caller mounted nothing at all — the headless runner does this
|
|
108
109
|
// — which is likewise "no explicit choice".
|
|
109
|
-
if (!isWslUnc(cwd)) return;
|
|
110
110
|
let fallback;
|
|
111
111
|
try {
|
|
112
112
|
fallback = ctx.agentPresets.defaultId;
|
|
113
113
|
} catch {
|
|
114
114
|
fallback = void 0;
|
|
115
115
|
}
|
|
116
|
-
if (
|
|
116
|
+
if (!shouldAdoptWslPreset({ cwd, current, fallback })) return;
|
|
117
117
|
trace(` -> acting: fallback=${String(fallback)} isWsl=${isWslUnc(cwd)}`);
|
|
118
118
|
|
|
119
119
|
// Deferred on purpose — see the module comment.
|
package/lib/index.js
CHANGED
|
@@ -46,7 +46,7 @@ import { isRelativeWorldPath, isWorldPathUnder, isWslUnc, isUnderDistro, posixTo
|
|
|
46
46
|
import { WslSandbox } from "./sandbox.js";
|
|
47
47
|
// `runCapture` and distro discovery live in ./wsl.js so the directory picker
|
|
48
48
|
// can use them without pulling this file's executor and filesystem peers in.
|
|
49
|
-
import { canonicalLinuxPath, copyModeInDistro, defaultDistro, defaultShell, linuxHomePath, shellArgs } from "./wsl.js";
|
|
49
|
+
import { canonicalLinuxPath, copyModeInDistro, defaultDistro, defaultShell, linuxHomePath, shellArgs, wslErrorCode } from "./wsl.js";
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Every model shell call gets a rebuilt managed `DSH_*` namespace (see
|
|
@@ -319,6 +319,11 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
319
319
|
cwd: this.hostCwd(),
|
|
320
320
|
env: {
|
|
321
321
|
...base.env,
|
|
322
|
+
// wsl.exe writes its own diagnostics as UTF-16LE unless this is set, and a
|
|
323
|
+
// UTF-16 message read as UTF-8 is mojibake — including the `WSL_E_*` codes
|
|
324
|
+
// that say why a command could not start. The distro-missing check below
|
|
325
|
+
// reads those codes, so the encoding has to be pinned here.
|
|
326
|
+
WSL_UTF8: "1",
|
|
322
327
|
// WSL imports only the names listed in WSLENV. The managed DSH_* facts are
|
|
323
328
|
// included by prefix so every contributed variable rides along. PATH is deliberately
|
|
324
329
|
// never listed: putting it here would replace the distro's PATH with
|
|
@@ -336,9 +341,14 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
336
341
|
*
|
|
337
342
|
* The seam requires this step to be synchronous — the tool layer calls
|
|
338
343
|
* `ctx.shell.execute(ctx.shell.resolve(…))` with no `await` between them — so
|
|
339
|
-
* neither the home nor a confinement decision can be made here.
|
|
340
|
-
*
|
|
341
|
-
*
|
|
344
|
+
* neither the home nor a confinement decision can be made here. Two request
|
|
345
|
+
* shapes default to the home when no `cwd` is configured: one that names no
|
|
346
|
+
* `workdir` at all, and one that names it only RELATIVELY — the parent
|
|
347
|
+
* resolves the first against `process.cwd()` (a Windows directory) and has
|
|
348
|
+
* nothing of its own to join the second onto. Both are stamped as the empty
|
|
349
|
+
* marker, with a relative tail carried verbatim in `relativeWorkdir`;
|
|
350
|
+
* {@link withDefaultWorkdir} resolves the pair against the home. The policy
|
|
351
|
+
* travels on the spec to {@link execute}, which awaits the wrap.
|
|
342
352
|
*
|
|
343
353
|
* @param request - the caller's request.
|
|
344
354
|
* @returns the fully-resolved spec.
|
|
@@ -346,27 +356,33 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
346
356
|
resolve(request) {
|
|
347
357
|
const spec = super.resolve(request);
|
|
348
358
|
const sandboxPolicy = this.sandbox === undefined ? undefined : (request?.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve());
|
|
349
|
-
const
|
|
359
|
+
const workdir = typeof request?.workdir === "string" ? request.workdir : undefined;
|
|
360
|
+
const needsHome =
|
|
361
|
+
configuredCwd(this.config).length === 0 && (workdir === undefined || isRelativeWorldPath(workdir));
|
|
350
362
|
if (!needsHome && sandboxPolicy === undefined) return spec;
|
|
351
363
|
return {
|
|
352
364
|
...spec,
|
|
353
|
-
...(needsHome ? { workdir: "" } : {}),
|
|
365
|
+
...(needsHome ? { workdir: "", relativeWorkdir: workdir } : {}),
|
|
354
366
|
...(sandboxPolicy === undefined ? {} : { sandboxPolicy }),
|
|
355
367
|
};
|
|
356
368
|
}
|
|
357
369
|
|
|
358
370
|
/**
|
|
359
|
-
* The spec to execute, with
|
|
360
|
-
* home
|
|
361
|
-
*
|
|
362
|
-
*
|
|
371
|
+
* The spec to execute, with the home marker resolved against the distro
|
|
372
|
+
* user's home: `workdir: ""` becomes the home itself, and a relative tail
|
|
373
|
+
* carried by {@link WslShellExecutor.resolve} is joined under it. That is one
|
|
374
|
+
* `sh -c 'printf %s "$HOME"'` per provider instance, cached, and it happens
|
|
375
|
+
* only for a request that defaulted — an absolute workdir never reaches here.
|
|
363
376
|
*
|
|
364
377
|
* @param spec - the resolved spec.
|
|
365
378
|
* @returns the spec to execute.
|
|
366
379
|
*/
|
|
367
380
|
async withDefaultWorkdir(spec) {
|
|
368
|
-
|
|
369
|
-
|
|
381
|
+
const tail = typeof spec?.relativeWorkdir === "string" ? spec.relativeWorkdir : undefined;
|
|
382
|
+
if (String(spec?.workdir ?? "").trim().length > 0 && tail === undefined) return spec;
|
|
383
|
+
const home = await distroHome(this);
|
|
384
|
+
if (tail === undefined || tail.length === 0) return { ...spec, workdir: home };
|
|
385
|
+
return { ...spec, workdir: toLinuxPath(tail, { distro: await this.distro(), cwd: home }) };
|
|
370
386
|
}
|
|
371
387
|
|
|
372
388
|
/**
|
|
@@ -426,13 +442,50 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
426
442
|
* @param spec - resolved execution settings.
|
|
427
443
|
* @returns the live shell process handle.
|
|
428
444
|
*/
|
|
445
|
+
/**
|
|
446
|
+
* Reject a settled command that failed because the distro itself is missing,
|
|
447
|
+
* stopped or unregistered.
|
|
448
|
+
*
|
|
449
|
+
* `wsl.exe` exits non-zero for that with an EMPTY stderr and its diagnostic on
|
|
450
|
+
* stdout, so nothing else here can see it: the denial signatures read stderr, and
|
|
451
|
+
* the result would otherwise reach the model as a bare non-zero code with no
|
|
452
|
+
* cause. The message names the remedy and says plainly that a wider permission
|
|
453
|
+
* cannot help — a sandbox flag does not create a distro.
|
|
454
|
+
*
|
|
455
|
+
* @param result - the settled shell result.
|
|
456
|
+
* @param distro - the distro the command asked for.
|
|
457
|
+
* @throws Error carrying the `WSL_E_*` code and the remedy.
|
|
458
|
+
*/
|
|
459
|
+
throwIfDistroUnavailable(result, distro) {
|
|
460
|
+
if (result?.exitCode === 0) return;
|
|
461
|
+
const code = wslErrorCode(result?.stdout?.text, result?.stderr?.text);
|
|
462
|
+
if (code === undefined) return;
|
|
463
|
+
throw new Error(
|
|
464
|
+
`wsl.exe reported ${code} for distro "${distro}": it is not installed, or not running. ` +
|
|
465
|
+
`Start it with \`wsl.exe -d ${distro}\`, check \`wsl.exe -l -v\`, or point \`distro\` at another one. ` +
|
|
466
|
+
`This is not a sandbox denial, so running with wider permissions will not help.`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
429
470
|
async execute(spec) {
|
|
430
471
|
const distro = await this.distro();
|
|
431
472
|
const shell = await this.shell();
|
|
432
473
|
const resolved = await this.withDefaultWorkdir(spec);
|
|
433
474
|
const policy = resolved.sandboxPolicy;
|
|
434
475
|
if (this.sandbox === undefined || policy === undefined || policy.mode === "danger-full-access") {
|
|
435
|
-
|
|
476
|
+
// `executeArgv` resolves to the execution handle, so it has to be awaited
|
|
477
|
+
// before it can be decorated — the confined branch below does the same.
|
|
478
|
+
const execution = await this.executeArgv(resolved, () => this.argv(resolved, distro, shell));
|
|
479
|
+
return WslShellExecutor.decorateResult(
|
|
480
|
+
execution,
|
|
481
|
+
(result) => {
|
|
482
|
+
this.throwIfDistroUnavailable(result, distro);
|
|
483
|
+
return result;
|
|
484
|
+
},
|
|
485
|
+
(error) => {
|
|
486
|
+
throw error;
|
|
487
|
+
},
|
|
488
|
+
);
|
|
436
489
|
}
|
|
437
490
|
const mode = policy.mode;
|
|
438
491
|
let confined;
|
|
@@ -458,6 +511,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
458
511
|
},
|
|
459
512
|
);
|
|
460
513
|
return WslShellExecutor.decorateResult(execution, (result) => {
|
|
514
|
+
this.throwIfDistroUnavailable(result, distro);
|
|
461
515
|
if (confined === undefined) return { ...result, sandbox: { mode, denied: false } };
|
|
462
516
|
const { enforcement, denialSignatures, runnerFailureRules } = confined;
|
|
463
517
|
// Runner failure outranks denial because the command did not run at all.
|
|
@@ -646,7 +700,14 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
646
700
|
}
|
|
647
701
|
const world = toWorldPath(path, { distro, cwd: base });
|
|
648
702
|
if (this.config.restrictToDistro && isWslUnc(world) && !isUnderDistro(world, distro)) {
|
|
649
|
-
|
|
703
|
+
// Deliberately NOT `FS_SANDBOX_DENIED`. The file-tool layer turns that code
|
|
704
|
+
// into a denial marker plus an escalation offer, and no wider permission can
|
|
705
|
+
// lift this fence: it is a configuration choice, not a sandbox decision.
|
|
706
|
+
throw new FsError(
|
|
707
|
+
`"${path}" names a path in another WSL distro, and this profile pins its filesystem to "${distro}" ` +
|
|
708
|
+
`(restrictToDistro: true). Open a session in that distro, or set restrictToDistro: false.`,
|
|
709
|
+
"FS_OUTSIDE_DISTRO",
|
|
710
|
+
);
|
|
650
711
|
}
|
|
651
712
|
return world;
|
|
652
713
|
}
|
package/lib/paths.js
CHANGED
|
@@ -46,16 +46,17 @@ export function isPosixAbsolute(value) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Whether a path
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* Whether a path names a location only relative to a base. The fs provider
|
|
50
|
+
* consults its default workdir exactly for this input; every absolute form —
|
|
51
|
+
* POSIX, drive, WSL UNC, or any other UNC share — is a location in its own
|
|
52
|
+
* right and short-circuits before a base is ever needed.
|
|
52
53
|
* @param value - candidate path.
|
|
53
54
|
* @returns true when the path names no absolute location.
|
|
54
55
|
*/
|
|
55
56
|
export function isRelativeWorldPath(value) {
|
|
56
57
|
const text = String(value ?? "");
|
|
57
58
|
if (text.length === 0) return false;
|
|
58
|
-
return !isWslUnc(text) && !isPosixAbsolute(text) && !DRIVE_RE.test(text);
|
|
59
|
+
return !isWslUnc(text) && !isPosixAbsolute(text) && !DRIVE_RE.test(text) && !text.startsWith("\\");
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/** Drop trailing separators, keeping a bare root (`C:\`, `\\wsl.localhost\x\`) meaningful. */
|
|
@@ -215,6 +216,10 @@ export function toWorldPath(value, options) {
|
|
|
215
216
|
if (isWslUnc(text)) return toWindowsSlashes(text);
|
|
216
217
|
if (isPosixAbsolute(text)) return posixToUnc(options?.distro ?? "Ubuntu", text);
|
|
217
218
|
if (DRIVE_RE.test(text)) return toWindowsSlashes(text);
|
|
219
|
+
// Any other UNC share is already a world path in its own right — the host
|
|
220
|
+
// can open it directly, and joining it onto a base would name a file that
|
|
221
|
+
// exists nowhere.
|
|
222
|
+
if (text.startsWith("\\")) return toWindowsSlashes(text);
|
|
218
223
|
const base = options?.cwd ?? process.cwd();
|
|
219
224
|
const baseWorld = toWorldPath(base, { distro: options?.distro });
|
|
220
225
|
return `${baseWorld.replace(/[\\/]+$/, "")}\\${toWindowsSlashes(text).replace(/^\.\\/, "")}`;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a newly created session should adopt the WSL preset.
|
|
3
|
+
*
|
|
4
|
+
* The rule is deliberately narrow, and it is the whole of "a session whose workspace
|
|
5
|
+
* is inside a distro gets the distro environment automatically":
|
|
6
|
+
*
|
|
7
|
+
* - the workspace must be a WSL UNC path, so a Windows folder keeps its own
|
|
8
|
+
* environment;
|
|
9
|
+
* - the session must not already carry an explicit choice. `undefined` counts as
|
|
10
|
+
* "no choice", because a caller that mounts nothing at all (the headless runner)
|
|
11
|
+
* is not asking for the host environment either; anything else must equal the
|
|
12
|
+
* registry's default id, which is what "the operator did not pick one" looks like
|
|
13
|
+
* after composition.
|
|
14
|
+
*
|
|
15
|
+
* Pure, so the decision is unit-testable without a boot — the frame timing that
|
|
16
|
+
* applies it still needs the harness.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ cwd?: string, current?: string, fallback?: string }} input - the new
|
|
19
|
+
* session's workspace, the preset it currently has, and the registry default.
|
|
20
|
+
* @returns true when the WSL preset should be selected for this session.
|
|
21
|
+
*/
|
|
22
|
+
import { isWslUnc } from "./paths.js";
|
|
23
|
+
|
|
24
|
+
export function shouldAdoptWslPreset({ cwd, current, fallback } = {}) {
|
|
25
|
+
if (!isWslUnc(cwd)) return false;
|
|
26
|
+
if (current === void 0) return true;
|
|
27
|
+
return current === fallback;
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The caching policy for a functional probe: which verdicts may be remembered.
|
|
3
|
+
*
|
|
4
|
+
* A SUCCESS is durable — if `bwrap` created a profile a minute ago it will create
|
|
5
|
+
* one now. A FAILURE is not: it describes the machine as it is at that moment, and
|
|
6
|
+
* the common case is a package that is not installed *yet*. Remembering a failure
|
|
7
|
+
* would make the documented remedy (`sudo apt install bubblewrap`) silently
|
|
8
|
+
* ineffective until the app restarts, while the error message tells the user to
|
|
9
|
+
* install exactly that. So a failure is dropped and the next call probes again.
|
|
10
|
+
*
|
|
11
|
+
* Probes are I/O, and concurrent first calls would otherwise each pay for one.
|
|
12
|
+
* Storing the in-flight promise makes them share a single probe.
|
|
13
|
+
*
|
|
14
|
+
* Pure by construction — no Node API, no clock — so the policy is unit-testable
|
|
15
|
+
* without the peers `lib/sandbox.js` needs.
|
|
16
|
+
*
|
|
17
|
+
* @param {() => Promise<boolean>|boolean} probe - the probe to run on a miss. A
|
|
18
|
+
* throw is a failed probe, never a propagated error.
|
|
19
|
+
* @returns {{ run: (key: string, probe: () => Promise<boolean>|boolean) => Promise<boolean>, size: number }}
|
|
20
|
+
*/
|
|
21
|
+
export function createProbeCache() {
|
|
22
|
+
/** @type {Map<string, true|Promise<boolean>>} */
|
|
23
|
+
const verdicts = new Map();
|
|
24
|
+
return {
|
|
25
|
+
async run(key, probe) {
|
|
26
|
+
const cached = verdicts.get(key);
|
|
27
|
+
if (cached !== undefined) return cached;
|
|
28
|
+
|
|
29
|
+
const pending = (async () => {
|
|
30
|
+
try {
|
|
31
|
+
return await probe();
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
verdicts.set(key, pending);
|
|
37
|
+
|
|
38
|
+
const verdict = await pending;
|
|
39
|
+
if (verdict) verdicts.set(key, true);
|
|
40
|
+
else verdicts.delete(key);
|
|
41
|
+
return verdict;
|
|
42
|
+
},
|
|
43
|
+
/** Remembered successes, for tests and diagnostics. */
|
|
44
|
+
get size() {
|
|
45
|
+
return verdicts.size;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
package/lib/sandbox.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { SandboxUnavailableError } from "@deepseek-ai/dsh-sandbox";
|
|
32
|
+
import { createProbeCache } from "./probe-cache.js";
|
|
32
33
|
import { toLinuxPath } from "./paths.js";
|
|
33
34
|
import { runCapture } from "./wsl.js";
|
|
34
35
|
|
|
@@ -59,8 +60,12 @@ const RUNNER_FAILURE_RULES = [{ fatalSignatures: ["bwrap: "] }];
|
|
|
59
60
|
*/
|
|
60
61
|
const ENFORCEMENT = "partial";
|
|
61
62
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Probe verdicts, keyed by `wslPath\0distro`. The policy — a success is durable, a
|
|
65
|
+
* failure is re-probed, concurrent probes are shared — lives in `probe-cache.js`,
|
|
66
|
+
* where it is unit-testable without the peers this module needs.
|
|
67
|
+
*/
|
|
68
|
+
const probeVerdicts = createProbeCache();
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
71
|
* Build the bwrap profile arguments for one file-effect policy, exactly as
|
|
@@ -101,12 +106,15 @@ export class WslSandbox {
|
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
/**
|
|
104
|
-
* Whether `bwrap` can create a profile inside this distro
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
109
|
+
* Whether `bwrap` can create a profile inside this distro. The probe mirrors
|
|
110
|
+
* upstream's: apply the real read-only profile around `true`, and treat a zero
|
|
111
|
+
* exit as the kernel having accepted and enforced it. A missing `bwrap` fails the
|
|
112
|
+
* spawn and probes unusable, which is what makes the provider fail closed rather
|
|
113
|
+
* than silently run unconfined.
|
|
114
|
+
*
|
|
115
|
+
* A success is remembered for the process lifetime; a failure is not, so a
|
|
116
|
+
* `bubblewrap` installed while the app is running is believed on the next
|
|
117
|
+
* command.
|
|
110
118
|
*
|
|
111
119
|
* @param distro - the distro to probe.
|
|
112
120
|
* @param signal - optional cancellation.
|
|
@@ -114,18 +122,11 @@ export class WslSandbox {
|
|
|
114
122
|
*/
|
|
115
123
|
async usable(distro, signal) {
|
|
116
124
|
const key = `${this.wslPath}\u0000${distro}`;
|
|
117
|
-
|
|
118
|
-
if (cached !== undefined) return cached;
|
|
119
|
-
let verdict = false;
|
|
120
|
-
try {
|
|
125
|
+
return probeVerdicts.run(key, async () => {
|
|
121
126
|
const profile = bwrapProfileArgs({ mode: "read-only", workspaceRoot: "/" });
|
|
122
127
|
await runCapture([this.wslPath, "-d", distro, "--exec", "bwrap", ...profile, "--", "true"], signal);
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
verdict = false;
|
|
126
|
-
}
|
|
127
|
-
probeVerdicts.set(key, verdict);
|
|
128
|
-
return verdict;
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
/**
|
package/lib/wsl.js
CHANGED
|
@@ -40,10 +40,15 @@ export function runCapture(argv, signal) {
|
|
|
40
40
|
// is unhelpful, so name the cause when it is recognisable.
|
|
41
41
|
const detail = `${stdout ?? ""}${stderr ?? ""}`.replace(/\u0000/g, "").trim();
|
|
42
42
|
const denied = /E_ACCESSDENIED/i.test(detail) || error.code === "EACCES";
|
|
43
|
+
const missing = error.code === "ENOENT";
|
|
43
44
|
error.message = `${file} failed: ${detail || error.message}${
|
|
44
45
|
denied
|
|
45
46
|
? " (WSL is unreachable from a sandboxed process: DSH's Windows ACL sandbox runs commands under a restricted low-integrity token. Compose the non-sandboxing providers.)"
|
|
46
47
|
: ""
|
|
48
|
+
}${
|
|
49
|
+
missing
|
|
50
|
+
? ` (the executable "${file}" could not be started: check \`wslPath\` in this row, and that WSL is installed)`
|
|
51
|
+
: ""
|
|
47
52
|
}`;
|
|
48
53
|
reject(error);
|
|
49
54
|
return;
|
|
@@ -285,6 +290,25 @@ export function wslEnvValue(names) {
|
|
|
285
290
|
* @param options - the `wsl.exe` path and optional cancellation.
|
|
286
291
|
* @returns an absolute path to the shell inside the distro.
|
|
287
292
|
*/
|
|
293
|
+
/**
|
|
294
|
+
* The `WSL_E_*` code inside `wsl.exe`'s own output, when it reported one.
|
|
295
|
+
*
|
|
296
|
+
* wsl.exe writes these diagnostics to STDOUT, and a build may encode them as
|
|
297
|
+
* UTF-16LE — a NUL byte between every ASCII character. Read as UTF-8 that leaves
|
|
298
|
+
* the code spelled with NULs in the middle, so the search strips them first and
|
|
299
|
+
* finds the code under either encoding. Verified against a missing distro:
|
|
300
|
+
* `wsl.exe -d nosuchdistro --exec …` exits 255 with an empty stderr and
|
|
301
|
+
* `Wsl/Service/WSL_E_DISTRO_NOT_FOUND` on stdout.
|
|
302
|
+
*
|
|
303
|
+
* @param stdout - a command's captured stdout.
|
|
304
|
+
* @param stderr - its captured stderr.
|
|
305
|
+
* @returns the code, for example `WSL_E_DISTRO_NOT_FOUND`, or undefined.
|
|
306
|
+
*/
|
|
307
|
+
export function wslErrorCode(stdout, stderr) {
|
|
308
|
+
const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
|
|
309
|
+
return /WSL_E_[A-Z0-9_]+/.exec(text)?.[0];
|
|
310
|
+
}
|
|
311
|
+
|
|
288
312
|
export async function defaultShell(distro, options = {}) {
|
|
289
313
|
const wslPath = options.wslPath ?? DEFAULT_WSL_PATH;
|
|
290
314
|
const ask = async (script) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wsl-env",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Run a DeepSeek Harness session against a WSL distro: a ctx.shell executor that runs commands inside the distro and a ctx.fs backend over its UNC share",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -46,12 +46,15 @@
|
|
|
46
46
|
],
|
|
47
47
|
"scripts": {
|
|
48
48
|
"test": "npm run lint:style && npm run test:syntax && npm run test:unit",
|
|
49
|
-
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=85 --test-coverage-functions=70 test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs",
|
|
49
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=85 --test-coverage-functions=70 test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preset-choice.test.mjs",
|
|
50
50
|
"test:syntax": "node test/syntax.mjs",
|
|
51
|
-
"test:unit": "node --test test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs",
|
|
51
|
+
"test:unit": "node --test test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs",
|
|
52
52
|
"lint:style": "node test/style.mjs",
|
|
53
53
|
"prepublishOnly": "npm test",
|
|
54
54
|
"probe": "bash test/probe/run.sh",
|
|
55
|
+
"probe:missing-wsl": "bash test/probe/missing-wsl.sh",
|
|
56
|
+
"probe:mode": "bash test/probe/mode.sh",
|
|
57
|
+
"probe:picker": "bash test/probe/picker.sh",
|
|
55
58
|
"probe:terminal": "bash test/probe/terminal.sh",
|
|
56
59
|
"probe:sandbox": "bash test/probe/sandbox.sh",
|
|
57
60
|
"probe:sandbox-shell": "bash test/probe/sandbox-shell.sh",
|