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.
Files changed (79) hide show
  1. package/CHANGELOG.md +278 -0
  2. package/CONTRIBUTING.md +129 -39
  3. package/README.md +20 -20
  4. package/docs/ARCHITECTURE.md +6 -3
  5. package/docs/LIMITATIONS.md +13 -0
  6. package/docs/README.zh.md +20 -20
  7. package/lib/agent-confined.d.ts +42 -0
  8. package/lib/agent-confined.js +25 -41
  9. package/lib/agent-errors.d.ts +31 -0
  10. package/lib/agent-errors.js +19 -16
  11. package/lib/agent-exec.d.ts +159 -0
  12. package/lib/agent-exec.js +151 -165
  13. package/lib/agent-protocol.d.ts +192 -0
  14. package/lib/agent-protocol.js +76 -100
  15. package/lib/agent-shared.d.ts +27 -0
  16. package/lib/agent-shared.js +12 -12
  17. package/lib/agent.d.ts +353 -0
  18. package/lib/agent.js +536 -552
  19. package/lib/auto-preset.d.ts +110 -0
  20. package/lib/auto-preset.js +157 -141
  21. package/lib/bwrap.d.ts +90 -0
  22. package/lib/bwrap.js +18 -18
  23. package/lib/file-reference-wsl.d.ts +160 -0
  24. package/lib/file-reference-wsl.js +192 -173
  25. package/lib/fs-decisions.d.ts +107 -0
  26. package/lib/fs-decisions.js +64 -60
  27. package/lib/fs-routing.d.ts +182 -0
  28. package/lib/fs-routing.js +173 -116
  29. package/lib/fs-substrate.d.ts +258 -0
  30. package/lib/fs-substrate.js +248 -248
  31. package/lib/fs.d.ts +10 -0
  32. package/lib/fs.js +5 -0
  33. package/lib/fsio-agent.d.ts +307 -0
  34. package/lib/fsio-agent.js +414 -410
  35. package/lib/fsio-text.d.ts +115 -0
  36. package/lib/fsio-text.js +104 -84
  37. package/lib/git-route.d.ts +100 -0
  38. package/lib/git-route.js +46 -47
  39. package/lib/index.d.ts +1003 -0
  40. package/lib/index.js +1101 -1003
  41. package/lib/listing.d.ts +114 -0
  42. package/lib/listing.js +68 -55
  43. package/lib/paths.d.ts +182 -0
  44. package/lib/paths.js +134 -109
  45. package/lib/picker.d.ts +266 -0
  46. package/lib/picker.js +371 -351
  47. package/lib/ports.d.ts +44 -0
  48. package/lib/ports.js +39 -29
  49. package/lib/preset-choice.d.ts +33 -0
  50. package/lib/preset-choice.js +15 -7
  51. package/lib/probe-cache.d.ts +40 -0
  52. package/lib/probe-cache.js +35 -29
  53. package/lib/sandbox-core.d.ts +79 -0
  54. package/lib/sandbox-core.js +71 -88
  55. package/lib/sandbox.d.ts +48 -0
  56. package/lib/sandbox.js +4 -4
  57. package/lib/search-exec.d.ts +96 -0
  58. package/lib/search-exec.js +52 -79
  59. package/lib/search-route.d.ts +75 -0
  60. package/lib/search-route.js +30 -31
  61. package/lib/shell-env.d.ts +88 -0
  62. package/lib/shell-env.js +78 -74
  63. package/lib/shell.d.ts +10 -0
  64. package/lib/shell.js +5 -0
  65. package/lib/subprocess.d.ts +313 -0
  66. package/lib/subprocess.js +267 -262
  67. package/lib/terminal-activity.d.ts +98 -0
  68. package/lib/terminal-activity.js +29 -39
  69. package/lib/terminal-route.d.ts +27 -0
  70. package/lib/terminal-route.js +16 -8
  71. package/lib/watcher.d.ts +77 -0
  72. package/lib/watcher.js +120 -126
  73. package/lib/workspace-files-route.d.ts +132 -0
  74. package/lib/workspace-files-route.js +73 -81
  75. package/lib/workspace-files-wsl.d.ts +257 -0
  76. package/lib/workspace-files-wsl.js +311 -251
  77. package/lib/wsl.d.ts +266 -0
  78. package/lib/wsl.js +144 -155
  79. 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
- * @param {string} text - the file's content.
22
- * @returns {number[]} deduplicated listening ports, ascending.
24
+ *
25
+ * @param text - the file's content.
26
+ * @returns deduplicated listening ports, ascending.
23
27
  */
24
28
  export function parseListeningPorts(text) {
25
- const ports = new Set();
26
- for (const line of text.split("\n").slice(1)) {
27
- const columns = line.trim().split(/\s+/);
28
- // sl, local_address, rem_address, st, ... — st 0A is LISTEN.
29
- if (columns.length < 4 || columns[3] !== "0A") continue;
30
- const portHex = columns[1].split(":")[1];
31
- if (portHex === undefined) continue;
32
- const port = Number.parseInt(portHex, 16);
33
- if (Number.isFinite(port) && port > 0) ports.add(port);
34
- }
35
- return [...ports].sort((a, b) => a - b);
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
- * @returns {string} the `sh -c` body.
47
+ *
48
+ * @returns the `sh -c` body.
41
49
  */
42
50
  export function procNetScript() {
43
- return "cat /proc/net/tcp /proc/net/tcp6 2>/dev/null";
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
- * @param {object} runner - the {@link WslAgent}-like runner.
49
- * @param {number} [timeoutMs] - the exec budget.
50
- * @returns {Promise<number[]>} listening ports, ascending; [] on any failure —
51
- * a snapshot is a convenience, never an error the session should see.
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
- try {
55
- const result = await runner.exec({ cwd: "/", argv: ["sh", "-c", procNetScript(), "ports"], timeoutMs });
56
- if (result.exitCode !== 0) return [];
57
- return parseListeningPorts(result.stdout.toString("utf8"));
58
- } catch {
59
- return [];
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;
@@ -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
- * @param {{ cwd?: string, current?: string, fallback?: string }} input - the new
19
- * session's workspace, the preset it currently has, and the registry default.
20
- * @returns true when the WSL preset should be selected for this session.
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
- if (!isWslUnc(cwd)) return false;
26
- if (current === void 0) return true;
27
- return current === fallback;
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;
@@ -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
- * @param {() => Promise<boolean>|boolean} probe - the probe to run on a miss. A
18
- * throw is a failed probe, never a propagated error.
19
- * @returns {{ run: (key: string, probe: () => Promise<boolean>|boolean) => Promise<boolean>, size: number }}
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
- /** @type {Map<string, true|Promise<boolean>>} */
23
- const verdicts = new Map();
24
- return {
25
- async run(key, probe) {
26
- const cached = verdicts.get(key);
27
- if (cached !== undefined) return cached;
28
-
29
- const pending = (async () => {
30
- try {
31
- return await probe();
32
- } catch {
33
- return false;
34
- }
35
- })();
36
- verdicts.set(key, pending);
37
-
38
- const verdict = await pending;
39
- if (verdict) verdicts.set(key, true);
40
- else verdicts.delete(key);
41
- return verdict;
42
- },
43
- /** Remembered successes, for tests and diagnostics. */
44
- get size() {
45
- return verdicts.size;
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;
@@ -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 {object} deps - the injected peer surface.
75
- * @param {new (mode: string, detail: string) => Error} deps.SandboxUnavailableError
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
- return class WslSandboxCore {
84
- /**
85
- * @param config - the `wsl.exe` path, overridable for a non-standard install.
86
- * @param config.maskWindowsDrive - shadow `/mnt` with an empty tmpfs in every
87
- * profile this sandbox builds, hiding the Windows drive from confined
88
- * commands (see the module doc above {@link ENFORCEMENT} for what that
89
- * does and does not close).
90
- */
91
- constructor(config = {}) {
92
- this.wslPath = config.wslPath ?? "wsl.exe";
93
- this.maskWindowsDrive = config.maskWindowsDrive ?? false;
94
- }
95
-
96
- /**
97
- * Whether `bwrap` can create a profile inside this distro. The probe mirrors
98
- * upstream's: apply the real read-only profile around `true`, and treat a zero
99
- * exit as the kernel having accepted and enforced it. A missing `bwrap` fails the
100
- * spawn and probes unusable, which is what makes the provider fail closed rather
101
- * than silently run unconfined.
102
- *
103
- * A success is remembered for the process lifetime; a failure is not, so a
104
- * `bubblewrap` installed while the app is running is believed on the next
105
- * command.
106
- *
107
- * @param distro - the distro to probe.
108
- * @param signal - optional cancellation.
109
- * @returns true when a confined command can run there.
110
- */
111
- async usable(distro, signal) {
112
- const key = `${this.wslPath}\u0000${distro}`;
113
- return probeVerdicts.run(key, async () => {
114
- const prefix = bwrapArgvPrefix({ mode: "read-only", workspaceRoot: "/" });
115
- await runCapture([this.wslPath, "-d", distro, "--exec", ...prefix, "true"], signal);
116
- return true;
117
- });
118
- }
119
-
120
- /**
121
- * Wrap one distro-side argv in the bwrap profile the policy asks for.
122
- *
123
- * The policy's `workspaceRoot` arrives in whichever coordinate system the
124
- * caller speaks — a session workspace is a UNC path — and is translated into the
125
- * Linux path `bwrap` must bind inside the distro.
126
- *
127
- * @param argv - the argv to run INSIDE the distro (shell, its flags, command).
128
- * @param policy - the resolved confined policy (`mode` is never
129
- * `danger-full-access`; the caller does not confine that case at all).
130
- * @param options - the distro to run in and optional cancellation.
131
- * @returns the wrapped argv plus the settlement-classification facts, in the
132
- * seam's `ConfinedArgv` shape.
133
- * @throws SandboxUnavailableError when the distro has no usable `bwrap`, so a
134
- * requested confinement never degrades into an unconfined run.
135
- */
136
- async confine(argv, policy, options) {
137
- const { distro, signal } = options;
138
- if (!(await this.usable(distro, signal))) {
139
- throw new SandboxUnavailableError(
140
- policy.mode,
141
- `bwrap is not usable inside distro "${distro}". Install it with ` +
142
- `"scripts/bootstrap.sh ${distro} --install" (from the package), or directly: ` +
143
- `wsl.exe -d ${distro} -u root -- apt-get install -y bubblewrap`,
144
- );
145
- }
146
- const workspaceRoot = toLinuxPath(policy.workspaceRoot, { distro });
147
- const prefix = bwrapArgvPrefix({ ...policy, workspaceRoot }, { maskWindowsDrive: this.maskWindowsDrive });
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
  }
@@ -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;