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/agent.js CHANGED
@@ -41,7 +41,7 @@
41
41
  */
42
42
  import { spawn } from "node:child_process";
43
43
  import { AgentUnavailableError, BASH_TIMEOUT_REASON, CwdError, SpawnError } from "./agent-errors.js";
44
- import { AGENT_NAME, MAX_FRAME_BYTES, PROTOCOL_VERSION, encodeExecFrame, encodeFsFrame, encodeKill, encodeSetEnv, parseAgentLine } from "./agent-protocol.js";
44
+ import { AGENT_NAME, MAX_FRAME_BYTES, PROTOCOL_VERSION, encodeExecFrame, encodeFsFrame, encodeKill, parseAgentLine } from "./agent-protocol.js";
45
45
  /**
46
46
  * A zeroed per-kind metrics record.
47
47
  * @returns the record.
@@ -172,6 +172,7 @@ export class WslAgent {
172
172
  this.wslPath = config.wslPath ?? "wsl.exe";
173
173
  this.argvPrefix = config.argvPrefix ?? [];
174
174
  this.signalable = config.signalable ?? this.argvPrefix.length === 0;
175
+ this.serialRequests = config.serialRequests ?? true;
175
176
  this.idleMs = config.idleMs ?? 600000;
176
177
  this.expectedDigest = config.expectedDigest;
177
178
  this.frameCapBytes = config.frameCapBytes ?? MAX_FRAME_BYTES;
@@ -294,6 +295,8 @@ export class WslAgent {
294
295
  this.expectedDigest = entry.expectedDigest;
295
296
  if (entry.signalable !== undefined)
296
297
  this.signalable = entry.signalable;
298
+ if (entry.serialRequests !== undefined)
299
+ this.serialRequests = entry.serialRequests;
297
300
  if (entry.spawnTransport !== undefined)
298
301
  this.configuredSpawn = entry.spawnTransport;
299
302
  }
@@ -472,6 +475,7 @@ export class WslAgent {
472
475
  this.clearPending(pending);
473
476
  this.pending.delete(message.id);
474
477
  this.rearmIdleTimer();
478
+ this.dispatchQueued();
475
479
  // A cancelled request keeps its entry only until this answer arrives.
476
480
  if (pending.settled)
477
481
  break;
@@ -487,6 +491,7 @@ export class WslAgent {
487
491
  this.clearPending(pending);
488
492
  this.pending.delete(message.id);
489
493
  this.rearmIdleTimer();
494
+ this.dispatchQueued();
490
495
  if (pending.settled)
491
496
  break;
492
497
  pending.settled = true;
@@ -554,7 +559,14 @@ export class WslAgent {
554
559
  this.clearPending(pending);
555
560
  continue;
556
561
  }
557
- if (isReplayableFrame(pending.lines)) {
562
+ if (!pending.sent) {
563
+ this.clearPending(pending);
564
+ if (pending.settled)
565
+ continue;
566
+ pending.settled = true;
567
+ pending.reject(new AgentUnavailableError(`agent exited before request ${pending.id} was sent; it never ran`));
568
+ }
569
+ else if (isReplayableFrame(pending.lines)) {
558
570
  if (pending.watchdog) {
559
571
  clearTimeout(pending.watchdog);
560
572
  pending.watchdog = null;
@@ -572,12 +584,18 @@ export class WslAgent {
572
584
  this.start()
573
585
  .then(() => {
574
586
  for (const pending of relayable) {
587
+ if (pending.settled || pending.signal?.aborted)
588
+ continue;
575
589
  const id = `r${this.nextRequestId++}`;
576
590
  pending.id = id;
591
+ pending.seq = Number(id.slice(1));
592
+ pending.lines = relabelFrame(pending.lines, id);
593
+ pending.sent = false;
594
+ pending.started = false;
577
595
  this.pending.set(id, pending);
578
596
  // The rebuilt transport can itself be gone by now; a frame that
579
597
  // cannot be written must fail its caller, not strand the entry.
580
- if (!this.writeLines(relabelFrame(pending.lines, id))) {
598
+ if (!this.serialRequests && !this.sendPending(pending)) {
581
599
  this.clearPending(pending);
582
600
  this.pending.delete(id);
583
601
  if (pending.settled)
@@ -586,6 +604,7 @@ export class WslAgent {
586
604
  pending.reject(new AgentUnavailableError(`agent exited before request ${id} could be re-sent; its state is unknown`));
587
605
  }
588
606
  }
607
+ this.dispatchQueued();
589
608
  })
590
609
  .catch((error) => {
591
610
  for (const pending of relayable) {
@@ -645,6 +664,7 @@ export class WslAgent {
645
664
  signal: signal ?? null,
646
665
  onAbort: null,
647
666
  started: false,
667
+ sent: false,
648
668
  cancelled: false,
649
669
  settled: false,
650
670
  answered: false,
@@ -681,6 +701,14 @@ export class WslAgent {
681
701
  }
682
702
  pending.onAbort = () => {
683
703
  pending.cancelled = true;
704
+ if (!pending.sent) {
705
+ this.pending.delete(pending.id);
706
+ this.clearPending(pending);
707
+ pending.settled = true;
708
+ pending.reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
709
+ this.rearmIdleTimer();
710
+ return;
711
+ }
684
712
  // The frame is the fallback for an agent that cannot be signalled (a
685
713
  // confined resident, a build without the USR1 trap); the signal is what
686
714
  // interrupts the command the agent is blocked on right now.
@@ -709,7 +737,10 @@ export class WslAgent {
709
737
  // dropping it used to strand the request: nothing would ever answer, the
710
738
  // watchdog (armed on an ACK that will not come) had no process to kill,
711
739
  // and with no timeout the caller waited forever.
712
- if (!this.writeLines(lines)) {
740
+ if (this.serialRequests) {
741
+ this.dispatchQueued();
742
+ }
743
+ else if (!this.sendPending(pending)) {
713
744
  this.clearPending(pending);
714
745
  this.pending.delete(pending.id);
715
746
  this.noteRejected(kind);
@@ -718,6 +749,30 @@ export class WslAgent {
718
749
  }
719
750
  this.rearmIdleTimer();
720
751
  }
752
+ /** Send a tracked request, recording dispatch before synchronous responses. */
753
+ sendPending(pending) {
754
+ pending.sent = true;
755
+ return this.writeLines(pending.lines);
756
+ }
757
+ /** Dispatch the next shell request only after the preceding answer. */
758
+ dispatchQueued() {
759
+ if (!this.serialRequests || [...this.pending.values()].some((pending) => pending.sent))
760
+ return;
761
+ const pending = this.pending.values().next().value;
762
+ if (pending === undefined)
763
+ return;
764
+ if (this.sendPending(pending))
765
+ return;
766
+ this.clearPending(pending);
767
+ this.pending.delete(pending.id);
768
+ if (!pending.settled) {
769
+ pending.settled = true;
770
+ pending.measured = true;
771
+ this.noteRejected(pending.kind);
772
+ pending.reject(new AgentUnavailableError("the agent exited before the request could be sent; it never ran"));
773
+ }
774
+ this.dispatchQueued();
775
+ }
721
776
  /**
722
777
  * Run one command inside the distro through the agent.
723
778
  *
@@ -726,12 +781,19 @@ export class WslAgent {
726
781
  * @throws {AgentUnavailableError} when the agent is out for this process.
727
782
  */
728
783
  async exec({ cwd, argv, timeoutMs = 0, maxOutputBytes = 0, env, signal }) {
784
+ const assignments = Object.entries(env ?? {}).map(([key, value]) => {
785
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || value.includes("\0")) {
786
+ throw new Error(`invalid environment variable: ${key}`);
787
+ }
788
+ return `${key}=${value}`;
789
+ });
790
+ if (assignments.length > 0 && argv.length === 0)
791
+ throw new Error("cannot execute an empty command");
729
792
  await this.start();
730
793
  const id = `r${this.nextRequestId++}`;
731
- const lines = [
732
- ...(env ? Object.entries(env).map(([key, value]) => encodeSetEnv(key, value)) : []),
733
- ...encodeExecFrame({ id, cwd, argv, timeoutMs, maxOutputBytes }),
734
- ];
794
+ const commandArgv = argv[0]?.includes("=") ? ["/bin/sh", "-c", "exec \"$@\"", "env", ...argv] : argv;
795
+ const requestArgv = assignments.length > 0 ? ["env", "--", ...assignments, ...commandArgv] : argv;
796
+ const lines = encodeExecFrame({ id, cwd, argv: requestArgv, timeoutMs, maxOutputBytes });
735
797
  return new Promise((resolve, reject) => {
736
798
  this.track({ id, kind: "exec", lines, resolve, reject, timeoutMs, signal });
737
799
  });
@@ -786,7 +848,7 @@ export class WslAgent {
786
848
  * @param signal - the signal to deliver (`SIGUSR1` is the cancel trap).
787
849
  */
788
850
  /**
789
- * The cumulative request metrics: queue wait (sent -> ACK) and execution
851
+ * The cumulative request metrics: queue wait (submission -> ACK) and execution
790
852
  * time (ACK -> settle) per request kind, plus the peak in-flight count.
791
853
  *
792
854
  * @returns a snapshot the caller can keep.
@@ -1024,8 +1086,7 @@ const REPLAYABLE_FS_OPS = new Set(["stat", "lstat", "list", "realpath", "read"])
1024
1086
  * Whether a stored request frame's first execution provably had no side
1025
1087
  * effects, so the frame may ride the one permitted rebuild. An EXEC never
1026
1088
  * qualifies — a command's effects are unknowable after the agent died — and
1027
- * neither does an FS `write`. (A SETENV prefix line never decides: the frame
1028
- * header sits at `lines[0]` only for FS frames, which carry no SETENV.)
1089
+ * neither does an FS `write`.
1029
1090
  *
1030
1091
  * @param lines - the stored frame.
1031
1092
  * @returns true when the frame may be replayed.
@@ -1039,8 +1100,7 @@ function isReplayableFrame(lines) {
1039
1100
  }
1040
1101
  /**
1041
1102
  * Rewrite the request id inside a stored frame so a resent request does not
1042
- * collide with ids the new process has already seen. SETENV lines pass
1043
- * through unchanged (they are id-less and idempotent).
1103
+ * collide with ids the new process has already seen.
1044
1104
  *
1045
1105
  * @param lines - the stored frame.
1046
1106
  * @param newId - the new request id.
@@ -30,6 +30,7 @@
30
30
  *
31
31
  * @module dsh-plugin-wsl/fs-routing
32
32
  */
33
+ import z from "@deepseek-ai/schemastery";
33
34
  import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
34
35
  import { WslFileSystem } from "./index.js";
35
36
  import type { Context } from "@deepseek-ai/cordis";
@@ -91,7 +92,7 @@ export declare function inputIsDistro(value: string | undefined): boolean;
91
92
  * mode the distro side reports through `sandboxMode`).
92
93
  */
93
94
  export declare class WslRoutingFileSystem extends LocalFileSystem {
94
- static Config: import("@deepseek-ai/schemastery").default<import("@deepseek-ai/dsh-fs-local").Config>;
95
+ static Config: z<RoutingConfig>;
95
96
  /**
96
97
  * The routed backend's constructor reads the deployment default mode from
97
98
  * `ctx.sandboxPolicy`, exactly as the shipped `fs-sandbox` row this class
package/lib/fs-routing.js CHANGED
@@ -31,6 +31,7 @@
31
31
  * @module dsh-plugin-wsl/fs-routing
32
32
  */
33
33
  import { isWslUnc, uncToPosix } from "./paths.js";
34
+ import z from "@deepseek-ai/schemastery";
34
35
  import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
35
36
  import { WslFileSystem } from "./index.js";
36
37
  /**
@@ -101,7 +102,18 @@ export function inputIsDistro(value) {
101
102
  * mode the distro side reports through `sandboxMode`).
102
103
  */
103
104
  export class WslRoutingFileSystem extends LocalFileSystem {
104
- static Config = LocalFileSystem.Config;
105
+ static Config = z.intersect([
106
+ LocalFileSystem.Config,
107
+ z.object({
108
+ distro: z.string().default(""),
109
+ wslPath: z.string().default("wsl.exe"),
110
+ distroCwd: z.string().default(""),
111
+ sandbox: z.boolean().default(true),
112
+ maskWindowsDrive: z.boolean().default(false),
113
+ restrictToDistro: z.boolean().default(true),
114
+ watchMaxDepth: z.natural().default(0),
115
+ }),
116
+ ]);
105
117
  /**
106
118
  * The routed backend's constructor reads the deployment default mode from
107
119
  * `ctx.sandboxPolicy`, exactly as the shipped `fs-sandbox` row this class
@@ -13,12 +13,9 @@
13
13
  * create publishes with the distro-side no-replace link, a real atomic
14
14
  * primitive that refuses to replace. An overwrite or edit carries the
15
15
  * version it was based on INTO the write op, and the agent re-verifies it
16
- * with a fresh stat one syscall before the rename: a concurrent writer
17
- * (an editor save, `git stash`, another session) wins, and the stale
18
- * write refuses with `FS_STALE_VERSION` instead of clobbering. That
19
- * narrows the share backend's check-then-write window from a host round
20
- * trip to kernel-adjacent — the honest wording; "closed" was true only
21
- * for the create case.
16
+ * with a fresh stat before the rename: an earlier conflicting write is
17
+ * refused with `FS_STALE_VERSION`. A writer between that stat and the
18
+ * rename can still be overwritten; only guarded create is atomic.
22
19
  * 2. A symlink needs no retry machinery: every identity is resolved where the
23
20
  * symlinks live.
24
21
  *
@@ -59,6 +56,7 @@ export declare class AgentSubstrate {
59
56
  agentFor: ((policy: {
60
57
  mode: string;
61
58
  workspaceRoot: string;
59
+ sessionId?: string;
62
60
  } | undefined) => Promise<WslAgent>) | undefined;
63
61
  /** The distro-side filesystem face. */
64
62
  fs: DistroFs;
@@ -83,6 +81,7 @@ export declare class AgentSubstrate {
83
81
  agentFor?: (policy: {
84
82
  mode: string;
85
83
  workspaceRoot: string;
84
+ sessionId?: string;
86
85
  } | undefined) => Promise<WslAgent>;
87
86
  });
88
87
  /**
@@ -95,6 +94,7 @@ export declare class AgentSubstrate {
95
94
  mutationAgent(policy: {
96
95
  mode: string;
97
96
  workspaceRoot: string;
97
+ sessionId?: string;
98
98
  } | undefined): Promise<WslAgent>;
99
99
  /**
100
100
  * Run `op` with exclusive access to `targetKey` (FIFO per key).
@@ -204,7 +204,7 @@ export declare class AgentSubstrate {
204
204
  * guard, best-effort diff basis, atomic publication, outcome shape. The
205
205
  * guard is FORWARDED — a guarded create publishes with the distro-side
206
206
  * no-replace link (atomic), and a `replaceIfVersion` write carries its
207
- * version into the op, where the agent re-verifies it one syscall before
207
+ * version into the op, where the agent re-verifies it before
208
208
  * the rename and refuses with `FS_STALE_VERSION` on a mismatch.
209
209
  *
210
210
  * @param target - the resolved target.
@@ -220,6 +220,7 @@ export declare class AgentSubstrate {
220
220
  } | undefined, signal?: AbortSignal, policy?: {
221
221
  mode: string;
222
222
  workspaceRoot: string;
223
+ sessionId?: string;
223
224
  } | undefined): Promise<{
224
225
  operation: string;
225
226
  version: string;
@@ -230,8 +231,8 @@ export declare class AgentSubstrate {
230
231
  * Edit a text file with the host backend's orchestration: probe, version
231
232
  * guard, LF-normalized read, literal replacement, line-ending restore,
232
233
  * atomic publication, outcome shape. The version the edit was based on
233
- * rides the write op and is re-verified distro-side one syscall before the
234
- * rename, so a concurrent writer wins instead of being clobbered.
234
+ * rides the write op and is re-verified distro-side before the
235
+ * rename, narrowing but not eliminating the concurrent-write race.
235
236
  *
236
237
  * @param target - the resolved target.
237
238
  * @param edit - the literal request.
@@ -249,6 +250,7 @@ export declare class AgentSubstrate {
249
250
  } | undefined, signal?: AbortSignal, policy?: {
250
251
  mode: string;
251
252
  workspaceRoot: string;
253
+ sessionId?: string;
252
254
  } | undefined): Promise<{
253
255
  version: string;
254
256
  before: string;
@@ -13,12 +13,9 @@
13
13
  * create publishes with the distro-side no-replace link, a real atomic
14
14
  * primitive that refuses to replace. An overwrite or edit carries the
15
15
  * version it was based on INTO the write op, and the agent re-verifies it
16
- * with a fresh stat one syscall before the rename: a concurrent writer
17
- * (an editor save, `git stash`, another session) wins, and the stale
18
- * write refuses with `FS_STALE_VERSION` instead of clobbering. That
19
- * narrows the share backend's check-then-write window from a host round
20
- * trip to kernel-adjacent — the honest wording; "closed" was true only
21
- * for the create case.
16
+ * with a fresh stat before the rename: an earlier conflicting write is
17
+ * refused with `FS_STALE_VERSION`. A writer between that stat and the
18
+ * rename can still be overwritten; only guarded create is atomic.
22
19
  * 2. A symlink needs no retry machinery: every identity is resolved where the
23
20
  * symlinks live.
24
21
  *
@@ -196,7 +193,7 @@ export class AgentSubstrate {
196
193
  * guard, best-effort diff basis, atomic publication, outcome shape. The
197
194
  * guard is FORWARDED — a guarded create publishes with the distro-side
198
195
  * no-replace link (atomic), and a `replaceIfVersion` write carries its
199
- * version into the op, where the agent re-verifies it one syscall before
196
+ * version into the op, where the agent re-verifies it before
200
197
  * the rename and refuses with `FS_STALE_VERSION` on a mismatch.
201
198
  *
202
199
  * @param target - the resolved target.
@@ -247,8 +244,8 @@ export class AgentSubstrate {
247
244
  * Edit a text file with the host backend's orchestration: probe, version
248
245
  * guard, LF-normalized read, literal replacement, line-ending restore,
249
246
  * atomic publication, outcome shape. The version the edit was based on
250
- * rides the write op and is re-verified distro-side one syscall before the
251
- * rename, so a concurrent writer wins instead of being clobbered.
247
+ * rides the write op and is re-verified distro-side before the
248
+ * rename, narrowing but not eliminating the concurrent-write race.
252
249
  *
253
250
  * @param target - the resolved target.
254
251
  * @param edit - the literal request.
@@ -244,9 +244,8 @@ export declare class DistroFs {
244
244
  * Publish `content` over `linuxPath` the POSIX way: the agent stages into a
245
245
  * private 0700 sibling, fsyncs, chmods, and renames — or hard links for a
246
246
  * guarded create. A `replace` publish carrying `expectedVersion` is
247
- * re-verified by the agent against a fresh stat one syscall before the
248
- * rename: a concurrent writer (an editor save, a `git stash`) wins, and the
249
- * stale write refuses with `FS_STALE_VERSION` instead of landing.
247
+ * re-verified by the agent against a fresh stat before the rename. A change
248
+ * after that check can still be overwritten by the rename.
250
249
  *
251
250
  * @param linuxPath - the target path inside the distro.
252
251
  * @param content - the full new content.
package/lib/fsio-agent.js CHANGED
@@ -407,9 +407,8 @@ export class DistroFs {
407
407
  * Publish `content` over `linuxPath` the POSIX way: the agent stages into a
408
408
  * private 0700 sibling, fsyncs, chmods, and renames — or hard links for a
409
409
  * guarded create. A `replace` publish carrying `expectedVersion` is
410
- * re-verified by the agent against a fresh stat one syscall before the
411
- * rename: a concurrent writer (an editor save, a `git stash`) wins, and the
412
- * stale write refuses with `FS_STALE_VERSION` instead of landing.
410
+ * re-verified by the agent against a fresh stat before the rename. A change
411
+ * after that check can still be overwritten by the rename.
413
412
  *
414
413
  * @param linuxPath - the target path inside the distro.
415
414
  * @param content - the full new content.
package/lib/index.d.ts CHANGED
@@ -83,6 +83,21 @@ export type WslExecSpec = ShellExecSpec & {
83
83
  * wrappers, so they are spelled as their `.get()` shape (the reading
84
84
  * {@link configuredCwd} and `hostCwd` perform).
85
85
  */
86
+ /**
87
+ * The effective `/mnt` mask for a config: `interopVisibility` is the stance
88
+ * key (WSL-314) and takes precedence whenever it names a stance; the legacy
89
+ * `maskWindowsDrive` boolean answers only when the stance key is left at its
90
+ * `visible` default. `disabled` masks in-session — the belt — while the
91
+ * distro-level closure (`[interop] enabled=false` + `wsl --shutdown`)
92
+ * remains a documented, user-applied step a config key must not perform.
93
+ *
94
+ * @param config - anything carrying the two keys.
95
+ * @returns whether confined profiles shadow `/mnt`.
96
+ */
97
+ export declare function effectiveMaskWindowsDrive(config: {
98
+ interopVisibility?: "visible" | "masked" | "disabled";
99
+ maskWindowsDrive?: boolean;
100
+ }): boolean;
86
101
  export interface WslShellExecutorConfig {
87
102
  /** The pinned distro; empty means WSL's default. */
88
103
  distro: string;
@@ -96,6 +111,8 @@ export interface WslShellExecutorConfig {
96
111
  sandbox: boolean;
97
112
  /** Whether confined profiles mask `/mnt`. */
98
113
  maskWindowsDrive: boolean;
114
+ /** The interop stance; set at all, it takes precedence over `maskWindowsDrive`. */
115
+ interopVisibility?: "visible" | "masked" | "disabled";
99
116
  /** The volatile default workdir. */
100
117
  cwd: {
101
118
  get(): string | undefined;
@@ -218,6 +235,26 @@ export declare class WslShellExecutor extends LocalBashExecutor {
218
235
  * complete closure is distro-level: `[interop] enabled=false` in
219
236
  * `wsl.conf`.
220
237
  */
238
+ /**
239
+ * The interop stance in one key (WSL-314), expanding to the per-key
240
+ * values the measured matrix documents:
241
+ *
242
+ * - `visible` (default) — nothing masked; a confined command can read
243
+ * `/mnt` and launch Windows executables through interop, which escapes
244
+ * the Linux sandbox entirely.
245
+ * - `masked` — `maskWindowsDrive: true`: `/mnt` is shadowed with an empty
246
+ * tmpfs; the drive's files and executables are unreachable (binfmt can
247
+ * still run a workspace-written .exe, so enforcement stays `partial`).
248
+ * - `disabled` — `masked`, plus the distro-level closure: `[interop]
249
+ * enabled=false` in `/etc/wsl.conf` + `wsl --shutdown`, which the
250
+ * doctor's remedy prints as copy-paste (a config key does not write
251
+ * distro config; enforcement is the kernel's, not the schema's).
252
+ *
253
+ * When set at all it takes precedence over the legacy
254
+ * `maskWindowsDrive` boolean below (unset — the default — lets that
255
+ * boolean answer); the boolean remains for existing profiles.
256
+ */
257
+ interopVisibility: z<"masked" | "visible" | "disabled", "masked" | "visible" | "disabled", "plain">;
221
258
  maskWindowsDrive: z<boolean, boolean, "defined">;
222
259
  /**
223
260
  * Default workdir for a request that names none; accepts `/home/andy/proj`,
@@ -296,6 +333,26 @@ export declare class WslShellExecutor extends LocalBashExecutor {
296
333
  * complete closure is distro-level: `[interop] enabled=false` in
297
334
  * `wsl.conf`.
298
335
  */
336
+ /**
337
+ * The interop stance in one key (WSL-314), expanding to the per-key
338
+ * values the measured matrix documents:
339
+ *
340
+ * - `visible` (default) — nothing masked; a confined command can read
341
+ * `/mnt` and launch Windows executables through interop, which escapes
342
+ * the Linux sandbox entirely.
343
+ * - `masked` — `maskWindowsDrive: true`: `/mnt` is shadowed with an empty
344
+ * tmpfs; the drive's files and executables are unreachable (binfmt can
345
+ * still run a workspace-written .exe, so enforcement stays `partial`).
346
+ * - `disabled` — `masked`, plus the distro-level closure: `[interop]
347
+ * enabled=false` in `/etc/wsl.conf` + `wsl --shutdown`, which the
348
+ * doctor's remedy prints as copy-paste (a config key does not write
349
+ * distro config; enforcement is the kernel's, not the schema's).
350
+ *
351
+ * When set at all it takes precedence over the legacy
352
+ * `maskWindowsDrive` boolean below (unset — the default — lets that
353
+ * boolean answer); the boolean remains for existing profiles.
354
+ */
355
+ interopVisibility: z<"masked" | "visible" | "disabled", "masked" | "visible" | "disabled", "plain">;
299
356
  maskWindowsDrive: z<boolean, boolean, "defined">;
300
357
  /**
301
358
  * Default workdir for a request that names none; accepts `/home/andy/proj`,
@@ -350,6 +407,8 @@ export declare class WslShellExecutor extends LocalBashExecutor {
350
407
  sessionRuntimes: SessionRuntimes;
351
408
  /** The runtimes this executor holds, by registry key. */
352
409
  sessionRuntimeEntries: Map<string, SessionRuntimeEntry>;
410
+ /** Isolates calls that have no session identity. */
411
+ sessionRuntimeOwner: string;
353
412
  /** The preflight's host runner; tests substitute it. */
354
413
  sessionScopeProbe: (argv: string[]) => Promise<string>;
355
414
  /** Distros whose session-runtime preflight has succeeded. */
@@ -434,7 +493,7 @@ export declare class WslShellExecutor extends LocalBashExecutor {
434
493
  executionAgent(distro: string): WslAgent;
435
494
  /**
436
495
  * The persistent session runtime for one policy, when `sessionSandbox` is
437
- * on. The executor holds one reference per `(distro, workspace, policy)` and
496
+ * on. The executor holds one reference per `(session, distro, workspace, policy)` and
438
497
  * releases them when the session's scope disposes.
439
498
  *
440
499
  * @param policy - the resolved file-effect policy, when the executor has one.
@@ -445,6 +504,7 @@ export declare class WslShellExecutor extends LocalBashExecutor {
445
504
  sessionRuntimeFor(policy: {
446
505
  mode: string;
447
506
  workspaceRoot: string;
507
+ sessionId?: string;
448
508
  } | undefined, resolved: WslExecSpec, distro: string): SessionRuntimeEntry | undefined;
449
509
  /**
450
510
  * Run the session runtime's preflight once per executor. A failure is a
@@ -758,11 +818,11 @@ export declare class WslFileSystem extends LocalFileSystem {
758
818
  sandbox: z<boolean, boolean, "defined">;
759
819
  /**
760
820
  * Run confined mutations in the session's systemd user scope — the same
761
- * runtime the shell executor acquires for the same (distro, workspace,
762
- * policy) — instead of a per-policy confined resident. The kernel
763
- * enforcement is the runtime's mount profile, so commands and writes see
764
- * the same `/tmp` and workspace; the scope stops when the session's
765
- * providers release it. Requires systemd + `Linger`, like `wsl-shell`'s
821
+ * runtime the shell executor acquires for the same session, distro,
822
+ * workspace and policy — instead of a per-policy confined resident. A
823
+ * missing session ID keeps the providers separate. The kernel enforcement
824
+ * is the runtime's mount profile; the scope stops when its providers
825
+ * release it. Requires systemd + `Linger`, like `wsl-shell`'s
766
826
  * `sessionSandbox`; the preflight refuses with the remedy. Off for direct
767
827
  * constructions; on in the shipped preset.
768
828
  */
@@ -833,11 +893,11 @@ export declare class WslFileSystem extends LocalFileSystem {
833
893
  sandbox: z<boolean, boolean, "defined">;
834
894
  /**
835
895
  * Run confined mutations in the session's systemd user scope — the same
836
- * runtime the shell executor acquires for the same (distro, workspace,
837
- * policy) — instead of a per-policy confined resident. The kernel
838
- * enforcement is the runtime's mount profile, so commands and writes see
839
- * the same `/tmp` and workspace; the scope stops when the session's
840
- * providers release it. Requires systemd + `Linger`, like `wsl-shell`'s
896
+ * runtime the shell executor acquires for the same session, distro,
897
+ * workspace and policy — instead of a per-policy confined resident. A
898
+ * missing session ID keeps the providers separate. The kernel enforcement
899
+ * is the runtime's mount profile; the scope stops when its providers
900
+ * release it. Requires systemd + `Linger`, like `wsl-shell`'s
841
901
  * `sessionSandbox`; the preflight refuses with the remedy. Off for direct
842
902
  * constructions; on in the shipped preset.
843
903
  */
@@ -905,6 +965,8 @@ export declare class WslFileSystem extends LocalFileSystem {
905
965
  sessionRuntimes: SessionRuntimes;
906
966
  /** The runtimes this provider holds, by registry key. */
907
967
  sessionRuntimeEntries: Map<string, SessionRuntimeEntry>;
968
+ /** Isolates calls that have no session identity. */
969
+ sessionRuntimeOwner: string;
908
970
  /** The preflight's host runner; tests substitute it. */
909
971
  sessionScopeProbe: (argv: string[]) => Promise<string>;
910
972
  /** Distros whose session-runtime preflight has succeeded. */
@@ -981,6 +1043,7 @@ export declare class WslFileSystem extends LocalFileSystem {
981
1043
  sessionRuntimeFor(policy: {
982
1044
  mode: string;
983
1045
  workspaceRoot: string;
1046
+ sessionId?: string;
984
1047
  }, distro: string): SessionRuntimeEntry | undefined;
985
1048
  /**
986
1049
  * Run the session runtime's preflight once per provider. Only success is
@@ -1025,6 +1088,7 @@ export declare class WslFileSystem extends LocalFileSystem {
1025
1088
  mutationAgent(policy: {
1026
1089
  mode: string;
1027
1090
  workspaceRoot: string;
1091
+ sessionId?: string;
1028
1092
  } | undefined, forDistro?: string): Promise<WslAgent>;
1029
1093
  /**
1030
1094
  * Map a substrate error onto the peer's `FsError`, so the tool layer reads
@@ -1235,8 +1299,8 @@ export declare class WslFileSystem extends LocalFileSystem {
1235
1299
  * Fence the write by the per-call policy, then publish through the
1236
1300
  * substrate. The fence's fresh target is the one checked and written, so a
1237
1301
  * refusal costs no I/O and cannot be raced by a swapped symlink; the guard
1238
- * rides the write op, where the agent re-verifies it one syscall before the
1239
- * rename and the confined resident enforces it in the kernel.
1302
+ * rides the write op, where the agent re-verifies it before the rename;
1303
+ * the confined resident enforces the mount boundary in the kernel.
1240
1304
  *
1241
1305
  * @param target - the resolved target.
1242
1306
  * @param content - the full new file content.