dsh-plugin-wsl-env 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +167 -14
- package/CONTRIBUTING.md +25 -33
- package/README.md +10 -10
- package/SECURITY.md +6 -4
- package/SUPPORT.md +8 -8
- package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
- package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
- package/agent/wsl-agent.sh +4 -5
- package/cordis.patch.yml +8 -1
- package/docs/ARCHITECTURE.md +42 -35
- package/docs/COMPATIBILITY.md +121 -0
- package/docs/CONFIGURATION.md +254 -272
- package/docs/LIMITATIONS.md +78 -35
- package/docs/MIGRATION.md +16 -13
- package/docs/PARITY.md +4 -3
- package/docs/PERFORMANCE.md +11 -9
- package/docs/QUICKSTART.md +60 -17
- package/docs/README.zh.md +7 -7
- package/docs/RELEASING.md +257 -0
- package/docs/REQUIREMENTS.zh.md +77 -0
- package/docs/ROADMAP-1.0.md +105 -61
- package/docs/ROADMAP.md +15 -9
- package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +3 -3
- package/docs/UPSTREAM-FSIO-EXPORT.md +7 -7
- package/docs/UPSTREAM-OPEN-IN-APP.md +57 -0
- package/docs/UPSTREAM-SESSION-NOTICE.md +4 -4
- package/docs/VALUE.zh.md +144 -0
- package/docs/adr/0002-native-agent.md +348 -0
- package/docs/root-fs-routing.md +186 -0
- package/docs/screenshots/picker.png +0 -0
- package/docs/screenshots/preview.png +0 -0
- package/docs/screenshots/sandbox.png +0 -0
- package/docs/screenshots/terminal.png +0 -0
- package/lib/agent-confined.d.ts +29 -23
- package/lib/agent-confined.js +58 -25
- package/lib/agent-exec.d.ts +2 -3
- package/lib/agent-shared.js +1 -0
- package/lib/agent.d.ts +19 -8
- package/lib/agent.js +73 -13
- package/lib/fs-routing.d.ts +2 -1
- package/lib/fs-routing.js +13 -1
- package/lib/fs-substrate.d.ts +11 -9
- package/lib/fs-substrate.js +6 -9
- package/lib/fsio-agent.d.ts +2 -3
- package/lib/fsio-agent.js +2 -3
- package/lib/index.d.ts +77 -13
- package/lib/index.js +63 -13
- package/lib/listing.d.ts +19 -12
- package/lib/listing.js +34 -19
- package/lib/picker.js +4 -4
- package/lib/session-runtime.d.ts +5 -3
- package/lib/session-runtime.js +5 -4
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +13 -4
- package/scripts/diagnose.sh +1 -1
- package/scripts/doctor.mjs +83 -9
package/lib/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,
|
|
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 (
|
|
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.
|
|
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 (
|
|
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
|
|
732
|
-
|
|
733
|
-
|
|
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 (
|
|
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`.
|
|
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.
|
|
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.
|
package/lib/fs-routing.d.ts
CHANGED
|
@@ -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:
|
|
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 =
|
|
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
|
package/lib/fs-substrate.d.ts
CHANGED
|
@@ -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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
234
|
-
* rename,
|
|
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;
|
package/lib/fs-substrate.js
CHANGED
|
@@ -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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
251
|
-
* rename,
|
|
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.
|
package/lib/fsio-agent.d.ts
CHANGED
|
@@ -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
|
|
248
|
-
*
|
|
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
|
|
411
|
-
*
|
|
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
|
|
762
|
-
* policy
|
|
763
|
-
*
|
|
764
|
-
* the
|
|
765
|
-
*
|
|
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
|
|
837
|
-
* policy
|
|
838
|
-
*
|
|
839
|
-
* the
|
|
840
|
-
*
|
|
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
|
|
1239
|
-
*
|
|
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.
|