dsh-plugin-wsl-env 0.9.0 → 0.10.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 (44) hide show
  1. package/CHANGELOG.md +417 -0
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.md +84 -68
  4. package/SUPPORT.md +3 -3
  5. package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
  6. package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
  7. package/agent/wsl-agent.sh +83 -16
  8. package/cordis.patch.yml +41 -35
  9. package/docs/ARCHITECTURE.md +9 -1
  10. package/docs/CONFIGURATION.md +7 -4
  11. package/docs/LIMITATIONS.md +53 -18
  12. package/docs/PARITY.md +3 -1
  13. package/docs/PEER-PARITY.md +18 -11
  14. package/docs/README.zh.md +97 -74
  15. package/docs/UPSTREAM-FSIO-EXPORT.md +3 -3
  16. package/docs/UPSTREAM-TERMINAL-TITLE.md +9 -2
  17. package/examples/preset-wsl-session-sandbox.cordis.patch.yml +178 -0
  18. package/lib/agent-protocol.d.ts +10 -4
  19. package/lib/agent-protocol.js +10 -3
  20. package/lib/agent.d.ts +84 -0
  21. package/lib/agent.js +167 -15
  22. package/lib/bwrap.d.ts +8 -0
  23. package/lib/bwrap.js +4 -1
  24. package/lib/fs-routing.d.ts +40 -4
  25. package/lib/fs-routing.js +80 -12
  26. package/lib/fsio-agent.js +1 -1
  27. package/lib/index.d.ts +134 -0
  28. package/lib/index.js +191 -3
  29. package/lib/native-agent.d.ts +124 -0
  30. package/lib/native-agent.js +198 -0
  31. package/lib/ports.d.ts +13 -2
  32. package/lib/ports.js +18 -5
  33. package/lib/preflight.js +24 -1
  34. package/lib/session-runtime.d.ts +161 -0
  35. package/lib/session-runtime.js +220 -0
  36. package/lib/session-scope.d.ts +124 -0
  37. package/lib/session-scope.js +191 -0
  38. package/lib/shell-env.js +10 -3
  39. package/lib/watcher.d.ts +25 -12
  40. package/lib/watcher.js +120 -32
  41. package/lib/workspace-files-route.d.ts +8 -4
  42. package/lib/workspace-files-route.js +42 -14
  43. package/lib/workspace-files-wsl.js +25 -8
  44. package/package.json +34 -25
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Host-side plumbing for the optional native in-distro agent (ADR 0002).
3
+ *
4
+ * The shell agent is the shipped peer. This module is the opt-in path that
5
+ * lets the host run the spike/native binary instead, for the session runtime
6
+ * only:
7
+ *
8
+ * 1. read the binary from a host path (`DSH_WSL_AGENT_NATIVE`);
9
+ * 2. sha256 it and refuse an architecture the binary was not built for;
10
+ * 3. stream it into `~/.cache/dsh-wsl/agent/<sha>/dsh-wsl-agent` inside the
11
+ * distro, chmod 0755, and verify the distro-side digest;
12
+ * 4. hand the resulting Linux path to the session-runtime factory.
13
+ *
14
+ * The binary is never executed from `/mnt/c` (DrvFs is often `noexec`), and a
15
+ * failed copy falls back to the shell agent — this module never throws to the
16
+ * command path.
17
+ *
18
+ * This is a TypeScript source built to `lib/native-agent.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
+ * @module dsh-plugin-wsl/native-agent
23
+ */
24
+ /** The host environment variable naming the native binary to opt in. */
25
+ export declare const NATIVE_AGENT_ENV = "DSH_WSL_AGENT_NATIVE";
26
+ /**
27
+ * The in-distro copy/verify script. The digest is embedded so `sh` needs no
28
+ * second argument and the cache path is derived from the content; stdout is
29
+ * the final Linux path the session runtime should execute.
30
+ *
31
+ * @param digest - lowercase hex sha256 of the binary.
32
+ * @returns the `sh -c` body (binary on stdin).
33
+ */
34
+ export declare function nativeCacheScript(digest: string): string;
35
+ /** The injectable edges. */
36
+ export interface NativeBackendOptions {
37
+ /** The `wsl.exe` path. */
38
+ wslPath: string;
39
+ /** The distro. */
40
+ distro: string;
41
+ /**
42
+ * The host path override (the development variable); empty resolves the
43
+ * packaged binary for the distro's architecture.
44
+ */
45
+ source: string;
46
+ /** The plugin package root; tests and non-standard layouts substitute it. */
47
+ packageRoot?: string;
48
+ /** Read the binary (tests substitute). */
49
+ readSource?: (path: string) => Promise<Buffer>;
50
+ /** Run a host command and return stdout (tests substitute). */
51
+ run?: (argv: string[]) => Promise<string>;
52
+ /** Copy the binary and return the Linux path (tests substitute). */
53
+ copy?: (script: string, binary: Buffer) => Promise<string>;
54
+ }
55
+ /** A prepared native binary: its in-distro path and the sha256 of its bytes. */
56
+ export interface NativeBackendResult {
57
+ /** The Linux path the session runtime spawns. */
58
+ path: string;
59
+ /** Lowercase hex sha256; the agent's HELLO digest must match it. */
60
+ digest: string;
61
+ }
62
+ /**
63
+ * Copy the binary into the distro and verify it. The result is cached per
64
+ * `(wslPath, distro, source)`; a failure is cached as `undefined` for this
65
+ * process (the caller falls back to the shell agent).
66
+ *
67
+ * @param options - the transport, distro, source and injectables.
68
+ * @returns the Linux binary path and digest, or undefined when unusable.
69
+ */
70
+ export declare function ensureNativeBackend({ wslPath, distro, source, packageRoot, readSource, run, copy, }: NativeBackendOptions): Promise<NativeBackendResult | undefined>;
71
+ /**
72
+ * Forget the cache; tests only.
73
+ */
74
+ export declare function clearNativeBackendCache(): void;
75
+ /**
76
+ * Stream the binary to the distro on stdin, run the copy/verify script and
77
+ * return its stdout (the cache path).
78
+ *
79
+ * Exported for its regression check; the copy path is injectable in
80
+ * {@link ensureNativeBackend}.
81
+ *
82
+ * @param wslPath - the `wsl.exe` path.
83
+ * @param distro - the distro.
84
+ * @param script - the copy/verify script.
85
+ * @param binary - the binary bytes.
86
+ * @returns the Linux path printed by the script.
87
+ */
88
+ export declare function copyBinary(wslPath: string, distro: string, script: string, binary: Buffer): Promise<string>;
89
+ /** The session runtime's agent backend, from `wsl-shell.agentBackend`. */
90
+ export type AgentBackend = "auto" | "native" | "shell";
91
+ /**
92
+ * Record the selected backend. The session runtime is shared by the shell and
93
+ * filesystem providers, so this one process-wide value is what both read when
94
+ * they prepare their runtime; it is set from the `wsl-shell` config.
95
+ *
96
+ * @param mode - the selected backend.
97
+ */
98
+ export declare function configureAgentBackend(mode: AgentBackend): void;
99
+ /** @returns the configured backend (for diagnostics and tests). */
100
+ export declare function configuredAgentBackend(): AgentBackend;
101
+ /**
102
+ * Select the native binary for a distro when the backend allows it.
103
+ *
104
+ * - `shell`: clear any native entry and use the shipped shell agent.
105
+ * - `auto`: use the native binary when a source exists and copy/verify
106
+ * succeeds; otherwise stay on the shell agent.
107
+ * - `native`: same preparation, but a missing source or a failed preparation
108
+ * throws a remedy instead of silently falling back.
109
+ *
110
+ * The source is the development override `DSH_WSL_AGENT_NATIVE`; the packaged
111
+ * per-arch binary will be added by the packaging task and preferred over it.
112
+ *
113
+ * @param options - the transport, distro, setter and injectable prepare step.
114
+ */
115
+ export declare function ensureNativeSessionAgent({ wslPath, distro, setNative, ensure, }: {
116
+ /** The `wsl.exe` path. */
117
+ wslPath: string;
118
+ /** The resolved distro. */
119
+ distro: string;
120
+ /** Store (or clear, with an empty path) the distro's native entrypoint. */
121
+ setNative: (distro: string, path: string, digest: string) => void;
122
+ /** Prepare the binary; tests substitute. */
123
+ ensure?: (options: NativeBackendOptions) => Promise<NativeBackendResult | undefined>;
124
+ }): Promise<void>;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Host-side plumbing for the optional native in-distro agent (ADR 0002).
3
+ *
4
+ * The shell agent is the shipped peer. This module is the opt-in path that
5
+ * lets the host run the spike/native binary instead, for the session runtime
6
+ * only:
7
+ *
8
+ * 1. read the binary from a host path (`DSH_WSL_AGENT_NATIVE`);
9
+ * 2. sha256 it and refuse an architecture the binary was not built for;
10
+ * 3. stream it into `~/.cache/dsh-wsl/agent/<sha>/dsh-wsl-agent` inside the
11
+ * distro, chmod 0755, and verify the distro-side digest;
12
+ * 4. hand the resulting Linux path to the session-runtime factory.
13
+ *
14
+ * The binary is never executed from `/mnt/c` (DrvFs is often `noexec`), and a
15
+ * failed copy falls back to the shell agent — this module never throws to the
16
+ * command path.
17
+ *
18
+ * This is a TypeScript source built to `lib/native-agent.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
+ * @module dsh-plugin-wsl/native-agent
23
+ */
24
+ import { spawn } from "node:child_process";
25
+ import { createHash } from "node:crypto";
26
+ import { readFile } from "node:fs/promises";
27
+ import { join } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import { pinnedWindowsEnv } from "./agent.js";
30
+ import { runCapture } from "./wsl.js";
31
+ /** The host environment variable naming the native binary to opt in. */
32
+ export const NATIVE_AGENT_ENV = "DSH_WSL_AGENT_NATIVE";
33
+ /** Distro `uname -m` values mapped to the packaged directory names. */
34
+ const ARCHS = new Map([
35
+ ["x86_64", "x86_64"],
36
+ ["amd64", "x86_64"],
37
+ ["aarch64", "aarch64"],
38
+ ["arm64", "aarch64"],
39
+ ]);
40
+ /** The plugin package root: `lib/native-agent.js` -> `<root>`. */
41
+ const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
42
+ /**
43
+ * The in-distro copy/verify script. The digest is embedded so `sh` needs no
44
+ * second argument and the cache path is derived from the content; stdout is
45
+ * the final Linux path the session runtime should execute.
46
+ *
47
+ * @param digest - lowercase hex sha256 of the binary.
48
+ * @returns the `sh -c` body (binary on stdin).
49
+ */
50
+ export function nativeCacheScript(digest) {
51
+ return [
52
+ "set -e",
53
+ "umask 077",
54
+ `dir="$HOME/.cache/dsh-wsl/agent/${digest}"`,
55
+ 'target="$dir/dsh-wsl-agent"',
56
+ // A live session already runs this exact binary; rewriting it would fail
57
+ // with ETXTBSY. A valid cache is the same verified artifact, so reuse it.
58
+ `if [ -x "$target" ] && [ "$(sha256sum "$target" | cut -d' ' -f1)" = "${digest}" ]; then printf '%s' "$target"; exit 0; fi`,
59
+ 'mkdir -p "$dir"',
60
+ 'cat > "$target"',
61
+ 'chmod 0755 "$target"',
62
+ "got=$(sha256sum \"$target\" | cut -d' ' -f1)",
63
+ `[ "$got" = "${digest}" ] || { echo digest-mismatch >&2; exit 1; }`,
64
+ '"$target" </dev/null >/dev/null 2>&1 || { echo cannot-execute >&2; exit 1; }',
65
+ 'printf "%s" "$target"',
66
+ ].join("\n");
67
+ }
68
+ /**
69
+ * Copy the binary into the distro and verify it. The result is cached per
70
+ * `(wslPath, distro, source)`; a failure is cached as `undefined` for this
71
+ * process (the caller falls back to the shell agent).
72
+ *
73
+ * @param options - the transport, distro, source and injectables.
74
+ * @returns the Linux binary path and digest, or undefined when unusable.
75
+ */
76
+ export async function ensureNativeBackend({ wslPath, distro, source, packageRoot = PACKAGE_ROOT, readSource = readFile, run = (argv) => runCapture(argv), copy, }) {
77
+ const key = `${wslPath}\u0000${distro}\u0000${source}\u0000${packageRoot}`;
78
+ const cached = NATIVE_CACHE.get(key);
79
+ if (cached !== undefined)
80
+ return cached;
81
+ const pending = (async () => {
82
+ const arch = (await run([wslPath, "-d", distro, "--exec", "uname", "-m"])).trim();
83
+ const normalized = ARCHS.get(arch);
84
+ if (normalized === undefined)
85
+ return undefined;
86
+ const hostPath = source.length > 0 ? source : join(packageRoot, "agent", "native", `linux-${normalized}`, "dsh-wsl-agent");
87
+ const binary = await readSource(hostPath);
88
+ const digest = createHash("sha256").update(binary).digest("hex");
89
+ const install = copy ?? ((script, bytes) => copyBinary(wslPath, distro, script, bytes));
90
+ const path = await install(nativeCacheScript(digest), binary);
91
+ return path.length > 0 ? { path, digest } : undefined;
92
+ })().catch(() => undefined);
93
+ NATIVE_CACHE.set(key, pending);
94
+ return pending;
95
+ }
96
+ /** The per-process cache; a failure is remembered as undefined. */
97
+ const NATIVE_CACHE = new Map();
98
+ /**
99
+ * Forget the cache; tests only.
100
+ */
101
+ export function clearNativeBackendCache() {
102
+ NATIVE_CACHE.clear();
103
+ }
104
+ /**
105
+ * Stream the binary to the distro on stdin, run the copy/verify script and
106
+ * return its stdout (the cache path).
107
+ *
108
+ * Exported for its regression check; the copy path is injectable in
109
+ * {@link ensureNativeBackend}.
110
+ *
111
+ * @param wslPath - the `wsl.exe` path.
112
+ * @param distro - the distro.
113
+ * @param script - the copy/verify script.
114
+ * @param binary - the binary bytes.
115
+ * @returns the Linux path printed by the script.
116
+ */
117
+ export function copyBinary(wslPath, distro, script, binary) {
118
+ return new Promise((resolve, reject) => {
119
+ const child = spawn(wslPath, ["-d", distro, "--exec", "sh", "-c", script, "sh"], {
120
+ stdio: ["pipe", "pipe", "pipe"],
121
+ windowsHide: true,
122
+ env: pinnedWindowsEnv(process.env),
123
+ });
124
+ let stdout = "";
125
+ let stderr = "";
126
+ child.stdout?.setEncoding("utf8");
127
+ child.stdout?.on("data", (chunk) => {
128
+ stdout += chunk;
129
+ });
130
+ child.stderr?.setEncoding("utf8");
131
+ child.stderr?.on("data", (chunk) => {
132
+ stderr += chunk;
133
+ });
134
+ child.on("error", reject);
135
+ child.on("exit", (code) => {
136
+ if (code === 0)
137
+ resolve(stdout.trim());
138
+ else
139
+ reject(new Error(stderr.trim() || `native agent copy exited with ${code}`));
140
+ });
141
+ // The script prints the cached path and exits WITHOUT reading stdin when
142
+ // the in-distro digest already matches. On Windows, finishing the binary
143
+ // write against that closed pipe surfaces as `write EOF` on stdin, and an
144
+ // unhandled stream error would take down the host process. The exit code
145
+ // and stdout above already decide the result, so absorb the write error.
146
+ child.stdin?.on("error", () => { });
147
+ try {
148
+ child.stdin?.end(binary);
149
+ }
150
+ catch {
151
+ // A pipe that is already gone is the same warm-cache case.
152
+ }
153
+ });
154
+ }
155
+ /** The process-wide mode; the shell executor's constructor owns it. */
156
+ let backend = "auto";
157
+ /**
158
+ * Record the selected backend. The session runtime is shared by the shell and
159
+ * filesystem providers, so this one process-wide value is what both read when
160
+ * they prepare their runtime; it is set from the `wsl-shell` config.
161
+ *
162
+ * @param mode - the selected backend.
163
+ */
164
+ export function configureAgentBackend(mode) {
165
+ backend = mode;
166
+ }
167
+ /** @returns the configured backend (for diagnostics and tests). */
168
+ export function configuredAgentBackend() {
169
+ return backend;
170
+ }
171
+ /**
172
+ * Select the native binary for a distro when the backend allows it.
173
+ *
174
+ * - `shell`: clear any native entry and use the shipped shell agent.
175
+ * - `auto`: use the native binary when a source exists and copy/verify
176
+ * succeeds; otherwise stay on the shell agent.
177
+ * - `native`: same preparation, but a missing source or a failed preparation
178
+ * throws a remedy instead of silently falling back.
179
+ *
180
+ * The source is the development override `DSH_WSL_AGENT_NATIVE`; the packaged
181
+ * per-arch binary will be added by the packaging task and preferred over it.
182
+ *
183
+ * @param options - the transport, distro, setter and injectable prepare step.
184
+ */
185
+ export async function ensureNativeSessionAgent({ wslPath, distro, setNative, ensure = ensureNativeBackend, }) {
186
+ if (backend === "shell") {
187
+ setNative(distro, "", "");
188
+ return;
189
+ }
190
+ const result = await ensure({ wslPath, distro, source: process.env[NATIVE_AGENT_ENV] ?? "" });
191
+ if (result === undefined) {
192
+ if (backend === "native") {
193
+ throw new Error(`agentBackend: "native" could not prepare a native binary: the packaged binary for this architecture is missing or failed the copy/digest/execute check, and ${NATIVE_AGENT_ENV} is unset or unusable; set a valid ${NATIVE_AGENT_ENV}, or use "auto"/"shell".`);
194
+ }
195
+ return;
196
+ }
197
+ setNative(distro, result.path, result.digest);
198
+ }
package/lib/ports.d.ts CHANGED
@@ -38,7 +38,18 @@ export declare function procNetScript(): string;
38
38
  *
39
39
  * @param runner - the {@link WslAgent}-like runner.
40
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.
41
+ * @returns listening ports, ascending; undefined when the scan could not
42
+ * answer (agent out, non-zero exit), so a caller can keep the last snapshot.
43
+ */
44
+ export declare function scanListeningPorts(runner: AgentExecRunner, timeoutMs?: number): Promise<number[] | undefined>;
45
+ /**
46
+ * Snapshot the distro's listening ports, collapsing a failed scan to an empty
47
+ * list. New callers that must distinguish "none listening" from "the scan
48
+ * failed" should use {@linkcode scanListeningPorts}; this wrapper exists for
49
+ * the probe/test shape that only needs the list.
50
+ *
51
+ * @param runner - the agent-like runner.
52
+ * @param timeoutMs - the exec budget.
53
+ * @returns listening ports, ascending; [] on a failed scan.
43
54
  */
44
55
  export declare function listeningPorts(runner: AgentExecRunner, timeoutMs?: number): Promise<number[]>;
package/lib/ports.js CHANGED
@@ -55,17 +55,30 @@ export function procNetScript() {
55
55
  *
56
56
  * @param runner - the {@link WslAgent}-like runner.
57
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.
58
+ * @returns listening ports, ascending; undefined when the scan could not
59
+ * answer (agent out, non-zero exit), so a caller can keep the last snapshot.
60
60
  */
61
- export async function listeningPorts(runner, timeoutMs = 10000) {
61
+ export async function scanListeningPorts(runner, timeoutMs = 10000) {
62
62
  try {
63
63
  const result = await runner.exec({ cwd: "/", argv: ["sh", "-c", procNetScript(), "ports"], timeoutMs });
64
64
  if (result.exitCode !== 0)
65
- return [];
65
+ return undefined;
66
66
  return parseListeningPorts(result.stdout.toString("utf8"));
67
67
  }
68
68
  catch {
69
- return [];
69
+ return undefined;
70
70
  }
71
71
  }
72
+ /**
73
+ * Snapshot the distro's listening ports, collapsing a failed scan to an empty
74
+ * list. New callers that must distinguish "none listening" from "the scan
75
+ * failed" should use {@linkcode scanListeningPorts}; this wrapper exists for
76
+ * the probe/test shape that only needs the list.
77
+ *
78
+ * @param runner - the agent-like runner.
79
+ * @param timeoutMs - the exec budget.
80
+ * @returns listening ports, ascending; [] on a failed scan.
81
+ */
82
+ export async function listeningPorts(runner, timeoutMs = 10000) {
83
+ return (await scanListeningPorts(runner, timeoutMs)) ?? [];
84
+ }
package/lib/preflight.js CHANGED
@@ -25,8 +25,9 @@
25
25
  */
26
26
  import z from "@deepseek-ai/schemastery";
27
27
  import { isWslUnc } from "./paths.js";
28
- import { defaultDistro } from "./wsl.js";
28
+ import { defaultDistro, runCapture } from "./wsl.js";
29
29
  import { bwrapFailure, bwrapUsable, missingBwrapRemedy } from "./sandbox-core.js";
30
+ import { sessionScopeSweepScript } from "./session-scope.js";
30
31
  export const name = "wsl-preflight";
31
32
  export const Config = z.object({
32
33
  /** Distro to preflight; empty means this machine's default distro. */
@@ -36,6 +37,25 @@ export const Config = z.object({
36
37
  /** Whether the session-start sandbox preflight runs at all. */
37
38
  sandbox: z.boolean().default(true),
38
39
  });
40
+ /** Distros whose stale-scope sweep has already run in this process. */
41
+ const sweptDistros = new Set();
42
+ /**
43
+ * Stop `dsh-wsl-*.scope` units a crashed app left behind. A scope is stale
44
+ * when its cgroup has no `wsl-agent.sh`; a live agent means another session
45
+ * still owns it. Advisory: a failed spawn re-arms the distro for the next
46
+ * session, and the once-per-distro cache keeps the cost off the normal path.
47
+ *
48
+ * @param wslPath - the `wsl.exe` path.
49
+ * @param distro - the resolved distro.
50
+ */
51
+ const sweepStaleScopes = (wslPath, distro) => {
52
+ if (sweptDistros.has(distro))
53
+ return;
54
+ sweptDistros.add(distro);
55
+ void runCapture([wslPath, "-d", distro, "--exec", "sh", "-c", sessionScopeSweepScript()]).catch(() => {
56
+ sweptDistros.delete(distro);
57
+ });
58
+ };
39
59
  /**
40
60
  * Mount the preflight service.
41
61
  *
@@ -73,6 +93,9 @@ export async function apply(ctx, config = {}) {
73
93
  // bwrapUsable answers false on a failed probe — it never rejects — so
74
94
  // nothing here can throw except a broken context, which is the caller's
75
95
  // bug, not the session's.
96
+ // Clean up what a hard crash left behind before this session creates its
97
+ // runtime; a live agent in another session's cgroup makes its scope safe.
98
+ sweepStaleScopes(wslPath, distro);
76
99
  const usable = await bwrapUsable({ wslPath, distro });
77
100
  if (usable) {
78
101
  ctx.logger?.debug?.(`wsl-preflight: bwrap is usable inside distro "${distro}"`);
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The session-scoped runtime registry (ADR 0001).
3
+ *
4
+ * One long-lived agent per `(distro, workspaceRoot, policy)`, shared by the
5
+ * providers that need it and refcounted: the first `acquire` spawns the agent
6
+ * inside a systemd user scope, the last `release` closes the agent and stops
7
+ * the scope (the cgroup then reaps any background dev server a command left
8
+ * behind). The agent runs with no idle close — its lifetime is the session's —
9
+ * but an explicit client lease still catches a wedged host.
10
+ *
11
+ * The confinement here is the agent's own bwrap mount profile, WITHOUT a PID
12
+ * namespace: commands are the agent's children, so a background process
13
+ * survives the command, `/tmp` is the distro's real `/tmp` under
14
+ * `workspace-write`, and `ps` sees the session. `danger-full-access` gets the
15
+ * scope but no mounts.
16
+ *
17
+ * Nothing in this module touches `wsl.exe` by default except through the
18
+ * injectable factories; `test/session-runtime.test.mjs` drives the refcount,
19
+ * the prefix composition and the stop ordering without systemd.
20
+ *
21
+ * This is a TypeScript source built to `lib/session-runtime.js`; edit THIS
22
+ * file and run `pnpm run build` — the artifact under `lib/` is generated, and
23
+ * `pnpm test` fails when it drifts.
24
+ *
25
+ * @module dsh-plugin-wsl/session-runtime
26
+ */
27
+ import { WslAgent } from "./agent.js";
28
+ /**
29
+ * Run the session runtime's preflight through an injected host runner and map
30
+ * the answer to a refusal. Shared by the shell executor and the filesystem
31
+ * provider, so both gate the runtime on the same four requirements.
32
+ *
33
+ * @param options - the transport, distro and runner.
34
+ * @returns the remedy-bearing refusal, or null when the platform is usable.
35
+ */
36
+ export declare function probeSessionRuntime({ wslPath, distro, run }: {
37
+ /** The `wsl.exe` path. */
38
+ wslPath: string;
39
+ /** The distro name. */
40
+ distro: string;
41
+ /** The host runner; throws when the probe could not run. */
42
+ run: (argv: string[]) => Promise<string>;
43
+ }): Promise<string | null>;
44
+ /** One request for a session runtime. */
45
+ export interface SessionRuntimeRequest {
46
+ /** The distro the runtime lives in. */
47
+ distro: string;
48
+ /** The `wsl.exe` path; defaults to `wsl.exe`. */
49
+ wslPath?: string;
50
+ /** The session workspace root, in any coordinate system. */
51
+ workspaceRoot: string;
52
+ /** The resolved file-effect mode (`read-only`, `workspace-write`, `danger-full-access`). */
53
+ mode: string;
54
+ /** Shadow `/mnt` in the runtime's mount profile. */
55
+ maskWindowsDrive?: boolean;
56
+ /** The agent's client lease; 0 disables. */
57
+ leaseMs?: number;
58
+ }
59
+ /** One live runtime and its refcount. */
60
+ export interface SessionRuntimeEntry {
61
+ /** The registry key (`distro`, workspace, mode, mask). */
62
+ key: string;
63
+ /** The systemd user unit name. */
64
+ unit: string;
65
+ /** The resident agent. */
66
+ agent: WslAgent;
67
+ /** The distro, for the stop command. */
68
+ distro: string;
69
+ /** The `wsl.exe` path, for the stop command. */
70
+ wslPath: string;
71
+ /** How many providers hold the runtime. */
72
+ refs: number;
73
+ }
74
+ /** The injectable edges, so the registry is testable without systemd. */
75
+ export interface SessionRuntimeOptions {
76
+ /** Create the agent; tests substitute a fake. */
77
+ spawnAgent?: (options: {
78
+ /** The distro. */
79
+ distro: string;
80
+ /** The `wsl.exe` path. */
81
+ wslPath: string;
82
+ /** The scope + mount prefix. */
83
+ argvPrefix: string[];
84
+ /** No idle close: the runtime lives for the session. */
85
+ idleMs: number;
86
+ /** The explicit client lease. */
87
+ leaseMs: number;
88
+ /** The prefix has no PID namespace, so USR1 can reach the agent. */
89
+ signalable: boolean;
90
+ }) => WslAgent;
91
+ /** Stop the scope; tests record it. */
92
+ stopScope?: (options: {
93
+ wslPath: string;
94
+ distro: string;
95
+ unit: string;
96
+ }) => void;
97
+ /** The unique token in the unit name; tests pin it. */
98
+ token?: () => string;
99
+ }
100
+ /**
101
+ * Set (or clear) the native binary the session runtime should spawn for one
102
+ * distro. The host calls this after the copy/verify step; an empty path
103
+ * clears it and restores the shell agent.
104
+ *
105
+ * @param distro - the distro.
106
+ * @param path - the Linux binary path, or empty to clear.
107
+ * @param digest - the binary's sha256, which the HELLO must present.
108
+ */
109
+ export declare function setSessionNativeAgent(distro: string, path: string, digest?: string): void;
110
+ /**
111
+ * The refcounted registry. One instance lives for the process; providers share
112
+ * it so the shell and filesystem sides address the same runtime for a session.
113
+ */
114
+ export declare class SessionRuntimes {
115
+ /** The live runtimes, by key. */
116
+ private readonly runtimes;
117
+ /** The agent factory. */
118
+ private readonly spawnAgent;
119
+ /** The scope stopper. */
120
+ private readonly stopScope;
121
+ /** The unit-token generator. */
122
+ private readonly token;
123
+ /**
124
+ * @param options - the injectable edges; production passes none.
125
+ */
126
+ constructor(options?: SessionRuntimeOptions);
127
+ /**
128
+ * The key a request addresses: distro, workspace, mode and mask. A policy
129
+ * change is a different runtime, because the mount profile is fixed for the
130
+ * agent's lifetime.
131
+ *
132
+ * @param request - the runtime request.
133
+ * @returns the registry key.
134
+ */
135
+ static key(request: Pick<SessionRuntimeRequest, "distro" | "workspaceRoot" | "mode" | "maskWindowsDrive">): string;
136
+ /**
137
+ * Acquire the runtime for one key, creating it on first use.
138
+ *
139
+ * @param request - the runtime request.
140
+ * @returns the live entry; the caller holds one reference.
141
+ */
142
+ acquire(request: SessionRuntimeRequest): SessionRuntimeEntry;
143
+ /**
144
+ * Release one reference. The last release closes the agent and stops the
145
+ * scope, in that order: the agent's exit ends the protocol, the stop reaps
146
+ * whatever the session left running.
147
+ *
148
+ * @param key - the entry's key.
149
+ * @returns resolved once the last holder's teardown finished.
150
+ */
151
+ release(key: string): Promise<void>;
152
+ /**
153
+ * Release every runtime, for a host shutdown or a test. Sequential on
154
+ * purpose: concurrent closes would race the shared agent's state.
155
+ *
156
+ * @returns resolved once every runtime is torn down.
157
+ */
158
+ disposeAll(): Promise<void>;
159
+ }
160
+ /** The process-wide registry the providers will share. */
161
+ export declare const sessionRuntimes: SessionRuntimes;