dsh-plugin-wsl-env 0.9.0 → 0.10.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 +417 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +84 -68
- package/SUPPORT.md +3 -3
- package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
- package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
- package/agent/wsl-agent.sh +83 -16
- package/cordis.patch.yml +41 -35
- package/docs/ARCHITECTURE.md +9 -1
- package/docs/CONFIGURATION.md +7 -4
- package/docs/LIMITATIONS.md +53 -18
- package/docs/PARITY.md +3 -1
- package/docs/PEER-PARITY.md +18 -11
- package/docs/README.zh.md +97 -74
- package/docs/UPSTREAM-FSIO-EXPORT.md +3 -3
- package/docs/UPSTREAM-TERMINAL-TITLE.md +9 -2
- package/examples/preset-wsl-session-sandbox.cordis.patch.yml +178 -0
- package/lib/agent-protocol.d.ts +10 -4
- package/lib/agent-protocol.js +10 -3
- package/lib/agent.d.ts +84 -0
- package/lib/agent.js +167 -15
- package/lib/bwrap.d.ts +8 -0
- package/lib/bwrap.js +4 -1
- package/lib/fs-routing.d.ts +40 -4
- package/lib/fs-routing.js +80 -12
- package/lib/fsio-agent.js +1 -1
- package/lib/index.d.ts +134 -0
- package/lib/index.js +191 -3
- package/lib/native-agent.d.ts +124 -0
- package/lib/native-agent.js +198 -0
- package/lib/ports.d.ts +13 -2
- package/lib/ports.js +18 -5
- package/lib/preflight.js +24 -1
- package/lib/session-runtime.d.ts +161 -0
- package/lib/session-runtime.js +220 -0
- package/lib/session-scope.d.ts +124 -0
- package/lib/session-scope.js +191 -0
- package/lib/shell-env.js +10 -3
- package/lib/watcher.d.ts +25 -12
- package/lib/watcher.js +120 -32
- package/lib/workspace-files-route.d.ts +8 -4
- package/lib/workspace-files-route.js +42 -14
- package/lib/workspace-files-wsl.js +25 -8
- package/package.json +34 -25
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,423 @@ were part of the record.
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.10.0] - 2026-10-18
|
|
20
|
+
|
|
21
|
+
The native in-distro agent is the default session-runtime backend: `agentBackend: auto`
|
|
22
|
+
selects the packaged static-musl binary for the distro's architecture, the shipped
|
|
23
|
+
shell agent stays as the fallback, and `native`/`shell` keep the strict and forced
|
|
24
|
+
modes. The peer line moves to DeepSeek Harness `0.2.1-rc.2`, which is the desktop
|
|
25
|
+
app's own version line. No module under `src/` changed, and that is the
|
|
26
|
+
finding, not an omission: every peer artifact this plugin subclasses or
|
|
27
|
+
replicates is byte-identical to the one shipped for `0.2.0-rc.2`, except one
|
|
28
|
+
added field in the `dsh-api-workspace-files` class that `WorkspaceFilesWsl`
|
|
29
|
+
extends (and that package's regenerated typert artifacts), so the seams the
|
|
30
|
+
code is built on did not move. What did move is the shipped composition — the
|
|
31
|
+
standard agent preset — and the plugin's embedded `preset-wsl` is regenerated
|
|
32
|
+
from it.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **The watcher uses inotify when it can.** With `inotifywait` installed and an
|
|
37
|
+
unbounded directory target, the in-distro watcher now uses recursive inotify
|
|
38
|
+
watches and coalesces events to one callback per 0.5s instead of polling
|
|
39
|
+
`find`. A file target, a positive `watchMaxDepth`, or a missing
|
|
40
|
+
`inotify-tools` keeps the polling fallback, so the backend is optional. The
|
|
41
|
+
polling mtime-old blind spot does not apply to the inotify path.
|
|
42
|
+
|
|
43
|
+
- **`sessionSandbox` runs one session's commands in one scope.** With the key
|
|
44
|
+
on, the shell executor acquires the runtime for `(distro, workspace,
|
|
45
|
+
policy)` on the first command, reuses it for the session, runs the command
|
|
46
|
+
WITHOUT the per-command bwrap wrap (the runtime's own mounts confine it),
|
|
47
|
+
and releases it from the preset scope's disposer — closing the agent and
|
|
48
|
+
stopping the unit, whose cgroup reaps the session's leftovers. On in the
|
|
49
|
+
shipped preset; requires `agent: true`, systemd as PID 1, a reachable user
|
|
50
|
+
manager, `systemd-run` and `Linger`. The preflight refuses with the remedy
|
|
51
|
+
when one is missing (only a success is cached, so following the remedy
|
|
52
|
+
needs no app restart); set `sessionSandbox: false` for the per-command
|
|
53
|
+
shape. Validated on a live session: `ps -p 1` is systemd, the command's
|
|
54
|
+
cgroup is the `dsh-wsl-*.scope`, a background process and `/tmp` survive
|
|
55
|
+
across commands, and the unit disappears after the session ends.
|
|
56
|
+
|
|
57
|
+
- **The session-runtime registry.** `src/session-runtime.ts` refcounts one
|
|
58
|
+
agent per `(distro, workspace, policy)`, composes the systemd user scope
|
|
59
|
+
prefix with a bwrap mount profile that avoids a PID namespace (and binds the
|
|
60
|
+
real `/tmp` under `workspace-write`), and tears a runtime down by closing the
|
|
61
|
+
agent and then stopping the scope. `--unshare-pid` is now a profile option
|
|
62
|
+
(default true), and the agent accepts an explicit lease independent of the
|
|
63
|
+
idle close. Tests cover the refcount, the mode-specific mounts, the mask in
|
|
64
|
+
the key, the explicit lease, and the close-then-stop order. It is wired into
|
|
65
|
+
the shell executor behind `sessionSandbox`, with a preflight that refuses
|
|
66
|
+
with a remedy; the filesystem provider's unification is next.
|
|
67
|
+
|
|
68
|
+
- **The session-scoped sandbox's launcher primitives.** `src/session-scope.ts`
|
|
69
|
+
builds the systemd user unit name, the `systemd-run --user --scope` prefix,
|
|
70
|
+
the stop argv and the preflight probe argv; unit tests pin the shapes and the
|
|
71
|
+
sanitization. ADR 0001 records the runtime-ownership decision the wiring
|
|
72
|
+
still has to make (the agent is a per-distro singleton today). No behavior
|
|
73
|
+
change yet.
|
|
74
|
+
|
|
75
|
+
- **`docs/VALUE.zh.md` states what the plugin is worth, in Chinese**, for a
|
|
76
|
+
reader deciding whether to install it: the three misalignments it removes,
|
|
77
|
+
six values each with its mechanism and the evidence that backs it, the
|
|
78
|
+
comparison against no integration / desktop remoting / putting the CLI in
|
|
79
|
+
WSL, the costs and limits stated before the sale, how the claims are
|
|
80
|
+
proven, and a rule for when not to use it. It is linked from both readme
|
|
81
|
+
indexes and sits beside the reference docs, never in place of them.
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- **The peer pins move to the `0.2.1-rc.2` line**: `@deepseek-ai/dsh-*` at
|
|
86
|
+
`0.2.1-rc.2`, `@deepseek-ai/cordis` at `~4.0.5`, `@deepseek-ai/schemastery`
|
|
87
|
+
at `~3.18.5`, with `pnpm-lock.yaml` refreshed. pnpm's release-age policy
|
|
88
|
+
records the exact closure it had to exempt in `pnpm-workspace.yaml`, the way
|
|
89
|
+
the harness repository does. The three seam packages were compared byte for
|
|
90
|
+
byte against the previous pin: `dsh-fs-local`'s `lib/index.js` (the fsio
|
|
91
|
+
dialect this plugin replicates), `dsh-fs`'s, and `dsh-bash-local`'s (the
|
|
92
|
+
spill machinery and the `spawnSpec` seam) are identical files, so every line
|
|
93
|
+
reference in `docs/PEER-PARITY.md` and `docs/UPSTREAM-SPAWN-SEAM.md` still
|
|
94
|
+
names the same code — only the tarballs' digests changed, and
|
|
95
|
+
`docs/PEER-PARITY.md` now carries the `0.2.1-rc.2` ones. `cordis@4.0.5` and
|
|
96
|
+
`schemastery@3.18.5` likewise publish the same bytes as `4.0.4` and
|
|
97
|
+
`3.18.4` except for their manifests. Elsewhere: `dsh-fs@0.2.1-rc.2` drops its
|
|
98
|
+
unpublished `./invariant` export subpath (nothing here imported it),
|
|
99
|
+
`dsh-subprocess-local`'s new code is Linux systemd scope selection (this
|
|
100
|
+
plugin spawns `wsl.exe` from a Windows host), `dsh-api-workspace-files` adds
|
|
101
|
+
one `resource` field to its own lookup registration, and the typert protocol
|
|
102
|
+
gained modules.
|
|
103
|
+
- **`preset-wsl` is regenerated from the shipped standard preset.** The rows
|
|
104
|
+
that arrived are the clock-reading `time-context` row, `tool-schedule`, and
|
|
105
|
+
the disabled `tool-plugin-market/tools` row; the rows that left are
|
|
106
|
+
`tool-ralph` and the disabled `subagent_codex` / `subagent_claude_code`
|
|
107
|
+
entries, whose providers the preset no longer declares anywhere. The two
|
|
108
|
+
`tool-subagent` rows drop `backgroundMode`, the plan-mode section's first
|
|
109
|
+
sentence now says the plan is approved *through* `exit_plan_mode`, and
|
|
110
|
+
`persona.suffix` is gone: the host plane's new `working-directory` row
|
|
111
|
+
reports the Session's cwd, so the "Your working directory is …" suffix is no
|
|
112
|
+
longer the mechanism. The three deliberate deviations from the shipped copy
|
|
113
|
+
— the `wsl-env` realm wrapping every row, the fixed shell-tool choice, and
|
|
114
|
+
the preset's identity — are unchanged, and the transformation is now
|
|
115
|
+
[scripts/build-preset-wsl.mjs](scripts/build-preset-wsl.mjs) rather than the
|
|
116
|
+
machine-local generator the block used to name: it takes the shipped file and
|
|
117
|
+
prints the row, and its output is the committed block byte for byte.
|
|
118
|
+
- `docs/UPSTREAM-TERMINAL-FONT.md` closes: the `0.2.1-rc.2` line made the
|
|
119
|
+
terminal font a user setting (`--dsh-font-family-terminal` /
|
|
120
|
+
`--dsh-terminal-font-size`), so the hardcoded stack it argued against is now
|
|
121
|
+
only the fallback appended after the user's list.
|
|
122
|
+
`docs/UPSTREAM-TERMINAL-TITLE.md` was re-checked against the new tag and is
|
|
123
|
+
unchanged, including after the terminal controller's 0.2.1 rework: the tab
|
|
124
|
+
title is still built from `shell.name` alone.
|
|
125
|
+
|
|
126
|
+
### Fixed
|
|
127
|
+
|
|
128
|
+
- **The GUI file service loads again from a linked install** (`workspace-files-wsl`
|
|
129
|
+
failed to import, and the Desktop host then died on a `write EOF`). The module
|
|
130
|
+
imports `@deepseek-ai/dsh-api-workspace-files` and
|
|
131
|
+
`@deepseek-ai/dsh-typert-protocol` at runtime, but both were declared only as
|
|
132
|
+
dev dependencies. The runtime's linked-plugin resolver routes a bare import
|
|
133
|
+
to the installation package only when the plugin's own manifest lists that
|
|
134
|
+
name under `peerDependencies` (`dsh-app-boot`'s `readPeerNames` /
|
|
135
|
+
`routeLinked`), so the import missed, the entry never activated, and Desktop
|
|
136
|
+
exited 1; the fatal `write EOF` is the shell writing to that already-exited
|
|
137
|
+
host. Both names are peers now, and `test/peers.test.mjs` scans every
|
|
138
|
+
`lib/*.js` import so an undeclared runtime package fails the check chain
|
|
139
|
+
instead of the app.
|
|
140
|
+
|
|
141
|
+
- **A warm native-agent cache no longer kills the Host on startup.** The
|
|
142
|
+
in-distro copy script prints the cache path and exits *without reading stdin*
|
|
143
|
+
when the digest already matches. `copyBinary` still streamed the packaged
|
|
144
|
+
binary into that closed pipe and left stdin's `error` event unhandled; on
|
|
145
|
+
Windows the write surfaced as `write EOF` and took the Host down before the
|
|
146
|
+
UI could load, so the first run (cold cache) worked and every later start
|
|
147
|
+
crashed. The write error is absorbed now — the exit code and stdout already
|
|
148
|
+
decide the result — and a POSIX regression check runs an early-exit script
|
|
149
|
+
against a payload larger than any pipe buffer.
|
|
150
|
+
|
|
151
|
+
- **The root-plane router survives its first distro target, and that path now
|
|
152
|
+
has a regression test.** Three defects sat behind the same runtime failure:
|
|
153
|
+
- `WslRoutingFileSystem` kept its state in ES `#private` fields. Cordis
|
|
154
|
+
invokes a service method with a shadow receiver (the traceable proxy
|
|
155
|
+
`ReflectService` builds), and private members do not cross it, so the first
|
|
156
|
+
routed call threw `Cannot read private member #distro from an object whose
|
|
157
|
+
class did not declare it`. The fields are plain now, exactly like the host
|
|
158
|
+
backend's own `config` and `locks`.
|
|
159
|
+
- Constructing the distro backend with the router's context re-registered
|
|
160
|
+
`fs` in the same scope (`service "fs" has been registered`). The backend
|
|
161
|
+
now receives an `fs`-isolated child scope, while `sandboxPolicy` still
|
|
162
|
+
resolves from the parent. The router captures its construction context,
|
|
163
|
+
because `this.ctx` inside a method is the *caller's* context, not its own.
|
|
164
|
+
- The row dropped the `static inject = ["sandboxPolicy"]` declaration the
|
|
165
|
+
shipped `fs-sandbox` row carries, although the backend it constructs reads
|
|
166
|
+
`ctx.sandboxPolicy.defaultMode`. That service is provided by a sibling row,
|
|
167
|
+
not an ancestor, so the fiber could not resolve it and construction failed
|
|
168
|
+
with `cannot get property "sandboxPolicy" without inject`. The declaration
|
|
169
|
+
is restored.
|
|
170
|
+
|
|
171
|
+
The `0.2.1-rc.2` line's new `working-directory` row resolves the session cwd
|
|
172
|
+
through the root `ctx.fs`, which is what first exercised the distro branch.
|
|
173
|
+
`test/composition.test.mjs` now boots the takeover row through the Loader
|
|
174
|
+
and drives the whole routing table through the service proxy — distro side
|
|
175
|
+
stubbed at the backend surface, host side over a temp workspace, with the
|
|
176
|
+
policy supplied from a sibling fiber as in the app — so all three defects
|
|
177
|
+
fail the unit suite instead of a session.
|
|
178
|
+
|
|
179
|
+
- **The GUI file service confines every read to the workspace, not just
|
|
180
|
+
listings.** `stat`, `read` and `readBytes` passed the joined Linux path
|
|
181
|
+
straight to `stat`/`sed`/`tail`, so an absolute path outside the workspace,
|
|
182
|
+
a `..` traversal, a symlink leaving it, or another distro's UNC name were
|
|
183
|
+
served — while `list` already resolved the real path and refused escapes
|
|
184
|
+
with `workspace-file/outside-workspace`. All four ops now share one guard:
|
|
185
|
+
`realpath -L` on the target and the root, then the same root/under-root
|
|
186
|
+
test, so an escape is the same refusal whichever op asks. The new checks
|
|
187
|
+
drive the real `sh` guard against a temp root (absolute sibling, `..`,
|
|
188
|
+
symlink out, symlink in, the root itself) and assert the refusal mapping in
|
|
189
|
+
all three ops.
|
|
190
|
+
|
|
191
|
+
- **The resident transport owns its generation and its pipes.** Three
|
|
192
|
+
lifecycle leaks shared one cause — state keyed by "the current process"
|
|
193
|
+
instead of by the process that actually emitted the event:
|
|
194
|
+
- Every spawn now owns a generation; its `error`, `exit`, `stdout` and
|
|
195
|
+
`stderr` handlers return unless `this.process` is still that child, so a
|
|
196
|
+
stale exit can no longer settle a newer start, null a newer transport, or
|
|
197
|
+
let a stale watchdog kill it. `handshakeTimer`, the ACK-armed watchdog and
|
|
198
|
+
the frame-cap kill all bind to their own child.
|
|
199
|
+
- `close()` settles the requests it interrupts, fails a start it interrupts,
|
|
200
|
+
and destroys the host's ends of `stdin`/`stdout`/`stderr` after the exit.
|
|
201
|
+
A descendant that inherited a pipe's write end can no longer keep the
|
|
202
|
+
read side — and the fd — open until it exits.
|
|
203
|
+
- The agent's lease and timeout watchdogs now spawn with `/dev/null` stdio,
|
|
204
|
+
so their `sleep` children do not hold the protocol pipes after the agent
|
|
205
|
+
exits. Without this, killing the watchdog subshell left the `sleep` as an
|
|
206
|
+
orphan holding the write ends for up to the full lease window (12 minutes
|
|
207
|
+
at the shipped 720 s), which is what kept a test process alive after every
|
|
208
|
+
check had already passed.
|
|
209
|
+
|
|
210
|
+
The fake transport covers the stale-exit and stdio-teardown checks; the real
|
|
211
|
+
`sh` agent is spawned with a 720 s lease and asserted to close its pipes
|
|
212
|
+
immediately after `SHUTDOWN`.
|
|
213
|
+
|
|
214
|
+
- **Cancellation reaches the command the agent is blocked on.** The stdin loop
|
|
215
|
+
is serial: a `KILL` frame written while a command ran sat in the pipe until
|
|
216
|
+
that command finished, and by then the id no longer matched, so the frame
|
|
217
|
+
was dropped. A request aborted mid-flight kept running to its own timeout; a
|
|
218
|
+
request aborted while queued ran in full later, with its answer discarded as
|
|
219
|
+
noise. Protocol 5 adds the agent shell's PID to `HELLO`; on abort the host
|
|
220
|
+
delivers `SIGUSR1` from a second `wsl.exe`, and the agent's trap kills the
|
|
221
|
+
request's process group — TERM, then KILL after the same 3 s grace the
|
|
222
|
+
timeout watcher uses. A request cancelled while it was still queued stays in
|
|
223
|
+
the pending map and is signalled on its `ACK`, so it is killed the moment it
|
|
224
|
+
becomes the running one; its late `RES` only cleans up. A PID-namespaced
|
|
225
|
+
confined resident is never signalled by its HELLO pid (that PID does not
|
|
226
|
+
exist in the distro and could name an unrelated process) — there the KILL
|
|
227
|
+
frame remains the fallback, and filesystem ops are short by construction.
|
|
228
|
+
The `KILL` frame stays as the fallback everywhere.
|
|
229
|
+
|
|
230
|
+
The fake transport covers the started, queued and confined cases; the real
|
|
231
|
+
`sh` agent is asked to run `sleep 30`, receives `USR1` after its ACK, and is
|
|
232
|
+
asserted to answer with a non-zero exit well before the sleep ends.
|
|
233
|
+
|
|
234
|
+
- **The agent's ACK is emitted only once the request's child exists.** It used
|
|
235
|
+
to be printed just before `run_capture`, i.e. before `CURRENT_PID` was set.
|
|
236
|
+
The host arms its execution watchdog and delivers `USR1` on the ACK, so both
|
|
237
|
+
could race the spawn: a cancellation right after the ACK found an empty
|
|
238
|
+
`CURRENT_PID`, dropped the signal, and the command ran to its own timeout.
|
|
239
|
+
`run_capture` now prints the ACK after `CURRENT_PID=$!`, which is what the
|
|
240
|
+
host's comment always claimed it meant.
|
|
241
|
+
|
|
242
|
+
- **The GUI page contract no longer returns its EOF probe line.** `pageArgv`
|
|
243
|
+
reads one line past the requested page so EOF is decidable from the yield;
|
|
244
|
+
`distroRead` returned every line it got and derived `lines`/`eof` from the
|
|
245
|
+
probe, so an N-line page carried N+1 lines, a page ending exactly at the file
|
|
246
|
+
reported `eof: false`, and `bytes` added the line count on top of the UTF-8
|
|
247
|
+
length (over-charging CRLF files and refusing pages that fit). The probe is
|
|
248
|
+
split off before counting, `eof` means "no line past the page", and `bytes`
|
|
249
|
+
is the page's own UTF-8 length. Tests cover truncation, exact EOF, and the
|
|
250
|
+
byte cap.
|
|
251
|
+
|
|
252
|
+
- **The resident's pending queue is bounded.** One resident refuses a request
|
|
253
|
+
past `maxPending` (64 by default) with a clear "requests in flight" error
|
|
254
|
+
instead of growing its map — and the retained frame lines — without bound.
|
|
255
|
+
A single frame's size is still the caller's.
|
|
256
|
+
|
|
257
|
+
- **The output budget now bounds the capture while the command runs.** The
|
|
258
|
+
agent wrote stdout and stderr to two unbounded files and applied
|
|
259
|
+
`maxOutputBytes` only while encoding the `RES`, so a chatty command could
|
|
260
|
+
fill the distro's `/tmp` — and a huge answer could exceed the 256 MiB frame
|
|
261
|
+
cap and kill the shared agent. With a budget, both streams are captured
|
|
262
|
+
through FIFOs drained by a bounded `dd` reader; the rest is discarded, so
|
|
263
|
+
the files stay within a block or two of the budget. The readers are given a
|
|
264
|
+
one-second grace once the request's own process is gone and then stopped, so
|
|
265
|
+
a background descendant holding the FIFO cannot delay the answer — and the
|
|
266
|
+
grace lets a just-exited command's output reach the file before the reader
|
|
267
|
+
is stopped. The block slack is what distinguishes "exactly at the cap" from
|
|
268
|
+
"cut". The unbounded path (`FS` frames) still writes files directly. Tests
|
|
269
|
+
cover `yes` capped at 1 KiB (`truncated: true`) and `sleep & echo` answered
|
|
270
|
+
without waiting for the background child.
|
|
271
|
+
|
|
272
|
+
- **Confined file mutations share the session runtime.** With `wsl-fs`
|
|
273
|
+
`sessionSandbox: true` (the shipped preset), a confined `writeText`/`editText`
|
|
274
|
+
acquires the same `(distro, workspace, policy)` runtime as the shell
|
|
275
|
+
executor: one agent, one mount profile, one real `/tmp`, and the same scope
|
|
276
|
+
lifecycle. Reads stay on the plain shared agent; the app-level `fs-routing`
|
|
277
|
+
backend keeps the mode off because it is not session-owned. A failed
|
|
278
|
+
preflight refuses the write with the systemd/linger remedy.
|
|
279
|
+
|
|
280
|
+
- **A closed watcher signals its loop by PID.** The heartbeat alone cannot
|
|
281
|
+
rely on the pipe closing when the host kills `wsl.exe`, so the loop now
|
|
282
|
+
announces its shell PID (`P <pid>`) before READY and `close()` sends it
|
|
283
|
+
`TERM` through a second `wsl.exe`; the TERM trap removes the stamp. The
|
|
284
|
+
real-machine watch probe now asserts the loop is gone, matching the script's
|
|
285
|
+
own argv rather than the `pgrep` wrapper. This is the fix the probe found
|
|
286
|
+
when the heartbeat-only version was tried on the real transport.
|
|
287
|
+
|
|
288
|
+
- **A failed port scan keeps the last successful snapshot.** The ports
|
|
289
|
+
environment fact was set to an empty list on any scan failure, which the
|
|
290
|
+
model reads as "nothing is listening". A failed scan (agent out, non-zero
|
|
291
|
+
exit) now leaves the previous list in place and logs a debug line; only a
|
|
292
|
+
successful empty scan produces an empty list.
|
|
293
|
+
|
|
294
|
+
- **The stale-scope sweep recognizes the native agent.** It looked only for
|
|
295
|
+
`wsl-agent.sh` in the scope's cgroup, so a live native session scope (whose
|
|
296
|
+
agent is `dsh-wsl-agent`) could be stopped at the next session preflight.
|
|
297
|
+
The liveness check now accepts either marker; a scope with only leftover
|
|
298
|
+
background processes is still reaped.
|
|
299
|
+
|
|
300
|
+
- **The native EXEC timeout is seconds.** The spike read the v5 EXEC frame's
|
|
301
|
+
timeout field as milliseconds, so a 120 s budget killed every command after
|
|
302
|
+
120 ms; longer commands exited 143 on the native session. It now converts
|
|
303
|
+
seconds (`Duration::from_secs`), and the spike's check runs a 300 ms command
|
|
304
|
+
under a 2 s budget so the unit cannot regress silently.
|
|
305
|
+
|
|
306
|
+
- **The inotify watcher is reaped promptly on close and on a vanished
|
|
307
|
+
directory.** The blocking FIFO read is woken by a one-second marker, so the
|
|
308
|
+
TERM trap runs and the target check reports `E` within a second of the
|
|
309
|
+
directory disappearing. The real-machine probe's close and vanished-target
|
|
310
|
+
checks now pass with `inotify-tools` installed; the pre-watch creation check
|
|
311
|
+
was also corrected (a file created before the watch is not the polling
|
|
312
|
+
mtime blind spot).
|
|
313
|
+
|
|
314
|
+
- **A closed watcher no longer leaks its distro-side loop.** `close()` kills
|
|
315
|
+
`wsl.exe` but cannot signal the loop inside the distro, and a loop that only
|
|
316
|
+
writes on changes would sleep and poll forever. Each tick now writes an
|
|
317
|
+
ignored `H` heartbeat; a `PIPE` trap turns the closed pipe's SIGPIPE into a
|
|
318
|
+
clean exit through the stamp-removing EXIT trap. The host ignores `H`; the
|
|
319
|
+
real-`sh` check closes the pipe and asserts the loop exits and the stamp is
|
|
320
|
+
gone.
|
|
321
|
+
|
|
322
|
+
- **A distro target contains only targets in the same distro.** `contains()`
|
|
323
|
+
compared Linux path strings, so another distro's share (or a `..`-bearing
|
|
324
|
+
key) could test as contained when the spelling lined up. It now compares the
|
|
325
|
+
distro coordinate and lexically normalizes the paths; the resolver remains
|
|
326
|
+
the canonicalizer for symlinks.
|
|
327
|
+
|
|
328
|
+
- **A `notdir` failure reports `FS_NOT_DIRECTORY`.** The wire reason was
|
|
329
|
+
mapped to `FS_NOT_FOUND`, so a path whose parent is a regular file looked
|
|
330
|
+
like a missing path. It now carries the specific code the tool layer already
|
|
331
|
+
knows, so the model gets the right remedy.
|
|
332
|
+
|
|
333
|
+
- **Stale session scopes are swept.** A hard kill of the app can leave a
|
|
334
|
+
`dsh-wsl-*.scope` and its background processes behind. The session preflight
|
|
335
|
+
now lists those units and stops one only when its cgroup has no
|
|
336
|
+
`wsl-agent.sh`; a live agent means another session still owns it. The sweep
|
|
337
|
+
runs once per distro per process (a failed spawn re-arms it) and is a no-op
|
|
338
|
+
without a user manager. Validated on a live user manager: two
|
|
339
|
+
`dsh-wsl-test-sweep*` scopes with only a `sleep` were stopped while the
|
|
340
|
+
session's own scope (with its live agent) stayed active.
|
|
341
|
+
|
|
342
|
+
- **`watch()` accepts a file target.** The poll loop treated every
|
|
343
|
+
non-directory as vanished, so `ctx.fs.watch` on a regular file failed
|
|
344
|
+
immediately with "no longer exists". A file is now watched by its
|
|
345
|
+
`mtime:size` fingerprint — a rename-over or an in-place write fires — and a
|
|
346
|
+
removal reports the target as gone. Directory watching is unchanged. A
|
|
347
|
+
real-`sh` check runs the loop against a temp file (READY, a size change, a
|
|
348
|
+
removal).
|
|
349
|
+
|
|
350
|
+
### Notes
|
|
351
|
+
|
|
352
|
+
- **The native-session probe.** `test/probe/native-session.mjs` (also in the
|
|
353
|
+
real-machine suite and the self-hosted probes job) exercises the packaged
|
|
354
|
+
native binary end to end: the cache/digest, the session scope, EXEC, FS
|
|
355
|
+
write/read, a KILL that reaps the request group while the agent survives, a
|
|
356
|
+
wrong HELLO digest failing the handshake, and the scope/cache teardown. It
|
|
357
|
+
is 8/8 on the live machine. The probe found two integration bugs, both
|
|
358
|
+
fixed here: a valid cache is now reused instead of rewritten (rewriting a
|
|
359
|
+
binary a live session is running fails with ETXTBSY), and the agent's stdin
|
|
360
|
+
now has an error listener so `close()` after a failed handshake (EPIPE) does
|
|
361
|
+
not crash the host.
|
|
362
|
+
|
|
363
|
+
- **Static musl packages for x86_64 and aarch64.** The package now ships
|
|
364
|
+
`agent/native/linux-x86_64/dsh-wsl-agent` (static-pie musl, 635,752 bytes,
|
|
365
|
+
sha256 `5920bdd5…`) and `agent/native/linux-aarch64/dsh-wsl-agent` (static
|
|
366
|
+
musl, 535,136 bytes, sha256 `cf7dd150…`), both stripped. One static binary
|
|
367
|
+
runs on glibc and musl distros, so Alpine no longer depends on the shell
|
|
368
|
+
fallback. A CI job builds both (x86_64 via musl-gcc, aarch64 via
|
|
369
|
+
cargo-zigbuild), tests the x86_64 build and uploads the `native-package`
|
|
370
|
+
artifact; the committed binaries are the reviewed result.
|
|
371
|
+
|
|
372
|
+
- **The native HELLO binds the process to the binary.** The native agent
|
|
373
|
+
hashes `/proc/self/exe` and puts the sha256 in its HELLO digest; the host
|
|
374
|
+
passes the digest it copied into the cache as `expectedDigest`, so a binary
|
|
375
|
+
swapped after the copy check fails the handshake before accepting a request.
|
|
376
|
+
The spike is 18/18 (the new check compares the handshake to the file's
|
|
377
|
+
sha256). The shell agent's script digest is unchanged.
|
|
378
|
+
|
|
379
|
+
- **The packaged native binary.** `agent/native/linux-x86_64/dsh-wsl-agent`
|
|
380
|
+
ships in the npm `files`; `agentBackend: auto` resolves it for a distro whose
|
|
381
|
+
`uname -m` is x86_64/amd64 (aarch64 is mapped but has no binary yet), with
|
|
382
|
+
`DSH_WSL_AGENT_NATIVE` still the development override. After the sha256 copy
|
|
383
|
+
the host executes the binary once with `</dev/null` and only then selects it,
|
|
384
|
+
so a glibc/musl mismatch falls back to the shell agent. The CI builds the
|
|
385
|
+
spike from source and runs its suite against both that build and the
|
|
386
|
+
committed package binary. The musl and aarch64 builds are the next step.
|
|
387
|
+
|
|
388
|
+
- Added **`wsl-shell.agentBackend`**: `auto` (the default) uses the native
|
|
389
|
+
in-distro agent when a verified source is available — the development
|
|
390
|
+
override `DSH_WSL_AGENT_NATIVE`, and later the packaged per-arch binary —
|
|
391
|
+
else the shipped shell agent; `native` fails the first command instead of
|
|
392
|
+
falling back; `shell` forces the shell agent. Commands and confined writes
|
|
393
|
+
share the session runtime, so both follow the selection. With no source and
|
|
394
|
+
the default, behavior is unchanged (shell).
|
|
395
|
+
|
|
396
|
+
- Added the opt-in **native agent host wiring**: `DSH_WSL_AGENT_NATIVE=<host
|
|
397
|
+
path>` makes the session runtime sha256 the binary into
|
|
398
|
+
`~/.cache/dsh-wsl/agent/<sha>/dsh-wsl-agent` (never from `/mnt/c`), verify
|
|
399
|
+
the distro-side digest, and spawn it under the session scope/bwrap with USR1
|
|
400
|
+
off (the native reads KILL frames). The shared FS-read agent, the one-shot
|
|
401
|
+
fallback and the default remain the shell agent; only x86_64 is accepted.
|
|
402
|
+
`scripts/native-wiring-check.mjs` verifies the copy + real runtime + EXEC/FS
|
|
403
|
+
end to end. Packaging/musl/default rollout is still pending.
|
|
404
|
+
|
|
405
|
+
- Added the **native agent spike** (ADR 0002 stage 2) under
|
|
406
|
+
`agent/native/spike`: a std-only Rust event loop for HELLO/PING/SHUTDOWN/
|
|
407
|
+
SETENV/EXEC/KILL with per-request process groups, plus the FS
|
|
408
|
+
`stat`/`lstat`/`read`/`write`/`list`/`realpath` surface (mode, no-replace,
|
|
409
|
+
expected version, staging + fsync + atomic publication; the `dsh-fs` side
|
|
410
|
+
channel in the RES stderr). Its manual check passes 16/16 (concurrent EXECs,
|
|
411
|
+
KILL without a second `wsl.exe`, timeout group kill, output cut with the pipe
|
|
412
|
+
still drained, FS stat/lstat/read/list/realpath matching the shell,
|
|
413
|
+
write/refusal, side channel, dot-dot refusal, expected-version stale replace,
|
|
414
|
+
setuid mode, EXEC differential). The host wiring remains; the shell agent
|
|
415
|
+
stays the shipped peer.
|
|
416
|
+
|
|
417
|
+
- Added **ADR 0002 — native in-distro agent (design)**: goals and non-goals,
|
|
418
|
+
language/distribution options (Rust static binaries, copied to the user's
|
|
419
|
+
cache), the v5-compatible spike and v6 concurrency protocol, the event-loop
|
|
420
|
+
process model, the host integration/copy/verify path, the security and test
|
|
421
|
+
matrix, the migration stages, and the triggers that would justify building
|
|
422
|
+
it. Implementation is deferred; the shell agent remains the shipped peer.
|
|
423
|
+
|
|
424
|
+
- Documented the one-shot fallback's timeout boundary: the host deadline kills
|
|
425
|
+
`wsl.exe`, not the distro-side tree; the session runtime's scope is the
|
|
426
|
+
cleanup path, and a systemd-scoped one-shot is not shipped.
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
- Gates on the new pins: style 9/9, syntax 7/7 members and 3/3 chain members
|
|
430
|
+
against `schemastery@3.18.5`, build freshness 74 artifacts, `tsc --noEmit`
|
|
431
|
+
clean, unit suite 30/30 files, coverage 98.42% lines / 84.20% branches /
|
|
432
|
+
96.01% functions. The real-machine probes were not re-run: they need
|
|
433
|
+
Windows and WSL, and their peer-verify legs are covered by the byte
|
|
434
|
+
comparison above.
|
|
435
|
+
|
|
19
436
|
## [0.9.0] - 2026-10-06
|
|
20
437
|
|
|
21
438
|
A usability round over the sandbox failure path: the failure stops being a
|
package/CONTRIBUTING.md
CHANGED
|
@@ -24,8 +24,8 @@ 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
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.
|
|
28
|
-
at `~4.0.
|
|
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
29
|
peer classes and mirrors peer protocol, so raising one pin alone is how the
|
|
30
30
|
seams drift: bump them in the commit that matches a harness release, re-run the
|
|
31
31
|
comparison `docs/PEER-PARITY.md` describes, and let the gate chain and the
|