dsh-plugin-wsl-env 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +278 -0
- package/CONTRIBUTING.md +129 -39
- package/README.md +20 -20
- package/docs/ARCHITECTURE.md +6 -3
- package/docs/LIMITATIONS.md +13 -0
- package/docs/README.zh.md +20 -20
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1101 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +40 -0
- package/lib/probe-cache.js +35 -29
- package/lib/sandbox-core.d.ts +79 -0
- package/lib/sandbox-core.js +71 -88
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +144 -155
- package/package.json +68 -20
package/lib/agent-protocol.js
CHANGED
|
@@ -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
|
-
*
|
|
38
|
-
* @
|
|
36
|
+
*
|
|
37
|
+
* @param value - the payload to encode.
|
|
38
|
+
* @returns the wire form.
|
|
39
39
|
*/
|
|
40
40
|
export function encodeB64(value) {
|
|
41
|
-
|
|
41
|
+
return Buffer.from(value).toString("base64");
|
|
42
42
|
}
|
|
43
|
-
|
|
44
43
|
/**
|
|
45
44
|
* Decode one protocol base64 field.
|
|
46
|
-
*
|
|
47
|
-
* @
|
|
48
|
-
*
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
*
|
|
61
|
-
* @param
|
|
62
|
-
* @
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
*
|
|
86
|
-
* @param
|
|
87
|
-
* @
|
|
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
|
-
|
|
82
|
+
return `SETENV|${key}|${encodeB64(value)}`;
|
|
91
83
|
}
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
115
|
-
* @
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
*
|
|
135
|
-
* @
|
|
136
|
-
*
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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;
|
package/lib/agent-shared.js
CHANGED
|
@@ -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
|
|
36
|
+
* @returns the shared agent.
|
|
37
37
|
*/
|
|
38
38
|
export function sharedAgent(distro) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|