dsh-plugin-wsl-env 0.1.6 → 0.1.8
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 +55 -0
- package/CONTRIBUTING.md +5 -5
- package/README.md +1 -0
- package/docs/ARCHITECTURE.md +24 -0
- package/docs/LIMITATIONS.md +5 -3
- package/docs/README.zh.md +1 -0
- package/lib/index.js +24 -1
- package/lib/wsl.js +23 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,61 @@ reference below points at that record's numbering.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.1.8] - 2026-10-02
|
|
16
|
+
|
|
17
|
+
A probe assertion for the one item the closure review had to record as unverifiable.
|
|
18
|
+
No runtime change.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `npm run probe:sandbox-shell` asserts that a command past its deadline is reported as
|
|
23
|
+
timed out **and leaves no process behind in the distro** (15 checks, up from 13). The
|
|
24
|
+
escalated path is used on purpose: a confined command also dies with its `bwrap`
|
|
25
|
+
parent, so only the unconfined path shows whether the subprocess service's kill
|
|
26
|
+
actually reaches the distro. It does — measured through `ctx.shell` here, and
|
|
27
|
+
directly by killing a `wsl.exe` relay and watching the distro-side `sleep` disappear
|
|
28
|
+
with it (1 process before, 0 after).
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
- The review's "unverified: orphaned processes after a timeout" item is closed. The
|
|
33
|
+
earlier attempt could not see the process at all, and said so: the agent's own shell
|
|
34
|
+
ran in a separate PID namespace, where `ps` cannot see the distro's processes.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## [0.1.7] - 2026-10-02
|
|
38
|
+
|
|
39
|
+
One correctness fix and the error-code reference the closure review asked for.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- A command whose working directory could not be entered was reported as a success.
|
|
44
|
+
`wsl.exe --cd <missing>` does not fail: it writes a relay error to stderr, runs the
|
|
45
|
+
command in `/`, and exits 0 — measured directly, and visible in the shell probe's own
|
|
46
|
+
transcript. A build or a write could therefore act on the wrong tree while every
|
|
47
|
+
signal said it worked. `WslShellExecutor` now matches that relay line and fails with
|
|
48
|
+
the directory and the fallback. Both markers are required (the relay prefix and
|
|
49
|
+
`CreateProcessCommon`), so a command printing its own `chdir(...)` message cannot be
|
|
50
|
+
mistaken for it; the matcher also finds the line when it arrives UTF-16 encoded.
|
|
51
|
+
`npm run probe:sandbox-shell` asserts the behaviour end to end, and
|
|
52
|
+
`test/shell.test.mjs` covers the matcher, including a false-positive case.
|
|
53
|
+
- The shell probe's own setup ran into this on the first run: it created the workspace
|
|
54
|
+
root while that not-yet-existing root was its workdir, which had "worked" only because
|
|
55
|
+
`mkdir -p` takes an absolute path. The setup now runs from the distro home, and the
|
|
56
|
+
new assertion pins the failure it used to hide.
|
|
57
|
+
|
|
58
|
+
### Documentation
|
|
59
|
+
|
|
60
|
+
- `docs/ARCHITECTURE.md` gains an **Error codes** table: what raises each `FS_*` code
|
|
61
|
+
and `SANDBOX_UNAVAILABLE`, what each one means, and what clears it — including that
|
|
62
|
+
`FS_OUTSIDE_DISTRO` cannot be lifted by a wider permission. It also documents the two
|
|
63
|
+
failures that come from `wsl.exe` itself: a missing, stopped or unregistered distro (a
|
|
64
|
+
`WSL_E_*` code on stdout, UTF-16 on some builds, with an empty stderr) and an
|
|
65
|
+
unenterable working directory. Both READMEs' troubleshooting tables now point at it.
|
|
66
|
+
- `docs/LIMITATIONS.md` separates the two workdir cases a deleted directory can produce:
|
|
67
|
+
the bind that bubblewrap refuses, and the directory that cannot be entered.
|
|
68
|
+
|
|
69
|
+
|
|
15
70
|
## [0.1.6] - 2026-10-02
|
|
16
71
|
|
|
17
72
|
Three probes that close coverage gaps a closure review named, the documentation of a
|
package/CONTRIBUTING.md
CHANGED
|
@@ -147,11 +147,11 @@ On Windows 11 + WSL2 (Ubuntu 26.04) the following has been verified:
|
|
|
147
147
|
outside the session workspace is refused inside the distro and the following
|
|
148
148
|
wider-permission request succeeds.
|
|
149
149
|
|
|
150
|
-
Counts:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
checks
|
|
154
|
-
|
|
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; 15 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.
|
|
155
155
|
|
|
156
156
|
## Commit and pull request conventions
|
|
157
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
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -165,6 +165,30 @@ code, because a wider permission cannot create a distro.
|
|
|
165
165
|
Set `sandbox: false` on either provider to opt out. Commands then run unconfined,
|
|
166
166
|
and `sandboxMode` returns `undefined`.
|
|
167
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
|
+
|
|
168
192
|
## The runtime mirror
|
|
169
193
|
|
|
170
194
|
The checkout lives inside the distro. A DSH profile can only link a Windows path,
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -19,9 +19,11 @@ boundary; one that is not is a bug report waiting to happen.
|
|
|
19
19
|
the file tools can only report a file-level error, because the share itself is
|
|
20
20
|
gone.
|
|
21
21
|
- `workspace-write` binds the workspace root as writable, and bubblewrap refuses a
|
|
22
|
-
bind whose source directory does not exist
|
|
23
|
-
deleted
|
|
24
|
-
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.
|
|
25
27
|
- New files get the distro's umask default (0644). A `chmod` from the Windows side on
|
|
26
28
|
a share path is silently ignored; overwrites and edits keep the original permission
|
|
27
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
|
|
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.
|
package/lib/wsl.js
CHANGED
|
@@ -194,6 +194,29 @@ const POSIX_LOGIN_SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "ksh93",
|
|
|
194
194
|
* @param login - whether login-shell semantics were requested.
|
|
195
195
|
* @returns the flags to place before the command string.
|
|
196
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
|
+
|
|
197
220
|
export function shellArgs(shellPath, login) {
|
|
198
221
|
const name = String(shellPath).slice(String(shellPath).lastIndexOf("/") + 1).toLowerCase();
|
|
199
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.8",
|
|
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",
|