dsh-plugin-wsl-env 0.1.5 → 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 CHANGED
@@ -12,6 +12,48 @@ 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
+
15
57
  ## [0.1.5] - 2026-10-02
16
58
 
17
59
  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: 50 unit assertions on a bare checkout, plus 5 in `test/sandbox.test.mjs`
148
- which need the `dsh-sandbox` peer and skip themselves without it; 19
149
- filesystem-probe assertions; 12 shell-probe checks; 10 sandbox expectations plus
150
- the recorded escape; and 3 terminal assertions.
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.
151
155
 
152
156
  ## Commit and pull request conventions
153
157
 
package/README.md CHANGED
@@ -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.
@@ -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
- That avoids a mismatch where `bash` could write `/tmp` but the write tool could
129
- not. The fence is complete for the model's reach: `writeText` and `editText` are
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
@@ -181,7 +184,8 @@ step with the checkout, and `npm run sync:windows` runs that script.
181
184
  | Syntax pass | [`test/syntax.mjs`](../test/syntax.mjs) | no |
182
185
  | Unit tests | `test/*.test.mjs` | no |
183
186
  | 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 |
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 |
185
189
  | Probe plugins | the `*-probe.mjs` and `*-probe.yml` files in `test/probe/` | loaded by the scripts above |
186
190
 
187
191
  The unit tests import only Node builtins, so they run on any platform. CI runs them
@@ -9,6 +9,11 @@ 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
package/docs/README.zh.md CHANGED
@@ -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`。
@@ -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 (current !== fallback && current !== void 0) return;
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.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-wsl-env",
3
- "version": "0.1.5",
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 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",