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.
Files changed (57) hide show
  1. package/CHANGELOG.md +167 -14
  2. package/CONTRIBUTING.md +25 -33
  3. package/README.md +10 -10
  4. package/SECURITY.md +6 -4
  5. package/SUPPORT.md +8 -8
  6. package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
  7. package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
  8. package/agent/wsl-agent.sh +4 -5
  9. package/cordis.patch.yml +8 -1
  10. package/docs/ARCHITECTURE.md +42 -35
  11. package/docs/COMPATIBILITY.md +121 -0
  12. package/docs/CONFIGURATION.md +254 -272
  13. package/docs/LIMITATIONS.md +78 -35
  14. package/docs/MIGRATION.md +16 -13
  15. package/docs/PARITY.md +4 -3
  16. package/docs/PERFORMANCE.md +11 -9
  17. package/docs/QUICKSTART.md +60 -17
  18. package/docs/README.zh.md +7 -7
  19. package/docs/RELEASING.md +257 -0
  20. package/docs/REQUIREMENTS.zh.md +77 -0
  21. package/docs/ROADMAP-1.0.md +105 -61
  22. package/docs/ROADMAP.md +15 -9
  23. package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +3 -3
  24. package/docs/UPSTREAM-FSIO-EXPORT.md +7 -7
  25. package/docs/UPSTREAM-OPEN-IN-APP.md +57 -0
  26. package/docs/UPSTREAM-SESSION-NOTICE.md +4 -4
  27. package/docs/VALUE.zh.md +144 -0
  28. package/docs/adr/0002-native-agent.md +348 -0
  29. package/docs/root-fs-routing.md +186 -0
  30. package/docs/screenshots/picker.png +0 -0
  31. package/docs/screenshots/preview.png +0 -0
  32. package/docs/screenshots/sandbox.png +0 -0
  33. package/docs/screenshots/terminal.png +0 -0
  34. package/lib/agent-confined.d.ts +29 -23
  35. package/lib/agent-confined.js +58 -25
  36. package/lib/agent-exec.d.ts +2 -3
  37. package/lib/agent-shared.js +1 -0
  38. package/lib/agent.d.ts +19 -8
  39. package/lib/agent.js +73 -13
  40. package/lib/fs-routing.d.ts +2 -1
  41. package/lib/fs-routing.js +13 -1
  42. package/lib/fs-substrate.d.ts +11 -9
  43. package/lib/fs-substrate.js +6 -9
  44. package/lib/fsio-agent.d.ts +2 -3
  45. package/lib/fsio-agent.js +2 -3
  46. package/lib/index.d.ts +77 -13
  47. package/lib/index.js +63 -13
  48. package/lib/listing.d.ts +19 -12
  49. package/lib/listing.js +34 -19
  50. package/lib/picker.js +4 -4
  51. package/lib/session-runtime.d.ts +5 -3
  52. package/lib/session-runtime.js +5 -4
  53. package/locale/en.json +1 -1
  54. package/locale/zh.json +1 -1
  55. package/package.json +13 -4
  56. package/scripts/diagnose.sh +1 -1
  57. 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.maskWindowsDrive })
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 key = SessionRuntimes.key({ distro, workspaceRoot, mode, maskWindowsDrive: config.maskWindowsDrive });
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.maskWindowsDrive,
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 as SETENV frames immediately before the command's EXEC, so
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 (distro, workspace,
1035
- * policy) — instead of a per-policy confined resident. The kernel
1036
- * enforcement is the runtime's mount profile, so commands and writes see
1037
- * the same `/tmp` and workspace; the scope stops when the session's
1038
- * providers release it. Requires systemd + `Linger`, like `wsl-shell`'s
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 key = SessionRuntimes.key({ distro, workspaceRoot, mode: policy.mode, maskWindowsDrive: config.maskWindowsDrive });
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 one syscall before the
1712
- * rename and the confined resident enforces it in the kernel.
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), and `-p` marks directories with the trailing
90
- * `/` the parser keys on.
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 lsListingArgv(linuxDir: string): string[];
101
+ export declare function dirListingArgv(linuxDir: string): string[];
96
102
  /**
97
- * Parse one `ls -1ALp` listing into the picker's rows.
103
+ * Parse one `find -print0` listing into the picker's rows.
98
104
  *
99
- * A line is a row when it ends with `/` — a directory, or a symlink `-L`
100
- * resolved to one; files and dangling links are skipped, matching the host
101
- * path's behaviour. The parse reads the listing as UTF-8 lines, so a filename
102
- * containing a newline would split into two rows — the same limitation class
103
- * the one-shot transport has always had, accepted here with the 9p walk it
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 parseLsListing(stdout: string | Buffer, parent: string, maxEntries: number): {
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), and `-p` marks directories with the trailing
133
- * `/` the parser keys on.
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 lsListingArgv(linuxDir) {
139
- return ["ls", "-1ALp", "--", linuxDir];
144
+ export function dirListingArgv(linuxDir) {
145
+ return ["find", "-L", linuxDir, "-maxdepth", "1", "-mindepth", "1", "-type", "d", "-print0"];
140
146
  }
141
147
  /**
142
- * Parse one `ls -1ALp` listing into the picker's rows.
148
+ * Parse one `find -print0` listing into the picker's rows.
143
149
  *
144
- * A line is a row when it ends with `/` — a directory, or a symlink `-L`
145
- * resolved to one; files and dangling links are skipped, matching the host
146
- * path's behaviour. The parse reads the listing as UTF-8 lines, so a filename
147
- * containing a newline would split into two rows — the same limitation class
148
- * the one-shot transport has always had, accepted here with the 9p walk it
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 parseLsListing(stdout, parent, maxEntries) {
157
- const rows = [];
158
- let truncated = false;
159
- for (const line of String(stdout).split("\n")) {
160
- if (!line.endsWith("/"))
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 = line.slice(0, -1);
163
- if (name.length === 0)
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, fullyQualified, lsListingArgv, parseLsListing } from "./listing.js";
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: lsListingArgv(linuxDir),
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", ...lsListingArgv(linuxDir)], signal);
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 parseLsListing(stdout, target, this.config.maxEntries);
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
@@ -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
  *
@@ -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": "Runs a DeepSeek Harness session against a WSL distro: shell, file reads/writes and command execution happen inside the distro, the directory picker and the GUI terminal target distro paths, and commands are confined by in-distro bubblewrap."
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": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离"
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.14.0",
4
- "description": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离",
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",
@@ -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 -- [distro] [--json]
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.