dsh-plugin-wsl-env 0.1.5 → 0.1.7
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 +75 -0
- package/CONTRIBUTING.md +8 -4
- package/README.md +4 -0
- package/docs/ARCHITECTURE.md +32 -4
- package/docs/LIMITATIONS.md +10 -3
- package/docs/README.zh.md +4 -0
- package/lib/auto-preset.js +2 -2
- package/lib/index.js +24 -1
- package/lib/preset-choice.js +28 -0
- package/lib/wsl.js +28 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,81 @@ reference below points at that record's numbering.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.1.7] - 2026-10-02
|
|
16
|
+
|
|
17
|
+
One correctness fix and the error-code reference the closure review asked for.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A command whose working directory could not be entered was reported as a success.
|
|
22
|
+
`wsl.exe --cd <missing>` does not fail: it writes a relay error to stderr, runs the
|
|
23
|
+
command in `/`, and exits 0 — measured directly, and visible in the shell probe's own
|
|
24
|
+
transcript. A build or a write could therefore act on the wrong tree while every
|
|
25
|
+
signal said it worked. `WslShellExecutor` now matches that relay line and fails with
|
|
26
|
+
the directory and the fallback. Both markers are required (the relay prefix and
|
|
27
|
+
`CreateProcessCommon`), so a command printing its own `chdir(...)` message cannot be
|
|
28
|
+
mistaken for it; the matcher also finds the line when it arrives UTF-16 encoded.
|
|
29
|
+
`npm run probe:sandbox-shell` asserts the behaviour end to end, and
|
|
30
|
+
`test/shell.test.mjs` covers the matcher, including a false-positive case.
|
|
31
|
+
- The shell probe's own setup ran into this on the first run: it created the workspace
|
|
32
|
+
root while that not-yet-existing root was its workdir, which had "worked" only because
|
|
33
|
+
`mkdir -p` takes an absolute path. The setup now runs from the distro home, and the
|
|
34
|
+
new assertion pins the failure it used to hide.
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
|
|
38
|
+
- `docs/ARCHITECTURE.md` gains an **Error codes** table: what raises each `FS_*` code
|
|
39
|
+
and `SANDBOX_UNAVAILABLE`, what each one means, and what clears it — including that
|
|
40
|
+
`FS_OUTSIDE_DISTRO` cannot be lifted by a wider permission. It also documents the two
|
|
41
|
+
failures that come from `wsl.exe` itself: a missing, stopped or unregistered distro (a
|
|
42
|
+
`WSL_E_*` code on stdout, UTF-16 on some builds, with an empty stderr) and an
|
|
43
|
+
unenterable working directory. Both READMEs' troubleshooting tables now point at it.
|
|
44
|
+
- `docs/LIMITATIONS.md` separates the two workdir cases a deleted directory can produce:
|
|
45
|
+
the bind that bubblewrap refuses, and the directory that cannot be entered.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## [0.1.6] - 2026-10-02
|
|
49
|
+
|
|
50
|
+
Three probes that close coverage gaps a closure review named, the documentation of a
|
|
51
|
+
`/tmp` asymmetry, and one clearer error message. No behaviour changed otherwise.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- `npm run probe:picker` drives the directory picker in a throwaway boot: the root
|
|
56
|
+
level's crumbs, the Windows home, every installed distro, the documented
|
|
57
|
+
`directory-unreadable` refusal for a relative path and for a directory that does not
|
|
58
|
+
exist, and `maxEntries` with its `truncated` flag. Nothing ran `lib/picker.js` before
|
|
59
|
+
this — the unit tests import only `lib/listing.js`.
|
|
60
|
+
- `npm run probe:missing-wsl` boots a profile whose `wslPath` cannot start and pins how
|
|
61
|
+
that is reported: the executable is named, `wslPath` is suggested, the failure is not
|
|
62
|
+
dressed up as a sandbox refusal, and no sandbox facts are claimed for a command that
|
|
63
|
+
never ran. The other fail-closed state — `wsl.exe` works but the distro has no
|
|
64
|
+
`bubblewrap` — is asserted in `test/sandbox.test.mjs`, which drives
|
|
65
|
+
`WslSandbox.confine` against a failing probe, because driving that end to end needs a
|
|
66
|
+
distro without `bubblewrap`.
|
|
67
|
+
- `npm run probe:mode` runs `test/probe/mode-probe.mjs`, which measures which POSIX-mode
|
|
68
|
+
facts survive the share. The file was cited by `docs/LIMITATIONS.md` and wired into
|
|
69
|
+
no script at all.
|
|
70
|
+
- `test/preset-choice.test.mjs` covers the session-preset decision, now that the rule
|
|
71
|
+
is the pure `lib/preset-choice.js`. The frame timing that applies the decision still
|
|
72
|
+
needs the harness, and the test says so.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- `runCapture` names the cause when the executable itself cannot start: the message now
|
|
77
|
+
points at `wslPath` and at WSL being installed, the same way it already did for a
|
|
78
|
+
token denied by the Windows sandbox. `npm run probe:missing-wsl` asserts it.
|
|
79
|
+
|
|
80
|
+
### Documentation
|
|
81
|
+
|
|
82
|
+
- `docs/LIMITATIONS.md` states the `/tmp` split: a confined command's `/tmp` is a fresh
|
|
83
|
+
tmpfs while the file tools' fence grants the distro's *real* `/tmp`, so both sides can
|
|
84
|
+
write but neither sees the other's files. Hand a file between them through the session
|
|
85
|
+
workspace. `docs/ARCHITECTURE.md` describes the same distinction where it explains the
|
|
86
|
+
fence, including why the Windows temp directory is deliberately not granted to a
|
|
87
|
+
distro session.
|
|
88
|
+
|
|
89
|
+
|
|
15
90
|
## [0.1.5] - 2026-10-02
|
|
16
91
|
|
|
17
92
|
Three defects from a design-closure review. Two are cases where a documented remedy
|
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
|
|
@@ -144,10 +147,11 @@ On Windows 11 + WSL2 (Ubuntu 26.04) the following has been verified:
|
|
|
144
147
|
outside the session workspace is refused inside the distro and the following
|
|
145
148
|
wider-permission request succeeds.
|
|
146
149
|
|
|
147
|
-
Counts:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
the recorded escape; and 3 terminal
|
|
150
|
+
Counts: 57 unit assertions on a bare checkout, plus 5 in `test/sandbox.test.mjs` which
|
|
151
|
+
need the `dsh-sandbox` peer and skip themselves without it; 19 filesystem-probe
|
|
152
|
+
assertions; 13 shell-probe checks; 9 picker checks; 6 missing-executable checks; 5 mode
|
|
153
|
+
checks and 10 sandbox expectations plus the recorded escape; and 3 terminal
|
|
154
|
+
assertions.
|
|
151
155
|
|
|
152
156
|
## Commit and pull request conventions
|
|
153
157
|
|
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) for the design, and [do
|
|
|
90
90
|
| `link:\\wsl.localhost\...` leaves a broken symlink | pnpm cannot link a UNC path | link a Windows path instead; developing inside the distro needs the runtime mirror, see [Development](#development) |
|
|
91
91
|
| `glob` and `grep` are slow | the Windows-side ripgrep walks the 9p share | expected; narrow the path, or use `bash` to call tools inside the distro |
|
|
92
92
|
| the terminal reports `unknown` activity | shell integration only covers `bash`/`zsh` started directly on a POSIX host | close the tab to release the process; idle reclamation does not run for these terminals |
|
|
93
|
+
| a result names an `FS_*` code | the code says what refused it, and what clears it | see the error-code table in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#error-codes) |
|
|
93
94
|
|
|
94
95
|
## Development
|
|
95
96
|
|
|
@@ -100,6 +101,9 @@ npm run probe:sandbox # measure inside the distro what bubblewrap does an
|
|
|
100
101
|
npm run probe # filesystem probe against a real distro (Windows + WSL only)
|
|
101
102
|
npm run probe:sandbox-shell # boot a real harness and drive the confined executor
|
|
102
103
|
npm run probe:terminal # open a PTY through the terminal provider
|
|
104
|
+
npm run probe:missing-wsl # boot a profile whose wslPath cannot start (Windows + WSL only)
|
|
105
|
+
npm run probe:picker # list the picker's root level, refusals and its cap (Windows + WSL only)
|
|
106
|
+
npm run probe:mode # which POSIX-mode facts survive the share (Windows node only)
|
|
103
107
|
```
|
|
104
108
|
|
|
105
109
|
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
|
@@ -124,9 +124,12 @@ behaviour and the error messages match a Linux host. The function
|
|
|
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
|
-
|
|
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
|
|
130
133
|
the only mutation calls the model-facing file tools make on the filesystem seam,
|
|
131
134
|
so checking those two checks everything the model can mutate. Both providers
|
|
132
135
|
report their mode through `sandboxMode`. The tool layer and the Permissions
|
|
@@ -162,6 +165,30 @@ code, because a wider permission cannot create a distro.
|
|
|
162
165
|
Set `sandbox: false` on either provider to opt out. Commands then run unconfined,
|
|
163
166
|
and `sandboxMode` returns `undefined`.
|
|
164
167
|
|
|
168
|
+
## Error codes
|
|
169
|
+
|
|
170
|
+
The plugin raises `FsError`s with a code the tool layer maps, and `wsl.exe`'s own
|
|
171
|
+
failures are named as well. A model sees the code; a reader asking what refused them
|
|
172
|
+
finds it here.
|
|
173
|
+
|
|
174
|
+
| Code | Raised by | Meaning | What clears it |
|
|
175
|
+
|---|---|---|---|
|
|
176
|
+
| `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` |
|
|
177
|
+
| `FS_SANDBOX_DENIED` | `WslFileSystem.checkedTarget` | the file-effect policy refused the write: `read-only`, or a target outside the writable roots | a wider permission for that one call, or a session workspace that contains the target |
|
|
178
|
+
| `FS_OUTSIDE_DISTRO` | `WslFileSystem.worldPath` | `restrictToDistro` is on and the path names **another distro**'s share — a configuration fence, not a sandbox decision | open a session in that distro, or set `restrictToDistro: false`. A wider permission does **not** lift it |
|
|
179
|
+
| `FS_NOT_OBSERVED` | `WslFileSystem.assertGuard` | a guarded create (`createIfAbsent`) targeted a file that exists and had not been read first | read the file, then overwrite with the version guard or edit it |
|
|
180
|
+
| `FS_STALE_VERSION` | `WslFileSystem.assertGuard` | the version the caller holds no longer matches: the file changed, or is gone | read it again and retry with the fresh version |
|
|
181
|
+
| `FS_IO_ERROR` | `WslFileSystem.watch` | `watch()` is refused, because a 9p share cannot be watched reliably | poll, or watch from inside the distro |
|
|
182
|
+
| `FS_NOT_FOUND` | the host filesystem stack | the path does not exist — the ordinary answer, passed through | — |
|
|
183
|
+
|
|
184
|
+
Two failures come from `wsl.exe` itself rather than from a code of ours, and both used
|
|
185
|
+
to be invisible:
|
|
186
|
+
|
|
187
|
+
| Failure | How it arrives | What the plugin does |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| the distro is missing, stopped or unregistered | a `Wsl/Service/WSL_E_*` code on **stdout**, UTF-16 on some builds, with an empty stderr and exit 255 | `WslShellExecutor` reads the code with the NULs stripped and fails with the distro name and the remedy. Deliberately not a sandbox code: a wider permission cannot create a distro |
|
|
190
|
+
| the working directory cannot be entered | a relay line on stderr, exit **0**, and the command runs in `/` | `WslShellExecutor` fails with the directory and the fallback, instead of reporting a success that acted on the wrong tree |
|
|
191
|
+
|
|
165
192
|
## The runtime mirror
|
|
166
193
|
|
|
167
194
|
The checkout lives inside the distro. A DSH profile can only link a Windows path,
|
|
@@ -181,7 +208,8 @@ step with the checkout, and `npm run sync:windows` runs that script.
|
|
|
181
208
|
| Syntax pass | [`test/syntax.mjs`](../test/syntax.mjs) | no |
|
|
182
209
|
| Unit tests | `test/*.test.mjs` | no |
|
|
183
210
|
| Sandbox probe | [`test/probe/sandbox.sh`](../test/probe/sandbox.sh) | no; it applies the profile arguments directly |
|
|
184
|
-
| 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 |
|
|
211
|
+
| 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 |
|
|
212
|
+
| Host probes | [`test/probe/mode.sh`](../test/probe/mode.sh) | Windows Node, no harness |
|
|
185
213
|
| Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
|
|
186
214
|
|
|
187
215
|
The unit tests import only Node builtins, so they run on any platform. CI runs them
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -9,14 +9,21 @@ boundary; one that is not is a bug report waiting to happen.
|
|
|
9
9
|
- **`bubblewrap` must be installed**, or both providers fail immediately. The
|
|
10
10
|
usability probe re-runs after a failure, so installing bubblewrap while the app
|
|
11
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.
|
|
12
17
|
- A distro that is stopped or unregistered while the app runs leaves the UNC share
|
|
13
18
|
unreachable: the shell tools report `wsl.exe`'s `WSL_E_*` code with the reason, but
|
|
14
19
|
the file tools can only report a file-level error, because the share itself is
|
|
15
20
|
gone.
|
|
16
21
|
- `workspace-write` binds the workspace root as writable, and bubblewrap refuses a
|
|
17
|
-
bind whose source directory does not exist
|
|
18
|
-
deleted
|
|
19
|
-
directory
|
|
22
|
+
bind whose source directory does not exist, so a session whose workspace directory was
|
|
23
|
+
deleted fails with a runner error; the plugin does not recreate it. A workdir that
|
|
24
|
+
does not exist — a configured `cwd` pointing at a deleted directory, for instance — is
|
|
25
|
+
a separate case and is now reported as an error naming that directory, because
|
|
26
|
+
`wsl.exe` would otherwise run the command in `/` and exit 0.
|
|
20
27
|
- New files get the distro's umask default (0644). A `chmod` from the Windows side on
|
|
21
28
|
a share path is silently ignored; overwrites and edits keep the original permission
|
|
22
29
|
bits. Run `chmod +x` inside the distro when you need an executable.
|
package/docs/README.zh.md
CHANGED
|
@@ -90,6 +90,7 @@ dsh --profile wsl # 4. 启动
|
|
|
90
90
|
| 用 `link:\\wsl.localhost\...` 安装后符号链接是坏的 | pnpm 无法链接 UNC 路径 | 改成链接 Windows 路径;在发行版内开发时用运行时镜像,见[开发](#开发) |
|
|
91
91
|
| `glob`/`grep` 很慢 | Windows 侧的 ripgrep 在 9p 共享上遍历 | 属于预期,收窄搜索路径,或改用 `bash` 调用发行版内的工具 |
|
|
92
92
|
| 终端活动显示 `unknown` | 官方 shell 集成只对 POSIX 主机上直接启动的 `bash`/`zsh` 生效 | 关闭标签页以释放进程;空闲回收不会对它触发 |
|
|
93
|
+
| 结果里出现 `FS_*` 码 | 码本身说明了是谁拒绝的、以及怎样解除 | 见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#error-codes) 的错误码表 |
|
|
93
94
|
|
|
94
95
|
## 开发
|
|
95
96
|
|
|
@@ -100,6 +101,9 @@ npm run probe:sandbox # 在发行版里实测 bubblewrap 能约束什么
|
|
|
100
101
|
npm run probe # 文件系统探针,需要真实发行版(仅 Windows + WSL)
|
|
101
102
|
npm run probe:sandbox-shell # 启动真实 harness,驱动受限执行器
|
|
102
103
|
npm run probe:terminal # 通过终端 provider 打开一个 PTY
|
|
104
|
+
npm run probe:missing-wsl # 启动一个 wslPath 无法启动的 profile(仅 Windows + WSL)
|
|
105
|
+
npm run probe:picker # 列出选择器的根级、拒绝路径与上限(仅 Windows + WSL)
|
|
106
|
+
npm run probe:mode # 哪些 POSIX 权限事实能穿过共享层(仅需 Windows node)
|
|
103
107
|
```
|
|
104
108
|
|
|
105
109
|
这个包没有依赖,被测模块只 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, wslErrorCode } from "./wsl.js";
|
|
49
|
+
import { canonicalLinuxPath, copyModeInDistro, defaultDistro, defaultShell, linuxHomePath, shellArgs, workdirFailure, wslErrorCode } from "./wsl.js";
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Every model shell call gets a rebuilt managed `DSH_*` namespace (see
|
|
@@ -467,6 +467,27 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
467
467
|
);
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Reject a command that ran in the wrong directory.
|
|
472
|
+
*
|
|
473
|
+
* `wsl.exe --cd <path>` does not fail on a missing directory: it warns on stderr,
|
|
474
|
+
* runs the command in `/`, and exits 0. Reporting that as success would let a write
|
|
475
|
+
* or a build act on the wrong tree while every signal says it worked, so it becomes
|
|
476
|
+
* an error naming the directory that could not be entered.
|
|
477
|
+
*
|
|
478
|
+
* @param result - the settled shell result.
|
|
479
|
+
* @param workdir - the Linux directory the request asked for.
|
|
480
|
+
* @throws Error naming the directory and the fallback that happened.
|
|
481
|
+
*/
|
|
482
|
+
throwIfWorkdirMissing(result, workdir) {
|
|
483
|
+
const missing = workdirFailure(result?.stdout?.text, result?.stderr?.text);
|
|
484
|
+
if (missing === undefined) return;
|
|
485
|
+
throw new Error(
|
|
486
|
+
`wsl.exe could not enter the working directory "${missing}" inside the distro, so the command ran from "/" instead. ` +
|
|
487
|
+
`Create it, or point \`cwd\` (or the request's \`workdir\`) at a directory that exists.`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
470
491
|
async execute(spec) {
|
|
471
492
|
const distro = await this.distro();
|
|
472
493
|
const shell = await this.shell();
|
|
@@ -480,6 +501,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
480
501
|
execution,
|
|
481
502
|
(result) => {
|
|
482
503
|
this.throwIfDistroUnavailable(result, distro);
|
|
504
|
+
this.throwIfWorkdirMissing(result, resolved.workdir);
|
|
483
505
|
return result;
|
|
484
506
|
},
|
|
485
507
|
(error) => {
|
|
@@ -512,6 +534,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
512
534
|
);
|
|
513
535
|
return WslShellExecutor.decorateResult(execution, (result) => {
|
|
514
536
|
this.throwIfDistroUnavailable(result, distro);
|
|
537
|
+
this.throwIfWorkdirMissing(result, resolved.workdir);
|
|
515
538
|
if (confined === undefined) return { ...result, sandbox: { mode, denied: false } };
|
|
516
539
|
const { enforcement, denialSignatures, runnerFailureRules } = confined;
|
|
517
540
|
// Runner failure outranks denial because the command did not run at all.
|
|
@@ -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
|
+
}
|
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;
|
|
@@ -189,6 +194,29 @@ const POSIX_LOGIN_SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "ksh93",
|
|
|
189
194
|
* @param login - whether login-shell semantics were requested.
|
|
190
195
|
* @returns the flags to place before the command string.
|
|
191
196
|
*/
|
|
197
|
+
/**
|
|
198
|
+
* The working directory `wsl.exe` could not enter, when its relay said so.
|
|
199
|
+
*
|
|
200
|
+
* `wsl.exe --cd <path>` does not fail when `<path>` is missing. It writes a relay
|
|
201
|
+
* error to STDERR, runs the command in `/` anyway, and exits 0 — so a caller would see
|
|
202
|
+
* a successful command that ran somewhere else. The shape is matched here for the
|
|
203
|
+
* callers to turn into a failure.
|
|
204
|
+
*
|
|
205
|
+
* Both markers are required: the relay prefix and `CreateProcessCommon`. A command is
|
|
206
|
+
* free to print "chdir(...) failed" itself, and a false positive would turn a correct
|
|
207
|
+
* run into an error. Verified against `wsl.exe -d <distro> --cd /missing --exec sh`:
|
|
208
|
+
* exit 0, `hi` on stdout, this shape on stderr.
|
|
209
|
+
*
|
|
210
|
+
* @param stdout - a command's captured stdout.
|
|
211
|
+
* @param stderr - its captured stderr.
|
|
212
|
+
* @returns the directory that could not be entered, or undefined.
|
|
213
|
+
*/
|
|
214
|
+
export function workdirFailure(stdout, stderr) {
|
|
215
|
+
const text = `${stdout ?? ""}\n${stderr ?? ""}`.replace(/\0/g, "");
|
|
216
|
+
const match = /<3>WSL[^\n]*CreateProcessCommon[^\n]*chdir\(([^)]*)\) failed/.exec(text);
|
|
217
|
+
return match?.[1];
|
|
218
|
+
}
|
|
219
|
+
|
|
192
220
|
export function shellArgs(shellPath, login) {
|
|
193
221
|
const name = String(shellPath).slice(String(shellPath).lastIndexOf("/") + 1).toLowerCase();
|
|
194
222
|
if (!POSIX_LOGIN_SHELLS.has(name)) return ["-c"];
|
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.7",
|
|
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 test/probe-cache.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 test/probe-cache.test.mjs test/sandbox.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",
|