dsh-plugin-wsl-env 0.7.5 → 0.9.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 +345 -8
- package/CONTRIBUTING.md +221 -68
- package/README.md +50 -35
- package/SECURITY.md +18 -2
- package/SUPPORT.md +23 -7
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +200 -57
- package/docs/CONFIGURATION.md +180 -16
- package/docs/LIMITATIONS.md +91 -33
- package/docs/PARITY.md +66 -38
- package/docs/PEER-PARITY.md +95 -32
- package/docs/README.zh.md +49 -31
- package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
- package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1106 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preflight.d.ts +91 -0
- package/lib/preflight.js +94 -0
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +51 -0
- package/lib/probe-cache.js +43 -29
- package/lib/sandbox-core.d.ts +149 -0
- package/lib/sandbox-core.js +239 -87
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +150 -155
- package/locale/preflight/en.json +6 -0
- package/locale/preflight/zh.json +6 -0
- package/package.json +78 -22
- package/scripts/diagnose.sh +92 -0
package/lib/ports.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listening-port visibility for the distro, the Remote-WSL port-forwarding
|
|
3
|
+
* parity row's DSH-side half.
|
|
4
|
+
*
|
|
5
|
+
* There is no DSH channel that streams live facts to the model; the sanctioned
|
|
6
|
+
* one is the managed `DSH_*` namespace (`ctx.shellEnv`), whose `resolve` is
|
|
7
|
+
* SYNCHRONOUS. So the snapshot is refreshed on a timer and served stale-but-
|
|
8
|
+
* recent: `DSH_WSL_PORTS` names the ports with a listener inside the distro,
|
|
9
|
+
* at most `refreshMs` old. A dev server the model starts shows up on the next
|
|
10
|
+
* refresh, which is the granularity that matters at conversational pace.
|
|
11
|
+
*
|
|
12
|
+
* The scan reads `/proc/net/tcp` and `/proc/net/tcp6` directly — no `ss`, no
|
|
13
|
+
* `netstat`, nothing to install — and keeps sockets in state `0A` (LISTEN).
|
|
14
|
+
* Pure parser, unit-testable without a distro.
|
|
15
|
+
*
|
|
16
|
+
* This is a TypeScript source built to `lib/ports.js`; edit THIS file and run
|
|
17
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
18
|
+
* fails when it drifts.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-plugin-wsl/ports
|
|
21
|
+
*/
|
|
22
|
+
import type { AgentExecRunner } from "./agent-exec.js";
|
|
23
|
+
/**
|
|
24
|
+
* Parse one `/proc/net/tcp{,6}` body into listening ports.
|
|
25
|
+
*
|
|
26
|
+
* @param text - the file's content.
|
|
27
|
+
* @returns deduplicated listening ports, ascending.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseListeningPorts(text: string): number[];
|
|
30
|
+
/**
|
|
31
|
+
* The shell script that prints both proc files' contents.
|
|
32
|
+
*
|
|
33
|
+
* @returns the `sh -c` body.
|
|
34
|
+
*/
|
|
35
|
+
export declare function procNetScript(): string;
|
|
36
|
+
/**
|
|
37
|
+
* Snapshot the distro's listening ports through the agent.
|
|
38
|
+
*
|
|
39
|
+
* @param runner - the {@link WslAgent}-like runner.
|
|
40
|
+
* @param timeoutMs - the exec budget.
|
|
41
|
+
* @returns listening ports, ascending; [] on any failure — a snapshot is a
|
|
42
|
+
* convenience, never an error the session should see.
|
|
43
|
+
*/
|
|
44
|
+
export declare function listeningPorts(runner: AgentExecRunner, timeoutMs?: number): Promise<number[]>;
|
package/lib/ports.js
CHANGED
|
@@ -13,49 +13,59 @@
|
|
|
13
13
|
* `netstat`, nothing to install — and keeps sockets in state `0A` (LISTEN).
|
|
14
14
|
* Pure parser, unit-testable without a distro.
|
|
15
15
|
*
|
|
16
|
+
* This is a TypeScript source built to `lib/ports.js`; edit THIS file and run
|
|
17
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
18
|
+
* fails when it drifts.
|
|
19
|
+
*
|
|
16
20
|
* @module dsh-plugin-wsl/ports
|
|
17
21
|
*/
|
|
18
|
-
|
|
19
22
|
/**
|
|
20
23
|
* Parse one `/proc/net/tcp{,6}` body into listening ports.
|
|
21
|
-
*
|
|
22
|
-
* @
|
|
24
|
+
*
|
|
25
|
+
* @param text - the file's content.
|
|
26
|
+
* @returns deduplicated listening ports, ascending.
|
|
23
27
|
*/
|
|
24
28
|
export function parseListeningPorts(text) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
const ports = new Set();
|
|
30
|
+
for (const line of text.split("\n").slice(1)) {
|
|
31
|
+
const columns = line.trim().split(/\s+/);
|
|
32
|
+
// sl, local_address, rem_address, st, ... — st 0A is LISTEN.
|
|
33
|
+
const localAddress = columns[1];
|
|
34
|
+
if (columns.length < 4 || localAddress === undefined || columns[3] !== "0A")
|
|
35
|
+
continue;
|
|
36
|
+
const portHex = localAddress.split(":")[1];
|
|
37
|
+
if (portHex === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const port = Number.parseInt(portHex, 16);
|
|
40
|
+
if (Number.isFinite(port) && port > 0)
|
|
41
|
+
ports.add(port);
|
|
42
|
+
}
|
|
43
|
+
return [...ports].sort((a, b) => a - b);
|
|
36
44
|
}
|
|
37
|
-
|
|
38
45
|
/**
|
|
39
46
|
* The shell script that prints both proc files' contents.
|
|
40
|
-
*
|
|
47
|
+
*
|
|
48
|
+
* @returns the `sh -c` body.
|
|
41
49
|
*/
|
|
42
50
|
export function procNetScript() {
|
|
43
|
-
|
|
51
|
+
return "cat /proc/net/tcp /proc/net/tcp6 2>/dev/null";
|
|
44
52
|
}
|
|
45
|
-
|
|
46
53
|
/**
|
|
47
54
|
* Snapshot the distro's listening ports through the agent.
|
|
48
|
-
*
|
|
49
|
-
* @param
|
|
50
|
-
* @
|
|
51
|
-
*
|
|
55
|
+
*
|
|
56
|
+
* @param runner - the {@link WslAgent}-like runner.
|
|
57
|
+
* @param timeoutMs - the exec budget.
|
|
58
|
+
* @returns listening ports, ascending; [] on any failure — a snapshot is a
|
|
59
|
+
* convenience, never an error the session should see.
|
|
52
60
|
*/
|
|
53
61
|
export async function listeningPorts(runner, timeoutMs = 10000) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
try {
|
|
63
|
+
const result = await runner.exec({ cwd: "/", argv: ["sh", "-c", procNetScript(), "ports"], timeoutMs });
|
|
64
|
+
if (result.exitCode !== 0)
|
|
65
|
+
return [];
|
|
66
|
+
return parseListeningPorts(result.stdout.toString("utf8"));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
61
71
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-start sandbox preflight: surface a missing or broken `bwrap` when a
|
|
3
|
+
* distro session OPENS, not when its first command fails closed.
|
|
4
|
+
*
|
|
5
|
+
* Without this the discovery moment is the model's first command, whose
|
|
6
|
+
* `SANDBOX_UNAVAILABLE` error scrolls past as one tool failure among many; the
|
|
7
|
+
* operator may not read it until several commands have failed the same way.
|
|
8
|
+
* The probe itself is not new — it is the providers' own usability probe, run
|
|
9
|
+
* through the same shared cache (`lib/sandbox-core.js`), so a session-start
|
|
10
|
+
* check costs a real probe only once per process, and the first confined
|
|
11
|
+
* command after it pays nothing. Nothing else changes: the failure semantics
|
|
12
|
+
* stay exactly as fail-closed as before, this service only MOVES THE NEWS.
|
|
13
|
+
*
|
|
14
|
+
* Advisory by construction. A preflight that cannot run (no WSL, a wedged
|
|
15
|
+
* `wsl.exe`) degrades to a log line; it must never fail a session that has not
|
|
16
|
+
* even asked for confinement yet. It also never installs anything: the remedy
|
|
17
|
+
* it prints is the same one the refusals carry, and running an installer under
|
|
18
|
+
* root uninvited would be a posture this plugin explicitly rejects.
|
|
19
|
+
*
|
|
20
|
+
* This is a TypeScript source built to `lib/preflight.js`; edit THIS file and
|
|
21
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
22
|
+
* `pnpm test` fails when it drifts.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-plugin-wsl/preflight
|
|
25
|
+
*/
|
|
26
|
+
import z from "@deepseek-ai/schemastery";
|
|
27
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
28
|
+
export declare const name = "wsl-preflight";
|
|
29
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
30
|
+
/** Distro to preflight; empty means this machine's default distro. */
|
|
31
|
+
distro: z<string, string, "defined">;
|
|
32
|
+
/** The `wsl.exe` path the probe spawns; keep in step with the providers' `wslPath`. */
|
|
33
|
+
wslPath: z<string, string, "defined">;
|
|
34
|
+
/** Whether the session-start sandbox preflight runs at all. */
|
|
35
|
+
sandbox: z<boolean, boolean, "defined">;
|
|
36
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
37
|
+
/** Distro to preflight; empty means this machine's default distro. */
|
|
38
|
+
distro: z<string, string, "defined">;
|
|
39
|
+
/** The `wsl.exe` path the probe spawns; keep in step with the providers' `wslPath`. */
|
|
40
|
+
wslPath: z<string, string, "defined">;
|
|
41
|
+
/** Whether the session-start sandbox preflight runs at all. */
|
|
42
|
+
sandbox: z<boolean, boolean, "defined">;
|
|
43
|
+
}>>, "plain">;
|
|
44
|
+
/** The config schema's validated output. */
|
|
45
|
+
export interface PreflightConfig {
|
|
46
|
+
/** Distro to preflight; empty means this machine's default distro. */
|
|
47
|
+
distro?: string;
|
|
48
|
+
/** The `wsl.exe` path the probe spawns. */
|
|
49
|
+
wslPath?: string;
|
|
50
|
+
/** Whether the session-start sandbox preflight runs at all. */
|
|
51
|
+
sandbox?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The slice of a created agent this service inspects; the harness's agent
|
|
55
|
+
* object is structural here, the same view `auto-preset` uses.
|
|
56
|
+
*/
|
|
57
|
+
export interface PreflightAgent {
|
|
58
|
+
/** The agent's id. */
|
|
59
|
+
id: string;
|
|
60
|
+
/** The session stub, when mounted. */
|
|
61
|
+
session?: {
|
|
62
|
+
header?: {
|
|
63
|
+
cwd?: string | undefined;
|
|
64
|
+
};
|
|
65
|
+
} | undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The cordis context extended with the slices this service consumes. `logger`
|
|
69
|
+
* and the agent service belong to the harness, whose declarations are not
|
|
70
|
+
* visible to this plugin, so they are spelled structurally on top of the
|
|
71
|
+
* framework's {@link Context}.
|
|
72
|
+
*/
|
|
73
|
+
export type PreflightContext = Context & {
|
|
74
|
+
logger?: {
|
|
75
|
+
debug?: (message: string) => void;
|
|
76
|
+
warn?: (message: string) => void;
|
|
77
|
+
};
|
|
78
|
+
agents?: {
|
|
79
|
+
list: () => PreflightAgent[];
|
|
80
|
+
};
|
|
81
|
+
on: (event: "agent/created", listener: (payload: {
|
|
82
|
+
agent?: PreflightAgent;
|
|
83
|
+
} | undefined) => void) => () => boolean;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Mount the preflight service.
|
|
87
|
+
*
|
|
88
|
+
* @param ctx - the cordis context.
|
|
89
|
+
* @param config - the schema-validated plugin config.
|
|
90
|
+
*/
|
|
91
|
+
export declare function apply(ctx: PreflightContext, config?: PreflightConfig): Promise<void>;
|
package/lib/preflight.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-start sandbox preflight: surface a missing or broken `bwrap` when a
|
|
3
|
+
* distro session OPENS, not when its first command fails closed.
|
|
4
|
+
*
|
|
5
|
+
* Without this the discovery moment is the model's first command, whose
|
|
6
|
+
* `SANDBOX_UNAVAILABLE` error scrolls past as one tool failure among many; the
|
|
7
|
+
* operator may not read it until several commands have failed the same way.
|
|
8
|
+
* The probe itself is not new — it is the providers' own usability probe, run
|
|
9
|
+
* through the same shared cache (`lib/sandbox-core.js`), so a session-start
|
|
10
|
+
* check costs a real probe only once per process, and the first confined
|
|
11
|
+
* command after it pays nothing. Nothing else changes: the failure semantics
|
|
12
|
+
* stay exactly as fail-closed as before, this service only MOVES THE NEWS.
|
|
13
|
+
*
|
|
14
|
+
* Advisory by construction. A preflight that cannot run (no WSL, a wedged
|
|
15
|
+
* `wsl.exe`) degrades to a log line; it must never fail a session that has not
|
|
16
|
+
* even asked for confinement yet. It also never installs anything: the remedy
|
|
17
|
+
* it prints is the same one the refusals carry, and running an installer under
|
|
18
|
+
* root uninvited would be a posture this plugin explicitly rejects.
|
|
19
|
+
*
|
|
20
|
+
* This is a TypeScript source built to `lib/preflight.js`; edit THIS file and
|
|
21
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
22
|
+
* `pnpm test` fails when it drifts.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-plugin-wsl/preflight
|
|
25
|
+
*/
|
|
26
|
+
import z from "@deepseek-ai/schemastery";
|
|
27
|
+
import { isWslUnc } from "./paths.js";
|
|
28
|
+
import { defaultDistro } from "./wsl.js";
|
|
29
|
+
import { bwrapFailure, bwrapUsable, missingBwrapRemedy } from "./sandbox-core.js";
|
|
30
|
+
export const name = "wsl-preflight";
|
|
31
|
+
export const Config = z.object({
|
|
32
|
+
/** Distro to preflight; empty means this machine's default distro. */
|
|
33
|
+
distro: z.string().default(""),
|
|
34
|
+
/** The `wsl.exe` path the probe spawns; keep in step with the providers' `wslPath`. */
|
|
35
|
+
wslPath: z.string().default("wsl.exe"),
|
|
36
|
+
/** Whether the session-start sandbox preflight runs at all. */
|
|
37
|
+
sandbox: z.boolean().default(true),
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Mount the preflight service.
|
|
41
|
+
*
|
|
42
|
+
* @param ctx - the cordis context.
|
|
43
|
+
* @param config - the schema-validated plugin config.
|
|
44
|
+
*/
|
|
45
|
+
export async function apply(ctx, config = {}) {
|
|
46
|
+
if (!(config.sandbox ?? true))
|
|
47
|
+
return;
|
|
48
|
+
const wslPath = config.wslPath ?? "wsl.exe";
|
|
49
|
+
const warn = (message) => ctx.logger?.warn?.(message);
|
|
50
|
+
/**
|
|
51
|
+
* Probe the distro once and surface an unusable `bwrap` as a warning.
|
|
52
|
+
*
|
|
53
|
+
* @param agent - the session that just opened, for the workspace scope.
|
|
54
|
+
*/
|
|
55
|
+
const check = async (agent) => {
|
|
56
|
+
// Only a distro workspace has commands that confine: a host session would
|
|
57
|
+
// warn about a sandbox it never uses.
|
|
58
|
+
const cwd = agent?.session?.header?.cwd;
|
|
59
|
+
if (cwd === undefined || !isWslUnc(cwd))
|
|
60
|
+
return;
|
|
61
|
+
let distro = config.distro ?? "";
|
|
62
|
+
if (distro.length === 0) {
|
|
63
|
+
try {
|
|
64
|
+
distro = await defaultDistro({ wslPath });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// No WSL at all is the missing-wsl probe's territory; the first
|
|
68
|
+
// command's own error is the precise one. Stay quiet at warn level.
|
|
69
|
+
ctx.logger?.debug?.(`wsl-preflight: no default distro to preflight (${error?.message ?? error})`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// bwrapUsable answers false on a failed probe — it never rejects — so
|
|
74
|
+
// nothing here can throw except a broken context, which is the caller's
|
|
75
|
+
// bug, not the session's.
|
|
76
|
+
const usable = await bwrapUsable({ wslPath, distro });
|
|
77
|
+
if (usable) {
|
|
78
|
+
ctx.logger?.debug?.(`wsl-preflight: bwrap is usable inside distro "${distro}"`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
warn(`wsl-preflight: ${bwrapFailure(wslPath, distro) ?? missingBwrapRemedy(distro)}`);
|
|
82
|
+
};
|
|
83
|
+
ctx.on("agent/created", (payload) => void check(payload?.agent));
|
|
84
|
+
// Mount order is not a guarantee: a session created during startup can finish
|
|
85
|
+
// before this service mounts and miss the event entirely (dsh-headless does
|
|
86
|
+
// exactly that, and auto-preset sweeps for the same reason).
|
|
87
|
+
try {
|
|
88
|
+
for (const agent of ctx.agents?.list() ?? [])
|
|
89
|
+
void check(agent);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
ctx.logger?.debug?.(`wsl-preflight: could not sweep already-live agents: ${error?.message ?? error}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a newly created session should adopt the WSL preset.
|
|
3
|
+
*
|
|
4
|
+
* The rule is deliberately narrow, and it is the whole of "a session whose workspace
|
|
5
|
+
* is inside a distro gets the distro environment automatically":
|
|
6
|
+
*
|
|
7
|
+
* - the workspace must be a WSL UNC path, so a Windows folder keeps its own
|
|
8
|
+
* environment;
|
|
9
|
+
* - the session must not already carry an explicit choice. `undefined` counts as
|
|
10
|
+
* "no choice", because a caller that mounts nothing at all (the headless runner)
|
|
11
|
+
* is not asking for the host environment either; anything else must equal the
|
|
12
|
+
* registry's default id, which is what "the operator did not pick one" looks like
|
|
13
|
+
* after composition.
|
|
14
|
+
*
|
|
15
|
+
* Pure, so the decision is unit-testable without a boot — the frame timing that
|
|
16
|
+
* applies it still needs the harness.
|
|
17
|
+
*
|
|
18
|
+
* This is a TypeScript source built to `lib/preset-choice.js`; edit THIS file
|
|
19
|
+
* and run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
20
|
+
* `pnpm test` fails when it drifts.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The adoption decision.
|
|
24
|
+
*
|
|
25
|
+
* @param input - the new session's workspace, the preset it currently has, and
|
|
26
|
+
* the registry default.
|
|
27
|
+
* @returns true when the WSL preset should be selected for this session.
|
|
28
|
+
*/
|
|
29
|
+
export declare function shouldAdoptWslPreset({ cwd, current, fallback }?: {
|
|
30
|
+
cwd?: string;
|
|
31
|
+
current?: string;
|
|
32
|
+
fallback?: string;
|
|
33
|
+
}): boolean;
|
package/lib/preset-choice.js
CHANGED
|
@@ -15,14 +15,22 @@
|
|
|
15
15
|
* Pure, so the decision is unit-testable without a boot — the frame timing that
|
|
16
16
|
* applies it still needs the harness.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* This is a TypeScript source built to `lib/preset-choice.js`; edit THIS file
|
|
19
|
+
* and run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
20
|
+
* `pnpm test` fails when it drifts.
|
|
21
21
|
*/
|
|
22
22
|
import { isWslUnc } from "./paths.js";
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The adoption decision.
|
|
25
|
+
*
|
|
26
|
+
* @param input - the new session's workspace, the preset it currently has, and
|
|
27
|
+
* the registry default.
|
|
28
|
+
* @returns true when the WSL preset should be selected for this session.
|
|
29
|
+
*/
|
|
24
30
|
export function shouldAdoptWslPreset({ cwd, current, fallback } = {}) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
if (cwd === undefined || !isWslUnc(cwd))
|
|
32
|
+
return false;
|
|
33
|
+
if (current === void 0)
|
|
34
|
+
return true;
|
|
35
|
+
return current === fallback;
|
|
28
36
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The caching policy for a functional probe: which verdicts may be remembered.
|
|
3
|
+
*
|
|
4
|
+
* A SUCCESS is durable — if `bwrap` created a profile a minute ago it will create
|
|
5
|
+
* one now. A FAILURE is not: it describes the machine as it is at that moment, and
|
|
6
|
+
* the common case is a package that is not installed *yet*. Remembering a failure
|
|
7
|
+
* would make the documented remedy (`sudo apt install bubblewrap`) silently
|
|
8
|
+
* ineffective until the app restarts, while the error message tells the user to
|
|
9
|
+
* install exactly that. So a failure is dropped and the next call probes again.
|
|
10
|
+
*
|
|
11
|
+
* Probes are I/O, and concurrent first calls would otherwise each pay for one.
|
|
12
|
+
* Storing the in-flight promise makes them share a single probe.
|
|
13
|
+
*
|
|
14
|
+
* Pure by construction — no Node API, no clock — so the policy is unit-testable
|
|
15
|
+
* without the peers `lib/sandbox.js` needs.
|
|
16
|
+
*
|
|
17
|
+
* This is a TypeScript source built to `lib/probe-cache.js`; edit THIS file and
|
|
18
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
19
|
+
* fails when it drifts.
|
|
20
|
+
*/
|
|
21
|
+
/** One probe cache's surface. */
|
|
22
|
+
export interface ProbeCache {
|
|
23
|
+
/**
|
|
24
|
+
* Run one probe under the policy.
|
|
25
|
+
*
|
|
26
|
+
* @param key - the probe's cache key.
|
|
27
|
+
* @param probe - the probe to run on a miss. A throw is a failed probe, never
|
|
28
|
+
* a propagated error; its message is remembered for {@link failure}.
|
|
29
|
+
* @returns the verdict; a success is remembered, a failure is not.
|
|
30
|
+
*/
|
|
31
|
+
run(key: string, probe: () => Promise<boolean> | boolean): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* The message of the probe failure the key last recorded, if any. A failed
|
|
34
|
+
* verdict is not remembered as a verdict, but its reason outlives the miss:
|
|
35
|
+
* the remedy a caller prints is composed from WHY the probe failed, and the
|
|
36
|
+
* next caller — often the very next command — must still see it.
|
|
37
|
+
*
|
|
38
|
+
* @param key - the probe's cache key.
|
|
39
|
+
* @returns the thrown error's message, or undefined while no failure is
|
|
40
|
+
* recorded; cleared on the key's next success.
|
|
41
|
+
*/
|
|
42
|
+
failure(key: string): string | undefined;
|
|
43
|
+
/** Remembered successes, for tests and diagnostics. */
|
|
44
|
+
readonly size: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build one probe cache.
|
|
48
|
+
*
|
|
49
|
+
* @returns the policy object `lib/sandbox-core.js` holds per provider.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createProbeCache(): ProbeCache;
|
package/lib/probe-cache.js
CHANGED
|
@@ -14,35 +14,49 @@
|
|
|
14
14
|
* Pure by construction — no Node API, no clock — so the policy is unit-testable
|
|
15
15
|
* without the peers `lib/sandbox.js` needs.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* This is a TypeScript source built to `lib/probe-cache.js`; edit THIS file and
|
|
18
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
19
|
+
* fails when it drifts.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Build one probe cache.
|
|
23
|
+
*
|
|
24
|
+
* @returns the policy object `lib/sandbox-core.js` holds per provider.
|
|
20
25
|
*/
|
|
21
26
|
export function createProbeCache() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
const verdicts = new Map();
|
|
28
|
+
const failures = new Map();
|
|
29
|
+
return {
|
|
30
|
+
async run(key, probe) {
|
|
31
|
+
const cached = verdicts.get(key);
|
|
32
|
+
if (cached !== undefined)
|
|
33
|
+
return cached;
|
|
34
|
+
const pending = (async () => {
|
|
35
|
+
try {
|
|
36
|
+
return await probe();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
failures.set(key, error instanceof Error ? error.message : String(error));
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
verdicts.set(key, pending);
|
|
44
|
+
const verdict = await pending;
|
|
45
|
+
if (verdict) {
|
|
46
|
+
verdicts.set(key, true);
|
|
47
|
+
failures.delete(key);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
verdicts.delete(key);
|
|
51
|
+
}
|
|
52
|
+
return verdict;
|
|
53
|
+
},
|
|
54
|
+
failure(key) {
|
|
55
|
+
return failures.get(key);
|
|
56
|
+
},
|
|
57
|
+
/** Remembered successes, for tests and diagnostics. */
|
|
58
|
+
get size() {
|
|
59
|
+
return verdicts.size;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
48
62
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandbox's probe and confinement logic, free of DSH peers.
|
|
3
|
+
*
|
|
4
|
+
* `lib/sandbox.js` binds this core to the peer's `SandboxUnavailableError` —
|
|
5
|
+
* the class identity `instanceof` consumers match — which is the ONLY thing
|
|
6
|
+
* that kept the whole policy out of unit tests: a checkout without the peers
|
|
7
|
+
* could not even import the module, so CI skipped every check of the probe
|
|
8
|
+
* caching policy and the argv composition. The core takes the error class as
|
|
9
|
+
* the one injected dependency; the tests inject a stand-in with the same
|
|
10
|
+
* contract (`code: "SANDBOX_UNAVAILABLE"`), and what CI runs is the code that
|
|
11
|
+
* ships.
|
|
12
|
+
*
|
|
13
|
+
* This is a TypeScript source built to `lib/sandbox-core.js`; edit THIS file
|
|
14
|
+
* and run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
15
|
+
* `pnpm test` fails when it drifts.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-plugin-wsl/sandbox-core
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The canonical remedy for a distro whose `bwrap` is missing — also the
|
|
21
|
+
* fallback for callers that know only "the probe failed", without a classified
|
|
22
|
+
* reason. The direct command is family-aware when the caller knows the family;
|
|
23
|
+
* the bootstrap script is always the first-choice path because it detects the
|
|
24
|
+
* family itself.
|
|
25
|
+
*
|
|
26
|
+
* @param distro - the distro to install into.
|
|
27
|
+
* @param family - the detected package family, when known.
|
|
28
|
+
* @returns the operator-facing remedy text.
|
|
29
|
+
*/
|
|
30
|
+
export declare function missingBwrapRemedy(distro: string, family?: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* The remedy for a bwrap that PROBED healthy but failed a run — the runtime
|
|
33
|
+
* twin of {@link missingBwrapRemedy}. No family knowledge exists on this path
|
|
34
|
+
* (the probe succeeded, so no family probe ever ran), and reinstalling is only
|
|
35
|
+
* one of the plausible fixes, so the hint leads with diagnosis.
|
|
36
|
+
*
|
|
37
|
+
* @param distro - the distro whose bwrap failed at run time.
|
|
38
|
+
* @returns the operator-facing hint text.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bwrapInstallHint(distro: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Append {@link bwrapInstallHint} to a runner-failure detail that names bwrap.
|
|
43
|
+
* A probe-time failure composes its own classified remedy; this covers the
|
|
44
|
+
* OTHER order — bwrap probed healthy, then failed a real command — where no
|
|
45
|
+
* probe failure is on record and the raw `bwrap: …` line is all the model
|
|
46
|
+
* would see. Details that do not name bwrap pass through untouched: the
|
|
47
|
+
* runner-failure rules match the confinement runner, and the confinement
|
|
48
|
+
* runner is always bwrap here, but the wording stays the decider so a future
|
|
49
|
+
* rule change cannot attach a bwrap remedy to a foreign failure.
|
|
50
|
+
*
|
|
51
|
+
* @param detail - the classified runner-failure detail.
|
|
52
|
+
* @param distro - the distro the command ran in.
|
|
53
|
+
* @returns the detail, with the remedy appended when it names bwrap.
|
|
54
|
+
*/
|
|
55
|
+
export declare function withBwrapHint(detail: string, distro: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Probe one distro's `bwrap` under the shared verdict cache. The probe mirrors
|
|
58
|
+
* upstream's: apply the real read-only profile around `true`, and treat a zero
|
|
59
|
+
* exit as the kernel having accepted and enforced it. A missing `bwrap` fails
|
|
60
|
+
* the spawn and probes unusable, which is what makes the providers fail closed
|
|
61
|
+
* rather than silently run unconfined.
|
|
62
|
+
*
|
|
63
|
+
* A success is remembered for the process lifetime; a failure is not, so a
|
|
64
|
+
* `bubblewrap` installed while the app is running is believed on the next
|
|
65
|
+
* command. The failure's composed remedy stays readable through
|
|
66
|
+
* {@link bwrapFailure}, which is how every refusal and the preflight surface it.
|
|
67
|
+
*
|
|
68
|
+
* @param options - the `wsl.exe` path and the distro to probe.
|
|
69
|
+
* @param options.wslPath - the `wsl.exe` path.
|
|
70
|
+
* @param options.distro - the distro to probe.
|
|
71
|
+
* @param options.signal - optional cancellation.
|
|
72
|
+
* @returns true when a confined command can run there.
|
|
73
|
+
*/
|
|
74
|
+
export declare function bwrapUsable(options: {
|
|
75
|
+
wslPath: string;
|
|
76
|
+
distro: string;
|
|
77
|
+
signal?: AbortSignal | undefined;
|
|
78
|
+
}): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* The remedy text of a key's last failed probe, as {@link bwrapUsable} composed
|
|
81
|
+
* it — the same string the providers' refusals carry. Undefined while the probe
|
|
82
|
+
* never failed (or last succeeded).
|
|
83
|
+
*
|
|
84
|
+
* @param wslPath - the `wsl.exe` path the probe used.
|
|
85
|
+
* @param distro - the distro the probe ran in.
|
|
86
|
+
* @returns the composed remedy, or undefined.
|
|
87
|
+
*/
|
|
88
|
+
export declare function bwrapFailure(wslPath: string, distro: string): string | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* The seam's `ConfinedArgv` settlement facts, plus this core's `windowsDrive`
|
|
91
|
+
* mitigation flag.
|
|
92
|
+
*/
|
|
93
|
+
export interface WslConfined {
|
|
94
|
+
/** The wrapped argv (bwrap prefix, separator, the caller's argv). */
|
|
95
|
+
argv: string[];
|
|
96
|
+
/**
|
|
97
|
+
* How completely the profile enforces the policy's file effects. Typed as
|
|
98
|
+
* the seam's union (upstream `SandboxEnforcement`) while this core always
|
|
99
|
+
* reports `partial` (see the `ENFORCEMENT` doc above): the value an executor
|
|
100
|
+
* passes through is the seam's, wherever it came from.
|
|
101
|
+
*/
|
|
102
|
+
enforcement: "full" | "partial";
|
|
103
|
+
/** The case-insensitive stderr substrings a bwrap denial produces. */
|
|
104
|
+
denialSignatures: string[];
|
|
105
|
+
/** Structured runner-failure evidence rules. */
|
|
106
|
+
runnerFailureRules: Array<{
|
|
107
|
+
fatalSignatures: string[];
|
|
108
|
+
allowedExitCodes?: number[];
|
|
109
|
+
informationalLines?: string[];
|
|
110
|
+
}>;
|
|
111
|
+
/** Whether the profile masks the Windows drive at `/mnt`. */
|
|
112
|
+
windowsDrive: "masked" | "visible";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The sandbox class `createSandboxCore` builds, as its consumers see it.
|
|
116
|
+
*/
|
|
117
|
+
export interface WslSandboxCoreInstance {
|
|
118
|
+
/** The `wsl.exe` path this instance spawns. */
|
|
119
|
+
wslPath: string;
|
|
120
|
+
/** Whether every profile masks `/mnt`. */
|
|
121
|
+
maskWindowsDrive: boolean;
|
|
122
|
+
/** Probe the distro's `bwrap`. */
|
|
123
|
+
usable: (distro: string, signal?: AbortSignal | undefined) => Promise<boolean>;
|
|
124
|
+
/** Wrap one argv in the profile. */
|
|
125
|
+
confine: (argv: string[], policy: {
|
|
126
|
+
mode: "read-only" | "workspace-write";
|
|
127
|
+
workspaceRoot: string;
|
|
128
|
+
}, options: {
|
|
129
|
+
distro: string;
|
|
130
|
+
signal?: AbortSignal | undefined;
|
|
131
|
+
}) => Promise<WslConfined>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build the sandbox class, bound to one unavailable-error implementation.
|
|
135
|
+
*
|
|
136
|
+
* @param deps - the injected peer surface.
|
|
137
|
+
* @returns the sandbox class (see `lib/sandbox.js` for the shipped binding).
|
|
138
|
+
*/
|
|
139
|
+
export declare function createSandboxCore({ SandboxUnavailableError }: {
|
|
140
|
+
/**
|
|
141
|
+
* The failure type `confine` throws; production binds the peer's class so
|
|
142
|
+
* `instanceof` consumers keep matching, tests bind a stand-in with the same
|
|
143
|
+
* `code`.
|
|
144
|
+
*/
|
|
145
|
+
SandboxUnavailableError: new (mode: "read-only" | "workspace-write", detail?: string) => Error;
|
|
146
|
+
}): new (config?: {
|
|
147
|
+
wslPath?: string;
|
|
148
|
+
maskWindowsDrive?: boolean;
|
|
149
|
+
} | undefined) => WslSandboxCoreInstance;
|