dsh-plugin-wsl-env 0.13.1 → 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,135 @@ 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
+
81
+ ## [0.13.2] - 2026-10-26
82
+
83
+ This release tracks DeepSeek Harness `0.2.1-rc.3`: the peer declarations become
84
+ floor ranges so a newer harness installs the plugin unchanged, and the
85
+ lockfile's transitive `undici` moves past a security advisory that Dependabot
86
+ could not clear on its own.
87
+
88
+ ### Fixed
89
+
90
+ - **`undici` in the lockfile moves to the security-fixed `8.12.3` via a pnpm
91
+ override.** `undici` rides in only as an exact transitive pin of
92
+ `@deepseek-ai/dsh-http-proxy` — a package upstream controls — so Dependabot's
93
+ security job could resolve no patched version (it reports "the latest
94
+ possible version … is 8.11.2" and fails). `pnpm-workspace.yaml` now carries
95
+ `overrides: { undici: '>=8.12.3' }` (the pnpm 11 settings home; the
96
+ `package.json` `pnpm` field is no longer read), the lockfile swaps the one
97
+ package, and the override comment records that it is retired when upstream
98
+ moves the pin. Nothing under `src/` reads `undici`; the check chain is
99
+ unaffected.
100
+
101
+ ### Changed
102
+
103
+ - **The peer declarations become floor ranges, so a newer harness installs
104
+ the plugin unchanged.** The `@deepseek-ai/dsh-*` entries in
105
+ `peerDependencies` move from the exact pin to `>=0.2.1-rc.3`, and
106
+ `@deepseek-ai/cordis` / `@deepseek-ai/schemastery` widen from `~` to `^`
107
+ (`^4.0.5`, `^3.18.5`). An app that ships a newer harness release now
108
+ satisfies the peers without waiting for a plugin release — same-line rc
109
+ bumps and every stable release install cleanly; only a brand-new
110
+ prerelease line (`0.2.2-rc.1` against `>=0.2.1-rc.3`) still warns, because
111
+ semver admits a prerelease only against a comparator on its own tuple and
112
+ no range can promise an unpublished line. That residue is what the weekly
113
+ peer-canary is for: it still resolves the `next` dist-tag into a scratch
114
+ tree and flags when the line moves past a floor — the signal to re-run the
115
+ `docs/PEER-PARITY.md` comparison and raise the floor in the commit that
116
+ matches the harness release. The devDependencies keep their exact pins, so
117
+ the gates stay reproducible and the parity audit keeps naming one line.
118
+ `scripts/doctor.mjs` learned the new shapes (`peerSatisfied` now reads `>=`
119
+ and `^`, and its `compareVersions` orders prerelease identifiers by semver
120
+ §11 — `rc.10` above `rc.9`), `scripts/peer-canary.mjs` parses and preserves
121
+ the shapes, and `AGENTS.md`, `CONTRIBUTING.md`, `SUPPORT.md` and
122
+ `docs/PEER-PARITY.md` state the new policy.
123
+
124
+ - **The dev pin line moves to DeepSeek Harness `0.2.1-rc.3`.** Every
125
+ `@deepseek-ai/dsh-*` pin in `devDependencies` goes from `0.2.1-rc.2` to
126
+ `0.2.1-rc.3` — the release the `next` dist-tag resolves to — and
127
+ `pnpm-lock.yaml` refreshes with it. `@deepseek-ai/cordis` and
128
+ `@deepseek-ai/schemastery` stay at `4.0.5` / `3.18.5`: upstream's `vendor/`
129
+ did not change on this line.
130
+
131
+ No module under `src/` changed, and that is the finding. The three peers
132
+ this plugin subclasses or replicates ship byte-identical `lib/index.js`
133
+ files on rc.3 — `dsh-fs-local` (the fsio dialect, still 909 lines), `dsh-fs`
134
+ (the `FsVersion` brand) and `dsh-bash-local` (the spill machinery) — so
135
+ every line reference in `docs/PEER-PARITY.md` still names the same code;
136
+ only the tarballs' digests changed, and the parity doc now carries the
137
+ `0.2.1-rc.3` ones with the re-check dated 2026-10-26. That byte-identity is
138
+ also the evidence behind the new peer floor: the same code answers above
139
+ rc.3. Upstream's release notes for the line are UI- and LLM-side
140
+ (navigation entry customization, analytics, the official `/models`
141
+ listing): the one composition-visible change is a new `entry-customization`
142
+ UI row in the app's own `cordis.patch.yml`, which rides with the app's
143
+ bundle rather than this plugin's layer. `preset-wsl` needed no
144
+ regeneration — `packages/bundle/web-app/presets/standard.patch.yml` is
145
+ byte-identical on rc.3 — and the provenance comment in `cordis.patch.yml`
146
+ now names the rc.3 tree.
147
+
19
148
  ## [0.13.1] - 2026-10-19
20
149
 
21
150
  A patch release restoring `wsl-shell-env`: the component mounts again, and the
package/CONTRIBUTING.md CHANGED
@@ -23,13 +23,24 @@ Developing and running the gates do have dependencies. The tests import pinned
23
23
  algorithm, and the boot test starts a row through the real Loader — so a fresh
24
24
  checkout needs one `pnpm install` before the gates pass.
25
25
 
26
- Those pins are deliberate, and they move with the harness rather than on their
27
- own: the `@deepseek-ai/dsh-*` peers sit at `0.2.1-rc.2`, `@deepseek-ai/cordis`
28
- at `~4.0.5` and `@deepseek-ai/schemastery` at `~3.18.5`. The plugin subclasses
29
- peer classes and mirrors peer protocol, so raising one pin alone is how the
30
- seams drift: bump them in the commit that matches a harness release, re-run the
31
- comparison `docs/PEER-PARITY.md` describes, and let the gate chain and the
32
- probes confirm the result.
26
+ Those pins are deliberate, and they split by role. The `devDependencies` stay
27
+ exact so the gates and the `docs/PEER-PARITY.md` audit name one reproducible
28
+ line. The `peerDependencies` are floors — `>=` for the `@deepseek-ai/dsh-*`
29
+ packages, caret for `@deepseek-ai/cordis` and `@deepseek-ai/schemastery` — so
30
+ an app that ships a newer harness keeps installing the plugin without waiting
31
+ for a plugin release. A floor is still raised on purpose, not by drift: it
32
+ moves in the commit that matches a harness release, with the parity comparison
33
+ and the gate chain re-run, and the weekly peer-canary
34
+ (`.github/workflows/peer-canary.yml`) resolves the `next` dist-tag into a
35
+ scratch tree and flags when the line has moved past the floor. One semver
36
+ limit is worth knowing: a floor range cannot name future prerelease lines —
37
+ `0.2.2-rc.1` does not satisfy `>=0.2.1-rc.3`, because the spec admits a
38
+ prerelease only against a comparator on its own `[major, minor, patch]` tuple
39
+ — so a brand-new rc line warns until the floor rises; same-line rcs and every
40
+ stable release never do.
41
+
42
+ The plugin subclasses peer classes and mirrors peer protocol, so the range
43
+ says nothing about the parity claims: those are re-verified per floor raise.
33
44
 
34
45
  pnpm runs the scripts in this repository, pinned through the `packageManager`
35
46
  field (corepack resolves it). Treat `pnpm run <script>` as the reference
package/SUPPORT.md CHANGED
@@ -6,7 +6,7 @@
6
6
  |---|---|
7
7
  | Operating system | Windows 10 and Windows 11 |
8
8
  | Linux side | WSL2 with at least one distro, and systemd as PID 1 with a lingering user manager (`loginctl enable-linger`) for the shipped session runtime. Set `sessionSandbox: false` on the preset's `wsl-shell` to keep the per-command sandbox instead |
9
- | DSH packages | the `0.2.1-rc.2`-era packages listed in `peerDependencies` |
9
+ | DSH packages | the `0.2.1-rc.3`-or-newer packages the `peerDependencies` floors declare; an app on a newer harness installs unchanged |
10
10
 
11
11
  The DSH row is checked weekly: `.github/workflows/peer-canary.yml` resolves the
12
12
  `next` dist-tag of every pinned peer, installs that line into a scratch copy and
package/cordis.patch.yml CHANGED
@@ -185,7 +185,8 @@
185
185
  # Generated from the shipped standard preset so the embedded plan-mode
186
186
  # section and every nested group survive byte-for-byte: this copy is
187
187
  # `packages/bundle/web-app/presets/standard.patch.yml` of the
188
- # `dsh-v0.2.1-rc.2` source tree, changed in exactly three ways —
188
+ # `dsh-v0.2.1-rc.3` source tree (unchanged since `0.2.1-rc.2`), changed
189
+ # in exactly three ways —
189
190
  # 1. every shipped row mounts inside `wsl-env`, so the preset's tools see
190
191
  # the distro's shell and fs (the realm is declared on the group);
191
192
  # 2. `tool-bash` is enabled and `tool-pwsh` disabled outright, where the
@@ -7,18 +7,20 @@ same refusal dialect on both substrates. The **read path** holds end to end;
7
7
  the **edit path** carries two declared divergences, listed at the end. This
8
8
  file records the verification against the real peers.
9
9
 
10
- - Peers: `@deepseek-ai/dsh-fs-local@0.2.1-rc.2` (the exact version
11
- `package.json` pins), plus `@deepseek-ai/dsh-fs@0.2.1-rc.2` for the
10
+ - Peers: `@deepseek-ai/dsh-fs-local@0.2.1-rc.3` (the exact version the
11
+ devDependencies pin, and the floor the peer range `>=0.2.1-rc.3` was
12
+ verified against), plus `@deepseek-ai/dsh-fs@0.2.1-rc.3` for the
12
13
  `FsVersion` brand the local package imports. Divergence 2 is measured
13
- against a third peer, `@deepseek-ai/dsh-bash-local@0.2.1-rc.2` — the shell
14
+ against a third peer, `@deepseek-ai/dsh-bash-local@0.2.1-rc.3` — the shell
14
15
  executor carries the collect-budget/spill machinery; `spill` and
15
16
  `maxSpillBytes` do not appear in `dsh-fs-local` at all.
16
- - The `0.2.1-rc.2` move leaves every line reference below intact: all three
17
- peers' shipped `lib/index.js` are **byte-identical** to the `0.2.0-rc.2`
17
+ - The `0.2.1-rc.3` move leaves every line reference below intact: all three
18
+ peers' shipped `lib/index.js` are **byte-identical** to the `0.2.1-rc.2`
18
19
  ones (`cmp` clean; `dsh-fs-local`'s is still 909 lines), so only the
19
- tarballs' digests changed. `dsh-fs@0.2.1-rc.2` did drop its unpublished
20
- `./invariant` export subpath; this plugin imports neither it nor
21
- `dsh-invariants`.
20
+ tarballs' digests changed — upstream shipped the manifests' version fields
21
+ and nothing else in these packages. `dsh-fs@0.2.1-rc.3` still has no
22
+ `./invariant` export subpath (dropped back at `0.2.1-rc.2`); this plugin
23
+ imports neither it nor `dsh-invariants`.
22
24
  - What "not imported" means: the peer's **fsio module** is not reachable from
23
25
  the package's exports, so `lib/fsio-text.js` reimplements it and imports
24
26
  nothing from it (the export is proposed in `docs/UPSTREAM-FSIO-EXPORT.md`).
@@ -33,24 +35,24 @@ file records the verification against the real peers.
33
35
 
34
36
  ```sh
35
37
  mkdir -p /tmp/peer-audit && cd /tmp/peer-audit
36
- npm pack @deepseek-ai/dsh-fs-local@0.2.1-rc.2 @deepseek-ai/dsh-fs@0.2.1-rc.2 --pack-destination .
37
- openssl dgst -sha512 -binary deepseek-ai-dsh-fs-local-0.2.1-rc.2.tgz | base64 -w0
38
- tar xzf deepseek-ai-dsh-fs-local-0.2.1-rc.2.tgz # -> package/lib/index.js
38
+ npm pack @deepseek-ai/dsh-fs-local@0.2.1-rc.3 @deepseek-ai/dsh-fs@0.2.1-rc.3 --pack-destination .
39
+ openssl dgst -sha512 -binary deepseek-ai-dsh-fs-local-0.2.1-rc.3.tgz | base64 -w0
40
+ tar xzf deepseek-ai-dsh-fs-local-0.2.1-rc.3.tgz # -> package/lib/index.js
39
41
  wc -l package/lib/index.js # expect 909
40
42
  ```
41
43
 
42
44
  The tarballs this audit compared, by content — the same values
43
- `npm view <pkg>@0.2.1-rc.2 dist.integrity` reports:
45
+ `npm view <pkg>@0.2.1-rc.3 dist.integrity` reports:
44
46
 
45
47
  ```
46
- @deepseek-ai/dsh-fs-local@0.2.1-rc.2 sha512-IZPJ12/EpV6fu3CCWtW4w4GthXAKLFjplm4Feoep2HAum7J1aGzHZwiw7ipsPCZiA5/TyK3lDUUAGZ/pcXgRcw==
47
- @deepseek-ai/dsh-fs@0.2.1-rc.2 sha512-Z6d2ZKsAiJc0FaiES8wC66vuUQMGohl9tCc2p3bAsPFYkJ0+L2zTViQ9Y5BArtRe9Br8qhNjNgLlN0jb/uwv1g==
48
+ @deepseek-ai/dsh-fs-local@0.2.1-rc.3 sha512-bB2CwCqvYAdAKl28+5XXMRRq1NBtRmYX3wNOpJ2JlGBm7uj2Aix9Vl9rFig4AQcXXd5Bk738b5KNsOIkbXy+XQ==
49
+ @deepseek-ai/dsh-fs@0.2.1-rc.3 sha512-tENLWe471WlAXMc13J4cns8xU6lFyu1smwpkSKR85vhGr6FyWZhTlpVohZYm66LbwKTmSWiDhd9Urk7VxN9B9w==
48
50
  ```
49
51
 
50
52
  Each row of the table below names the peer's `lib/index.js` line and the
51
53
  literal both sides must produce. Verified 2026-10-03; the read/edit split
52
54
  re-verified 2026-10-06; the whole table re-checked against `0.2.1-rc.2`,
53
- 2026-10-17.
55
+ 2026-10-17 and against `0.2.1-rc.3`, 2026-10-26.
54
56
 
55
57
  ## Verdict
56
58
 
@@ -93,7 +95,7 @@ declared extensions, listed at the end.
93
95
  2. **Per-stream truncation flags.** The EXEC frame carries an output budget;
94
96
  the RES line reports which streams the agent cut, surfaced as honest
95
97
  `truncated` flags. The collect-budget/spill machinery of the shell peer,
96
- `@deepseek-ai/dsh-bash-local@0.2.1-rc.2` (`lib/index.js:30`, `:110`), is the
98
+ `@deepseek-ai/dsh-bash-local@0.2.1-rc.3` (`lib/index.js:30`, `:110`), is the
97
99
  spill-file path we deliberately do not run.
98
100
  3. **The transport around the ops** (ACK, KILL, the confined resident) is
99
101
  protocol this package owns; the peer has no agent.
@@ -120,9 +122,12 @@ declared extensions, listed at the end.
120
122
 
121
123
  ## Maintenance
122
124
 
123
- When `package.json` bumps the peer pins, re-run the comparison above — pack the
124
- new tarballs, check the sha512, and walk the table's peer line references
125
- against the replicas. The fastest canary: stat a file, list its directory, and
125
+ When a peer floor or dev pin moves in `package.json`, re-run the comparison
126
+ above — pack the new tarballs, check the sha512, and walk the table's peer line
127
+ references against the replicas. The weekly peer-canary resolves the `next`
128
+ dist-tag into a scratch tree and runs the gate chain against it, so the signal
129
+ to raise the floor arrives before a user reports a drift. The fastest canary:
130
+ stat a file, list its directory, and
126
131
  compare the versions (`test/fsio-agent.test.mjs` asserts the intra-distro half
127
132
  of this); the strings above are the other half. The split dialect has its own
128
133
  regression: `test/fs-substrate.test.mjs` asserts the missing-file refusal, and
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-wsl-env",
3
- "version": "0.13.1",
4
- "description": "\u8ba9 DeepSeek Harness \u4ee5 WSL \u5b50\u7cfb\u7edf\u4f5c\u4e3a\u4f1a\u8bdd\u7684\u6267\u884c\u73af\u5883\uff1ashell\u3001\u6587\u4ef6\u8bfb\u5199\u4e0e\u547d\u4ee4\u6267\u884c\u5747\u5728\u5b50\u7cfb\u7edf\u5185\u5b8c\u6210\uff0c\u76ee\u5f55\u9009\u62e9\u5668\u4e0e GUI \u7ec8\u7aef\u540c\u6837\u5b9a\u4f4d\u5230\u5b50\u7cfb\u7edf\u8def\u5f84\uff0c\u5e76\u901a\u8fc7\u5b50\u7cfb\u7edf\u5185\u7684 bubblewrap \u5bf9\u547d\u4ee4\u8fdb\u884c\u6c99\u7bb1\u9694\u79bb",
3
+ "version": "0.14.0",
4
+ "description": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -179,32 +179,32 @@
179
179
  "@deepseek-ai/cordis": "4.0.5",
180
180
  "@deepseek-ai/cordis-plugin-include": "*",
181
181
  "@deepseek-ai/cordis-plugin-loader": "*",
182
- "@deepseek-ai/dsh-bash-local": "0.2.1-rc.2",
183
- "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
184
- "@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
185
- "@deepseek-ai/dsh-fs-sandbox": "0.2.1-rc.2",
186
- "@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.2",
187
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
188
- "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.2",
189
- "@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
190
- "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.2",
191
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
182
+ "@deepseek-ai/dsh-bash-local": "0.2.1-rc.3",
183
+ "@deepseek-ai/dsh-fs": "0.2.1-rc.3",
184
+ "@deepseek-ai/dsh-fs-local": "0.2.1-rc.3",
185
+ "@deepseek-ai/dsh-fs-sandbox": "0.2.1-rc.3",
186
+ "@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.3",
187
+ "@deepseek-ai/dsh-sandbox": "0.2.1-rc.3",
188
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.3",
189
+ "@deepseek-ai/dsh-subprocess": "0.2.1-rc.3",
190
+ "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.3",
191
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.3",
192
192
  "@types/js-yaml": "^4.0.9",
193
193
  "@types/node": "~22.20.5",
194
194
  "js-yaml": "4.3.2",
195
195
  "typescript": "~6.0.3"
196
196
  },
197
197
  "peerDependencies": {
198
- "@deepseek-ai/cordis": "~4.0.5",
199
- "@deepseek-ai/dsh-bash-local": "0.2.1-rc.2",
200
- "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
201
- "@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
202
- "@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.2",
203
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
204
- "@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
205
- "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.2",
206
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
207
- "@deepseek-ai/schemastery": "~3.18.5"
198
+ "@deepseek-ai/cordis": "^4.0.5",
199
+ "@deepseek-ai/dsh-bash-local": ">=0.2.1-rc.3",
200
+ "@deepseek-ai/dsh-fs": ">=0.2.1-rc.3",
201
+ "@deepseek-ai/dsh-fs-local": ">=0.2.1-rc.3",
202
+ "@deepseek-ai/dsh-host-directory-picker": ">=0.2.1-rc.3",
203
+ "@deepseek-ai/dsh-sandbox": ">=0.2.1-rc.3",
204
+ "@deepseek-ai/dsh-subprocess": ">=0.2.1-rc.3",
205
+ "@deepseek-ai/dsh-subprocess-local": ">=0.2.1-rc.3",
206
+ "@deepseek-ai/dsh-typert-protocol": ">=0.2.1-rc.3",
207
+ "@deepseek-ai/schemastery": "^3.18.5"
208
208
  },
209
209
  "peerDependenciesMeta": {
210
210
  "@deepseek-ai/cordis": {
@@ -145,19 +145,24 @@ function parseArgs(argv) {
145
145
  }
146
146
 
147
147
  /**
148
- * Whether an installed version satisfies one peer range. The plugin's pins are
149
- * exact or tilde ranges, so those two shapes are all this understands; anything
150
- * else is reported as unchecked rather than guessed at.
148
+ * Whether an installed version satisfies one peer range. The manifest's peer
149
+ * ranges are floor ranges (`>=`) and caret ranges (`^`), and the
150
+ * devDependencies carry exact pins — those three shapes are all this
151
+ * understands; anything else is reported as unchecked rather than guessed at.
152
+ * The floor is the plugin's own compatibility claim, so it compares plainly
153
+ * against the floor version; the semver gate that excludes other lines'
154
+ * prereleases is an installer concern, not this check's.
151
155
  * @param {string} installed - the installed version.
152
156
  * @param {string} range - the declared peer range.
153
157
  * @returns {boolean | undefined} true/false, or undefined for an unsupported shape.
154
158
  */
155
159
  export function peerSatisfied(installed, range) {
156
- if (range.startsWith("~")) {
160
+ if (range.startsWith(">=")) return compareVersions(installed, range.slice(2)) >= 0;
161
+ if (range.startsWith("^")) {
157
162
  const [major, minor, patch] = range.slice(1).split(".");
158
163
  return compareVersions(installed, `${major}.${minor}.${patch ?? "0"}`) >= 0
159
164
  && installed.split(".")[0] === major
160
- && installed.split(".")[1] === minor;
165
+ && (major !== "0" || installed.split(".")[1] === minor);
161
166
  }
162
167
  if (/^[0-9]/.test(range)) return installed === range;
163
168
  return undefined;
@@ -188,7 +193,35 @@ export function compareVersions(left, right) {
188
193
  if (a.pre === b.pre) return 0;
189
194
  if (a.pre === "") return 1;
190
195
  if (b.pre === "") return -1;
191
- return a.pre.localeCompare(b.pre);
196
+ return comparePrerelease(a.pre, b.pre);
197
+ }
198
+
199
+ /**
200
+ * Compare two prerelease strings by semver §11: dot-separated identifiers,
201
+ * numeric ones numerically and below alphanumeric ones, the rest lexically,
202
+ * a longer list above its own prefix. Plain `localeCompare` would order
203
+ * `rc.10` below `rc.9`.
204
+ * @param {string} left - the first prerelease.
205
+ * @param {string} right - the second prerelease.
206
+ * @returns {number} negative, zero or positive like a comparator.
207
+ */
208
+ function comparePrerelease(left, right) {
209
+ const a = left.split(".");
210
+ const b = right.split(".");
211
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
212
+ const x = a[i];
213
+ const y = b[i];
214
+ if (x === undefined) return -1;
215
+ if (y === undefined) return 1;
216
+ if (x === y) continue;
217
+ const numericX = /^\d+$/.test(x);
218
+ const numericY = /^\d+$/.test(y);
219
+ if (numericX && numericY) return Number(x) - Number(y);
220
+ if (numericX) return -1;
221
+ if (numericY) return 1;
222
+ return x < y ? -1 : 1;
223
+ }
224
+ return 0;
192
225
  }
193
226
 
194
227
  /**