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
@@ -7,22 +7,22 @@
7
7
  * agent script's header for the grammar. Base64 was chosen over JSON because
8
8
  * the agent must run under a bare POSIX sh with nothing but coreutils.
9
9
  *
10
+ * This module is the migration's first TypeScript source. The build emits it
11
+ * to `lib/agent-protocol.js`; edit THIS file and run `pnpm run build` — the
12
+ * artifact under `lib/` is generated, and `pnpm test` fails when it drifts.
13
+ *
10
14
  * @module dsh-plugin-wsl/agent-protocol
11
15
  */
12
-
13
16
  /** The protocol version this host side speaks; the agent must match. */
14
17
  export const PROTOCOL_VERSION = 4;
15
-
16
18
  /**
17
19
  * Upper bound on one `read` op's payload, set by the HOST looping over windowed
18
20
  * requests. Keeping each frame's decoded payload here means the agent's RES
19
21
  * line stays modest even for a large file; the host accumulates chunks.
20
22
  */
21
23
  export const FS_READ_CHUNK_BYTES = 1024 * 1024;
22
-
23
24
  /** The agent identifies itself with this name in its HELLO line. */
24
25
  export const AGENT_NAME = "wsl-agent";
25
-
26
26
  /**
27
27
  * Rough ceiling on one protocol line, chosen well above the executor's own
28
28
  * 64 KB output budget spill: stdout and stderr ride ONE line each, so a frame
@@ -30,71 +30,66 @@ export const AGENT_NAME = "wsl-agent";
30
30
  * future streaming design does not inherit it silently.
31
31
  */
32
32
  export const MAX_FRAME_BYTES = 256 * 1024 * 1024;
33
-
34
33
  /**
35
34
  * Encode a buffer as the protocol's single-line base64 (no line wraps, no
36
35
  * padding surprises — standard base64 including `=` padding).
37
- * @param {string|Uint8Array} value - the payload to encode.
38
- * @returns {string} the wire form.
36
+ *
37
+ * @param value - the payload to encode.
38
+ * @returns the wire form.
39
39
  */
40
40
  export function encodeB64(value) {
41
- return Buffer.from(value).toString("base64");
41
+ return Buffer.from(value).toString("base64");
42
42
  }
43
-
44
43
  /**
45
44
  * Decode one protocol base64 field.
46
- * @param {string} value - the wire form.
47
- * @returns {Buffer} the decoded bytes; a malformed field decodes to empty
48
- * rather than throwing, mirroring the agent's own lenient decode.
45
+ *
46
+ * @param value - the wire form.
47
+ * @returns the decoded bytes; a malformed field decodes to empty rather than
48
+ * throwing, mirroring the agent's own lenient decode.
49
49
  */
50
50
  export function decodeB64(value) {
51
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value)) return Buffer.alloc(0);
52
- return Buffer.from(value, "base64");
51
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value))
52
+ return Buffer.alloc(0);
53
+ return Buffer.from(value, "base64");
53
54
  }
54
-
55
55
  /**
56
56
  * Build the request lines for one EXEC frame: a header line naming the cwd,
57
57
  * the whole-request timeout in whole seconds (0 = none), the argv word count
58
58
  * and the per-stream output capture budget in bytes (0 = unlimited), followed
59
59
  * by one base64 line per argv word.
60
- * @param {object} request - the execution request.
61
- * @param {string} request.id - caller-chosen request id (unique per agent).
62
- * @param {string} request.cwd - Linux path to cd into before exec.
63
- * @param {string[]} request.argv - the argv to execute, already bwrap-wrapped.
64
- * @param {number} [request.timeoutMs] - in-distro timeout; sub-second
65
- * remainders round up, and `<= 0` disables the timeout.
66
- * @param {number} [request.maxOutputBytes] - per-stream capture ceiling the
67
- * AGENT enforces: stdout and stderr are each cut at this many bytes and the
68
- * RES line reports what was cut. 0 (the default) keeps the historical
69
- * unbounded capture.
70
- * @returns {string[]} the lines to write to the agent's stdin, in order.
60
+ *
61
+ * @param request - the execution request.
62
+ * @returns the lines to write to the agent's stdin, in order.
71
63
  */
72
64
  export function encodeExecFrame({ id, cwd, argv, timeoutMs = 0, maxOutputBytes = 0 }) {
73
- const timeoutSeconds = timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : 0;
74
- const cap = maxOutputBytes > 0 ? Math.floor(maxOutputBytes) : 0;
75
- const lines = [
76
- `EXEC|${id}|${encodeB64(cwd)}|${timeoutSeconds}|${argv.length}|${cap}`,
77
- ...argv.map((word) => encodeB64(word)),
78
- ];
79
- return lines;
65
+ const timeoutSeconds = timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : 0;
66
+ const cap = maxOutputBytes > 0 ? Math.floor(maxOutputBytes) : 0;
67
+ const lines = [
68
+ `EXEC|${id}|${encodeB64(cwd)}|${timeoutSeconds}|${argv.length}|${cap}`,
69
+ ...argv.map((word) => encodeB64(word)),
70
+ ];
71
+ return lines;
80
72
  }
81
-
82
73
  /**
83
74
  * Build a SETENV request line. Note the agent's decode strips trailing
84
75
  * newlines from the value — the same leniency the shell itself applies.
85
- * @param {string} key - the variable name.
86
- * @param {string} value - the variable value.
87
- * @returns {string} the line to write.
76
+ *
77
+ * @param key - the variable name.
78
+ * @param value - the variable value.
79
+ * @returns the line to write.
88
80
  */
89
81
  export function encodeSetEnv(key, value) {
90
- return `SETENV|${key}|${encodeB64(value)}`;
82
+ return `SETENV|${key}|${encodeB64(value)}`;
91
83
  }
92
-
93
- /** @param {string} id - the request id to interrupt. @returns {string} */
84
+ /**
85
+ * Build a KILL request line.
86
+ *
87
+ * @param id - the request id to interrupt.
88
+ * @returns the line to write.
89
+ */
94
90
  export function encodeKill(id) {
95
- return `KILL|${id}`;
91
+ return `KILL|${id}`;
96
92
  }
97
-
98
93
  /**
99
94
  * Build the request lines for one FS frame — the filesystem-substrate request
100
95
  * that lets the model's file tools run inside the distro. The shape mirrors
@@ -111,69 +106,50 @@ export function encodeKill(id) {
111
106
  * content is no exception — the agent pipes its still-encoded wire line
112
107
  * straight to `base64 -d`, so raw bytes never pass through a shell variable.
113
108
  *
114
- * @param {object} request - the filesystem request.
115
- * @param {string} request.id - caller-chosen request id (unique per agent).
116
- * @param {string} request.op - one of the op names above.
117
- * @param {string[]} request.args - op arguments, base64-line encoded here.
118
- * @param {number} [request.timeoutMs] - in-distro timeout; sub-second
119
- * remainders round up, and `<= 0` disables the timeout.
120
- * @returns {string[]} the lines to write to the agent's stdin, in order.
109
+ * @param request - the filesystem request.
110
+ * @returns the lines to write to the agent's stdin, in order.
121
111
  */
122
112
  export function encodeFsFrame({ id, op, args, timeoutMs = 0 }) {
123
- const timeoutSeconds = timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : 0;
124
- return [
125
- `FS|${id}|${op}|${timeoutSeconds}|${args.length}`,
126
- ...args.map((word) => encodeB64(word)),
127
- ];
113
+ const timeoutSeconds = timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : 0;
114
+ return [
115
+ `FS|${id}|${op}|${timeoutSeconds}|${args.length}`,
116
+ ...args.map((word) => encodeB64(word)),
117
+ ];
128
118
  }
129
-
130
- /** The line parser's verdict for one line of agent stdout. */
131
-
132
119
  /**
133
120
  * Parse one line of the agent's stdout.
134
- * @param {string} line - a line without its terminator.
135
- * @returns {object} one of:
136
- * - `{ type: "hello", name, version, digest }` — `digest` is the sha256 of
137
- * the script file the agent is executing (empty when it could not hash
138
- * it); the host compares it against its own shipped copy
139
- * - `{ type: "pong" }`
140
- * - `{ type: "ack", id }` — the agent dequeued the request for execution;
141
- * the host arms its stuck-request watchdog here, so queue time never
142
- * counts against a budget
143
- * - `{ type: "result", id, exitCode, stdout, stderr }` — outputs as Buffers
144
- * - `{ type: "agentError", id, reason, message }` — `id` is `""` when the
145
- * agent reports a request-less problem (a protocol violation)
146
- * - `{ type: "unknown", line }` — never thrown; the host decides whether an
147
- * unrecognized line is fatal. (Anything during the handshake is; after it,
148
- * garbage on stdout is logged and dropped, because a `RES` for a request
149
- * the host already gave up on is expected noise.)
121
+ *
122
+ * @param line - a line without its terminator.
123
+ * @returns the line's verdict; the message interfaces document each variant.
150
124
  */
151
125
  export function parseAgentLine(line) {
152
- if (line.startsWith("HELLO|")) {
153
- const [, name, version, digest] = line.split("|");
154
- return { type: "hello", name, version: Number(version), digest: digest ?? "" };
155
- }
156
- if (line === "PONG") return { type: "pong" };
157
- if (line.startsWith("ACK|")) return { type: "ack", id: line.slice(4) };
158
- if (line.startsWith("RES|")) {
159
- const [, id, exitCode, stdout, stderr, outTruncated, errTruncated] = line.split("|");
160
- // A non-numeric code is protocol garbage, not a success: -1 is a value
161
- // no `wait` can produce, so a caller can tell it from every real code.
162
- const code = Number(exitCode);
163
- return {
164
- type: "result",
165
- id,
166
- exitCode: Number.isSafeInteger(code) ? code : -1,
167
- stdout: decodeB64(stdout ?? ""),
168
- stderr: decodeB64(stderr ?? ""),
169
- // The capture-cap flags the agent appends when the EXEC frame carried a
170
- // budget; frames without them (fakes, FS ops) are simply not truncated.
171
- truncated: { stdout: outTruncated === "1", stderr: errTruncated === "1" },
172
- };
173
- }
174
- if (line.startsWith("ERR|")) {
175
- const [, id, reason, message] = line.split("|");
176
- return { type: "agentError", id: id ?? "", reason: reason ?? "protocol", message: decodeB64(message ?? "").toString("utf8") };
177
- }
178
- return { type: "unknown", line };
126
+ if (line.startsWith("HELLO|")) {
127
+ const [, name, version, digest] = line.split("|");
128
+ return { type: "hello", name: name ?? "", version: Number(version), digest: digest ?? "" };
129
+ }
130
+ if (line === "PONG")
131
+ return { type: "pong" };
132
+ if (line.startsWith("ACK|"))
133
+ return { type: "ack", id: line.slice(4) };
134
+ if (line.startsWith("RES|")) {
135
+ const [, id, exitCode, stdout, stderr, outTruncated, errTruncated] = line.split("|");
136
+ // A non-numeric code is protocol garbage, not a success: -1 is a value
137
+ // no `wait` can produce, so a caller can tell it from every real code.
138
+ const code = Number(exitCode);
139
+ return {
140
+ type: "result",
141
+ id: id ?? "",
142
+ exitCode: Number.isSafeInteger(code) ? code : -1,
143
+ stdout: decodeB64(stdout ?? ""),
144
+ stderr: decodeB64(stderr ?? ""),
145
+ // The capture-cap flags the agent appends when the EXEC frame carried a
146
+ // budget; frames without them (fakes, FS ops) are simply not truncated.
147
+ truncated: { stdout: outTruncated === "1", stderr: errTruncated === "1" },
148
+ };
149
+ }
150
+ if (line.startsWith("ERR|")) {
151
+ const [, id, reason, message] = line.split("|");
152
+ return { type: "agentError", id: id ?? "", reason: reason ?? "protocol", message: decodeB64(message ?? "").toString("utf8") };
153
+ }
154
+ return { type: "unknown", line };
179
155
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The per-distro resident agent shared by every provider in this process.
3
+ *
4
+ * Extracted from `lib/index.js` so modules the index does not import back
5
+ * (`shell-env`, future consumers) can use the same peer without a cycle: the
6
+ * shell executor's commands, the filesystem's in-distro side work and the
7
+ * port snapshot all queue on ONE agent per distro, and the per-distro startup
8
+ * cost is paid once per process.
9
+ *
10
+ * This is a TypeScript source built to `lib/agent-shared.js`; edit THIS file
11
+ * and run `pnpm run build` — the artifact under `lib/` is generated, and
12
+ * `pnpm test` fails when it drifts.
13
+ *
14
+ * @module dsh-plugin-wsl/agent-shared
15
+ */
16
+ import { WslAgent } from "./agent.js";
17
+ /** The agent script as a host-side path, for factories that need both spellings. */
18
+ export declare const agentScriptPath: string;
19
+ /** The sha256 of the shipped agent script; every resident verifies its HELLO against it. */
20
+ export declare const agentScriptDigest: string;
21
+ /**
22
+ * The shared agent for one distro, created on first use.
23
+ *
24
+ * @param distro - the distro name.
25
+ * @returns the shared agent.
26
+ */
27
+ export declare function sharedAgent(distro: string): WslAgent;
@@ -7,15 +7,17 @@
7
7
  * port snapshot all queue on ONE agent per distro, and the per-distro startup
8
8
  * cost is paid once per process.
9
9
  *
10
+ * This is a TypeScript source built to `lib/agent-shared.js`; edit THIS file
11
+ * and run `pnpm run build` — the artifact under `lib/` is generated, and
12
+ * `pnpm test` fails when it drifts.
13
+ *
10
14
  * @module dsh-plugin-wsl/agent-shared
11
15
  */
12
-
13
16
  import { createHash } from "node:crypto";
14
17
  import { readFileSync } from "node:fs";
15
18
  import { fileURLToPath } from "node:url";
16
19
  import { toLinuxPath } from "./paths.js";
17
20
  import { WslAgent } from "./agent.js";
18
-
19
21
  const AGENT_SCRIPT_PATH = fileURLToPath(new URL("../agent/wsl-agent.sh", import.meta.url));
20
22
  // The script the distro reads must be byte-identical to the copy this
21
23
  // package shipped: the deployed layout reads it from the /mnt/c mirror, and
@@ -23,23 +25,21 @@ const AGENT_SCRIPT_PATH = fileURLToPath(new URL("../agent/wsl-agent.sh", import.
23
25
  // Both resident factories (plain and confined) pin it.
24
26
  const AGENT_SCRIPT_DIGEST = createHash("sha256").update(readFileSync(AGENT_SCRIPT_PATH)).digest("hex");
25
27
  const sharedAgents = new Map();
26
-
27
28
  /** The agent script as a host-side path, for factories that need both spellings. */
28
29
  export const agentScriptPath = AGENT_SCRIPT_PATH;
29
-
30
30
  /** The sha256 of the shipped agent script; every resident verifies its HELLO against it. */
31
31
  export const agentScriptDigest = AGENT_SCRIPT_DIGEST;
32
-
33
32
  /**
34
33
  * The shared agent for one distro, created on first use.
34
+ *
35
35
  * @param distro - the distro name.
36
- * @returns {WslAgent} the shared agent.
36
+ * @returns the shared agent.
37
37
  */
38
38
  export function sharedAgent(distro) {
39
- let agent = sharedAgents.get(distro);
40
- if (agent === undefined) {
41
- agent = new WslAgent({ distro, scriptPath: toLinuxPath(AGENT_SCRIPT_PATH), expectedDigest: AGENT_SCRIPT_DIGEST });
42
- sharedAgents.set(distro, agent);
43
- }
44
- return agent;
39
+ let agent = sharedAgents.get(distro);
40
+ if (agent === undefined) {
41
+ agent = new WslAgent({ distro, scriptPath: toLinuxPath(AGENT_SCRIPT_PATH), expectedDigest: AGENT_SCRIPT_DIGEST });
42
+ sharedAgents.set(distro, agent);
43
+ }
44
+ return agent;
45
45
  }
package/lib/agent.d.ts ADDED
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Host side of the resident in-distro agent.
3
+ *
4
+ * One {@link WslAgent} instance owns one long-lived `wsl.exe` process per
5
+ * distro. The lifetime rules, in order of importance:
6
+ *
7
+ * 1. **Correctness before speed.** Any agent failure is answered by at most
8
+ * one rebuild; the requests that were in flight ride it ONLY when their
9
+ * first execution provably changed nothing (the read-only FS ops). A
10
+ * command or a write that was in flight has UNKNOWN state — replaying it
11
+ * would run non-idempotent work twice or report a create that actually
12
+ * succeeded as a refusal — so it fails instead, and the caller re-reads
13
+ * before retrying. A second failure marks the agent permanently dead and
14
+ * every future call rejects with {@link AgentUnavailableError} — the
15
+ * executor's cue to fall back to the one-shot `wsl.exe` path it shipped
16
+ * with, so the worst case of the agent is "today's behaviour, plus one
17
+ * failed round trip".
18
+ * 2. **The in-distro timeout is the real one.** The EXEC frame carries the
19
+ * timeout in whole seconds and the agent SIGTERMs the child itself, so a
20
+ * timed-out command dies where it lives instead of leaving an orphan
21
+ * behind a dead `wsl.exe`. The host-side watchdog is armed on the agent's
22
+ * `ACK` — the dequeue signal — so it measures EXECUTION, never the queue
23
+ * wait: a short-budget request queued behind a long one can no longer
24
+ * kill the shared transport for a stall that was never its own. It still
25
+ * catches an agent that accepted a request and then stopped answering at
26
+ * all.
27
+ * 3. **Idle agents go away.** `idleMs` after the last exchange the process is
28
+ * killed; the next call starts a fresh one. WSL VMs are shared, but every
29
+ * process is a promise someone has to keep.
30
+ *
31
+ * The transport is injectable so the lifecycle and the fallback decision are
32
+ * unit-testable without a distro.
33
+ *
34
+ * This is a TypeScript source built to `lib/agent.js`; edit THIS file and run
35
+ * `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
36
+ * fails when it drifts.
37
+ *
38
+ * @module dsh-plugin-wsl/agent
39
+ */
40
+ import type { ChildProcess } from "node:child_process";
41
+ /** One settled command result, as `exec` resolves it. */
42
+ export interface ExecResult {
43
+ /** The command's exit status. */
44
+ exitCode: number;
45
+ /** Captured stdout. */
46
+ stdout: Buffer;
47
+ /** Captured stderr. */
48
+ stderr: Buffer;
49
+ /** Which streams the capture cap cut. */
50
+ truncated: {
51
+ stdout: boolean;
52
+ stderr: boolean;
53
+ };
54
+ }
55
+ /**
56
+ * One request in flight on an agent: its frame, its settlement callbacks, and
57
+ * the ACK-armed watchdog.
58
+ */
59
+ export interface PendingRequest {
60
+ /** The request id, as the agent echoes it. */
61
+ id: string;
62
+ /** The frame lines (kept so a rebuild can re-send). */
63
+ lines: string[];
64
+ /** Settles a RES. */
65
+ resolve: (result: ExecResult) => void;
66
+ /** Settles a failure. */
67
+ reject: (error: Error) => void;
68
+ /** The ACK-armed watchdog timer. */
69
+ watchdog: NodeJS.Timeout | null;
70
+ /** The caller's cancellation, when any. */
71
+ signal: AbortSignal | null;
72
+ /** The abort listener, for removal. */
73
+ onAbort: (() => void) | null;
74
+ /** Arms `watchdog`; null when the request carries no timeout. */
75
+ armWatchdog: (() => void) | null;
76
+ }
77
+ /**
78
+ * The Windows environment the transport starts from, pinned.
79
+ *
80
+ * Spawned bare, `wsl.exe` would inherit this process's whole environment — and
81
+ * the `WSLENV` variable riding along decides which of those names are
82
+ * FORWARDED into the distro, where every model command inherits them. Dev
83
+ * machines routinely route secrets that way (`WSLENV=GITHUB_TOKEN/u` is the
84
+ * documented example), so the resident's env is built from scratch instead:
85
+ * the Windows essentials `wsl.exe` itself needs, the managed `DSH_*` facts,
86
+ * and a `WSLENV` that names only those managed entries — preserving their
87
+ * original forwarding flags. Nothing else on the Windows side can reach a
88
+ * model command.
89
+ *
90
+ * Deliberately NOT here: the executor's `forwardEnv` names. They ride the
91
+ * one-shot path's WSLENV (`spawnSpec` builds it per call); the resident path
92
+ * has never forwarded them, and this pin keeps that posture instead of
93
+ * widening it silently.
94
+ *
95
+ * @param parent - this process's environment.
96
+ * @returns the pinned environment for `wsl.exe`.
97
+ */
98
+ export declare function pinnedWindowsEnv(parent: Record<string, string | undefined>): Record<string, string>;
99
+ /**
100
+ * The spawn environment for one resident, with the client lease folded in.
101
+ * The pinned environment forwards only the managed `DSH_` namespace, and it
102
+ * forwards exactly the names the parent `WSLENV` lists — so the lease rides
103
+ * both: the value on the variable, the name on `WSLENV`. A non-positive
104
+ * lease leaves the environment as the pin built it (the script treats an
105
+ * unset variable as no lease).
106
+ *
107
+ * @param env - the pinned environment.
108
+ * @param leaseMs - the agent's client lease in ms; 0 disables.
109
+ * @returns the environment to spawn with.
110
+ */
111
+ export declare function agentLeaseEnv(env: Record<string, string>, leaseMs: number): Record<string, string>;
112
+ /** The resident agent for one distro. */
113
+ export declare class WslAgent {
114
+ /** The distro this agent serves. */
115
+ distro: string;
116
+ /** The agent script as a Linux path inside the distro (see {@link spawnDefault}). */
117
+ scriptPath: string;
118
+ /** The `wsl.exe` path. */
119
+ wslPath: string;
120
+ /** Arguments between `wsl.exe --exec` and the `sh script` pair. */
121
+ argvPrefix: string[];
122
+ /** Idle shutdown delay; 0 disables. */
123
+ idleMs: number;
124
+ /** The sha256 of the agent script as this package shipped it. */
125
+ expectedDigest: string | undefined;
126
+ /**
127
+ * Hard ceiling on one stdout protocol line; a longer line is a broken or
128
+ * hostile agent, not data to hold — readline used to accumulate it all.
129
+ */
130
+ frameCapBytes: number;
131
+ /** The injectable process factory, when the caller supplied one. */
132
+ configuredSpawn: ((options: {
133
+ wslPath: string;
134
+ distro: string;
135
+ scriptPath: string;
136
+ argvPrefix: string[];
137
+ }) => ChildProcess) | undefined;
138
+ /** The live resident process, when any. */
139
+ process: ChildProcess | null;
140
+ /** The lifecycle state. */
141
+ state: "idle" | "starting" | "ready" | "dead";
142
+ /** The in-flight start, shared by concurrent callers. */
143
+ startPromise: Promise<void> | null;
144
+ /** The next request id. */
145
+ nextRequestId: number;
146
+ /** The requests in flight, keyed by their current id. */
147
+ pending: Map<string, PendingRequest>;
148
+ /** Whether the one permitted rebuild has been spent. */
149
+ rebuildUsed: boolean;
150
+ /** The idle-shutdown timer. */
151
+ idleTimer: NodeJS.Timeout | null;
152
+ /** The last failure, for `unavailableReason` and log messages. */
153
+ lastError: string;
154
+ /**
155
+ * @param config - the agent configuration.
156
+ */
157
+ constructor(config: {
158
+ /** The distro this agent serves. */
159
+ distro: string;
160
+ /**
161
+ * The agent script as a Linux path inside the distro (see
162
+ * {@link spawnDefault}).
163
+ */
164
+ scriptPath: string;
165
+ /** The `wsl.exe` path. */
166
+ wslPath?: string;
167
+ /** Idle shutdown delay; 0 disables. */
168
+ idleMs?: number;
169
+ /**
170
+ * The sha256 of the agent script as this package shipped it; a HELLO whose
171
+ * digest differs is refused the same way a version mismatch is (the script
172
+ * is read in place, so a stale or half-synced runtime copy is the failure
173
+ * this catches).
174
+ */
175
+ expectedDigest?: string;
176
+ /**
177
+ * Arguments inserted between `wsl.exe --exec` and the `sh script` pair —
178
+ * the confinement wrapper (`bwrap … --`) a confined agent runs its whole
179
+ * lifetime under, so every filesystem op it performs is kernel-enforced.
180
+ * Empty for the plain agent.
181
+ */
182
+ argvPrefix?: string[];
183
+ /** Hard ceiling in bytes on one stdout protocol line; defaults to {@link MAX_FRAME_BYTES}. */
184
+ frameCapBytes?: number;
185
+ /** Injectable process factory for tests. */
186
+ spawnTransport?: (options: {
187
+ wslPath: string;
188
+ distro: string;
189
+ scriptPath: string;
190
+ argvPrefix: string[];
191
+ }) => ChildProcess;
192
+ });
193
+ /**
194
+ * Whether the agent is permanently out — the executor's fallback cue.
195
+ *
196
+ * @returns true once the rebuild budget is spent.
197
+ */
198
+ get unavailable(): boolean;
199
+ /** Human-readable reason for the last transition to `dead`, for logging. */
200
+ get unavailableReason(): string;
201
+ /**
202
+ * Start the process and wait for the handshake. Re-entrant: concurrent
203
+ * callers share the one start.
204
+ *
205
+ * @returns resolved once the state is `ready`.
206
+ */
207
+ start(): Promise<void>;
208
+ /**
209
+ * One stdout line from the agent. The first must be a matching HELLO.
210
+ *
211
+ * @param line - one protocol line, terminator stripped.
212
+ * @param settleStart - the handshake settler.
213
+ * @param handshakeTimer - the handshake watchdog.
214
+ */
215
+ private handleLine;
216
+ /**
217
+ * The process died. Requests in flight ride exactly one rebuild; a second
218
+ * death is permanent.
219
+ *
220
+ * @param code - the exit code, null when killed by a signal.
221
+ * @param signalName - the killing signal, null on a normal exit.
222
+ */
223
+ private handleProcessExit;
224
+ /**
225
+ * Register one request's pending entry and write its frame: the shared
226
+ * request machinery (abort, submission, the ACK-armed watchdog). The
227
+ * watchdog is deliberately NOT armed here — it starts at the agent's
228
+ * dequeue `ACK`, so time spent queued behind an earlier request never
229
+ * spends this request's budget and never kills the shared transport for a
230
+ * stall that was not its own.
231
+ *
232
+ * @param request - the request to register.
233
+ */
234
+ private track;
235
+ /**
236
+ * Run one command inside the distro through the agent.
237
+ *
238
+ * @param request - the execution request.
239
+ * @returns the settled result.
240
+ * @throws {AgentUnavailableError} when the agent is out for this process.
241
+ */
242
+ exec({ cwd, argv, timeoutMs, maxOutputBytes, env, signal }: {
243
+ /** Linux path to run in. */
244
+ cwd: string;
245
+ /** The argv, already bwrap-wrapped when the caller confines. */
246
+ argv: string[];
247
+ /**
248
+ * In-distro timeout, whole seconds upward, measured from the agent's
249
+ * dequeue ACK; `<= 0` or omitted means none. Queue time ahead of the
250
+ * dispatch is never counted.
251
+ */
252
+ timeoutMs?: number;
253
+ /**
254
+ * Extra environment for this request only; values lose trailing newlines
255
+ * (protocol limitation).
256
+ */
257
+ env?: Record<string, string>;
258
+ /**
259
+ * Per-stream capture ceiling the agent enforces (carried on the EXEC
260
+ * frame); the RES line reports which streams were cut. 0 (the default)
261
+ * keeps the unbounded capture.
262
+ */
263
+ maxOutputBytes?: number;
264
+ /** Cancels the request (KILL + reject). */
265
+ signal?: AbortSignal;
266
+ }): Promise<{
267
+ exitCode: number;
268
+ stdout: Buffer;
269
+ stderr: Buffer;
270
+ truncated: {
271
+ stdout: boolean;
272
+ stderr: boolean;
273
+ };
274
+ }>;
275
+ /**
276
+ * Run one filesystem-substrate op inside the distro through the agent.
277
+ * Responses reuse the EXEC `ACK`/`RES` lines, so cancellation and the
278
+ * watchdog are exactly the command path's. The rebuild relay differs by op:
279
+ * a read-only op rides a rebuild, a `write` in flight fails with an
280
+ * unknown-state error instead of being replayed.
281
+ *
282
+ * @param request - the filesystem request.
283
+ * @returns the settled result.
284
+ * @throws {AgentUnavailableError} when the agent is out for this process.
285
+ */
286
+ fs({ op, args, timeoutMs, signal }: {
287
+ /**
288
+ * Op name (`stat`, `lstat`, `list`, `realpath`, `read`, `write`),
289
+ * dispatched by `agent/wsl-agent.sh`.
290
+ */
291
+ op: string;
292
+ /** Op arguments; each rides one base64 line. */
293
+ args: string[];
294
+ /**
295
+ * In-distro timeout, whole seconds upward, measured from the agent's
296
+ * dequeue ACK; `<= 0` or omitted means none.
297
+ */
298
+ timeoutMs?: number;
299
+ /** Cancels the request (KILL + reject). */
300
+ signal?: AbortSignal;
301
+ }): Promise<{
302
+ exitCode: number;
303
+ stdout: Buffer;
304
+ stderr: Buffer;
305
+ }>;
306
+ /**
307
+ * Liveness probe, also used to warm the agent up.
308
+ *
309
+ * @param signal - cancels the wait.
310
+ * @returns resolved on PONG.
311
+ */
312
+ ping(signal?: AbortSignal): Promise<void>;
313
+ /**
314
+ * Shut the agent down: SHUTDOWN first, then kill after a short grace. The
315
+ * agent becomes reusable — a later `exec` starts a fresh process.
316
+ *
317
+ * @returns resolved once the process is gone.
318
+ */
319
+ close(): Promise<void>;
320
+ /**
321
+ * Write one frame to the live process.
322
+ *
323
+ * @param lines - the frame lines to write.
324
+ * @returns whether the frame was written.
325
+ */
326
+ private writeLines;
327
+ /**
328
+ * Write one frame to a specific process, tolerating a dead pipe.
329
+ *
330
+ * @param child - the process being shut down.
331
+ * @param lines - the lines to write.
332
+ */
333
+ private writeLinesSafe;
334
+ /** Restart the idle-shutdown countdown. */
335
+ private rearmIdleTimer;
336
+ /**
337
+ * Detach an entry's timer and abort listener.
338
+ *
339
+ * @param pending - the entry to clear.
340
+ */
341
+ private clearPending;
342
+ }
343
+ /**
344
+ * Rewrite the request id inside a stored frame so a resent request does not
345
+ * collide with ids the new process has already seen. SETENV lines pass
346
+ * through unchanged (they are id-less and idempotent).
347
+ *
348
+ * @param lines - the stored frame.
349
+ * @param newId - the new request id.
350
+ * @returns the relabelled frame.
351
+ */
352
+ export declare function relabelFrame(lines: string[], newId: string): string[];
353
+ export default WslAgent;