dsh-plugin-wsl-env 0.14.0 → 0.16.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 +167 -14
- package/CONTRIBUTING.md +25 -33
- package/README.md +10 -10
- package/SECURITY.md +6 -4
- package/SUPPORT.md +8 -8
- 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 +4 -5
- package/cordis.patch.yml +8 -1
- package/docs/ARCHITECTURE.md +42 -35
- package/docs/COMPATIBILITY.md +121 -0
- package/docs/CONFIGURATION.md +254 -272
- package/docs/LIMITATIONS.md +78 -35
- package/docs/MIGRATION.md +16 -13
- package/docs/PARITY.md +4 -3
- package/docs/PERFORMANCE.md +11 -9
- package/docs/QUICKSTART.md +60 -17
- package/docs/README.zh.md +7 -7
- package/docs/RELEASING.md +257 -0
- package/docs/REQUIREMENTS.zh.md +77 -0
- package/docs/ROADMAP-1.0.md +105 -61
- package/docs/ROADMAP.md +15 -9
- package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +3 -3
- package/docs/UPSTREAM-FSIO-EXPORT.md +7 -7
- package/docs/UPSTREAM-OPEN-IN-APP.md +57 -0
- package/docs/UPSTREAM-SESSION-NOTICE.md +4 -4
- package/docs/VALUE.zh.md +144 -0
- package/docs/adr/0002-native-agent.md +348 -0
- package/docs/root-fs-routing.md +186 -0
- package/docs/screenshots/picker.png +0 -0
- package/docs/screenshots/preview.png +0 -0
- package/docs/screenshots/sandbox.png +0 -0
- package/docs/screenshots/terminal.png +0 -0
- package/lib/agent-confined.d.ts +29 -23
- package/lib/agent-confined.js +58 -25
- package/lib/agent-exec.d.ts +2 -3
- package/lib/agent-shared.js +1 -0
- package/lib/agent.d.ts +19 -8
- package/lib/agent.js +73 -13
- package/lib/fs-routing.d.ts +2 -1
- package/lib/fs-routing.js +13 -1
- package/lib/fs-substrate.d.ts +11 -9
- package/lib/fs-substrate.js +6 -9
- package/lib/fsio-agent.d.ts +2 -3
- package/lib/fsio-agent.js +2 -3
- package/lib/index.d.ts +77 -13
- package/lib/index.js +63 -13
- package/lib/listing.d.ts +19 -12
- package/lib/listing.js +34 -19
- package/lib/picker.js +4 -4
- package/lib/session-runtime.d.ts +5 -3
- package/lib/session-runtime.js +5 -4
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +13 -4
- package/scripts/diagnose.sh +1 -1
- package/scripts/doctor.mjs +83 -9
package/lib/index.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
* @module dsh-plugin-wsl
|
|
41
41
|
*/
|
|
42
42
|
import z from "@deepseek-ai/schemastery";
|
|
43
|
+
import { randomUUID } from "node:crypto";
|
|
43
44
|
import { LocalBashExecutor } from "@deepseek-ai/dsh-bash-local";
|
|
44
45
|
import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
|
|
45
46
|
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
@@ -159,6 +160,29 @@ function wslWritableRoots(policy, distro) {
|
|
|
159
160
|
}
|
|
160
161
|
return [...roots];
|
|
161
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* The WSL shell executor's validated config — the keys of
|
|
165
|
+
* {@link WslShellExecutor.Config}, which the upstream `LocalBashExecutor`
|
|
166
|
+
* config type does not know. The seven volatile fields arrive as live
|
|
167
|
+
* wrappers, so they are spelled as their `.get()` shape (the reading
|
|
168
|
+
* {@link configuredCwd} and `hostCwd` perform).
|
|
169
|
+
*/
|
|
170
|
+
/**
|
|
171
|
+
* The effective `/mnt` mask for a config: `interopVisibility` is the stance
|
|
172
|
+
* key (WSL-314) and takes precedence whenever it names a stance; the legacy
|
|
173
|
+
* `maskWindowsDrive` boolean answers only when the stance key is left at its
|
|
174
|
+
* `visible` default. `disabled` masks in-session — the belt — while the
|
|
175
|
+
* distro-level closure (`[interop] enabled=false` + `wsl --shutdown`)
|
|
176
|
+
* remains a documented, user-applied step a config key must not perform.
|
|
177
|
+
*
|
|
178
|
+
* @param config - anything carrying the two keys.
|
|
179
|
+
* @returns whether confined profiles shadow `/mnt`.
|
|
180
|
+
*/
|
|
181
|
+
export function effectiveMaskWindowsDrive(config) {
|
|
182
|
+
if (config.interopVisibility !== undefined)
|
|
183
|
+
return config.interopVisibility !== "visible";
|
|
184
|
+
return config.maskWindowsDrive === true;
|
|
185
|
+
}
|
|
162
186
|
const inheritedSpawnSpec = LocalBashExecutor.prototype.spawnSpec;
|
|
163
187
|
/**
|
|
164
188
|
* How long an `auto` distro that could not host a session runtime stays
|
|
@@ -257,6 +281,26 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
257
281
|
* complete closure is distro-level: `[interop] enabled=false` in
|
|
258
282
|
* `wsl.conf`.
|
|
259
283
|
*/
|
|
284
|
+
/**
|
|
285
|
+
* The interop stance in one key (WSL-314), expanding to the per-key
|
|
286
|
+
* values the measured matrix documents:
|
|
287
|
+
*
|
|
288
|
+
* - `visible` (default) — nothing masked; a confined command can read
|
|
289
|
+
* `/mnt` and launch Windows executables through interop, which escapes
|
|
290
|
+
* the Linux sandbox entirely.
|
|
291
|
+
* - `masked` — `maskWindowsDrive: true`: `/mnt` is shadowed with an empty
|
|
292
|
+
* tmpfs; the drive's files and executables are unreachable (binfmt can
|
|
293
|
+
* still run a workspace-written .exe, so enforcement stays `partial`).
|
|
294
|
+
* - `disabled` — `masked`, plus the distro-level closure: `[interop]
|
|
295
|
+
* enabled=false` in `/etc/wsl.conf` + `wsl --shutdown`, which the
|
|
296
|
+
* doctor's remedy prints as copy-paste (a config key does not write
|
|
297
|
+
* distro config; enforcement is the kernel's, not the schema's).
|
|
298
|
+
*
|
|
299
|
+
* When set at all it takes precedence over the legacy
|
|
300
|
+
* `maskWindowsDrive` boolean below (unset — the default — lets that
|
|
301
|
+
* boolean answer); the boolean remains for existing profiles.
|
|
302
|
+
*/
|
|
303
|
+
interopVisibility: z.union(["visible", "masked", "disabled"]),
|
|
260
304
|
maskWindowsDrive: z.boolean().default(false),
|
|
261
305
|
/**
|
|
262
306
|
* Default workdir for a request that names none; accepts `/home/andy/proj`,
|
|
@@ -307,11 +351,12 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
307
351
|
this.resolvedHomes = new Map();
|
|
308
352
|
this.mode = config.sandbox ? ctx.sandboxPolicy.defaultMode : undefined;
|
|
309
353
|
this.sandbox = config.sandbox
|
|
310
|
-
? new WslSandbox({ wslPath: config.wslPath, maskWindowsDrive: config
|
|
354
|
+
? new WslSandbox({ wslPath: config.wslPath, maskWindowsDrive: effectiveMaskWindowsDrive(config) })
|
|
311
355
|
: undefined;
|
|
312
356
|
this.processFacts = new Map();
|
|
313
357
|
this.sessionRuntimes = sessionRuntimes;
|
|
314
358
|
this.sessionRuntimeEntries = new Map();
|
|
359
|
+
this.sessionRuntimeOwner = randomUUID();
|
|
315
360
|
this.sessionScopeProbe = (argv) => runCapture(argv);
|
|
316
361
|
this.sessionRuntimePreflight = new Set();
|
|
317
362
|
this.autoSessionScope = new Map();
|
|
@@ -442,7 +487,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
442
487
|
}
|
|
443
488
|
/**
|
|
444
489
|
* The persistent session runtime for one policy, when `sessionSandbox` is
|
|
445
|
-
* on. The executor holds one reference per `(distro, workspace, policy)` and
|
|
490
|
+
* on. The executor holds one reference per `(session, distro, workspace, policy)` and
|
|
446
491
|
* releases them when the session's scope disposes.
|
|
447
492
|
*
|
|
448
493
|
* @param policy - the resolved file-effect policy, when the executor has one.
|
|
@@ -456,16 +501,18 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
456
501
|
return undefined;
|
|
457
502
|
const mode = policy?.mode ?? "danger-full-access";
|
|
458
503
|
const workspaceRoot = policy?.workspaceRoot ?? (configuredCwd(this.config) || resolved.workdir);
|
|
459
|
-
const
|
|
504
|
+
const sessionId = policy?.sessionId || resolved.dshEnv?.DSH_SESSION_ID || this.sessionRuntimeOwner;
|
|
505
|
+
const key = SessionRuntimes.key({ sessionId, distro, workspaceRoot, mode, maskWindowsDrive: effectiveMaskWindowsDrive(config) });
|
|
460
506
|
const held = this.sessionRuntimeEntries.get(key);
|
|
461
507
|
if (held !== undefined)
|
|
462
508
|
return held;
|
|
463
509
|
const entry = this.sessionRuntimes.acquire({
|
|
510
|
+
sessionId,
|
|
464
511
|
distro,
|
|
465
512
|
wslPath: config.wslPath,
|
|
466
513
|
workspaceRoot,
|
|
467
514
|
mode,
|
|
468
|
-
maskWindowsDrive: config
|
|
515
|
+
maskWindowsDrive: effectiveMaskWindowsDrive(config),
|
|
469
516
|
});
|
|
470
517
|
this.sessionRuntimeEntries.set(key, entry);
|
|
471
518
|
return entry;
|
|
@@ -833,7 +880,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
833
880
|
: confined !== undefined ? confined.argv : this.distroArgv(resolved, shell);
|
|
834
881
|
// The managed `DSH_*` snapshot merges last, exactly as the one-shot path's
|
|
835
882
|
// spawn env does (`ShellExecSpec`: `dshEnv` wins over `env`). The agent
|
|
836
|
-
// applies these
|
|
883
|
+
// applies these to the individual command, so
|
|
837
884
|
// the resident path now carries `DSH_HOME`, `DSH_SESSION_ID` and the
|
|
838
885
|
// `DSH_WSL_*` facts too — without this the whole registry was invisible on
|
|
839
886
|
// the default path while the one-shot fallback forwarded it.
|
|
@@ -1031,11 +1078,11 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1031
1078
|
sandbox: z.boolean().default(true),
|
|
1032
1079
|
/**
|
|
1033
1080
|
* Run confined mutations in the session's systemd user scope — the same
|
|
1034
|
-
* runtime the shell executor acquires for the same
|
|
1035
|
-
* policy
|
|
1036
|
-
*
|
|
1037
|
-
* the
|
|
1038
|
-
*
|
|
1081
|
+
* runtime the shell executor acquires for the same session, distro,
|
|
1082
|
+
* workspace and policy — instead of a per-policy confined resident. A
|
|
1083
|
+
* missing session ID keeps the providers separate. The kernel enforcement
|
|
1084
|
+
* is the runtime's mount profile; the scope stops when its providers
|
|
1085
|
+
* release it. Requires systemd + `Linger`, like `wsl-shell`'s
|
|
1039
1086
|
* `sessionSandbox`; the preflight refuses with the remedy. Off for direct
|
|
1040
1087
|
* constructions; on in the shipped preset.
|
|
1041
1088
|
*/
|
|
@@ -1104,6 +1151,7 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1104
1151
|
this.agentInstances = new Map();
|
|
1105
1152
|
this.sessionRuntimes = sessionRuntimes;
|
|
1106
1153
|
this.sessionRuntimeEntries = new Map();
|
|
1154
|
+
this.sessionRuntimeOwner = randomUUID();
|
|
1107
1155
|
this.sessionScopeProbe = (argv) => runCapture(argv);
|
|
1108
1156
|
this.sessionRuntimePreflight = new Set();
|
|
1109
1157
|
this.autoSessionScope = new Map();
|
|
@@ -1220,11 +1268,13 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1220
1268
|
if (!config.sessionSandbox)
|
|
1221
1269
|
return undefined;
|
|
1222
1270
|
const workspaceRoot = policy.workspaceRoot || config.cwd;
|
|
1223
|
-
const
|
|
1271
|
+
const sessionId = policy.sessionId || this.sessionRuntimeOwner;
|
|
1272
|
+
const key = SessionRuntimes.key({ sessionId, distro, workspaceRoot, mode: policy.mode, maskWindowsDrive: config.maskWindowsDrive });
|
|
1224
1273
|
const held = this.sessionRuntimeEntries.get(key);
|
|
1225
1274
|
if (held !== undefined)
|
|
1226
1275
|
return held;
|
|
1227
1276
|
const entry = this.sessionRuntimes.acquire({
|
|
1277
|
+
sessionId,
|
|
1228
1278
|
distro,
|
|
1229
1279
|
wslPath: config.wslPath,
|
|
1230
1280
|
workspaceRoot,
|
|
@@ -1708,8 +1758,8 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1708
1758
|
* Fence the write by the per-call policy, then publish through the
|
|
1709
1759
|
* substrate. The fence's fresh target is the one checked and written, so a
|
|
1710
1760
|
* refusal costs no I/O and cannot be raced by a swapped symlink; the guard
|
|
1711
|
-
* rides the write op, where the agent re-verifies it
|
|
1712
|
-
*
|
|
1761
|
+
* rides the write op, where the agent re-verifies it before the rename;
|
|
1762
|
+
* the confined resident enforces the mount boundary in the kernel.
|
|
1713
1763
|
*
|
|
1714
1764
|
* @param target - the resolved target.
|
|
1715
1765
|
* @param content - the full new file content.
|
package/lib/listing.d.ts
CHANGED
|
@@ -83,32 +83,39 @@ export declare function boundedInsert<T extends {
|
|
|
83
83
|
/**
|
|
84
84
|
* The distro-side argv one picker level is listed with.
|
|
85
85
|
*
|
|
86
|
-
* `-1` is one entry per line, `-A` keeps dot-entries but drops `.` and `..`,
|
|
87
86
|
* `-L` follows symlinks so a link to a directory reports as a directory and a
|
|
88
87
|
* broken link drops out (exactly what the per-entry `stat` the host path used
|
|
89
|
-
* to pay one 9p round trip for)
|
|
90
|
-
*
|
|
88
|
+
* to pay one 9p round trip for); `-maxdepth 1` keeps the listing to one level;
|
|
89
|
+
* `-mindepth 1` drops the start point the way `ls -A` dropped `.` and `..`;
|
|
90
|
+
* `-type d` keeps only enterable rows; `-print0` separates entries with NUL —
|
|
91
|
+
* a filename containing a newline arrives whole instead of splitting into two
|
|
92
|
+
* rows, which is the bug that retired the `ls -1ALp` line parse (WSL-306).
|
|
93
|
+
* `find` is unsorted; the parser sorts.
|
|
94
|
+
*
|
|
95
|
+
* Busybox find (Alpine) supports every flag here — verified on alpine:3,
|
|
96
|
+
* debian:stable-slim and fedora:latest with a newline-named fixture.
|
|
91
97
|
*
|
|
92
98
|
* @param linuxDir - the absolute Linux directory to list.
|
|
93
99
|
* @returns the argv for the agent or a `wsl.exe --exec` launch.
|
|
94
100
|
*/
|
|
95
|
-
export declare function
|
|
101
|
+
export declare function dirListingArgv(linuxDir: string): string[];
|
|
96
102
|
/**
|
|
97
|
-
* Parse one `
|
|
103
|
+
* Parse one `find -print0` listing into the picker's rows.
|
|
98
104
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* replaces.
|
|
105
|
+
* Entries arrive NUL-separated as `<linuxDir>/<name>`; the linuxDir prefix is
|
|
106
|
+
* stripped to recover the bare name (the root listing's `/name` form
|
|
107
|
+
* included). `find` is unsorted, so the names are sorted host-side — a
|
|
108
|
+
* deterministic code-point order, which is what the picker shows. The first
|
|
109
|
+
* `maxEntries` names become rows; a level with more is flagged truncated.
|
|
105
110
|
*
|
|
106
111
|
* @param stdout - the listing's raw stdout.
|
|
112
|
+
* @param linuxDir - the absolute Linux directory that was listed (the argv's
|
|
113
|
+
* own spelling — the prefix stripped from every entry).
|
|
107
114
|
* @param parent - the listed directory, in the UNC form the rows carry.
|
|
108
115
|
* @param maxEntries - the row bound; a level with more is flagged truncated.
|
|
109
116
|
* @returns the first `maxEntries` rows and whether the level had more.
|
|
110
117
|
*/
|
|
111
|
-
export declare function
|
|
118
|
+
export declare function parseDirListing(stdout: string | Buffer, linuxDir: string, parent: string, maxEntries: number): {
|
|
112
119
|
rows: ListingRow[];
|
|
113
120
|
truncated: boolean;
|
|
114
121
|
};
|
package/lib/listing.js
CHANGED
|
@@ -126,42 +126,57 @@ export function boundedInsert(window, candidate, keep) {
|
|
|
126
126
|
/**
|
|
127
127
|
* The distro-side argv one picker level is listed with.
|
|
128
128
|
*
|
|
129
|
-
* `-1` is one entry per line, `-A` keeps dot-entries but drops `.` and `..`,
|
|
130
129
|
* `-L` follows symlinks so a link to a directory reports as a directory and a
|
|
131
130
|
* broken link drops out (exactly what the per-entry `stat` the host path used
|
|
132
|
-
* to pay one 9p round trip for)
|
|
133
|
-
*
|
|
131
|
+
* to pay one 9p round trip for); `-maxdepth 1` keeps the listing to one level;
|
|
132
|
+
* `-mindepth 1` drops the start point the way `ls -A` dropped `.` and `..`;
|
|
133
|
+
* `-type d` keeps only enterable rows; `-print0` separates entries with NUL —
|
|
134
|
+
* a filename containing a newline arrives whole instead of splitting into two
|
|
135
|
+
* rows, which is the bug that retired the `ls -1ALp` line parse (WSL-306).
|
|
136
|
+
* `find` is unsorted; the parser sorts.
|
|
137
|
+
*
|
|
138
|
+
* Busybox find (Alpine) supports every flag here — verified on alpine:3,
|
|
139
|
+
* debian:stable-slim and fedora:latest with a newline-named fixture.
|
|
134
140
|
*
|
|
135
141
|
* @param linuxDir - the absolute Linux directory to list.
|
|
136
142
|
* @returns the argv for the agent or a `wsl.exe --exec` launch.
|
|
137
143
|
*/
|
|
138
|
-
export function
|
|
139
|
-
return ["
|
|
144
|
+
export function dirListingArgv(linuxDir) {
|
|
145
|
+
return ["find", "-L", linuxDir, "-maxdepth", "1", "-mindepth", "1", "-type", "d", "-print0"];
|
|
140
146
|
}
|
|
141
147
|
/**
|
|
142
|
-
* Parse one `
|
|
148
|
+
* Parse one `find -print0` listing into the picker's rows.
|
|
143
149
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* replaces.
|
|
150
|
+
* Entries arrive NUL-separated as `<linuxDir>/<name>`; the linuxDir prefix is
|
|
151
|
+
* stripped to recover the bare name (the root listing's `/name` form
|
|
152
|
+
* included). `find` is unsorted, so the names are sorted host-side — a
|
|
153
|
+
* deterministic code-point order, which is what the picker shows. The first
|
|
154
|
+
* `maxEntries` names become rows; a level with more is flagged truncated.
|
|
150
155
|
*
|
|
151
156
|
* @param stdout - the listing's raw stdout.
|
|
157
|
+
* @param linuxDir - the absolute Linux directory that was listed (the argv's
|
|
158
|
+
* own spelling — the prefix stripped from every entry).
|
|
152
159
|
* @param parent - the listed directory, in the UNC form the rows carry.
|
|
153
160
|
* @param maxEntries - the row bound; a level with more is flagged truncated.
|
|
154
161
|
* @returns the first `maxEntries` rows and whether the level had more.
|
|
155
162
|
*/
|
|
156
|
-
export function
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
for (const
|
|
160
|
-
if (
|
|
163
|
+
export function parseDirListing(stdout, linuxDir, parent, maxEntries) {
|
|
164
|
+
const prefix = linuxDir === "/" ? "/" : `${linuxDir}/`;
|
|
165
|
+
const names = [];
|
|
166
|
+
for (const entry of String(stdout).split("\0")) {
|
|
167
|
+
if (entry.length === 0)
|
|
161
168
|
continue;
|
|
162
|
-
const name =
|
|
163
|
-
|
|
169
|
+
const name = entry.startsWith(prefix) ? entry.slice(prefix.length) : entry;
|
|
170
|
+
// A defensive guard, not an expected shape: `-maxdepth 1` output is
|
|
171
|
+
// always `<dir>/<name>`, so a stray separator means a broken listing.
|
|
172
|
+
if (name.length === 0 || name.includes("/"))
|
|
164
173
|
continue;
|
|
174
|
+
names.push(name);
|
|
175
|
+
}
|
|
176
|
+
names.sort();
|
|
177
|
+
const rows = [];
|
|
178
|
+
let truncated = false;
|
|
179
|
+
for (const name of names) {
|
|
165
180
|
if (rows.length === maxEntries) {
|
|
166
181
|
truncated = true;
|
|
167
182
|
break;
|
package/lib/picker.js
CHANGED
|
@@ -53,7 +53,7 @@ import { join, resolve } from "node:path";
|
|
|
53
53
|
import z from "@deepseek-ai/schemastery";
|
|
54
54
|
import { DirectoryPicker, DirectoryPickerError } from "@deepseek-ai/dsh-host-directory-picker";
|
|
55
55
|
import { UNC_PROVIDER_ROOT, distroRoot, isProviderRoot, isWslUnc, uncToPosix } from "./paths.js";
|
|
56
|
-
import { breadcrumbs, boundedInsert,
|
|
56
|
+
import { breadcrumbs, boundedInsert, dirListingArgv, fullyQualified, parseDirListing } from "./listing.js";
|
|
57
57
|
import { listDistros, linuxHome, runCapture } from "./wsl.js";
|
|
58
58
|
import { sharedAgent } from "./agent-shared.js";
|
|
59
59
|
import { AgentUnavailableError } from "./agent-errors.js";
|
|
@@ -321,7 +321,7 @@ export class WslDirectoryPicker extends DirectoryPicker {
|
|
|
321
321
|
try {
|
|
322
322
|
const result = await sharedAgent(parsed.distro).exec({
|
|
323
323
|
cwd: linuxDir,
|
|
324
|
-
argv:
|
|
324
|
+
argv: dirListingArgv(linuxDir),
|
|
325
325
|
timeoutMs: 15_000,
|
|
326
326
|
signal,
|
|
327
327
|
});
|
|
@@ -335,13 +335,13 @@ export class WslDirectoryPicker extends DirectoryPicker {
|
|
|
335
335
|
if (!(error instanceof AgentUnavailableError))
|
|
336
336
|
throw error;
|
|
337
337
|
try {
|
|
338
|
-
stdout = await runCapture([this.config.wslPath, "-d", parsed.distro, "--cd", linuxDir, "--exec", ...
|
|
338
|
+
stdout = await runCapture([this.config.wslPath, "-d", parsed.distro, "--cd", linuxDir, "--exec", ...dirListingArgv(linuxDir)], signal);
|
|
339
339
|
}
|
|
340
340
|
catch (fallbackError) {
|
|
341
341
|
throw new DirectoryPickerError("directory-unreadable", target, `cannot list ${target}: ${messageOf(fallbackError)}`);
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
|
-
return
|
|
344
|
+
return parseDirListing(stdout, linuxDir, target, this.config.maxEntries);
|
|
345
345
|
}
|
|
346
346
|
/**
|
|
347
347
|
* List one Windows level with the host's own walk — native I/O on a native
|
package/lib/session-runtime.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session-scoped runtime registry (ADR 0001).
|
|
3
3
|
*
|
|
4
|
-
* One long-lived agent per `(distro, workspaceRoot, policy)`, shared by the
|
|
4
|
+
* One long-lived agent per `(sessionId, distro, workspaceRoot, policy)`, shared by the
|
|
5
5
|
* providers that need it and refcounted: the first `acquire` spawns the agent
|
|
6
6
|
* inside a systemd user scope, the last `release` closes the agent and stops
|
|
7
7
|
* the scope (the cgroup then reaps any background dev server a command left
|
|
@@ -43,6 +43,8 @@ export declare function probeSessionRuntime({ wslPath, distro, run }: {
|
|
|
43
43
|
}): Promise<string | null>;
|
|
44
44
|
/** One request for a session runtime. */
|
|
45
45
|
export interface SessionRuntimeRequest {
|
|
46
|
+
/** The session identity; absent requests each get a separate runtime. */
|
|
47
|
+
sessionId?: string;
|
|
46
48
|
/** The distro the runtime lives in. */
|
|
47
49
|
distro: string;
|
|
48
50
|
/** The `wsl.exe` path; defaults to `wsl.exe`. */
|
|
@@ -125,14 +127,14 @@ export declare class SessionRuntimes {
|
|
|
125
127
|
*/
|
|
126
128
|
constructor(options?: SessionRuntimeOptions);
|
|
127
129
|
/**
|
|
128
|
-
* The key a request addresses: distro, workspace, mode and mask. A policy
|
|
130
|
+
* The key a request addresses: session, distro, workspace, mode and mask. A policy
|
|
129
131
|
* change is a different runtime, because the mount profile is fixed for the
|
|
130
132
|
* agent's lifetime.
|
|
131
133
|
*
|
|
132
134
|
* @param request - the runtime request.
|
|
133
135
|
* @returns the registry key.
|
|
134
136
|
*/
|
|
135
|
-
static key(request: Pick<SessionRuntimeRequest, "distro" | "workspaceRoot" | "mode" | "maskWindowsDrive">): string;
|
|
137
|
+
static key(request: Pick<SessionRuntimeRequest, "distro" | "workspaceRoot" | "mode" | "maskWindowsDrive" | "sessionId">): string;
|
|
136
138
|
/**
|
|
137
139
|
* Acquire the runtime for one key, creating it on first use.
|
|
138
140
|
*
|
package/lib/session-runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session-scoped runtime registry (ADR 0001).
|
|
3
3
|
*
|
|
4
|
-
* One long-lived agent per `(distro, workspaceRoot, policy)`, shared by the
|
|
4
|
+
* One long-lived agent per `(sessionId, distro, workspaceRoot, policy)`, shared by the
|
|
5
5
|
* providers that need it and refcounted: the first `acquire` spawns the agent
|
|
6
6
|
* inside a systemd user scope, the last `release` closes the agent and stops
|
|
7
7
|
* the scope (the cgroup then reaps any background dev server a command left
|
|
@@ -83,6 +83,7 @@ const spawnSessionAgent = (options) => {
|
|
|
83
83
|
idleMs: options.idleMs,
|
|
84
84
|
leaseMs: options.leaseMs,
|
|
85
85
|
signalable: false,
|
|
86
|
+
serialRequests: false,
|
|
86
87
|
spawnTransport: ({ wslPath, distro, argvPrefix }) => spawn(wslPath, ["-d", distro, "--exec", ...argvPrefix, native.path], {
|
|
87
88
|
stdio: ["pipe", "pipe", "pipe"],
|
|
88
89
|
windowsHide: true,
|
|
@@ -137,7 +138,7 @@ export class SessionRuntimes {
|
|
|
137
138
|
this.token = options.token ?? defaultToken;
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
140
|
-
* The key a request addresses: distro, workspace, mode and mask. A policy
|
|
141
|
+
* The key a request addresses: session, distro, workspace, mode and mask. A policy
|
|
141
142
|
* change is a different runtime, because the mount profile is fixed for the
|
|
142
143
|
* agent's lifetime.
|
|
143
144
|
*
|
|
@@ -145,7 +146,7 @@ export class SessionRuntimes {
|
|
|
145
146
|
* @returns the registry key.
|
|
146
147
|
*/
|
|
147
148
|
static key(request) {
|
|
148
|
-
return [request.distro, request.workspaceRoot, request.mode, request.maskWindowsDrive === true ? "masked" : "visible"].join("\u0000");
|
|
149
|
+
return [request.sessionId ?? "", request.distro, request.workspaceRoot, request.mode, request.maskWindowsDrive === true ? "masked" : "visible"].join("\u0000");
|
|
149
150
|
}
|
|
150
151
|
/**
|
|
151
152
|
* Acquire the runtime for one key, creating it on first use.
|
|
@@ -154,7 +155,7 @@ export class SessionRuntimes {
|
|
|
154
155
|
* @returns the live entry; the caller holds one reference.
|
|
155
156
|
*/
|
|
156
157
|
acquire(request) {
|
|
157
|
-
const key = SessionRuntimes.key(request);
|
|
158
|
+
const key = SessionRuntimes.key({ ...request, sessionId: request.sessionId || defaultToken() });
|
|
158
159
|
const existing = this.runtimes.get(key);
|
|
159
160
|
if (existing !== undefined) {
|
|
160
161
|
existing.refs += 1;
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "WSL execution environment",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Opens the project workspace inside a WSL Linux instance: command execution, shell scripts and file access all run in the Linux environment for the full Linux shell toolchain, with commands confined by bubblewrap."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "WSL 执行环境",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "在 WSL 的 Linux 实例中打开项目工作区,在对应 Linux 空间中进行命令执行、运行 shell 脚本和文件访问,以获得全套 Linux Shell 工具链收益,并通过 bubblewrap 对命令进行沙箱隔离"
|
|
5
5
|
}
|
|
6
6
|
}
|
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.16.0",
|
|
4
|
+
"description": "在 WSL 的 Linux 实例中打开项目工作区,在对应 Linux 空间中进行命令执行、运行 shell 脚本和文件访问,以获得全套 Linux Shell 工具链收益,并通过 bubblewrap 对命令进行沙箱隔离",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
@@ -96,9 +96,17 @@
|
|
|
96
96
|
"README.md",
|
|
97
97
|
"docs/README.zh.md",
|
|
98
98
|
"docs/QUICKSTART.md",
|
|
99
|
+
"docs/screenshots/*.png",
|
|
99
100
|
"docs/PERFORMANCE.md",
|
|
101
|
+
"docs/COMPATIBILITY.md",
|
|
100
102
|
"docs/CONFIGURATION.md",
|
|
101
103
|
"docs/LIMITATIONS.md",
|
|
104
|
+
"docs/RELEASING.md",
|
|
105
|
+
"docs/REQUIREMENTS.zh.md",
|
|
106
|
+
"docs/root-fs-routing.md",
|
|
107
|
+
"docs/VALUE.zh.md",
|
|
108
|
+
"docs/UPSTREAM-OPEN-IN-APP.md",
|
|
109
|
+
"docs/adr/0002-native-agent.md",
|
|
102
110
|
"docs/PARITY.md",
|
|
103
111
|
"docs/PEER-PARITY.md",
|
|
104
112
|
"docs/UPSTREAM-FSIO-EXPORT.md",
|
|
@@ -125,9 +133,9 @@
|
|
|
125
133
|
"lint:types": "tsc --noEmit",
|
|
126
134
|
"build": "node scripts/build.mjs",
|
|
127
135
|
"lint:build": "node test/build-freshness.mjs",
|
|
128
|
-
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 --test-coverage-exclude='scripts/**' test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/shell.test.mjs test/shell-env.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs test/distro-route.test.mjs test/fs-routing.test.mjs test/doctor.test.mjs test/profile-preset.test.mjs test/packaging.test.mjs test/peer-canary.test.mjs",
|
|
136
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 --test-coverage-exclude='scripts/**' --test-coverage-exclude='test/**' test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/shell.test.mjs test/shell-env.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs test/distro-route.test.mjs test/fs-routing.test.mjs test/doctor.test.mjs test/profile-preset.test.mjs test/packaging.test.mjs test/peer-canary.test.mjs test/probe-runner.test.mjs",
|
|
129
137
|
"test:syntax": "node test/syntax.mjs",
|
|
130
|
-
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/shell-env.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs test/distro-route.test.mjs test/fs-routing.test.mjs test/doctor.test.mjs test/profile-preset.test.mjs test/packaging.test.mjs test/peer-canary.test.mjs",
|
|
138
|
+
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/shell-env.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs test/distro-route.test.mjs test/fs-routing.test.mjs test/doctor.test.mjs test/profile-preset.test.mjs test/packaging.test.mjs test/peer-canary.test.mjs test/probe-runner.test.mjs",
|
|
131
139
|
"lint:style": "node test/style.mjs",
|
|
132
140
|
"lint:links": "node scripts/check-links.mjs",
|
|
133
141
|
"prepublishOnly": "pnpm test",
|
|
@@ -139,6 +147,7 @@
|
|
|
139
147
|
"probe:terminal": "bash test/probe/terminal.sh",
|
|
140
148
|
"probe:sandbox": "bash test/probe/sandbox.sh",
|
|
141
149
|
"probe:sandbox-shell": "bash test/probe/sandbox-shell.sh",
|
|
150
|
+
"probe:interop-shell": "bash test/probe/interop-shell.sh",
|
|
142
151
|
"sync:windows": "bash test/probe/sync-to-windows.sh",
|
|
143
152
|
"probe:watch": "bash test/probe/watch.sh",
|
|
144
153
|
"probe:agent": "bash test/probe/agent.sh",
|
package/scripts/diagnose.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# to change the machine. `--fix`/`--install` are refused here on purpose —
|
|
4
4
|
# diagnostics collected for a bug report must not repair anything.
|
|
5
5
|
#
|
|
6
|
-
# pnpm run diagnose
|
|
6
|
+
# pnpm run diagnose [distro] [--json]
|
|
7
7
|
#
|
|
8
8
|
# Doctor is `scripts/doctor.mjs`; see its header for what it covers. Exit status
|
|
9
9
|
# follows doctor: 0 when no check failed, 1 when one did.
|