dsh-plugin-wsl-env 0.7.5 → 0.8.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 +278 -0
- package/CONTRIBUTING.md +129 -39
- package/README.md +20 -20
- package/docs/ARCHITECTURE.md +6 -3
- package/docs/LIMITATIONS.md +13 -0
- package/docs/README.zh.md +20 -20
- 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 +1101 -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/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +40 -0
- package/lib/probe-cache.js +35 -29
- package/lib/sandbox-core.d.ts +79 -0
- package/lib/sandbox-core.js +71 -88
- 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 +144 -155
- package/package.json +68 -20
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,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,40 @@
|
|
|
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.
|
|
29
|
+
* @returns the verdict; a success is remembered, a failure is not.
|
|
30
|
+
*/
|
|
31
|
+
run(key: string, probe: () => Promise<boolean> | boolean): Promise<boolean>;
|
|
32
|
+
/** Remembered successes, for tests and diagnostics. */
|
|
33
|
+
readonly size: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build one probe cache.
|
|
37
|
+
*
|
|
38
|
+
* @returns the policy object `lib/sandbox-core.js` holds per provider.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createProbeCache(): ProbeCache;
|
package/lib/probe-cache.js
CHANGED
|
@@ -14,35 +14,41 @@
|
|
|
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
|
+
return {
|
|
29
|
+
async run(key, probe) {
|
|
30
|
+
const cached = verdicts.get(key);
|
|
31
|
+
if (cached !== undefined)
|
|
32
|
+
return cached;
|
|
33
|
+
const pending = (async () => {
|
|
34
|
+
try {
|
|
35
|
+
return await probe();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
verdicts.set(key, pending);
|
|
42
|
+
const verdict = await pending;
|
|
43
|
+
if (verdict)
|
|
44
|
+
verdicts.set(key, true);
|
|
45
|
+
else
|
|
46
|
+
verdicts.delete(key);
|
|
47
|
+
return verdict;
|
|
48
|
+
},
|
|
49
|
+
/** Remembered successes, for tests and diagnostics. */
|
|
50
|
+
get size() {
|
|
51
|
+
return verdicts.size;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
48
54
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 seam's `ConfinedArgv` settlement facts, plus this core's `windowsDrive`
|
|
21
|
+
* mitigation flag.
|
|
22
|
+
*/
|
|
23
|
+
export interface WslConfined {
|
|
24
|
+
/** The wrapped argv (bwrap prefix, separator, the caller's argv). */
|
|
25
|
+
argv: string[];
|
|
26
|
+
/**
|
|
27
|
+
* How completely the profile enforces the policy's file effects. Typed as
|
|
28
|
+
* the seam's union (upstream `SandboxEnforcement`) while this core always
|
|
29
|
+
* reports `partial` (see the `ENFORCEMENT` doc above): the value an executor
|
|
30
|
+
* passes through is the seam's, wherever it came from.
|
|
31
|
+
*/
|
|
32
|
+
enforcement: "full" | "partial";
|
|
33
|
+
/** The case-insensitive stderr substrings a bwrap denial produces. */
|
|
34
|
+
denialSignatures: string[];
|
|
35
|
+
/** Structured runner-failure evidence rules. */
|
|
36
|
+
runnerFailureRules: Array<{
|
|
37
|
+
fatalSignatures: string[];
|
|
38
|
+
allowedExitCodes?: number[];
|
|
39
|
+
informationalLines?: string[];
|
|
40
|
+
}>;
|
|
41
|
+
/** Whether the profile masks the Windows drive at `/mnt`. */
|
|
42
|
+
windowsDrive: "masked" | "visible";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The sandbox class `createSandboxCore` builds, as its consumers see it.
|
|
46
|
+
*/
|
|
47
|
+
export interface WslSandboxCoreInstance {
|
|
48
|
+
/** The `wsl.exe` path this instance spawns. */
|
|
49
|
+
wslPath: string;
|
|
50
|
+
/** Whether every profile masks `/mnt`. */
|
|
51
|
+
maskWindowsDrive: boolean;
|
|
52
|
+
/** Probe the distro's `bwrap`. */
|
|
53
|
+
usable: (distro: string, signal?: AbortSignal | undefined) => Promise<boolean>;
|
|
54
|
+
/** Wrap one argv in the profile. */
|
|
55
|
+
confine: (argv: string[], policy: {
|
|
56
|
+
mode: "read-only" | "workspace-write";
|
|
57
|
+
workspaceRoot: string;
|
|
58
|
+
}, options: {
|
|
59
|
+
distro: string;
|
|
60
|
+
signal?: AbortSignal | undefined;
|
|
61
|
+
}) => Promise<WslConfined>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build the sandbox class, bound to one unavailable-error implementation.
|
|
65
|
+
*
|
|
66
|
+
* @param deps - the injected peer surface.
|
|
67
|
+
* @returns the sandbox class (see `lib/sandbox.js` for the shipped binding).
|
|
68
|
+
*/
|
|
69
|
+
export declare function createSandboxCore({ SandboxUnavailableError }: {
|
|
70
|
+
/**
|
|
71
|
+
* The failure type `confine` throws; production binds the peer's class so
|
|
72
|
+
* `instanceof` consumers keep matching, tests bind a stand-in with the same
|
|
73
|
+
* `code`.
|
|
74
|
+
*/
|
|
75
|
+
SandboxUnavailableError: new (mode: "read-only" | "workspace-write", detail?: string) => Error;
|
|
76
|
+
}): new (config?: {
|
|
77
|
+
wslPath?: string;
|
|
78
|
+
maskWindowsDrive?: boolean;
|
|
79
|
+
} | undefined) => WslSandboxCoreInstance;
|
package/lib/sandbox-core.js
CHANGED
|
@@ -10,20 +10,21 @@
|
|
|
10
10
|
* contract (`code: "SANDBOX_UNAVAILABLE"`), and what CI runs is the code that
|
|
11
11
|
* ships.
|
|
12
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
|
+
*
|
|
13
17
|
* @module dsh-plugin-wsl/sandbox-core
|
|
14
18
|
*/
|
|
15
|
-
|
|
16
19
|
import { createProbeCache } from "./probe-cache.js";
|
|
17
20
|
import { toLinuxPath } from "./paths.js";
|
|
18
21
|
import { runCapture } from "./wsl.js";
|
|
19
22
|
import { bwrapArgvPrefix } from "./bwrap.js";
|
|
20
|
-
|
|
21
23
|
/**
|
|
22
24
|
* bwrap's denial dialect: a write outside the granted mounts fails with EROFS,
|
|
23
25
|
* whose message is "Read-only file system". Classified case-insensitively.
|
|
24
26
|
*/
|
|
25
27
|
const DENIAL_SIGNATURES = ["read-only file system"];
|
|
26
|
-
|
|
27
28
|
/**
|
|
28
29
|
* Runner-side diagnostics. Bubblewrap reports its own failures with a `bwrap: `
|
|
29
30
|
* prefix and exits 1; exit status alone never proves runner failure (the
|
|
@@ -31,7 +32,6 @@ const DENIAL_SIGNATURES = ["read-only file system"];
|
|
|
31
32
|
* command that merely prints the signature is not misread as "never ran".
|
|
32
33
|
*/
|
|
33
34
|
const RUNNER_FAILURE_RULES = [{ fatalSignatures: ["bwrap: "] }];
|
|
34
|
-
|
|
35
35
|
/**
|
|
36
36
|
* Enforcement is **partial**, not `full` like upstream's Linux rung.
|
|
37
37
|
*
|
|
@@ -44,7 +44,6 @@ const RUNNER_FAILURE_RULES = [{ fatalSignatures: ["bwrap: "] }];
|
|
|
44
44
|
* the honest `partial` and README states where the hole is.
|
|
45
45
|
*/
|
|
46
46
|
const ENFORCEMENT = "partial";
|
|
47
|
-
|
|
48
47
|
/**
|
|
49
48
|
* The one mitigation the profile can offer, and its honest ceiling.
|
|
50
49
|
*
|
|
@@ -60,98 +59,82 @@ const ENFORCEMENT = "partial";
|
|
|
60
59
|
* either way, and the mask is a decision an operator makes to shrink the hole,
|
|
61
60
|
* recorded as the `windowsDrive: "masked"` fact the model can see.
|
|
62
61
|
*/
|
|
63
|
-
|
|
64
62
|
/**
|
|
65
63
|
* Probe verdicts, keyed by `wslPath\0distro`. The policy — a success is durable, a
|
|
66
64
|
* failure is re-probed, concurrent probes are shared — lives in `probe-cache.js`,
|
|
67
65
|
* where it is unit-testable without the peers this module needs.
|
|
68
66
|
*/
|
|
69
67
|
const probeVerdicts = createProbeCache();
|
|
70
|
-
|
|
71
68
|
/**
|
|
72
69
|
* Build the sandbox class, bound to one unavailable-error implementation.
|
|
73
70
|
*
|
|
74
|
-
* @param
|
|
75
|
-
* @
|
|
76
|
-
* the failure type `confine` throws; production binds the peer's class so
|
|
77
|
-
* `instanceof` consumers keep matching, tests bind a stand-in with the same
|
|
78
|
-
* `code`.
|
|
79
|
-
* @returns {typeof WslSandboxCore} the sandbox class (see `lib/sandbox.js` for
|
|
80
|
-
* the shipped binding).
|
|
71
|
+
* @param deps - the injected peer surface.
|
|
72
|
+
* @returns the sandbox class (see `lib/sandbox.js` for the shipped binding).
|
|
81
73
|
*/
|
|
82
74
|
export function createSandboxCore({ SandboxUnavailableError }) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
argv: [...prefix, ...argv],
|
|
150
|
-
enforcement: ENFORCEMENT,
|
|
151
|
-
denialSignatures: DENIAL_SIGNATURES,
|
|
152
|
-
runnerFailureRules: RUNNER_FAILURE_RULES,
|
|
153
|
-
windowsDrive: this.maskWindowsDrive ? "masked" : "visible",
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
};
|
|
75
|
+
return class WslSandboxCore {
|
|
76
|
+
/**
|
|
77
|
+
* @param config - the `wsl.exe` path, overridable for a non-standard install.
|
|
78
|
+
*/
|
|
79
|
+
constructor(config = {}) {
|
|
80
|
+
this.wslPath = config.wslPath ?? "wsl.exe";
|
|
81
|
+
this.maskWindowsDrive = config.maskWindowsDrive ?? false;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether `bwrap` can create a profile inside this distro. The probe mirrors
|
|
85
|
+
* upstream's: apply the real read-only profile around `true`, and treat a zero
|
|
86
|
+
* exit as the kernel having accepted and enforced it. A missing `bwrap` fails the
|
|
87
|
+
* spawn and probes unusable, which is what makes the provider fail closed rather
|
|
88
|
+
* than silently run unconfined.
|
|
89
|
+
*
|
|
90
|
+
* A success is remembered for the process lifetime; a failure is not, so a
|
|
91
|
+
* `bubblewrap` installed while the app is running is believed on the next
|
|
92
|
+
* command.
|
|
93
|
+
*
|
|
94
|
+
* @param distro - the distro to probe.
|
|
95
|
+
* @param signal - optional cancellation.
|
|
96
|
+
* @returns true when a confined command can run there.
|
|
97
|
+
*/
|
|
98
|
+
async usable(distro, signal) {
|
|
99
|
+
const key = `${this.wslPath}\u0000${distro}`;
|
|
100
|
+
return probeVerdicts.run(key, async () => {
|
|
101
|
+
const prefix = bwrapArgvPrefix({ mode: "read-only", workspaceRoot: "/" });
|
|
102
|
+
await runCapture([this.wslPath, "-d", distro, "--exec", ...prefix, "true"], signal);
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Wrap one distro-side argv in the bwrap profile the policy asks for.
|
|
108
|
+
*
|
|
109
|
+
* The policy's `workspaceRoot` arrives in whichever coordinate system the
|
|
110
|
+
* caller speaks — a session workspace is a UNC path — and is translated into the
|
|
111
|
+
* Linux path `bwrap` must bind inside the distro.
|
|
112
|
+
*
|
|
113
|
+
* @param argv - the argv to run INSIDE the distro (shell, its flags, command).
|
|
114
|
+
* @param policy - the resolved confined policy (`mode` is never
|
|
115
|
+
* `danger-full-access`; the caller does not confine that case at all).
|
|
116
|
+
* @param options - the distro to run in and optional cancellation.
|
|
117
|
+
* @returns the wrapped argv plus the settlement-classification facts, in the
|
|
118
|
+
* seam's `ConfinedArgv` shape.
|
|
119
|
+
* @throws SandboxUnavailableError when the distro has no usable `bwrap`, so a
|
|
120
|
+
* requested confinement never degrades into an unconfined run.
|
|
121
|
+
*/
|
|
122
|
+
async confine(argv, policy, options) {
|
|
123
|
+
const { distro, signal } = options;
|
|
124
|
+
if (!(await this.usable(distro, signal))) {
|
|
125
|
+
throw new SandboxUnavailableError(policy.mode, `bwrap is not usable inside distro "${distro}". Install it with ` +
|
|
126
|
+
`"scripts/bootstrap.sh ${distro} --install" (from the package), or directly: ` +
|
|
127
|
+
`wsl.exe -d ${distro} -u root -- apt-get install -y bubblewrap`);
|
|
128
|
+
}
|
|
129
|
+
const workspaceRoot = toLinuxPath(policy.workspaceRoot, { distro });
|
|
130
|
+
const prefix = bwrapArgvPrefix({ ...policy, workspaceRoot }, { maskWindowsDrive: this.maskWindowsDrive });
|
|
131
|
+
return {
|
|
132
|
+
argv: [...prefix, ...argv],
|
|
133
|
+
enforcement: ENFORCEMENT,
|
|
134
|
+
denialSignatures: DENIAL_SIGNATURES,
|
|
135
|
+
runnerFailureRules: RUNNER_FAILURE_RULES,
|
|
136
|
+
windowsDrive: this.maskWindowsDrive ? "masked" : "visible",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
157
140
|
}
|
package/lib/sandbox.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Linux-side confinement this plugin applies to commands that run inside a
|
|
3
|
+
* distro.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is not `ctx.sandbox`
|
|
6
|
+
*
|
|
7
|
+
* DSH's local sandbox provider selects its runner chain by `process.platform`.
|
|
8
|
+
* This harness process is Windows, so it selects the windows-acl runner — and
|
|
9
|
+
* that runner's restricted, low-integrity token **cannot reach WSL at all**
|
|
10
|
+
* (`wsl.exe` fails with `Wsl/E_ACCESSDENIED`). The confinement that governs a
|
|
11
|
+
* distro command therefore has to be *built* on the host and *executed* inside
|
|
12
|
+
* the distro, which is what this module produces: an argv whose program is
|
|
13
|
+
* `bwrap`, handed to `wsl.exe --exec` like any other distro command.
|
|
14
|
+
*
|
|
15
|
+
* Consuming the seam is deliberately not part of this: `dsh-tool-bash` reads
|
|
16
|
+
* `ctx.shell.sandboxMode` (the executor's capability fact) plus
|
|
17
|
+
* `ctx.sandboxPolicy`, and never asks `ctx.sandbox` for anything. A WSL
|
|
18
|
+
* executor can therefore own its confinement without shadowing the
|
|
19
|
+
* composition-level provider that host sessions still need.
|
|
20
|
+
*
|
|
21
|
+
* ## Fidelity to the upstream Linux rung
|
|
22
|
+
*
|
|
23
|
+
* The profile arguments, the denial dialect and the runner-failure rules mirror
|
|
24
|
+
* `@deepseek-ai/dsh-sandbox-local`'s `bwrap` rung, so a denial reads the same to
|
|
25
|
+
* the tool layer whether the command ran on a Linux host or inside a distro.
|
|
26
|
+
* The one deliberate difference is the core's `partial` enforcement: see
|
|
27
|
+
* `lib/sandbox-core.js` for what it reports and why.
|
|
28
|
+
*
|
|
29
|
+
* The class itself lives in `lib/sandbox-core.js`, free of DSH peers — this
|
|
30
|
+
* module is the shipped binding that hands it the peer's `SandboxUnavailableError`,
|
|
31
|
+
* so `instanceof` consumers keep matching the class their composition knows.
|
|
32
|
+
*
|
|
33
|
+
* This is a TypeScript source built to `lib/sandbox.js`; edit THIS file and run
|
|
34
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
35
|
+
* fails when it drifts.
|
|
36
|
+
*
|
|
37
|
+
* @module dsh-plugin-wsl/sandbox
|
|
38
|
+
*/
|
|
39
|
+
export { bwrapProfileArgs } from "./bwrap.js";
|
|
40
|
+
/**
|
|
41
|
+
* The confinement over WSL: the probe-then-confine class bound to the peer's
|
|
42
|
+
* failure type. See the core module for the probe policy and the profile.
|
|
43
|
+
*/
|
|
44
|
+
export declare const WslSandbox: new (config?: {
|
|
45
|
+
wslPath?: string;
|
|
46
|
+
maskWindowsDrive?: boolean;
|
|
47
|
+
} | undefined) => import("./sandbox-core.js").WslSandboxCoreInstance;
|
|
48
|
+
export default WslSandbox;
|