dsh-plugin-wsl-env 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,68 @@ were part of the record.
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.14.0] - 2026-10-27
20
+
21
+ This release fixes the first-run trap the compatibility-matrix run found on a
22
+ freshly imported distro, and ships the measured support table the run produced.
23
+
24
+ ### Fixed
25
+
26
+ - **WSL-305: the confined resident now creates the workspace root before its
27
+ first spawn.** The bwrap profile binds the workspace root and bwrap refuses
28
+ a missing bind source, so on a freshly imported distro the first confined
29
+ write failed with a bare `FS_IO_ERROR: agent exited during handshake
30
+ (code=1)` and a misleading remedy. `agent-confined.js` gained a `prepare`
31
+ hook (workspace-write only — the only mode that binds the root) that runs
32
+ `mkdir -p` on the Linux-spelled root before bwrap runs, idempotently and
33
+ ignoring its own outcome: bwrap stays the authority, and a refusal still
34
+ reaches the caller. That refusal now carries evidence: the handshake
35
+ failure's message includes a bounded tail of the resident's stderr (the
36
+ half-day of diagnosis this cost came from it being swallowed). Three new
37
+ checks cover the prepare hook, its argv, and the stderr ride-on; a
38
+ fresh-root leg in `substrate.sh` exercises the absent-root spawn end to
39
+ end on a real distro.
40
+
41
+ ### Changed
42
+
43
+ - **Debian is verified end to end, and the run found a first-run trap.**
44
+ The four harness-level probes pass against a `wslfs-debian` profile on a
45
+ freshly imported Debian 13.5, completing the full stack on top of the
46
+ distro-internal suite (`docs/COMPATIBILITY.md`). Getting there surfaced one
47
+ real finding: the confined resident refuses to start when the workspace
48
+ root does not exist yet — the bwrap profile binds the root, bwrap requires
49
+ the bind source to exist, and a fresh distro has nothing to bind. Creating
50
+ the root first turns all ten failing steps green; prior Ubuntu runs had it
51
+ left over from earlier probes. The finding is documented in the matrix page
52
+ with the suggested spawn-preparation fix.
53
+
54
+ - **The compatibility matrix moves from prose to a measured page.**
55
+ `docs/COMPATIBILITY.md` records the WSL-202 evidence: six real WSL2 distros
56
+ (Ubuntu, Debian 13.5, Fedora 44, openSUSE Tumbleweed, Arch, Alpine 3.22)
57
+ executed the distro-internal probe suite on one machine on 2026-10-26, and
58
+ the page explains the two observed differences instead of hiding them — the
59
+ `session-sandbox` background-survival leg races WSL session teardown on
60
+ three systemd distros (probe artifact, not a plugin claim), and on Alpine
61
+ the same leg fails by declaration, because non-systemd distros get the
62
+ narrower `sessionSandbox: auto` scope exactly as P1.1/WSL-201 promises. The
63
+ page also records the per-distro import traps (Debian OOBE wedge, Alpine
64
+ minirootfs defaulting to root, busybox mktemp, zypper's interactive prompt)
65
+ and the reproduction commands. Not yet measured: real aarch64 hardware, the
66
+ container-image matrix.
67
+
68
+ - **The aarch64 native agent is executed in CI, not only built.** The
69
+ `native packages` job installs qemu's user-mode emulator and binfmt
70
+ registration (`qemu-user` + `qemu-user-binfmt`; noble has no real
71
+ `qemu-user-static` package) and runs the full
72
+ 18-check spike suite under qemu-user against both the freshly built aarch64
73
+ musl binary and the committed `agent/native/linux-aarch64/dsh-wsl-agent` —
74
+ the binary a Windows-on-ARM session would load. User-mode emulation
75
+ exercises every check (the static musl binary and the v5 frames are
76
+ host-OS-free), verified locally first: 18/18 under `qemu-aarch64`, with
77
+ `/proc/self/exe` resolving to the target so the HELLO digest binding holds
78
+ unchanged. This closes the "build-tested only" gap on WSL-202; a run on
79
+ real aarch64 hardware remains open.
80
+
19
81
  ## [0.13.2] - 2026-10-26
20
82
 
21
83
  This release tracks DeepSeek Harness `0.2.1-rc.3`: the peer declarations become
@@ -1,4 +1,36 @@
1
1
  import { WslAgent } from "./agent.js";
2
+ /**
3
+ * The spawn function `ensureWorkspaceRoot` uses; injectable so a unit test can
4
+ * observe the argv without a real `wsl.exe`.
5
+ */
6
+ type SpawnLike = (file: string, args: string[], options: {
7
+ stdio: "ignore";
8
+ windowsHide: boolean;
9
+ env: NodeJS.ProcessEnv;
10
+ }) => {
11
+ on: (event: string, handler: (...args: unknown[]) => void) => void;
12
+ };
13
+ /**
14
+ * Make sure the workspace root exists in the distro before the confined
15
+ * resident's first spawn: the bwrap profile binds the root
16
+ * (`--bind <root> <root>`) and bwrap refuses a missing bind source — on a
17
+ * freshly imported distro the first write's workspace may not exist yet, and
18
+ * the resident would exit during its handshake with a bare code 1 and no
19
+ * hint (WSL-305). Idempotent (`mkdir -p`), and its outcome is deliberately
20
+ * ignored here: bwrap remains the authority on whether the profile can be
21
+ * built, and its refusal now reaches the caller through the handshake
22
+ * error's stderr tail.
23
+ *
24
+ * @param options - the transport, distro, root, and an injectable spawn.
25
+ * @returns resolves once the `mkdir -p` has been issued (settled, whatever
26
+ * its result — bwrap decides).
27
+ */
28
+ export declare function ensureWorkspaceRoot({ wslPath, distro, root, spawnFn }: {
29
+ wslPath: string;
30
+ distro: string;
31
+ root: string;
32
+ spawnFn?: SpawnLike;
33
+ }): Promise<void>;
2
34
  /** One confinement request. */
3
35
  export interface ConfinedAgentRequest {
4
36
  /** The distro to run in. */
@@ -31,11 +31,39 @@
31
31
  *
32
32
  * @module dsh-plugin-wsl/agent-confined
33
33
  */
34
+ import { spawn } from "node:child_process";
34
35
  import { bwrapArgvPrefix } from "./bwrap.js";
35
36
  import { toLinuxPath } from "./paths.js";
36
37
  import { agentScriptDigest, agentScriptPath } from "./agent-shared.js";
37
- import { WslAgent } from "./agent.js";
38
+ import { pinnedWindowsEnv, WslAgent } from "./agent.js";
38
39
  const confinedAgents = new Map();
40
+ /**
41
+ * Make sure the workspace root exists in the distro before the confined
42
+ * resident's first spawn: the bwrap profile binds the root
43
+ * (`--bind <root> <root>`) and bwrap refuses a missing bind source — on a
44
+ * freshly imported distro the first write's workspace may not exist yet, and
45
+ * the resident would exit during its handshake with a bare code 1 and no
46
+ * hint (WSL-305). Idempotent (`mkdir -p`), and its outcome is deliberately
47
+ * ignored here: bwrap remains the authority on whether the profile can be
48
+ * built, and its refusal now reaches the caller through the handshake
49
+ * error's stderr tail.
50
+ *
51
+ * @param options - the transport, distro, root, and an injectable spawn.
52
+ * @returns resolves once the `mkdir -p` has been issued (settled, whatever
53
+ * its result — bwrap decides).
54
+ */
55
+ export function ensureWorkspaceRoot({ wslPath, distro, root, spawnFn }) {
56
+ return new Promise((resolve) => {
57
+ const doSpawn = spawnFn ?? ((file, args, options) => spawn(file, args, options));
58
+ const child = doSpawn(wslPath, ["-d", distro, "--exec", "mkdir", "-p", root], {
59
+ stdio: "ignore",
60
+ windowsHide: true,
61
+ env: pinnedWindowsEnv(process.env),
62
+ });
63
+ child.on("error", () => resolve());
64
+ child.on("exit", () => resolve());
65
+ });
66
+ }
39
67
  /**
40
68
  * The confined agent for one distro and one resolved policy, created on first
41
69
  * use and shared for the process lifetime. A success is remembered; a failed
@@ -48,6 +76,7 @@ export function confinedAgent({ distro, wslPath = "wsl.exe", policy, maskWindows
48
76
  const key = `${wslPath}\0${distro}\0${policy.mode}\0${policy.workspaceRoot}\0${maskWindowsDrive ? "masked" : "visible"}\0${realTmp ? "real" : "ephemeral"}`;
49
77
  let agent = confinedAgents.get(key);
50
78
  if (agent === undefined) {
79
+ const linuxRoot = toLinuxPath(policy.workspaceRoot, { distro });
51
80
  agent = new WslAgent({
52
81
  distro,
53
82
  wslPath,
@@ -58,7 +87,15 @@ export function confinedAgent({ distro, wslPath = "wsl.exe", policy, maskWindows
58
87
  // `wsl.exe --exec` and the `sh <script>` pair (`lib/agent.js`'s
59
88
  // `spawnDefault`). Both confinement sites share that assembly in
60
89
  // `lib/bwrap.js`, whose module doc records what happened when they did not.
61
- argvPrefix: bwrapArgvPrefix({ mode: policy.mode, workspaceRoot: toLinuxPath(policy.workspaceRoot, { distro }) }, { maskWindowsDrive, realTmp }),
90
+ argvPrefix: bwrapArgvPrefix({ mode: policy.mode, workspaceRoot: linuxRoot }, { maskWindowsDrive, realTmp }),
91
+ // workspace-write is the only mode that binds the workspace root, and
92
+ // bwrap refuses a missing bind source (WSL-305): on a freshly imported
93
+ // distro the first write's workspace may not exist yet, and the resident
94
+ // used to exit during its handshake with a bare code 1. Create the root
95
+ // before the first spawn; every later start re-runs it harmlessly.
96
+ prepare: policy.mode === "workspace-write"
97
+ ? () => ensureWorkspaceRoot({ wslPath, distro, root: linuxRoot }).then(() => undefined)
98
+ : undefined,
62
99
  });
63
100
  confinedAgents.set(key, agent);
64
101
  }
package/lib/agent.js CHANGED
@@ -352,6 +352,14 @@ export class WslAgent {
352
352
  // reflected in the state/pending requests; an unhandled stream error
353
353
  // would crash the host process instead.
354
354
  child.stdin?.on("error", () => { });
355
+ // The resident's own stderr is the only diagnostic a handshake failure
356
+ // carries (bwrap's refusal names the missing bind source, a libc
357
+ // mismatch names itself); surface a bounded tail on the lastError
358
+ // instead of leaving the caller a bare exit code.
359
+ let hsErr = "";
360
+ child.stderr?.on("data", (chunk) => {
361
+ hsErr += String(chunk);
362
+ });
355
363
  child.on("exit", (code, signalName) => {
356
364
  if (!current()) {
357
365
  // A stale child's exit: release its pipes, but never touch the
@@ -360,7 +368,7 @@ export class WslAgent {
360
368
  return;
361
369
  }
362
370
  if (this.state === "starting") {
363
- this.lastError = `agent exited during handshake (code=${code} signal=${signalName ?? "none"})`;
371
+ this.lastError = `agent exited during handshake (code=${code} signal=${signalName ?? "none"})${hsErr ? ` stderr=${hsErr.trim().slice(-400)}` : ""}`;
364
372
  settleStart(new Error(this.lastError));
365
373
  }
366
374
  this.handleProcessExit(child, code, signalName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-wsl-env",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离",
5
5
  "keywords": [
6
6
  "deepseek-harness",