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 +62 -0
- package/lib/agent-confined.d.ts +32 -0
- package/lib/agent-confined.js +39 -2
- package/lib/agent.js +9 -1
- package/package.json +1 -1
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
|
package/lib/agent-confined.d.ts
CHANGED
|
@@ -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. */
|
package/lib/agent-confined.js
CHANGED
|
@@ -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:
|
|
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