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 +129 -0
- package/CONTRIBUTING.md +18 -7
- package/SUPPORT.md +1 -1
- package/cordis.patch.yml +2 -1
- package/docs/PEER-PARITY.md +24 -19
- package/lib/agent-confined.d.ts +32 -0
- package/lib/agent-confined.js +39 -2
- package/lib/agent.js +9 -1
- package/package.json +22 -22
- package/scripts/doctor.mjs +39 -6
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
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.
|
|
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
|
package/docs/PEER-PARITY.md
CHANGED
|
@@ -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.
|
|
11
|
-
|
|
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.
|
|
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.
|
|
17
|
-
peers' shipped `lib/index.js` are **byte-identical** to the `0.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
|
|
20
|
-
|
|
21
|
-
`
|
|
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.
|
|
37
|
-
openssl dgst -sha512 -binary deepseek-ai-dsh-fs-local-0.2.1-rc.
|
|
38
|
-
tar xzf deepseek-ai-dsh-fs-local-0.2.1-rc.
|
|
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.
|
|
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.
|
|
47
|
-
@deepseek-ai/dsh-fs@0.2.1-rc.
|
|
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.
|
|
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
|
|
124
|
-
new tarballs, check the sha512, and walk the table's peer line
|
|
125
|
-
against the replicas. The
|
|
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
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wsl-env",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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.
|
|
183
|
-
"@deepseek-ai/dsh-fs": "0.2.1-rc.
|
|
184
|
-
"@deepseek-ai/dsh-fs-local": "0.2.1-rc.
|
|
185
|
-
"@deepseek-ai/dsh-fs-sandbox": "0.2.1-rc.
|
|
186
|
-
"@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.
|
|
187
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.
|
|
188
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.
|
|
189
|
-
"@deepseek-ai/dsh-subprocess": "0.2.1-rc.
|
|
190
|
-
"@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.
|
|
191
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.
|
|
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": "
|
|
199
|
-
"@deepseek-ai/dsh-bash-local": "0.2.1-rc.
|
|
200
|
-
"@deepseek-ai/dsh-fs": "0.2.1-rc.
|
|
201
|
-
"@deepseek-ai/dsh-fs-local": "0.2.1-rc.
|
|
202
|
-
"@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.
|
|
203
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.
|
|
204
|
-
"@deepseek-ai/dsh-subprocess": "0.2.1-rc.
|
|
205
|
-
"@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.
|
|
206
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.
|
|
207
|
-
"@deepseek-ai/schemastery": "
|
|
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": {
|
package/scripts/doctor.mjs
CHANGED
|
@@ -145,19 +145,24 @@ function parseArgs(argv) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
|
-
* Whether an installed version satisfies one peer range. The
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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
|
|
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
|
/**
|