dsh-plugin-wsl-env 0.12.0 → 0.13.1

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/lib/index.js CHANGED
@@ -46,10 +46,11 @@ import { FsError } from "@deepseek-ai/dsh-fs";
46
46
  import { canonicalPath, classifyRunnerFailure, isRunnerSpawnFailure, matchesSignature, SandboxUnavailableError, writableRoots } from "@deepseek-ai/dsh-sandbox";
47
47
  import { isAnotherDistrosUnc, isRelativeWorldPath, isWorldPathUnder, isWslUnc, posixToUnc, toLinuxPath, toWorldPath, uncToPosix, windowsToLinuxMount } from "./paths.js";
48
48
  import { isConfinedMutation, mutationModeRefusal, outsideDistroRefusal, shareSubstrateRefusal, substrateFailure, workspaceWriteDenial } from "./fs-decisions.js";
49
+ import { distroOfUnc, isDefaultDistro, pinnedDistro, selectDistro } from "./distro-route.js";
49
50
  import { WslSandbox } from "./sandbox.js";
50
51
  import { bwrapFailure, missingBwrapRemedy, withBwrapHint } from "./sandbox-core.js";
51
52
  import { armDistroWatcher } from "./watcher.js";
52
- import { AgentUnavailableError } from "./agent-errors.js";
53
+ import { AgentUnavailableError, SpawnError } from "./agent-errors.js";
53
54
  import { agentExecutionHandle } from "./agent-exec.js";
54
55
  import { sharedAgent } from "./agent-shared.js";
55
56
  import { AgentSubstrate } from "./fs-substrate.js";
@@ -102,20 +103,32 @@ function configuredCwd(config) {
102
103
  * An empty `cwd` cannot mean the shipped default (`process.cwd()`, a Windows
103
104
  * directory no `wsl.exe --cd` accepts) and it must not mean a hardcoded author
104
105
  * path either, so both providers agree on this instead: the distro's own home,
105
- * asked of the distro once per instance.
106
+ * asked of the distro once per (provider, distro) pair.
106
107
  *
107
108
  * @param owner - a WslShellExecutor or WslFileSystem.
109
+ * @param distro - the distro whose home is wanted.
108
110
  * @returns an absolute Linux path.
109
111
  */
110
- async function distroHome(owner) {
111
- if (owner.resolvedHome === undefined) {
112
- // Both providers carry `wslPath` in their own config; the base classes'
113
- // `config` types do not know the key, so the shape is asserted here.
114
- const config = owner.config;
115
- const distro = await owner.distro();
116
- owner.resolvedHome = await linuxHomePath(distro, { wslPath: config.wslPath });
117
- }
118
- return owner.resolvedHome;
112
+ async function distroHome(owner, distro) {
113
+ const cached = owner.resolvedHomes.get(distro);
114
+ if (cached !== undefined)
115
+ return cached;
116
+ // The single-home field predates the per-distro map and is still the test
117
+ // substitute for "a home is already known"; honour it before spawning.
118
+ if (owner.resolvedHome !== undefined && owner.resolvedDistro.length > 0 && distro === owner.resolvedDistro) {
119
+ owner.resolvedHomes.set(distro, owner.resolvedHome);
120
+ return owner.resolvedHome;
121
+ }
122
+ // Both providers carry `wslPath` in their own config; the base classes'
123
+ // `config` types do not know the key, so the shape is asserted here.
124
+ const config = owner.config;
125
+ const home = await linuxHomePath(distro, { wslPath: config.wslPath });
126
+ owner.resolvedHomes.set(distro, home);
127
+ // The legacy single-home cache is still what `processPathFromHostPath` and
128
+ // the existing tests read; keep it warm for the resolved default/pin.
129
+ if (owner.resolvedDistro.length > 0 && distro === owner.resolvedDistro)
130
+ owner.resolvedHome = home;
131
+ return home;
119
132
  }
120
133
  /**
121
134
  * The roots a `workspace-write` policy may write under, in the vocabulary this
@@ -147,6 +160,48 @@ function wslWritableRoots(policy, distro) {
147
160
  return [...roots];
148
161
  }
149
162
  const inheritedSpawnSpec = LocalBashExecutor.prototype.spawnSpec;
163
+ /**
164
+ * How long an `auto` distro that could not host a session runtime stays
165
+ * downgraded before the probe is retried.
166
+ */
167
+ const AUTO_SCOPE_RECHECK_MS = 60_000;
168
+ /**
169
+ * Whether a provider should use the session runtime for one distro.
170
+ *
171
+ * - `false` is off;
172
+ * - `true` refuses with the preflight remedy when the platform cannot host it;
173
+ * - `"auto"` downgrades to the per-command sandbox and reports that narrower
174
+ * scope in the result, re-probing a failed verdict at most once a minute so
175
+ * following the remedy (systemd, Linger) upgrades without an app restart.
176
+ *
177
+ * @param value - the configured `sessionSandbox` value.
178
+ * @param distro - the resolved distro.
179
+ * @param ensure - the provider's preflight, which throws the remedy on failure.
180
+ * @param cache - the provider's per-distro `auto` verdicts.
181
+ * @returns whether the session runtime should be used.
182
+ */
183
+ async function sessionRuntimeWanted(value, distro, ensure, cache) {
184
+ if (value === false)
185
+ return false;
186
+ if (value === true) {
187
+ await ensure();
188
+ return true;
189
+ }
190
+ const cached = cache.get(distro);
191
+ if (cached?.usable === true)
192
+ return true;
193
+ if (cached !== undefined && Date.now() - cached.checkedAt < AUTO_SCOPE_RECHECK_MS)
194
+ return false;
195
+ try {
196
+ await ensure();
197
+ cache.set(distro, { usable: true, checkedAt: Date.now() });
198
+ return true;
199
+ }
200
+ catch {
201
+ cache.set(distro, { usable: false, checkedAt: Date.now() });
202
+ return false;
203
+ }
204
+ }
150
205
  /**
151
206
  * Bash executor that runs every command inside a WSL distro.
152
207
  *
@@ -160,7 +215,10 @@ const inheritedSpawnSpec = LocalBashExecutor.prototype.spawnSpec;
160
215
  export class WslShellExecutor extends LocalBashExecutor {
161
216
  static inject = ["subprocess", "sandboxPolicy"];
162
217
  static Config = z.object({
163
- /** Distro name; empty means WSL's own default distro. */
218
+ /**
219
+ * Distro mode: empty/`"auto"` follows the request workdir, `"default"`
220
+ * forces WSL's default, and an explicit name pins every command.
221
+ */
164
222
  distro: z.string().default(""),
165
223
  /** Path to `wsl.exe`; overridable for a non-standard install. */
166
224
  wslPath: z.string().default("wsl.exe"),
@@ -233,7 +291,7 @@ export class WslShellExecutor extends LocalBashExecutor {
233
291
  * `Linger`; the preflight refuses with the remedy when one is missing, and
234
292
  * only a success is cached. Off by default; requires `agent: true`.
235
293
  */
236
- sessionSandbox: z.boolean().default(false),
294
+ sessionSandbox: z.union([z.boolean(), z.const("auto")]).default(false),
237
295
  });
238
296
  /**
239
297
  * @param ctx - the composition context.
@@ -242,9 +300,11 @@ export class WslShellExecutor extends LocalBashExecutor {
242
300
  constructor(ctx, config) {
243
301
  super(ctx, config);
244
302
  configureAgentBackend(config.agentBackend ?? "auto");
245
- this.resolvedDistro = config.distro;
303
+ this.resolvedDistro = pinnedDistro(config.distro) ?? "";
246
304
  this.resolvedShell = config.shell;
305
+ this.resolvedShells = new Map();
247
306
  this.resolvedHome = undefined;
307
+ this.resolvedHomes = new Map();
248
308
  this.mode = config.sandbox ? ctx.sandboxPolicy.defaultMode : undefined;
249
309
  this.sandbox = config.sandbox
250
310
  ? new WslSandbox({ wslPath: config.wslPath, maskWindowsDrive: config.maskWindowsDrive })
@@ -253,7 +313,8 @@ export class WslShellExecutor extends LocalBashExecutor {
253
313
  this.sessionRuntimes = sessionRuntimes;
254
314
  this.sessionRuntimeEntries = new Map();
255
315
  this.sessionScopeProbe = (argv) => runCapture(argv);
256
- this.sessionRuntimePreflight = "unknown";
316
+ this.sessionRuntimePreflight = new Set();
317
+ this.autoSessionScope = new Map();
257
318
  // The preset realm disposes when the session ends; the scope must go with
258
319
  // it, or a dev server outlives the session that started it.
259
320
  ctx.effect(() => () => this.disposeSessionRuntimes().catch(() => { }), "wsl session runtimes");
@@ -270,7 +331,8 @@ export class WslShellExecutor extends LocalBashExecutor {
270
331
  return this.mode;
271
332
  }
272
333
  /**
273
- * The distro every command runs in, resolving WSL's default on first use.
334
+ * The distro used when no path supplies evidence: the pinned name, WSL's
335
+ * default, or the default for the automatic modes.
274
336
  *
275
337
  * @returns the distro name.
276
338
  */
@@ -281,10 +343,33 @@ export class WslShellExecutor extends LocalBashExecutor {
281
343
  return this.resolvedDistro;
282
344
  }
283
345
  /**
284
- * The shell every command runs in: the configured one, else the login shell of
285
- * the distro user, resolved once and cached.
346
+ * The distro one command belongs to: an explicit pin wins; otherwise the
347
+ * request workdir, then the configured `cwd`, may name a distro; both
348
+ * absent falls back to WSL's default.
286
349
  *
287
- * @returns an absolute path to the shell inside the distro.
350
+ * @param workdir - the request's workdir, in any coordinate system.
351
+ * @param cwd - the configured default workdir, when the caller has it.
352
+ * @returns the distro name.
353
+ */
354
+ async distroFor(workdir, cwd) {
355
+ const config = this.config;
356
+ const pinned = pinnedDistro(config.distro);
357
+ if (pinned !== undefined)
358
+ return pinned;
359
+ if (!isDefaultDistro(config.distro)) {
360
+ for (const candidate of [workdir, cwd ?? configuredCwd(this.config)]) {
361
+ const named = distroOfUnc(candidate);
362
+ if (named !== undefined)
363
+ return named;
364
+ }
365
+ }
366
+ return this.distro();
367
+ }
368
+ /**
369
+ * The shell name for the default/pinned distro, as the shell tool presents
370
+ * it.
371
+ *
372
+ * @returns the basename of the shell, or `bash` when it cannot be named.
288
373
  */
289
374
  async shellName() {
290
375
  // Consumed by the shell tool after upstream change A: it names and describes
@@ -295,8 +380,30 @@ export class WslShellExecutor extends LocalBashExecutor {
295
380
  return /^[a-z0-9_+-]+$/i.test(name) ? name : "bash";
296
381
  }
297
382
  /**
298
- * The shell every command runs in, resolving the distro login shell on
299
- * first use.
383
+ * The shell for one distro: the configured shell, else that distro's login
384
+ * shell. The default/pinned distro keeps the legacy {@link resolvedShell}
385
+ * cache; every other distro gets an entry in {@link resolvedShells}.
386
+ *
387
+ * @param distro - the distro name.
388
+ * @returns an absolute path to the shell inside the distro.
389
+ */
390
+ async shellFor(distro) {
391
+ const config = this.config;
392
+ if (config.shell.length > 0)
393
+ return config.shell;
394
+ if (this.resolvedDistro.length > 0 && distro === this.resolvedDistro)
395
+ return this.shell();
396
+ const cached = this.resolvedShells.get(distro);
397
+ if (cached !== undefined)
398
+ return cached;
399
+ const resolved = await defaultShell(distro, { wslPath: config.wslPath });
400
+ if (resolved !== undefined)
401
+ this.resolvedShells.set(distro, resolved);
402
+ return resolved ?? "bash";
403
+ }
404
+ /**
405
+ * The shell for the default/pinned distro: the configured one, else the
406
+ * login shell, resolved once and cached.
300
407
  *
301
408
  * @returns an absolute path to the shell inside the distro.
302
409
  */
@@ -376,14 +483,26 @@ export class WslShellExecutor extends LocalBashExecutor {
376
483
  * @throws {Error} with the remedy when it cannot.
377
484
  */
378
485
  async ensureSessionRuntimeUsable(distro) {
379
- if (this.sessionRuntimePreflight === "ready")
486
+ if (this.sessionRuntimePreflight.has(distro))
380
487
  return;
381
488
  const config = this.config;
382
489
  const problem = await probeSessionRuntime({ wslPath: config.wslPath, distro, run: this.sessionScopeProbe });
383
490
  if (problem !== null)
384
491
  throw new Error(problem);
385
492
  await ensureNativeSessionAgent({ wslPath: config.wslPath, distro, setNative: setSessionNativeAgent });
386
- this.sessionRuntimePreflight = "ready";
493
+ this.sessionRuntimePreflight.add(distro);
494
+ }
495
+ /**
496
+ * Whether this executor should use the session runtime for one distro, with
497
+ * the `auto` downgrade and its bounded re-probe. `true` keeps the refusing
498
+ * behavior; `false` stays off.
499
+ *
500
+ * @param distro - the resolved distro.
501
+ * @returns whether to acquire the per-session scope.
502
+ */
503
+ async wantsSessionRuntime(distro) {
504
+ const config = this.config;
505
+ return sessionRuntimeWanted(config.sessionSandbox, distro, () => this.ensureSessionRuntimeUsable(distro), this.autoSessionScope);
387
506
  }
388
507
  /**
389
508
  * Release every runtime this executor holds, stopping each scope. Called
@@ -532,10 +651,11 @@ export class WslShellExecutor extends LocalBashExecutor {
532
651
  const tail = typeof spec?.relativeWorkdir === "string" ? spec.relativeWorkdir : undefined;
533
652
  if (String(spec?.workdir ?? "").trim().length > 0 && tail === undefined)
534
653
  return spec;
535
- const home = await distroHome(this);
654
+ const distro = await this.distroFor(spec.workdir, configuredCwd(this.config));
655
+ const home = await distroHome(this, distro);
536
656
  if (tail === undefined || tail.length === 0)
537
657
  return { ...spec, workdir: home };
538
- return { ...spec, workdir: toLinuxPath(tail, { distro: await this.distro(), cwd: home }) };
658
+ return { ...spec, workdir: toLinuxPath(tail, { distro, cwd: home }) };
539
659
  }
540
660
  /**
541
661
  * Decorate a handle's foreground projection in place, memoized once. The
@@ -584,6 +704,7 @@ export class WslShellExecutor extends LocalBashExecutor {
584
704
  denied: !runnerFailed && matchesSignature(proc.exitCode, stderr, facts.denialSignatures),
585
705
  enforcement: facts.enforcement,
586
706
  ...(facts.windowsDrive ? { windowsDrive: facts.windowsDrive } : {}),
707
+ sessionScope: facts.sessionScope,
587
708
  ...(runnerFailed ? { runnerFailed } : {}),
588
709
  };
589
710
  }
@@ -647,9 +768,9 @@ export class WslShellExecutor extends LocalBashExecutor {
647
768
  */
648
769
  async execute(spec) {
649
770
  const config = this.config;
650
- const distro = await this.distro();
651
- const shell = await this.shell();
652
771
  const resolved = await this.withDefaultWorkdir(spec);
772
+ const distro = await this.distroFor(resolved.workdir, configuredCwd(config));
773
+ const shell = await this.shellFor(distro);
653
774
  this.throwIfWorkdirInAnotherDistro(resolved.workdir, distro);
654
775
  if (config.agent) {
655
776
  try {
@@ -700,9 +821,8 @@ export class WslShellExecutor extends LocalBashExecutor {
700
821
  ? await this.sandbox.confine(this.distroArgv(resolved, shell), { ...policy, mode: policy.mode }, { distro, signal: resolved.signal })
701
822
  : undefined;
702
823
  const mode = policy?.mode;
703
- if (config.sessionSandbox)
704
- await this.ensureSessionRuntimeUsable(distro);
705
- const runtime = this.sessionRuntimeFor(policy, resolved, distro);
824
+ const useScope = await this.wantsSessionRuntime(distro);
825
+ const runtime = useScope ? this.sessionRuntimeFor(policy, resolved, distro) : undefined;
706
826
  const agent = runtime !== undefined ? runtime.agent : await this.executionAgent(distro);
707
827
  // A session runtime's own bwrap profile applies the mounts, so the
708
828
  // command must NOT be wrapped a second time: the per-command probe still
@@ -711,6 +831,13 @@ export class WslShellExecutor extends LocalBashExecutor {
711
831
  const argv = runtime !== undefined
712
832
  ? this.distroArgv(resolved, shell)
713
833
  : confined !== undefined ? confined.argv : this.distroArgv(resolved, shell);
834
+ // The managed `DSH_*` snapshot merges last, exactly as the one-shot path's
835
+ // spawn env does (`ShellExecSpec`: `dshEnv` wins over `env`). The agent
836
+ // applies these as SETENV frames immediately before the command's EXEC, so
837
+ // the resident path now carries `DSH_HOME`, `DSH_SESSION_ID` and the
838
+ // `DSH_WSL_*` facts too — without this the whole registry was invisible on
839
+ // the default path while the one-shot fallback forwarded it.
840
+ const env = { ...resolved.env, ...resolved.dshEnv };
714
841
  const execution = agentExecutionHandle({
715
842
  agent,
716
843
  // The one-shot path converts the spec's workdir into the distro's
@@ -719,6 +846,7 @@ export class WslShellExecutor extends LocalBashExecutor {
719
846
  // reach `chdir` (that is the bug this line fixes).
720
847
  cwd: toLinuxPath(resolved.workdir, { distro }),
721
848
  argv,
849
+ env: Object.keys(env).length > 0 ? env : undefined,
722
850
  timeoutMs: resolved.timeoutMs,
723
851
  // The agent enforces the collect budget distro-side (the EXEC frame
724
852
  // carries it); `maxSpillBytes` stays one-shot-only — the agent path has
@@ -742,6 +870,7 @@ export class WslShellExecutor extends LocalBashExecutor {
742
870
  runnerProgram: profile.argv[0] ?? "",
743
871
  workdir: resolved.workdir,
744
872
  windowsDrive: profile.windowsDrive,
873
+ sessionScope: useScope ? "session" : "per-command",
745
874
  });
746
875
  }
747
876
  : undefined,
@@ -752,7 +881,7 @@ export class WslShellExecutor extends LocalBashExecutor {
752
881
  if (this.sandbox === undefined)
753
882
  return result;
754
883
  if (confined === undefined) {
755
- return ({ ...result, sandbox: { mode, denied: false } });
884
+ return ({ ...result, sandbox: { mode, denied: false, sessionScope: useScope ? "session" : "per-command" } });
756
885
  }
757
886
  const { enforcement, denialSignatures, runnerFailureRules, windowsDrive } = confined;
758
887
  const runnerFailure = classifyRunnerFailure(result.exitCode, result.stderr.text, runnerFailureRules);
@@ -769,11 +898,19 @@ export class WslShellExecutor extends LocalBashExecutor {
769
898
  denied: matchesSignature(result.exitCode, result.stderr.text, denialSignatures),
770
899
  enforcement,
771
900
  windowsDrive,
901
+ sessionScope: useScope ? "session" : "per-command",
772
902
  },
773
903
  });
774
904
  }, (error) => {
775
905
  if (resolved.signal?.aborted === true)
776
906
  resolved.signal.throwIfAborted();
907
+ // The native agent reports a missing workdir as a spawn failure without
908
+ // naming it. Retry that command on the one-shot path, whose `--cd`
909
+ // warning names the directory — the failure path pays the extra spawn,
910
+ // the hot path does not.
911
+ if (error instanceof SpawnError) {
912
+ return this.executeOneShot(resolved, distro, shell).then((fallback) => fallback.result());
913
+ }
777
914
  throw error;
778
915
  });
779
916
  // The agent handle IS the upstream execution shape at runtime; its typedef
@@ -831,13 +968,14 @@ export class WslShellExecutor extends LocalBashExecutor {
831
968
  runnerProgram: profile.argv[0] ?? "",
832
969
  workdir: resolved.workdir,
833
970
  windowsDrive: profile.windowsDrive,
971
+ sessionScope: "per-command",
834
972
  });
835
973
  });
836
974
  return WslShellExecutor.decorateResult(execution, (result) => {
837
975
  this.throwIfDistroUnavailable(result, distro);
838
976
  this.throwIfWorkdirMissing(result, resolved.workdir);
839
977
  if (confined === undefined) {
840
- return ({ ...result, sandbox: { mode, denied: false } });
978
+ return ({ ...result, sandbox: { mode, denied: false, sessionScope: "per-command" } });
841
979
  }
842
980
  const { enforcement, denialSignatures, runnerFailureRules, windowsDrive } = confined;
843
981
  // Runner failure outranks denial because the command did not run at all.
@@ -851,6 +989,7 @@ export class WslShellExecutor extends LocalBashExecutor {
851
989
  denied: matchesSignature(result.exitCode, result.stderr.text, denialSignatures),
852
990
  enforcement,
853
991
  windowsDrive,
992
+ sessionScope: "per-command",
854
993
  },
855
994
  });
856
995
  }, (error) => {
@@ -867,7 +1006,10 @@ export class WslShellExecutor extends LocalBashExecutor {
867
1006
  export class WslFileSystem extends LocalFileSystem {
868
1007
  static inject = ["sandboxPolicy"];
869
1008
  static Config = z.object({
870
- /** Distro name; empty resolves WSL's default on first use. */
1009
+ /**
1010
+ * Distro mode: empty/`"auto"` follows the target path, `"default"` forces
1011
+ * WSL's default, and an explicit name pins the filesystem.
1012
+ */
871
1013
  distro: z.string().default(""),
872
1014
  /**
873
1015
  * Default workdir for relative input; accepts a Linux path, a UNC path, or
@@ -897,7 +1039,7 @@ export class WslFileSystem extends LocalFileSystem {
897
1039
  * `sessionSandbox`; the preflight refuses with the remedy. Off for direct
898
1040
  * constructions; on in the shipped preset.
899
1041
  */
900
- sessionSandbox: z.boolean().default(false),
1042
+ sessionSandbox: z.union([z.boolean(), z.const("auto")]).default(false),
901
1043
  /**
902
1044
  * Shadow `/mnt` with an empty tmpfs in every confined profile, hiding the
903
1045
  * Windows drive from confined commands — same meaning and same honest
@@ -951,16 +1093,20 @@ export class WslFileSystem extends LocalFileSystem {
951
1093
  if (refusal !== null)
952
1094
  throw new Error(refusal.message);
953
1095
  super(ctx, config);
954
- this.resolvedDistro = config.distro;
1096
+ this.resolvedDistro = pinnedDistro(config.distro) ?? "";
955
1097
  this.resolvedHome = undefined;
1098
+ this.resolvedHomes = new Map();
956
1099
  this.defaultMode = ctx.sandboxPolicy.defaultMode;
957
1100
  this.agentSubstrateInstance = undefined;
1101
+ this.agentSubstrates = new Map();
958
1102
  this.sandbox = new WslSandbox({ wslPath: config.wslPath, maskWindowsDrive: config.maskWindowsDrive });
959
1103
  this.agentInstance = undefined;
1104
+ this.agentInstances = new Map();
960
1105
  this.sessionRuntimes = sessionRuntimes;
961
1106
  this.sessionRuntimeEntries = new Map();
962
1107
  this.sessionScopeProbe = (argv) => runCapture(argv);
963
- this.sessionRuntimePreflight = "unknown";
1108
+ this.sessionRuntimePreflight = new Set();
1109
+ this.autoSessionScope = new Map();
964
1110
  // The preset's disposer releases the runtime; the shell executor's effect
965
1111
  // releases its own reference, so the scope stops after the last one.
966
1112
  ctx.effect(() => () => this.disposeSessionRuntimes().catch(() => { }), "wsl fs session runtimes");
@@ -978,11 +1124,10 @@ export class WslFileSystem extends LocalFileSystem {
978
1124
  return config.sandbox ? this.defaultMode : undefined;
979
1125
  }
980
1126
  /**
981
- * The resident agent serving this filesystem's in-distro side work, created
982
- * on first use and kept for the provider's lifetime (its own idle timer
983
- * retires the process; a later call starts a fresh one).
1127
+ * The resident agent serving the default/pinned distro's in-distro side
1128
+ * work, created on first use and kept for the provider's lifetime.
984
1129
  *
985
- * @returns the agent for the pinned distro.
1130
+ * @returns the agent for the default/pinned distro.
986
1131
  */
987
1132
  async publicationAgent() {
988
1133
  if (this.agentInstance === undefined) {
@@ -991,8 +1136,26 @@ export class WslFileSystem extends LocalFileSystem {
991
1136
  return this.agentInstance;
992
1137
  }
993
1138
  /**
994
- * The agent-backed substrate, built on first use over the same resident
995
- * agent every other distro side work shares.
1139
+ * The resident agent for one distro. The default/pinned distro keeps the
1140
+ * legacy {@link publicationAgent} cache; every other distro gets its own
1141
+ * entry, because the shared agent is a per-distro process singleton.
1142
+ *
1143
+ * @param distro - the distro name.
1144
+ * @returns the agent for that distro.
1145
+ */
1146
+ async publicationAgentFor(distro) {
1147
+ if (this.resolvedDistro.length > 0 && distro === this.resolvedDistro)
1148
+ return this.publicationAgent();
1149
+ const cached = this.agentInstances.get(distro);
1150
+ if (cached !== undefined)
1151
+ return cached;
1152
+ const agent = sharedAgent(distro);
1153
+ this.agentInstances.set(distro, agent);
1154
+ return agent;
1155
+ }
1156
+ /**
1157
+ * The agent-backed substrate for the default/pinned distro, built on first
1158
+ * use over the shared resident agent.
996
1159
  *
997
1160
  * @returns the substrate.
998
1161
  */
@@ -1008,6 +1171,40 @@ export class WslFileSystem extends LocalFileSystem {
1008
1171
  }
1009
1172
  return this.agentSubstrateInstance;
1010
1173
  }
1174
+ /**
1175
+ * The agent-backed substrate for one distro. The default/pinned distro keeps
1176
+ * the legacy {@link agentSubstrate} accessor (and its test override); every
1177
+ * other distro gets a substrate that mints its own UNC identities and
1178
+ * addresses its own resident.
1179
+ *
1180
+ * @param distro - the distro name.
1181
+ * @returns the substrate for that distro.
1182
+ */
1183
+ async agentSubstrateFor(distro) {
1184
+ if (this.resolvedDistro.length > 0 && distro === this.resolvedDistro)
1185
+ return this.agentSubstrate();
1186
+ const cached = this.agentSubstrates.get(distro);
1187
+ if (cached !== undefined)
1188
+ return cached;
1189
+ const config = this.config;
1190
+ const substrate = new AgentSubstrate({
1191
+ agent: await this.publicationAgentFor(distro),
1192
+ distro,
1193
+ diffBasisMaxBytes: config.diffBasisMaxBytes,
1194
+ agentFor: (policy) => this.mutationAgent(policy, distro),
1195
+ });
1196
+ this.agentSubstrates.set(distro, substrate);
1197
+ return substrate;
1198
+ }
1199
+ /**
1200
+ * The substrate for one resolved target, following the target's UNC identity.
1201
+ *
1202
+ * @param target - the resolved target.
1203
+ * @returns the substrate for the target's distro.
1204
+ */
1205
+ async substrateForTarget(target) {
1206
+ return this.agentSubstrateFor(await this.distroForTarget(target));
1207
+ }
1011
1208
  /**
1012
1209
  * The persistent session runtime for one confined mutation, when
1013
1210
  * `sessionSandbox` is on. The key is the shell executor's key for the same
@@ -1046,14 +1243,26 @@ export class WslFileSystem extends LocalFileSystem {
1046
1243
  * @throws {Error} with the remedy when it cannot.
1047
1244
  */
1048
1245
  async ensureSessionRuntimeUsable(distro) {
1049
- if (this.sessionRuntimePreflight === "ready")
1246
+ if (this.sessionRuntimePreflight.has(distro))
1050
1247
  return;
1051
1248
  const config = this.config;
1052
1249
  const problem = await probeSessionRuntime({ wslPath: config.wslPath, distro, run: this.sessionScopeProbe });
1053
1250
  if (problem !== null)
1054
1251
  throw new Error(problem);
1055
1252
  await ensureNativeSessionAgent({ wslPath: config.wslPath, distro, setNative: setSessionNativeAgent });
1056
- this.sessionRuntimePreflight = "ready";
1253
+ this.sessionRuntimePreflight.add(distro);
1254
+ }
1255
+ /**
1256
+ * Whether this provider should use the session runtime for one distro, with
1257
+ * the `auto` downgrade and its bounded re-probe. `true` keeps the refusing
1258
+ * behavior; `false` stays off.
1259
+ *
1260
+ * @param distro - the resolved distro.
1261
+ * @returns whether to acquire the per-session scope.
1262
+ */
1263
+ async wantsSessionRuntime(distro) {
1264
+ const config = this.config;
1265
+ return sessionRuntimeWanted(config.sessionSandbox, distro, () => this.ensureSessionRuntimeUsable(distro), this.autoSessionScope);
1057
1266
  }
1058
1267
  /**
1059
1268
  * Release every runtime this provider holds. Called from the preset's
@@ -1078,26 +1287,27 @@ export class WslFileSystem extends LocalFileSystem {
1078
1287
  *
1079
1288
  * @param policy - the resolved file-effect policy, or undefined when the
1080
1289
  * caller pre-checked.
1290
+ * @param forDistro - the distro the mutation belongs to; derived from the
1291
+ * policy when the caller does not know it.
1081
1292
  * @returns the agent to publish through.
1082
1293
  */
1083
- async mutationAgent(policy) {
1294
+ async mutationAgent(policy, forDistro) {
1084
1295
  const config = this.config;
1296
+ const distro = forDistro ?? await this.distroForPolicy(policy);
1085
1297
  if (!isConfinedMutation({ policy, sandboxEnabled: config.sandbox })) {
1086
- return this.publicationAgent();
1298
+ return this.publicationAgentFor(distro);
1087
1299
  }
1088
1300
  // The untyped guard decided this is a confined policy but cannot narrow
1089
1301
  // the parameter, so the confined shape is asserted once here (type-level
1090
1302
  // only; `confinedPolicy` is the same value as `policy`).
1091
1303
  const confinedPolicy = policy;
1092
- const distro = await this.distro();
1093
1304
  if (!(await this.sandbox.usable(distro))) {
1094
1305
  // The probe's classified remedy — missing vs present-but-broken, with the
1095
1306
  // distro's own package family — is what every other refusal surfaces;
1096
1307
  // this path has the same failure, so it carries the same text.
1097
1308
  throw new SandboxUnavailableError(confinedPolicy.mode, bwrapFailure(config.wslPath, distro) ?? missingBwrapRemedy(distro));
1098
1309
  }
1099
- if (config.sessionSandbox) {
1100
- await this.ensureSessionRuntimeUsable(distro);
1310
+ if (await this.wantsSessionRuntime(distro)) {
1101
1311
  const runtime = this.sessionRuntimeFor(confinedPolicy, distro);
1102
1312
  if (runtime !== undefined)
1103
1313
  return runtime.agent;
@@ -1133,7 +1343,8 @@ export class WslFileSystem extends LocalFileSystem {
1133
1343
  return uncToPosix(target.targetKey)?.linuxPath ?? target.displayPath;
1134
1344
  }
1135
1345
  /**
1136
- * The pinned distro, resolving WSL's default on first use.
1346
+ * The distro used when no path supplies evidence: the pinned name, WSL's
1347
+ * default, or the default for the automatic modes.
1137
1348
  *
1138
1349
  * @returns the distro name.
1139
1350
  */
@@ -1143,6 +1354,60 @@ export class WslFileSystem extends LocalFileSystem {
1143
1354
  this.resolvedDistro = await defaultDistro({ wslPath: config.wslPath });
1144
1355
  return this.resolvedDistro;
1145
1356
  }
1357
+ /**
1358
+ * The distro one path belongs to: an explicit pin wins; otherwise the path
1359
+ * itself, then the cwd or configured cwd, may name a distro; both absent
1360
+ * falls back to WSL's default.
1361
+ *
1362
+ * @param path - the caller path, in any coordinate system.
1363
+ * @param cwd - the caller's optional cwd override.
1364
+ * @returns the distro name.
1365
+ */
1366
+ async distroForPath(path, cwd) {
1367
+ const config = this.config;
1368
+ const pinned = pinnedDistro(config.distro);
1369
+ if (pinned !== undefined)
1370
+ return pinned;
1371
+ if (!isDefaultDistro(config.distro)) {
1372
+ for (const candidate of [path, cwd ?? config.cwd]) {
1373
+ const named = distroOfUnc(candidate);
1374
+ if (named !== undefined)
1375
+ return named;
1376
+ }
1377
+ }
1378
+ return this.distro();
1379
+ }
1380
+ /**
1381
+ * The distro a resolved target belongs to. A UNC identity names it outright;
1382
+ * a host-coordinate target falls back to the configured/default distro.
1383
+ *
1384
+ * @param target - the resolved target.
1385
+ * @returns the distro name.
1386
+ */
1387
+ async distroForTarget(target) {
1388
+ return uncToPosix(target.targetKey)?.distro ?? this.distroForPath(target.displayPath);
1389
+ }
1390
+ /**
1391
+ * The distro a mutation's policy belongs to: the workspace root is the
1392
+ * strongest evidence, then the configured cwd, then the default.
1393
+ *
1394
+ * @param policy - the resolved policy, when the caller has one.
1395
+ * @returns the distro name.
1396
+ */
1397
+ async distroForPolicy(policy) {
1398
+ const config = this.config;
1399
+ const pinned = pinnedDistro(config.distro);
1400
+ if (pinned !== undefined)
1401
+ return pinned;
1402
+ if (!isDefaultDistro(config.distro)) {
1403
+ for (const candidate of [policy?.workspaceRoot, config.cwd]) {
1404
+ const named = distroOfUnc(candidate);
1405
+ if (named !== undefined)
1406
+ return named;
1407
+ }
1408
+ }
1409
+ return this.distro();
1410
+ }
1146
1411
  /**
1147
1412
  * Translate one caller path into the world path, enforcing the distro fence.
1148
1413
  *
@@ -1152,13 +1417,13 @@ export class WslFileSystem extends LocalFileSystem {
1152
1417
  */
1153
1418
  async worldPath(path, cwd) {
1154
1419
  const config = this.config;
1155
- const distro = await this.distro();
1420
+ const distro = await this.distroForPath(path, cwd);
1156
1421
  let base = cwd ?? config.cwd;
1157
1422
  if (String(base ?? "").trim().length === 0 && isRelativeWorldPath(path)) {
1158
1423
  // An empty `cwd` means the distro user's home. Only relative input
1159
1424
  // consults the base at all, which is also the only case that has to pay
1160
1425
  // for the `wsl.exe` query — an absolute path never does.
1161
- base = await distroHome(this);
1426
+ base = await distroHome(this, distro);
1162
1427
  }
1163
1428
  const world = toWorldPath(path, { distro, cwd: base });
1164
1429
  const refusal = outsideDistroRefusal({ path, world, distro, restrictToDistro: config.restrictToDistro });
@@ -1183,10 +1448,11 @@ export class WslFileSystem extends LocalFileSystem {
1183
1448
  const linux = uncToPosix(world)?.linuxPath ?? windowsToLinuxMount(world);
1184
1449
  if (linux === undefined)
1185
1450
  throw new FsError(`cannot serve "${path}" from inside the distro`, "FS_IO_ERROR");
1451
+ const distro = uncToPosix(world)?.distro ?? await this.distroForPath(path, opts?.cwd);
1186
1452
  try {
1187
1453
  // The substrate's resolved shape carries plain-string keys; the seam's
1188
1454
  // branded tokens ride one type-level cast (runtime shape unchanged).
1189
- return (await (await this.agentSubstrate()).resolve(linux, opts?.signal));
1455
+ return (await (await this.agentSubstrateFor(distro)).resolve(linux, opts?.signal));
1190
1456
  }
1191
1457
  catch (error) {
1192
1458
  throw this.peerError(error);
@@ -1222,8 +1488,8 @@ export class WslFileSystem extends LocalFileSystem {
1222
1488
  }
1223
1489
  if (policy.mode !== "workspace-write")
1224
1490
  return target;
1225
- const fresh = await this.resolve(target.displayPath);
1226
- const distro = await this.distro();
1491
+ const distro = await this.distroForTarget(target);
1492
+ const fresh = await this.resolve(target.targetKey);
1227
1493
  for (const root of wslWritableRoots(policy, distro)) {
1228
1494
  if (isWorldPathUnder(fresh.targetKey, root, { distro }))
1229
1495
  return fresh;
@@ -1240,7 +1506,7 @@ export class WslFileSystem extends LocalFileSystem {
1240
1506
  */
1241
1507
  async stat(target, signal) {
1242
1508
  try {
1243
- return (await (await this.agentSubstrate()).stat(target, signal));
1509
+ return (await (await this.substrateForTarget(target)).stat(target, signal));
1244
1510
  }
1245
1511
  catch (error) {
1246
1512
  throw this.peerError(error);
@@ -1260,8 +1526,9 @@ export class WslFileSystem extends LocalFileSystem {
1260
1526
  const linux = uncToPosix(world)?.linuxPath ?? windowsToLinuxMount(world);
1261
1527
  if (linux === undefined)
1262
1528
  throw new FsError(`cannot inspect "${path}" from inside the distro`, "FS_IO_ERROR");
1529
+ const distro = uncToPosix(world)?.distro ?? await this.distroForPath(path, opts?.cwd);
1263
1530
  try {
1264
- return (await (await this.agentSubstrate()).lstat(linux, signal));
1531
+ return (await (await this.agentSubstrateFor(distro)).lstat(linux, signal));
1265
1532
  }
1266
1533
  catch (error) {
1267
1534
  throw this.peerError(error);
@@ -1276,7 +1543,7 @@ export class WslFileSystem extends LocalFileSystem {
1276
1543
  */
1277
1544
  async readText(target, signal) {
1278
1545
  try {
1279
- return await (await this.agentSubstrate()).readText(target, signal);
1546
+ return await (await this.substrateForTarget(target)).readText(target, signal);
1280
1547
  }
1281
1548
  catch (error) {
1282
1549
  throw this.peerError(error);
@@ -1291,7 +1558,7 @@ export class WslFileSystem extends LocalFileSystem {
1291
1558
  */
1292
1559
  async streamText(target, signal) {
1293
1560
  try {
1294
- return await (await this.agentSubstrate()).streamText(target, signal);
1561
+ return await (await this.substrateForTarget(target)).streamText(target, signal);
1295
1562
  }
1296
1563
  catch (error) {
1297
1564
  throw this.peerError(error);
@@ -1307,7 +1574,7 @@ export class WslFileSystem extends LocalFileSystem {
1307
1574
  */
1308
1575
  async readBytes(target, signal, maxBytes) {
1309
1576
  try {
1310
- return await (await this.agentSubstrate()).readBytes(target, signal, maxBytes);
1577
+ return await (await this.substrateForTarget(target)).readBytes(target, signal, maxBytes);
1311
1578
  }
1312
1579
  catch (error) {
1313
1580
  throw this.peerError(error);
@@ -1323,7 +1590,7 @@ export class WslFileSystem extends LocalFileSystem {
1323
1590
  */
1324
1591
  async readByteRange(target, range, signal) {
1325
1592
  try {
1326
- return await (await this.agentSubstrate()).readByteRange(target, range, signal);
1593
+ return await (await this.substrateForTarget(target)).readByteRange(target, range, signal);
1327
1594
  }
1328
1595
  catch (error) {
1329
1596
  throw this.peerError(error);
@@ -1339,7 +1606,7 @@ export class WslFileSystem extends LocalFileSystem {
1339
1606
  */
1340
1607
  async listDir(target, signal) {
1341
1608
  try {
1342
- return (await (await this.agentSubstrate()).listDir(target, signal));
1609
+ return (await (await this.substrateForTarget(target)).listDir(target, signal));
1343
1610
  }
1344
1611
  catch (error) {
1345
1612
  throw this.peerError(error);
@@ -1352,10 +1619,10 @@ export class WslFileSystem extends LocalFileSystem {
1352
1619
  * plain callback over image attachments, and `packages/api/session-controller`
1353
1620
  * plus the UI's open/reveal path compare the return value against `undefined`
1354
1621
  * without awaiting. The mapping is therefore built from facts that are already
1355
- * known — the distro, once `distro()` has resolved it (the pinned config, or
1356
- * the default `wsl.exe` answered earlier: the first filesystem operation
1357
- * resolves it), and the configured `cwd`, or the resolved home for a relative
1358
- * path. A call that would have to ask `wsl.exe` — a cold default distro, or a
1622
+ * known — a UNC host path names its own distro outright; a non-UNC path uses
1623
+ * the pinned/config default once `distro()` has resolved it, and the
1624
+ * configured `cwd`, or the resolved home for a relative path. A call that
1625
+ * would have to ask `wsl.exe` — a cold default distro with no UNC, or a
1359
1626
  * relative path before the home is known — answers `undefined`, which every
1360
1627
  * consumer reads as "no mapping", the same answer the ssh provider gives for
1361
1628
  * a path it cannot map.
@@ -1373,16 +1640,21 @@ export class WslFileSystem extends LocalFileSystem {
1373
1640
  */
1374
1641
  processPathFromHostPath(hostPath) {
1375
1642
  const config = this.config;
1376
- if (this.resolvedDistro.length === 0)
1643
+ const choice = selectDistro({
1644
+ configured: config.distro,
1645
+ candidates: [hostPath, config.cwd],
1646
+ fallback: this.resolvedDistro,
1647
+ });
1648
+ if (choice.distro.length === 0)
1377
1649
  return undefined;
1378
1650
  let base = config.cwd;
1379
1651
  if (String(base ?? "").trim().length === 0 && isRelativeWorldPath(hostPath)) {
1380
- if (this.resolvedHome === undefined)
1652
+ base = this.resolvedHomes.get(choice.distro) ?? (choice.distro === this.resolvedDistro ? this.resolvedHome : undefined);
1653
+ if (base === undefined)
1381
1654
  return undefined;
1382
- base = this.resolvedHome;
1383
1655
  }
1384
- const world = toWorldPath(hostPath, { distro: this.resolvedDistro, cwd: base });
1385
- const refusal = outsideDistroRefusal({ path: hostPath, world, distro: this.resolvedDistro, restrictToDistro: config.restrictToDistro });
1656
+ const world = toWorldPath(hostPath, { distro: choice.distro, cwd: base });
1657
+ const refusal = outsideDistroRefusal({ path: hostPath, world, distro: choice.distro, restrictToDistro: config.restrictToDistro });
1386
1658
  return refusal === null ? world : undefined;
1387
1659
  }
1388
1660
  /**
@@ -1409,7 +1681,19 @@ export class WslFileSystem extends LocalFileSystem {
1409
1681
  if (parsed === undefined) {
1410
1682
  throw new FsError(`cannot watch "${target.displayPath}": not a path inside the distro`, "FS_IO_ERROR");
1411
1683
  }
1412
- const distro = await this.distro();
1684
+ const pinned = pinnedDistro(config.distro);
1685
+ const configuredName = pinned ?? (isDefaultDistro(config.distro) ? await this.distro() : undefined);
1686
+ if (configuredName !== undefined) {
1687
+ const refusal = outsideDistroRefusal({
1688
+ path: target.displayPath,
1689
+ world: target.targetKey,
1690
+ distro: configuredName,
1691
+ restrictToDistro: config.restrictToDistro,
1692
+ });
1693
+ if (refusal !== null)
1694
+ throw new FsError(refusal.message, refusal.code);
1695
+ }
1696
+ const distro = parsed.distro;
1413
1697
  return armDistroWatcher({
1414
1698
  wslPath: config.wslPath,
1415
1699
  distro,
@@ -1439,7 +1723,7 @@ export class WslFileSystem extends LocalFileSystem {
1439
1723
  const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
1440
1724
  const fenced = await this.checkedTarget(target, policy);
1441
1725
  try {
1442
- return (await (await this.agentSubstrate()).writeText(fenced, content, expected, signal, policy));
1726
+ return (await (await this.substrateForTarget(fenced)).writeText(fenced, content, expected, signal, policy));
1443
1727
  }
1444
1728
  catch (error) {
1445
1729
  throw this.peerError(error);
@@ -1461,7 +1745,7 @@ export class WslFileSystem extends LocalFileSystem {
1461
1745
  const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
1462
1746
  const fenced = await this.checkedTarget(target, policy);
1463
1747
  try {
1464
- return (await (await this.agentSubstrate()).editText(fenced, edit, expected, signal, policy));
1748
+ return (await (await this.substrateForTarget(fenced)).editText(fenced, edit, expected, signal, policy));
1465
1749
  }
1466
1750
  catch (error) {
1467
1751
  throw this.peerError(error);