@rudderhq/agent-runtime-utils 0.7.11 → 0.7.13

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 (57) hide show
  1. package/dist/index.d.ts +3 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/native-command.d.ts +12 -0
  6. package/dist/native-command.d.ts.map +1 -0
  7. package/dist/native-command.js +13 -0
  8. package/dist/native-command.js.map +1 -0
  9. package/dist/native-command.test.d.ts +2 -0
  10. package/dist/native-command.test.d.ts.map +1 -0
  11. package/dist/native-command.test.js +23 -0
  12. package/dist/native-command.test.js.map +1 -0
  13. package/dist/native-process-runner.d.ts +35 -0
  14. package/dist/native-process-runner.d.ts.map +1 -0
  15. package/dist/native-process-runner.js +635 -0
  16. package/dist/native-process-runner.js.map +1 -0
  17. package/dist/native-process-runner.test.d.ts +2 -0
  18. package/dist/native-process-runner.test.d.ts.map +1 -0
  19. package/dist/native-process-runner.test.js +301 -0
  20. package/dist/native-process-runner.test.js.map +1 -0
  21. package/dist/network-suspension.d.ts +22 -0
  22. package/dist/network-suspension.d.ts.map +1 -0
  23. package/dist/network-suspension.js +97 -0
  24. package/dist/network-suspension.js.map +1 -0
  25. package/dist/network-suspension.test.d.ts +2 -0
  26. package/dist/network-suspension.test.d.ts.map +1 -0
  27. package/dist/network-suspension.test.js +41 -0
  28. package/dist/network-suspension.test.js.map +1 -0
  29. package/dist/rudder-mcp-contract.d.ts +19 -1
  30. package/dist/rudder-mcp-contract.d.ts.map +1 -1
  31. package/dist/rudder-mcp-contract.js +23 -2
  32. package/dist/rudder-mcp-contract.js.map +1 -1
  33. package/dist/rudder-mcp-tool-descriptors.generated.d.ts +17 -1
  34. package/dist/rudder-mcp-tool-descriptors.generated.d.ts.map +1 -1
  35. package/dist/rudder-mcp-tool-descriptors.generated.js +19 -1
  36. package/dist/rudder-mcp-tool-descriptors.generated.js.map +1 -1
  37. package/dist/server-utils.cli.d.ts.map +1 -1
  38. package/dist/server-utils.cli.js +5 -2
  39. package/dist/server-utils.cli.js.map +1 -1
  40. package/dist/server-utils.instructions.d.ts +2 -3
  41. package/dist/server-utils.instructions.d.ts.map +1 -1
  42. package/dist/server-utils.instructions.js +6 -13
  43. package/dist/server-utils.instructions.js.map +1 -1
  44. package/dist/server-utils.process.d.ts +3 -0
  45. package/dist/server-utils.process.d.ts.map +1 -1
  46. package/dist/server-utils.process.js +64 -1
  47. package/dist/server-utils.process.js.map +1 -1
  48. package/dist/server-utils.prompts.d.ts +2 -2
  49. package/dist/server-utils.prompts.d.ts.map +1 -1
  50. package/dist/server-utils.prompts.js +3 -3
  51. package/dist/server-utils.prompts.test.js +1 -0
  52. package/dist/server-utils.prompts.test.js.map +1 -1
  53. package/dist/server-utils.test.js +50 -23
  54. package/dist/server-utils.test.js.map +1 -1
  55. package/dist/types.d.ts +30 -2
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +4 -1
@@ -0,0 +1,635 @@
1
+ import { createRudderNativeDiagnostic, resolveRudderNativeCapability, resolveRudderNativeTarget, } from "@rudderhq/shared";
2
+ import { spawn } from "node:child_process";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { EventEmitter } from "node:events";
5
+ import { existsSync } from "node:fs";
6
+ import { mkdir, readFile } from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { appendWithCap, runningProcesses, } from "./server-utils.process.js";
11
+ const PROTOCOL_VERSION = { major: 1, minor: 0 };
12
+ const MAX_LIFECYCLE_FRAME_BYTES = 64 * 1024;
13
+ const MAX_OUTPUT_QUEUE_BYTES = 4 * 1024 * 1024;
14
+ const MAX_OUTPUT_QUEUE_ITEMS = 1_024;
15
+ const HANDSHAKE_TIMEOUT_MS = 5_000;
16
+ export class NativeProcessUnavailableError extends Error {
17
+ fallbackCode;
18
+ accepted;
19
+ diagnostic;
20
+ constructor(message, fallbackCode, accepted = false, options) {
21
+ super(message, options);
22
+ this.name = "NativeProcessUnavailableError";
23
+ this.fallbackCode = fallbackCode;
24
+ this.accepted = accepted;
25
+ this.diagnostic = createRudderNativeDiagnostic({
26
+ capability: "agent-run-process",
27
+ effectiveEngine: accepted ? "rust" : "node",
28
+ fallbackCode,
29
+ protocolVersion: `${PROTOCOL_VERSION.major}.${PROTOCOL_VERSION.minor}`,
30
+ });
31
+ }
32
+ }
33
+ export function nativeAgentRunPolicy(env = process.env) {
34
+ return resolveRudderNativeCapability({
35
+ capability: "agent-run-process",
36
+ env,
37
+ legacyToggleEnvs: ["RUDDER_NATIVE_PROCESS_HOST", "RUDDER_NATIVE_AGENT_RUN_PROCESS"],
38
+ });
39
+ }
40
+ export async function runNativeChildProcessOrFallback(runId, command, args, env, opts) {
41
+ const policy = nativeAgentRunPolicy(env);
42
+ if (!policy.enabled)
43
+ return null;
44
+ const onLogError = opts.onLogError ?? ((error, id, message) => console.warn({ error, runId: id }, message));
45
+ try {
46
+ return await runNativeChildProcess(runId, command, args, {
47
+ ...opts,
48
+ onLogError,
49
+ env: Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string")),
50
+ });
51
+ }
52
+ catch (error) {
53
+ const nativeError = error instanceof NativeProcessUnavailableError ? error : null;
54
+ if (policy.required || nativeError?.accepted !== false)
55
+ throw error;
56
+ onLogError(error, runId, `Rust agent-run process host unavailable before acceptance; using Node (${nativeError.fallbackCode})`);
57
+ return null;
58
+ }
59
+ }
60
+ function nativeTarget() {
61
+ if (process.platform === "darwin" && process.arch === "arm64")
62
+ return "aarch64-apple-darwin";
63
+ if (process.platform === "darwin" && process.arch === "x64")
64
+ return "x86_64-apple-darwin";
65
+ if (process.platform === "win32" && process.arch === "x64")
66
+ return "x86_64-pc-windows-msvc";
67
+ if (process.platform === "linux" && process.arch === "x64")
68
+ return "x86_64-unknown-linux-gnu";
69
+ return null;
70
+ }
71
+ export function resolveNativeProcessHostPath(env = process.env) {
72
+ const configured = env.RUDDER_NATIVE_PROCESS_HOST_PATH?.trim();
73
+ if (configured)
74
+ return path.resolve(configured);
75
+ const target = nativeTarget();
76
+ if (!target)
77
+ return null;
78
+ const binary = process.platform === "win32" ? "rudder-process-host.exe" : "rudder-process-host";
79
+ const resourcesPath = process.resourcesPath;
80
+ if (resourcesPath) {
81
+ const packaged = path.join(resourcesPath, "native", target, binary);
82
+ if (existsSync(packaged))
83
+ return packaged;
84
+ }
85
+ let current = path.dirname(fileURLToPath(import.meta.url));
86
+ for (let depth = 0; depth < 10; depth += 1) {
87
+ const candidate = path.join(current, "native", "target", "debug", binary);
88
+ if (existsSync(candidate))
89
+ return candidate;
90
+ const parent = path.dirname(current);
91
+ if (parent === current)
92
+ break;
93
+ current = parent;
94
+ }
95
+ return null;
96
+ }
97
+ function defaultRuntimeRoot(env) {
98
+ const configured = env.RUDDER_NATIVE_PROCESS_RUNTIME_ROOT?.trim();
99
+ if (configured)
100
+ return path.resolve(configured);
101
+ const base = env.RUDDER_HOME?.trim()
102
+ ? path.resolve(env.RUDDER_HOME)
103
+ : path.join(os.tmpdir(), `rudder-${typeof process.getuid === "function" ? process.getuid() : "user"}`);
104
+ return path.join(base, "native", "process-runs");
105
+ }
106
+ function ownerToken(runId) {
107
+ return createHash("sha256").update(`${runId}\0${randomUUID()}`).digest("hex");
108
+ }
109
+ function processGroupAlive(pid) {
110
+ try {
111
+ process.kill(process.platform === "win32" ? pid : -pid, 0);
112
+ return true;
113
+ }
114
+ catch (error) {
115
+ return error.code === "EPERM";
116
+ }
117
+ }
118
+ async function proveCleanupAfterHostLoss(pid) {
119
+ if (process.platform === "win32") {
120
+ await new Promise((resolveCleanup) => {
121
+ const killer = spawn("taskkill.exe", ["/pid", String(pid), "/t", "/f"], {
122
+ stdio: "ignore",
123
+ windowsHide: true,
124
+ });
125
+ killer.once("error", () => resolveCleanup());
126
+ killer.once("close", () => resolveCleanup());
127
+ });
128
+ }
129
+ else {
130
+ try {
131
+ process.kill(-pid, "SIGTERM");
132
+ }
133
+ catch { /* Already gone. */ }
134
+ const termDeadline = Date.now() + 500;
135
+ while (processGroupAlive(pid) && Date.now() < termDeadline) {
136
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 20));
137
+ }
138
+ if (processGroupAlive(pid)) {
139
+ try {
140
+ process.kill(-pid, "SIGKILL");
141
+ }
142
+ catch { /* Already gone. */ }
143
+ }
144
+ }
145
+ const deadline = Date.now() + 2_000;
146
+ while (processGroupAlive(pid) && Date.now() < deadline) {
147
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 20));
148
+ }
149
+ if (processGroupAlive(pid))
150
+ throw new Error(`Native-owned process tree ${pid} survived host loss`);
151
+ }
152
+ function asFrame(value) {
153
+ return value && typeof value === "object" && !Array.isArray(value)
154
+ ? value
155
+ : null;
156
+ }
157
+ function compatibleProtocol(value) {
158
+ const version = asFrame(value);
159
+ return version?.major === PROTOCOL_VERSION.major
160
+ && typeof version.minor === "number"
161
+ && version.minor <= PROTOCOL_VERSION.minor;
162
+ }
163
+ function protocolVersion(value) {
164
+ const version = asFrame(value);
165
+ return version
166
+ && typeof version.major === "number"
167
+ && typeof version.minor === "number"
168
+ ? { major: version.major, minor: version.minor }
169
+ : null;
170
+ }
171
+ function nativeChildProxy(pid, sendStop, host) {
172
+ const proxy = new EventEmitter();
173
+ Object.defineProperties(proxy, {
174
+ pid: { enumerable: true, value: pid },
175
+ exitCode: { enumerable: true, get: () => host.exitCode },
176
+ signalCode: { enumerable: true, get: () => host.signalCode },
177
+ });
178
+ proxy.kill = (() => {
179
+ sendStop();
180
+ return true;
181
+ });
182
+ proxy.terminateTree = () => sendStop();
183
+ return proxy;
184
+ }
185
+ export async function runNativeChildProcess(runId, executable, args, opts) {
186
+ const configuredBinaryPath = opts.binaryPath === undefined
187
+ ? resolveNativeProcessHostPath()
188
+ : opts.binaryPath;
189
+ const binaryPath = configuredBinaryPath && path.resolve(configuredBinaryPath);
190
+ if (!binaryPath) {
191
+ throw new NativeProcessUnavailableError("Rust process host binary is unavailable for this platform", nativeTarget() ? "binary_unavailable" : "target_unsupported");
192
+ }
193
+ const runtimeRoot = path.resolve(opts.runtimeRoot ?? defaultRuntimeRoot(process.env));
194
+ await mkdir(runtimeRoot, { recursive: true, mode: 0o700 }).catch((error) => {
195
+ throw new NativeProcessUnavailableError("Rust process host receipt root is unavailable", "runtime_root_unavailable", false, { cause: error });
196
+ });
197
+ return await new Promise((resolve, reject) => {
198
+ const spawnHost = opts.spawnHost ?? ((command, argv, options) => spawn(command, argv, options));
199
+ let host;
200
+ try {
201
+ host = spawnHost(binaryPath, [], {
202
+ cwd: opts.cwd,
203
+ env: { ...process.env },
204
+ shell: false,
205
+ detached: false,
206
+ windowsHide: true,
207
+ stdio: ["pipe", "ignore", "pipe", "pipe", "pipe", "pipe"],
208
+ });
209
+ }
210
+ catch (error) {
211
+ reject(new NativeProcessUnavailableError("Rust process host could not be launched", "host_spawn_failed", false, { cause: error }));
212
+ return;
213
+ }
214
+ const stdio = host.stdio;
215
+ const commandInput = host.stdin;
216
+ const lifecycle = stdio[3];
217
+ const rawStdout = stdio[4];
218
+ const rawStderr = stdio[5];
219
+ if (!commandInput || !lifecycle || !rawStdout || !rawStderr) {
220
+ host.kill("SIGKILL");
221
+ reject(new NativeProcessUnavailableError("Rust process host did not expose managed channels", "channel_unavailable"));
222
+ return;
223
+ }
224
+ rawStdout.resume();
225
+ rawStderr.resume();
226
+ const requestId = ownerToken(runId);
227
+ const startedAt = new Date().toISOString();
228
+ let accepted = false;
229
+ let nativeIdentity = {
230
+ target: resolveRudderNativeTarget() ?? "unsupported",
231
+ binaryVersion: "unavailable",
232
+ protocolVersion: `${PROTOCOL_VERSION.major}.${PROTOCOL_VERSION.minor}`,
233
+ };
234
+ let spawnedPid = null;
235
+ let appExitCode = null;
236
+ let appSignal = null;
237
+ let terminalSeen = false;
238
+ let cleanupReceiptTrusted = false;
239
+ let timedOut = false;
240
+ let aborted = false;
241
+ let operatorInterrupted = false;
242
+ let stopSent = false;
243
+ let rawOutputTransport = null;
244
+ let rawStdoutEnded = false;
245
+ let rawStderrEnded = false;
246
+ let rawOutputWaiters = [];
247
+ let settled = false;
248
+ let stdout = "";
249
+ let stderr = "";
250
+ let logDeliveryActive = false;
251
+ let logDeliveryWaiters = [];
252
+ let frameParts = [];
253
+ let frameBytes = 0;
254
+ const pendingOutput = [];
255
+ const pendingRawOutput = [];
256
+ let pendingRawOutputBytes = 0;
257
+ const outputQueue = [];
258
+ let queuedOutputBytes = 0;
259
+ let fatalError = null;
260
+ const rejectImmediately = (error) => {
261
+ if (settled)
262
+ return;
263
+ settled = true;
264
+ clearTimeout(handshakeTimeout);
265
+ if (timeout)
266
+ clearTimeout(timeout);
267
+ abortCleanup?.();
268
+ reject(error);
269
+ };
270
+ const settleReject = (error) => {
271
+ if (settled)
272
+ return;
273
+ if (error instanceof NativeProcessUnavailableError)
274
+ Object.assign(error.diagnostic, nativeIdentity);
275
+ if (accepted) {
276
+ fatalError ??= error;
277
+ sendStop(Math.max(1, opts.graceSec) * 1_000);
278
+ return;
279
+ }
280
+ host.kill("SIGKILL");
281
+ rejectImmediately(error);
282
+ };
283
+ const waitForLogDelivery = () => {
284
+ if (!logDeliveryActive && outputQueue.length === 0)
285
+ return Promise.resolve();
286
+ return new Promise((resolveDelivery) => logDeliveryWaiters.push(resolveDelivery));
287
+ };
288
+ const waitForRawOutput = () => {
289
+ if (rawOutputTransport !== true || (rawStdoutEnded && rawStderrEnded))
290
+ return Promise.resolve();
291
+ return new Promise((resolveDelivery) => rawOutputWaiters.push(resolveDelivery));
292
+ };
293
+ const markRawOutputEnded = (stream) => {
294
+ if (stream === "stdout")
295
+ rawStdoutEnded = true;
296
+ else
297
+ rawStderrEnded = true;
298
+ if (rawStdoutEnded && rawStderrEnded) {
299
+ for (const resolveDelivery of rawOutputWaiters.splice(0))
300
+ resolveDelivery();
301
+ }
302
+ };
303
+ const finish = () => {
304
+ if (settled || !terminalSeen)
305
+ return;
306
+ settled = true;
307
+ clearTimeout(handshakeTimeout);
308
+ if (timeout)
309
+ clearTimeout(timeout);
310
+ abortCleanup?.();
311
+ void Promise.all([waitForLogDelivery(), waitForRawOutput()]).finally(() => {
312
+ if (fatalError)
313
+ reject(fatalError);
314
+ else
315
+ resolve({
316
+ exitCode: appExitCode,
317
+ signal: aborted || timedOut ? "SIGTERM" : appSignal,
318
+ timedOut,
319
+ stdout,
320
+ stderr,
321
+ pid: spawnedPid,
322
+ startedAt,
323
+ diagnostic: createRudderNativeDiagnostic({
324
+ capability: "agent-run-process",
325
+ target: nativeIdentity.target,
326
+ binaryVersion: nativeIdentity.binaryVersion,
327
+ protocolVersion: nativeIdentity.protocolVersion,
328
+ effectiveEngine: "rust",
329
+ fallbackCode: null,
330
+ }),
331
+ });
332
+ });
333
+ };
334
+ const drainOutput = async () => {
335
+ if (logDeliveryActive)
336
+ return;
337
+ logDeliveryActive = true;
338
+ while (outputQueue.length > 0) {
339
+ const output = outputQueue.shift();
340
+ queuedOutputBytes -= Buffer.byteLength(output.data);
341
+ if (!operatorInterrupted) {
342
+ try {
343
+ await opts.onLog(output.stream, output.data);
344
+ }
345
+ catch (error) {
346
+ opts.onLogError(error, runId, `failed to append native ${output.stream} log chunk`);
347
+ }
348
+ }
349
+ }
350
+ logDeliveryActive = false;
351
+ for (const resolveDelivery of logDeliveryWaiters.splice(0))
352
+ resolveDelivery();
353
+ };
354
+ const queueOutput = (output) => {
355
+ const outputBytes = Buffer.byteLength(output.data);
356
+ const queuedItems = pendingOutput.length + outputQueue.length;
357
+ if (queuedItems >= MAX_OUTPUT_QUEUE_ITEMS || queuedOutputBytes + outputBytes > MAX_OUTPUT_QUEUE_BYTES) {
358
+ settleReject(new NativeProcessUnavailableError("Rust process host output spool exceeded its bounded capacity", "output_spool_overflow", accepted));
359
+ return;
360
+ }
361
+ queuedOutputBytes += outputBytes;
362
+ if (accepted) {
363
+ outputQueue.push(output);
364
+ void drainOutput();
365
+ }
366
+ else {
367
+ pendingOutput.push(output);
368
+ }
369
+ };
370
+ const appendOutput = (stream, data) => {
371
+ if (stream === "stdout")
372
+ stdout = appendWithCap(stdout, data);
373
+ else
374
+ stderr = appendWithCap(stderr, data);
375
+ if (operatorInterrupted)
376
+ return;
377
+ queueOutput({ stream, data });
378
+ };
379
+ // Agent Run output is byte-relayed on the host's dedicated fd 4/5
380
+ // channels. Keep accepting lifecycle output frames for older hosts, but
381
+ // consume the dedicated channels so large writes cannot fill lifecycle.
382
+ const handleRawOutput = (stream, chunk) => {
383
+ const data = String(chunk);
384
+ if (rawOutputTransport === true) {
385
+ appendOutput(stream, data);
386
+ return;
387
+ }
388
+ if (rawOutputTransport === false)
389
+ return;
390
+ const outputBytes = Buffer.byteLength(data);
391
+ if (pendingRawOutput.length >= MAX_OUTPUT_QUEUE_ITEMS
392
+ || pendingRawOutputBytes + outputBytes > MAX_OUTPUT_QUEUE_BYTES) {
393
+ settleReject(new NativeProcessUnavailableError("Rust process host pre-negotiation output spool exceeded its bounded capacity", "output_spool_overflow", accepted));
394
+ return;
395
+ }
396
+ pendingRawOutput.push({ stream, data });
397
+ pendingRawOutputBytes += outputBytes;
398
+ };
399
+ rawStdout.on("data", (chunk) => handleRawOutput("stdout", chunk));
400
+ rawStderr.on("data", (chunk) => handleRawOutput("stderr", chunk));
401
+ rawStdout.once("end", () => markRawOutputEnded("stdout"));
402
+ rawStderr.once("end", () => markRawOutputEnded("stderr"));
403
+ rawStdout.once("close", () => markRawOutputEnded("stdout"));
404
+ rawStderr.once("close", () => markRawOutputEnded("stderr"));
405
+ const send = (message) => {
406
+ if (commandInput.destroyed || commandInput.writableEnded)
407
+ return false;
408
+ return commandInput.write(`${JSON.stringify(message)}\n`);
409
+ };
410
+ const sendStop = (graceMs) => {
411
+ if (stopSent || !accepted)
412
+ return;
413
+ stopSent = true;
414
+ send({
415
+ type: "stop",
416
+ protocolVersion: PROTOCOL_VERSION,
417
+ requestId,
418
+ ...(graceMs === undefined ? {} : { graceMs: Math.max(1, Math.min(60_000, Math.floor(graceMs))) }),
419
+ });
420
+ };
421
+ const handshakeTimeout = setTimeout(() => {
422
+ host.kill("SIGKILL");
423
+ settleReject(new NativeProcessUnavailableError("Rust process host handshake timed out", "handshake_timeout", accepted));
424
+ }, HANDSHAKE_TIMEOUT_MS);
425
+ let timeout = null;
426
+ let abortCleanup = null;
427
+ const handleFrame = (frame) => {
428
+ const type = frame.type;
429
+ if (type === "handshake") {
430
+ const version = protocolVersion(frame.protocolVersion);
431
+ if (accepted || !version || !compatibleProtocol(version)) {
432
+ settleReject(new NativeProcessUnavailableError("Rust process host handshake is incompatible", "protocol_mismatch", accepted));
433
+ return;
434
+ }
435
+ const capabilities = frame.capabilities;
436
+ nativeIdentity = {
437
+ target: typeof frame.target === "string" ? frame.target : nativeIdentity.target,
438
+ binaryVersion: typeof frame.binaryVersion === "string" ? frame.binaryVersion : nativeIdentity.binaryVersion,
439
+ protocolVersion: `${version.major}.${version.minor}`,
440
+ };
441
+ if (!Array.isArray(capabilities)
442
+ || !["process_spawn", "process_group_cleanup", "parent_eof_cleanup", "owner_receipt", "stdout_relay", "stderr_relay"]
443
+ .every((capability) => capabilities.includes(capability))) {
444
+ settleReject(new NativeProcessUnavailableError("Rust process host capabilities are incomplete", "capability_mismatch"));
445
+ return;
446
+ }
447
+ clearTimeout(handshakeTimeout);
448
+ send({
449
+ type: "startProcess",
450
+ protocolVersion: PROTOCOL_VERSION,
451
+ requestId,
452
+ executable,
453
+ argv: args,
454
+ cwd: opts.cwd,
455
+ env: opts.env,
456
+ ownerToken: requestId,
457
+ runtimeRoot,
458
+ ...(opts.stdin === undefined ? {} : { stdin: opts.stdin }),
459
+ graceMs: Math.max(1, Math.min(60_000, Math.floor(opts.graceSec * 1_000))),
460
+ });
461
+ return;
462
+ }
463
+ if (type !== "handshake"
464
+ && (!compatibleProtocol(frame.protocolVersion)
465
+ || frame.requestId !== requestId
466
+ || (accepted ? frame.ownerToken !== requestId : frame.ownerToken !== undefined && frame.ownerToken !== requestId))) {
467
+ settleReject(new NativeProcessUnavailableError("Rust process host lifecycle identity is invalid", "lifecycle_identity_invalid", accepted));
468
+ return;
469
+ }
470
+ if (type === "output") {
471
+ if ((frame.stream !== "stdout" && frame.stream !== "stderr") || typeof frame.data !== "string") {
472
+ settleReject(new NativeProcessUnavailableError("Rust process host emitted invalid output", "output_invalid", accepted));
473
+ return;
474
+ }
475
+ const output = {
476
+ stream: frame.stream,
477
+ data: frame.data,
478
+ };
479
+ appendOutput(output.stream, output.data);
480
+ return;
481
+ }
482
+ if (type === "accepted") {
483
+ if (accepted) {
484
+ settleReject(new NativeProcessUnavailableError("Rust process host accepted twice", "accepted_twice", true));
485
+ return;
486
+ }
487
+ accepted = true;
488
+ rawOutputTransport = frame.outputTransport === "raw";
489
+ if (rawOutputTransport) {
490
+ for (const output of pendingRawOutput.splice(0))
491
+ appendOutput(output.stream, output.data);
492
+ }
493
+ pendingRawOutputBytes = 0;
494
+ outputQueue.push(...pendingOutput.splice(0));
495
+ void drainOutput();
496
+ return;
497
+ }
498
+ if (type === "spawned") {
499
+ if (!accepted || typeof frame.pid !== "number" || frame.pid < 2 || spawnedPid !== null) {
500
+ settleReject(new NativeProcessUnavailableError("Rust process host spawn frame is invalid", "spawn_frame_invalid", accepted));
501
+ return;
502
+ }
503
+ spawnedPid = frame.pid;
504
+ const proxy = nativeChildProxy(frame.pid, sendStop, host);
505
+ if (!settled)
506
+ runningProcesses.set(runId, { child: proxy, graceSec: opts.graceSec });
507
+ if (opts.onSpawn) {
508
+ void opts.onSpawn({ pid: frame.pid, startedAt }).catch((error) => {
509
+ opts.onLogError(error, runId, "failed to record native child process metadata");
510
+ });
511
+ }
512
+ return;
513
+ }
514
+ if (type === "app-exit") {
515
+ appExitCode = typeof frame.code === "number" ? frame.code : null;
516
+ appSignal = typeof frame.signal === "string" ? frame.signal : null;
517
+ return;
518
+ }
519
+ if (type === "terminal") {
520
+ if (!accepted) {
521
+ settleReject(new NativeProcessUnavailableError(typeof frame.errorCode === "string" ? `Rust process host rejected launch: ${frame.errorCode}` : "Rust process host rejected launch", typeof frame.errorCode === "string" ? frame.errorCode : "launch_rejected"));
522
+ return;
523
+ }
524
+ if (frame.cleanupProven !== true || frame.receiptWritten !== true) {
525
+ fatalError ??= new NativeProcessUnavailableError("Rust process host could not prove process-tree cleanup and receipt durability", typeof frame.errorCode === "string" ? frame.errorCode : "cleanup_unproven", true);
526
+ terminalSeen = true;
527
+ return;
528
+ }
529
+ cleanupReceiptTrusted = true;
530
+ terminalSeen = true;
531
+ return;
532
+ }
533
+ if (type === "error" || type === "stop-accepted" || type === "stopped" || type === "listener-verified")
534
+ return;
535
+ settleReject(new NativeProcessUnavailableError("Rust process host emitted an unknown lifecycle frame", "unknown_frame", accepted));
536
+ };
537
+ lifecycle.on("data", (chunk) => {
538
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
539
+ let start = 0;
540
+ for (let index = 0; index < bytes.length; index += 1) {
541
+ if (bytes[index] !== 0x0a)
542
+ continue;
543
+ const segment = bytes.subarray(start, index);
544
+ start = index + 1;
545
+ if (frameBytes + segment.length > MAX_LIFECYCLE_FRAME_BYTES) {
546
+ settleReject(new NativeProcessUnavailableError("Rust process host lifecycle frame exceeded its bound", "frame_too_large", accepted));
547
+ return;
548
+ }
549
+ frameParts.push(Buffer.from(segment));
550
+ frameBytes += segment.length;
551
+ const frame = Buffer.concat(frameParts, frameBytes).toString("utf8").replace(/\r$/u, "");
552
+ frameParts = [];
553
+ frameBytes = 0;
554
+ if (!frame.trim())
555
+ continue;
556
+ try {
557
+ const parsed = asFrame(JSON.parse(frame));
558
+ if (!parsed)
559
+ throw new Error("not an object");
560
+ handleFrame(parsed);
561
+ }
562
+ catch (error) {
563
+ settleReject(new NativeProcessUnavailableError("Rust process host emitted invalid lifecycle JSON", "invalid_json", accepted, { cause: error }));
564
+ }
565
+ }
566
+ const remainder = bytes.subarray(start);
567
+ if (remainder.length > 0) {
568
+ frameParts.push(Buffer.from(remainder));
569
+ frameBytes += remainder.length;
570
+ }
571
+ });
572
+ host.stderr?.on("data", (chunk) => {
573
+ stderr = appendWithCap(stderr, String(chunk));
574
+ });
575
+ host.once("error", (error) => {
576
+ settleReject(new NativeProcessUnavailableError("Rust process host failed", "host_error", accepted, { cause: error }));
577
+ });
578
+ host.once("close", (code, signal) => {
579
+ runningProcesses.delete(runId);
580
+ if (terminalSeen && cleanupReceiptTrusted) {
581
+ finish();
582
+ return;
583
+ }
584
+ const controlError = fatalError ?? new NativeProcessUnavailableError(`Rust process host exited before a terminal receipt (${signal ?? code ?? "unknown"})`, accepted ? "control_lost" : "host_exit_before_accept", accepted);
585
+ if (!accepted) {
586
+ rejectImmediately(controlError);
587
+ return;
588
+ }
589
+ void (async () => {
590
+ let ownedPid = spawnedPid;
591
+ if (ownedPid === null) {
592
+ try {
593
+ const descriptor = JSON.parse(await readFile(path.join(runtimeRoot, requestId, "owner-descriptor.json"), "utf8"));
594
+ if (typeof descriptor.childPid === "number")
595
+ ownedPid = descriptor.childPid;
596
+ }
597
+ catch {
598
+ // The host may have died before persisting ownership.
599
+ }
600
+ }
601
+ if (ownedPid !== null)
602
+ await proveCleanupAfterHostLoss(ownedPid);
603
+ })().then(() => rejectImmediately(controlError), (cleanupError) => rejectImmediately(new NativeProcessUnavailableError("Rust process host was lost and emergency process-tree cleanup was not proven", "control_lost_cleanup_unproven", true, { cause: cleanupError })));
604
+ });
605
+ if (opts.timeoutSec > 0) {
606
+ timeout = setTimeout(() => {
607
+ timedOut = true;
608
+ sendStop(Math.max(1, opts.graceSec) * 1_000);
609
+ }, opts.timeoutSec * 1_000);
610
+ }
611
+ if (opts.abortSignal) {
612
+ const onAbort = () => {
613
+ aborted = true;
614
+ const reason = opts.abortSignal?.reason;
615
+ operatorInterrupted = Boolean(reason
616
+ && typeof reason === "object"
617
+ && reason.kind === "operator_interrupt");
618
+ const operatorDeadline = operatorInterrupted
619
+ && typeof reason === "object"
620
+ && reason !== null
621
+ && typeof reason.hardDeadlineMs === "number"
622
+ ? reason.hardDeadlineMs
623
+ : null;
624
+ sendStop(operatorDeadline ?? Math.max(1, opts.graceSec) * 1_000);
625
+ };
626
+ if (opts.abortSignal.aborted)
627
+ onAbort();
628
+ else {
629
+ opts.abortSignal.addEventListener("abort", onAbort, { once: true });
630
+ abortCleanup = () => opts.abortSignal?.removeEventListener("abort", onAbort);
631
+ }
632
+ }
633
+ });
634
+ }
635
+ //# sourceMappingURL=native-process-runner.js.map