dsh-plugin-wsl-env 0.8.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 (54) hide show
  1. package/CHANGELOG.md +488 -12
  2. package/CONTRIBUTING.md +125 -62
  3. package/README.md +99 -68
  4. package/SECURITY.md +18 -2
  5. package/SUPPORT.md +26 -10
  6. package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
  7. package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
  8. package/agent/wsl-agent.sh +83 -16
  9. package/cordis.patch.yml +50 -35
  10. package/docs/ARCHITECTURE.md +203 -55
  11. package/docs/CONFIGURATION.md +185 -18
  12. package/docs/LIMITATIONS.md +125 -45
  13. package/docs/PARITY.md +68 -38
  14. package/docs/PEER-PARITY.md +103 -33
  15. package/docs/README.zh.md +105 -64
  16. package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
  17. package/docs/UPSTREAM-TERMINAL-TITLE.md +56 -14
  18. package/examples/preset-wsl-session-sandbox.cordis.patch.yml +178 -0
  19. package/lib/agent-protocol.d.ts +10 -4
  20. package/lib/agent-protocol.js +10 -3
  21. package/lib/agent.d.ts +84 -0
  22. package/lib/agent.js +167 -15
  23. package/lib/bwrap.d.ts +8 -0
  24. package/lib/bwrap.js +4 -1
  25. package/lib/fs-routing.d.ts +40 -4
  26. package/lib/fs-routing.js +80 -12
  27. package/lib/fsio-agent.js +1 -1
  28. package/lib/index.d.ts +134 -0
  29. package/lib/index.js +202 -9
  30. package/lib/native-agent.d.ts +124 -0
  31. package/lib/native-agent.js +198 -0
  32. package/lib/ports.d.ts +13 -2
  33. package/lib/ports.js +18 -5
  34. package/lib/preflight.d.ts +91 -0
  35. package/lib/preflight.js +117 -0
  36. package/lib/probe-cache.d.ts +12 -1
  37. package/lib/probe-cache.js +11 -3
  38. package/lib/sandbox-core.d.ts +70 -0
  39. package/lib/sandbox-core.js +187 -18
  40. package/lib/session-runtime.d.ts +161 -0
  41. package/lib/session-runtime.js +220 -0
  42. package/lib/session-scope.d.ts +124 -0
  43. package/lib/session-scope.js +191 -0
  44. package/lib/shell-env.js +10 -3
  45. package/lib/watcher.d.ts +25 -12
  46. package/lib/watcher.js +120 -32
  47. package/lib/workspace-files-route.d.ts +8 -4
  48. package/lib/workspace-files-route.js +42 -14
  49. package/lib/workspace-files-wsl.js +25 -8
  50. package/lib/wsl.js +10 -4
  51. package/locale/preflight/en.json +6 -0
  52. package/locale/preflight/zh.json +6 -0
  53. package/package.json +44 -27
  54. package/scripts/diagnose.sh +92 -0
@@ -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;
@@ -0,0 +1,220 @@
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 { spawn } from "node:child_process";
28
+ import { randomUUID } from "node:crypto";
29
+ import { WslAgent, pinnedWindowsEnv } from "./agent.js";
30
+ import { agentScriptDigest, agentScriptPath } from "./agent-shared.js";
31
+ import { bwrapArgvPrefix } from "./bwrap.js";
32
+ import { toLinuxPath } from "./paths.js";
33
+ import { sessionScopePrefix, sessionScopeProblem, sessionScopeProbeArgv, sessionScopeStopArgv, sessionScopeUnit } from "./session-scope.js";
34
+ /**
35
+ * Run the session runtime's preflight through an injected host runner and map
36
+ * the answer to a refusal. Shared by the shell executor and the filesystem
37
+ * provider, so both gate the runtime on the same four requirements.
38
+ *
39
+ * @param options - the transport, distro and runner.
40
+ * @returns the remedy-bearing refusal, or null when the platform is usable.
41
+ */
42
+ export async function probeSessionRuntime({ wslPath, distro, run }) {
43
+ /** @type {{ stdout: string, error?: string }} */
44
+ let output;
45
+ try {
46
+ output = { stdout: await run(sessionScopeProbeArgv({ wslPath, distro })) };
47
+ }
48
+ catch (error) {
49
+ output = { stdout: "", error: String(error) };
50
+ }
51
+ return sessionScopeProblem({ output, distro });
52
+ }
53
+ /** The native binary path and HELLO digest per distro, when opted in. */
54
+ const NATIVE_AGENTS = new Map();
55
+ /**
56
+ * Set (or clear) the native binary the session runtime should spawn for one
57
+ * distro. The host calls this after the copy/verify step; an empty path
58
+ * clears it and restores the shell agent.
59
+ *
60
+ * @param distro - the distro.
61
+ * @param path - the Linux binary path, or empty to clear.
62
+ * @param digest - the binary's sha256, which the HELLO must present.
63
+ */
64
+ export function setSessionNativeAgent(distro, path, digest = "") {
65
+ if (path.length === 0)
66
+ NATIVE_AGENTS.delete(distro);
67
+ else
68
+ NATIVE_AGENTS.set(distro, { path, digest });
69
+ }
70
+ /** The default agent factory: the same script/digest the shared resident pins. */
71
+ const spawnSessionAgent = (options) => {
72
+ const native = NATIVE_AGENTS.get(options.distro);
73
+ if (native !== undefined) {
74
+ // The native agent reads its own KILL frame while a command runs, so USR1
75
+ // is both unnecessary and fatal (it has no handler); cancellation is the
76
+ // frame path. The HELLO digest binds the process to the copied binary.
77
+ return new WslAgent({
78
+ distro: options.distro,
79
+ wslPath: options.wslPath,
80
+ scriptPath: native.path,
81
+ expectedDigest: native.digest,
82
+ argvPrefix: options.argvPrefix,
83
+ idleMs: options.idleMs,
84
+ leaseMs: options.leaseMs,
85
+ signalable: false,
86
+ spawnTransport: ({ wslPath, distro, argvPrefix }) => spawn(wslPath, ["-d", distro, "--exec", ...argvPrefix, native.path], {
87
+ stdio: ["pipe", "pipe", "pipe"],
88
+ windowsHide: true,
89
+ env: pinnedWindowsEnv(process.env),
90
+ }),
91
+ });
92
+ }
93
+ return new WslAgent({
94
+ distro: options.distro,
95
+ wslPath: options.wslPath,
96
+ scriptPath: toLinuxPath(agentScriptPath),
97
+ expectedDigest: agentScriptDigest,
98
+ argvPrefix: options.argvPrefix,
99
+ idleMs: options.idleMs,
100
+ leaseMs: options.leaseMs,
101
+ signalable: options.signalable,
102
+ });
103
+ };
104
+ /** The default stop: a second `wsl.exe` asks the user manager to stop the unit. */
105
+ const stopSessionScope = ({ wslPath, distro, unit }) => {
106
+ const argv = sessionScopeStopArgv({ wslPath, distro, unit });
107
+ const child = spawn(argv[0] ?? wslPath, argv.slice(1), {
108
+ stdio: "ignore",
109
+ windowsHide: true,
110
+ env: pinnedWindowsEnv(process.env),
111
+ });
112
+ // The scope may already be gone (the agent's exit, a restart). The stop is
113
+ // best effort; a stale unit is visible to `systemctl --user list-units`.
114
+ child.on("error", () => { });
115
+ };
116
+ /** The default token: short, unique, and sanitized by {@link sessionScopeUnit}. */
117
+ const defaultToken = () => randomUUID().replace(/-/gu, "").slice(0, 12);
118
+ /**
119
+ * The refcounted registry. One instance lives for the process; providers share
120
+ * it so the shell and filesystem sides address the same runtime for a session.
121
+ */
122
+ export class SessionRuntimes {
123
+ /** The live runtimes, by key. */
124
+ runtimes = new Map();
125
+ /** The agent factory. */
126
+ spawnAgent;
127
+ /** The scope stopper. */
128
+ stopScope;
129
+ /** The unit-token generator. */
130
+ token;
131
+ /**
132
+ * @param options - the injectable edges; production passes none.
133
+ */
134
+ constructor(options = {}) {
135
+ this.spawnAgent = options.spawnAgent ?? spawnSessionAgent;
136
+ this.stopScope = options.stopScope ?? stopSessionScope;
137
+ this.token = options.token ?? defaultToken;
138
+ }
139
+ /**
140
+ * The key a request addresses: distro, workspace, mode and mask. A policy
141
+ * change is a different runtime, because the mount profile is fixed for the
142
+ * agent's lifetime.
143
+ *
144
+ * @param request - the runtime request.
145
+ * @returns the registry key.
146
+ */
147
+ static key(request) {
148
+ return [request.distro, request.workspaceRoot, request.mode, request.maskWindowsDrive === true ? "masked" : "visible"].join("\u0000");
149
+ }
150
+ /**
151
+ * Acquire the runtime for one key, creating it on first use.
152
+ *
153
+ * @param request - the runtime request.
154
+ * @returns the live entry; the caller holds one reference.
155
+ */
156
+ acquire(request) {
157
+ const key = SessionRuntimes.key(request);
158
+ const existing = this.runtimes.get(key);
159
+ if (existing !== undefined) {
160
+ existing.refs += 1;
161
+ return existing;
162
+ }
163
+ const wslPath = request.wslPath ?? "wsl.exe";
164
+ const unit = sessionScopeUnit(request.distro, this.token());
165
+ const prefix = sessionScopePrefix(unit);
166
+ if (request.mode !== "danger-full-access") {
167
+ prefix.push(...bwrapArgvPrefix({ mode: request.mode, workspaceRoot: toLinuxPath(request.workspaceRoot, { distro: request.distro }) }, { maskWindowsDrive: request.maskWindowsDrive, realTmp: true, pidNamespace: false }));
168
+ }
169
+ const agent = this.spawnAgent({
170
+ distro: request.distro,
171
+ wslPath,
172
+ argvPrefix: prefix,
173
+ idleMs: 0,
174
+ leaseMs: request.leaseMs ?? 0,
175
+ signalable: true,
176
+ });
177
+ const entry = { key, unit, agent, distro: request.distro, wslPath, refs: 1 };
178
+ this.runtimes.set(key, entry);
179
+ return entry;
180
+ }
181
+ /**
182
+ * Release one reference. The last release closes the agent and stops the
183
+ * scope, in that order: the agent's exit ends the protocol, the stop reaps
184
+ * whatever the session left running.
185
+ *
186
+ * @param key - the entry's key.
187
+ * @returns resolved once the last holder's teardown finished.
188
+ */
189
+ async release(key) {
190
+ const entry = this.runtimes.get(key);
191
+ if (entry === undefined)
192
+ return;
193
+ entry.refs -= 1;
194
+ if (entry.refs > 0)
195
+ return;
196
+ this.runtimes.delete(key);
197
+ try {
198
+ await entry.agent.close();
199
+ }
200
+ finally {
201
+ this.stopScope({ wslPath: entry.wslPath, distro: entry.distro, unit: entry.unit });
202
+ }
203
+ }
204
+ /**
205
+ * Release every runtime, for a host shutdown or a test. Sequential on
206
+ * purpose: concurrent closes would race the shared agent's state.
207
+ *
208
+ * @returns resolved once every runtime is torn down.
209
+ */
210
+ async disposeAll() {
211
+ for (const key of [...this.runtimes.keys()]) {
212
+ const entry = this.runtimes.get(key);
213
+ if (entry !== undefined)
214
+ entry.refs = 1;
215
+ await this.release(key);
216
+ }
217
+ }
218
+ }
219
+ /** The process-wide registry the providers will share. */
220
+ export const sessionRuntimes = new SessionRuntimes();
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pure builders for the session-scoped sandbox launcher (ADR 0001).
3
+ *
4
+ * The runtime is one transient systemd **user scope** per session, running the
5
+ * agent as its main process; commands are the agent's children, so a
6
+ * background dev server stays in the scope's cgroup, `/tmp` is the distro's
7
+ * real `/tmp`, and `ps` sees the session. Session end stops the scope and the
8
+ * cgroup reaps everything.
9
+ *
10
+ * Nothing here spawns: the host inserts {@link sessionScopePrefix} between
11
+ * `wsl.exe --exec` and the agent's own argv, runs
12
+ * {@link sessionScopeProbeArgv} before enabling the mode, and runs
13
+ * {@link sessionScopeStopArgv} when the session ends. Keeping the shapes pure
14
+ * is what lets the launcher be tested without systemd.
15
+ *
16
+ * This is a TypeScript source built to `lib/session-scope.js`; edit THIS file
17
+ * and run `pnpm run build` — the artifact under `lib/` is generated, and
18
+ * `pnpm test` fails when it drifts.
19
+ *
20
+ * @module dsh-plugin-wsl/session-scope
21
+ */
22
+ /**
23
+ * Build the systemd user unit name for one session runtime.
24
+ *
25
+ * The caller supplies a token that is unique for the session (a random hex
26
+ * string, a session id); the distro name is part of the name so two distros'
27
+ * runtimes are never confused in `systemctl --user list-units`. The result is
28
+ * deterministic for the pair, which is what lets the host stop the unit later
29
+ * without keeping a map.
30
+ *
31
+ * @param distro - the distro name.
32
+ * @param token - the per-session unique token.
33
+ * @returns a unit name safe for `systemd-run --unit`.
34
+ */
35
+ export declare function sessionScopeUnit(distro: string, token: string): string;
36
+ /**
37
+ * The arguments inserted after `wsl.exe --exec` to run the agent in a
38
+ * transient user scope. The agent's own argv (its confinement prefix, the
39
+ * shell and the script) follows the `--` terminator.
40
+ *
41
+ * `--scope` is deliberate: a scope's stdio is inherited, so the protocol's
42
+ * pipes stay the transport's; a transient *service* would need `--pipe` and a
43
+ * fresh verification of its EOF/exit-code semantics (ADR open question).
44
+ *
45
+ * @param unit - the unit name from {@link sessionScopeUnit}.
46
+ * @returns the argv fragment, ending with `--`.
47
+ */
48
+ export declare function sessionScopePrefix(unit: string): string[];
49
+ /**
50
+ * The host argv that stops one session runtime. Stopping the scope is the
51
+ * cleanup step that replaces the per-command PID namespace: the cgroup kills
52
+ * every descendant, including a background dev server a command left behind.
53
+ *
54
+ * @param options - the transport path, distro and unit name.
55
+ * @returns the `wsl.exe --exec systemctl --user stop` argv.
56
+ */
57
+ export declare function sessionScopeStopArgv({ wslPath, distro, unit }: {
58
+ /** The `wsl.exe` path. */
59
+ wslPath: string;
60
+ /** The distro name; empty means the default distro. */
61
+ distro: string;
62
+ /** The unit name from {@link sessionScopeUnit}. */
63
+ unit: string;
64
+ }): string[];
65
+ /**
66
+ * The script the preflight runs inside the distro before the session runtime
67
+ * is enabled. It answers one word so the host can map the answer to a remedy:
68
+ *
69
+ * - `ok` — systemd is PID 1, the user manager answers, `systemd-run` exists
70
+ * and the user lingers (a headless `wsl.exe --exec` has no login session).
71
+ * - `no-systemd` — `/run/systemd/system` is absent (systemd is not PID 1).
72
+ * - `no-user-manager` — `systemctl --user` does not report running/degraded.
73
+ * - `no-systemd-run` — the `systemd-run` binary is missing.
74
+ * - `no-linger` — the user manager would stop with the last session; the
75
+ * remedy is `loginctl enable-linger <user>`.
76
+ *
77
+ * `degraded` is accepted: a partially failed unit elsewhere does not make the
78
+ * user manager unable to create a scope.
79
+ *
80
+ * @returns the shell script.
81
+ */
82
+ export declare function sessionScopeProbeScript(): string;
83
+ /**
84
+ * The script the session preflight runs to stop scopes a crashed app left
85
+ * behind. A scope is stale when its cgroup has no agent process: the shell
86
+ * `wsl-agent.sh` or the native `dsh-wsl-agent` is the liveness marker. A live
87
+ * agent means another session still owns it, so it must not be stopped.
88
+ * The script only lists `dsh-wsl-*.scope` units and only stops the ones it
89
+ * can prove stale; if there is no user manager it exits 0 (the session
90
+ * preflight reports that condition separately).
91
+ *
92
+ * @returns the shell script.
93
+ */
94
+ export declare function sessionScopeSweepScript(): string;
95
+ /**
96
+ * Map the preflight's answer to a user-facing refusal, or `null` when the
97
+ * platform can host a session runtime. The probe prints one word (it always
98
+ * exits 0 so the host reads a verdict, not a spawn error); an empty or unknown
99
+ * answer is reported with whatever diagnostic the host captured.
100
+ *
101
+ * @param options - the probe output and the distro it ran in.
102
+ * @returns the refusal with a remedy, or null.
103
+ */
104
+ export declare function sessionScopeProblem({ output, distro }: {
105
+ /** The probe's stdout, or the host error when it could not run. */
106
+ output: {
107
+ stdout: string;
108
+ error?: string;
109
+ };
110
+ /** The distro the probe ran in. */
111
+ distro: string;
112
+ }): string | null;
113
+ /**
114
+ * The host argv for the preflight probe.
115
+ *
116
+ * @param options - the transport path and distro.
117
+ * @returns the `wsl.exe --exec sh -c <script>` argv.
118
+ */
119
+ export declare function sessionScopeProbeArgv({ wslPath, distro }: {
120
+ /** The `wsl.exe` path. */
121
+ wslPath: string;
122
+ /** The distro name; empty means the default distro. */
123
+ distro: string;
124
+ }): string[];