@agent-device/proxy 0.0.0-stage → 0.21.24

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.
@@ -0,0 +1,1312 @@
1
+ import { i as redactDiagnosticData, n as createRequestCanceledError, t as AppError } from "./errors.mjs";
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import fs from "node:fs";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { Readable } from "node:stream";
8
+ import { pipeline } from "node:stream/promises";
9
+ //#region ../host-kit/src/internal/diagnostics.ts
10
+ const diagnosticsStorage = new AsyncLocalStorage();
11
+ function updateDiagnosticsScope(options) {
12
+ const scope = diagnosticsStorage.getStore();
13
+ if (!scope) return;
14
+ Object.assign(scope, options);
15
+ }
16
+ function getDiagnosticsMeta() {
17
+ const scope = diagnosticsStorage.getStore();
18
+ if (!scope) return {};
19
+ const { diagnosticId, requestId, session, command, debug, flushOnSuccess } = scope;
20
+ return {
21
+ diagnosticId,
22
+ requestId,
23
+ session,
24
+ command,
25
+ debug,
26
+ flushOnSuccess
27
+ };
28
+ }
29
+ function emitDiagnostic(event) {
30
+ const scope = diagnosticsStorage.getStore();
31
+ if (!scope) return;
32
+ const payload = {
33
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
34
+ level: event.level ?? "info",
35
+ phase: event.phase,
36
+ session: scope.session,
37
+ requestId: scope.requestId,
38
+ command: scope.command,
39
+ durationMs: event.durationMs,
40
+ data: event.data ? redactScopeData(scope, event.data) : void 0
41
+ };
42
+ scope.events.push(payload);
43
+ scope.phaseCounts.set(event.phase, (scope.phaseCounts.get(event.phase) ?? 0) + 1);
44
+ if (!scope.debug && !scope.traceLogPath) return;
45
+ const fileLine = `${JSON.stringify(payload)}\n`;
46
+ try {
47
+ if (scope.debug && scope.logPath) {
48
+ appendDiagnosticLine(scope, scope.logPath, fileLine);
49
+ scope.liveWrittenEventCount = scope.events.length;
50
+ }
51
+ if (scope.traceLogPath) appendDiagnosticLine(scope, scope.traceLogPath, fileLine);
52
+ if (scope.debug && !scope.logPath && !scope.traceLogPath) process.stderr.write(`[agent-device][diag] ${fileLine}`);
53
+ } catch {}
54
+ }
55
+ /** Longest-first so an overlapping shorter literal cannot truncate a longer one. */
56
+ function sortedScopeSensitiveValues(scope) {
57
+ scope.sortedSensitiveValues ??= [...scope.sensitiveValues].sort((left, right) => right.length - left.length);
58
+ return scope.sortedSensitiveValues;
59
+ }
60
+ function redactScopeData(scope, input) {
61
+ return replaceSensitiveValues(redactDiagnosticData(input), sortedScopeSensitiveValues(scope));
62
+ }
63
+ function replaceSensitiveValues(value, sensitiveValues) {
64
+ if (typeof value === "string") return sensitiveValues.reduce((output, sensitive) => output.replaceAll(sensitive, "[REDACTED]"), value);
65
+ if (Array.isArray(value)) return value.map((entry) => replaceSensitiveValues(entry, sensitiveValues));
66
+ if (!value || typeof value !== "object") return value;
67
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, replaceSensitiveValues(entry, sensitiveValues)]));
68
+ }
69
+ function appendDiagnosticLine(scope, logPath, line) {
70
+ const dir = path.dirname(logPath);
71
+ if (!scope.ensuredDirectories.has(dir)) {
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ scope.ensuredDirectories.add(dir);
74
+ }
75
+ fs.appendFileSync(logPath, line, "utf8");
76
+ }
77
+ //#endregion
78
+ //#region ../kernel/src/source-value.ts
79
+ const BOOLEAN_TRUE_VALUES = /* @__PURE__ */ new Set([
80
+ "1",
81
+ "true",
82
+ "yes",
83
+ "on"
84
+ ]);
85
+ const BOOLEAN_FALSE_VALUES = /* @__PURE__ */ new Set([
86
+ "0",
87
+ "false",
88
+ "no",
89
+ "off"
90
+ ]);
91
+ function parseBooleanLiteral(value) {
92
+ const normalized = value.trim().toLowerCase();
93
+ if (BOOLEAN_TRUE_VALUES.has(normalized)) return true;
94
+ if (BOOLEAN_FALSE_VALUES.has(normalized)) return false;
95
+ }
96
+ //#endregion
97
+ //#region ../host-kit/src/internal/exec.ts
98
+ const BARE_COMMAND_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
99
+ const EXEC_DIAGNOSTIC_ARG_LIMIT = 6;
100
+ const commandExecutorOverrideScope = new AsyncLocalStorage();
101
+ const requestCommandEnvScope = new AsyncLocalStorage();
102
+ /** Effective toolchain selection; an explicitly empty request value selects `xcode-select`. */
103
+ function commandDeveloperDir() {
104
+ const requestEnv = requestCommandEnvScope.getStore();
105
+ if (requestEnv && Object.hasOwn(requestEnv, "DEVELOPER_DIR")) return requestEnv.DEVELOPER_DIR || void 0;
106
+ return process.env.DEVELOPER_DIR || void 0;
107
+ }
108
+ function resolveSpawnEnv(env) {
109
+ const requestEnv = requestCommandEnvScope.getStore();
110
+ if (!requestEnv) return env;
111
+ return {
112
+ ...env ?? process.env,
113
+ ...requestEnv
114
+ };
115
+ }
116
+ async function runCmd(cmd, args, options = {}) {
117
+ const overrideResult = commandExecutorOverrideScope.getStore()?.(cmd, args, options);
118
+ if (overrideResult) return coerceExecResult(await overrideResult);
119
+ return await runSpawnedCommand(cmd, args, options);
120
+ }
121
+ /**
122
+ * Normalize an exec result produced outside this module. Tool providers,
123
+ * executor overrides, and SDK-supplied adb executors are plain callbacks whose
124
+ * results cross an unchecked boundary; coercing once here lets downstream code
125
+ * trust the ExecResult types instead of re-wrapping fields defensively at every
126
+ * use. A non-number exitCode coerces to 1 — the same failure branch such a
127
+ * result already landed in at every `exitCode !== 0` guard.
128
+ */
129
+ function coerceExecResult(result) {
130
+ const stdout = typeof result.stdout === "string" ? result.stdout : String(result.stdout ?? "");
131
+ const stderr = typeof result.stderr === "string" ? result.stderr : String(result.stderr ?? "");
132
+ const exitCode = typeof result.exitCode === "number" ? result.exitCode : 1;
133
+ if (stdout === result.stdout && stderr === result.stderr && exitCode === result.exitCode) return result;
134
+ return {
135
+ ...result,
136
+ stdout,
137
+ stderr,
138
+ exitCode
139
+ };
140
+ }
141
+ function runSpawnedCommand(cmd, args, options = {}) {
142
+ const executable = normalizeExecutableCommand(cmd);
143
+ const execTrace = createExecTraceContext();
144
+ return new Promise((resolve, reject) => {
145
+ const child = spawn(executable, args, {
146
+ cwd: options.cwd,
147
+ env: resolveSpawnEnv(options.env),
148
+ stdio: [
149
+ "pipe",
150
+ "pipe",
151
+ "pipe"
152
+ ],
153
+ detached: options.detached,
154
+ windowsHide: true,
155
+ shell: false
156
+ });
157
+ options.onSpawn?.(child);
158
+ let stdout = "";
159
+ const stdoutChunks = options.binaryStdout ? [] : void 0;
160
+ let stderr = "";
161
+ let didTimeout = false;
162
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
163
+ let timeoutHandle = null;
164
+ let settled = false;
165
+ function finish() {
166
+ if (settled) return false;
167
+ settled = true;
168
+ if (timeoutHandle) clearTimeout(timeoutHandle);
169
+ abort.dispose();
170
+ destroyCommandStreams(child);
171
+ execTrace.emitForegroundCompletion(cmd, args);
172
+ return true;
173
+ }
174
+ function fail(error) {
175
+ if (finish()) reject(error);
176
+ }
177
+ function settle(code) {
178
+ if (!finish()) return;
179
+ const finalExitCode = code ?? 1;
180
+ if (!abort.didAbort && didTimeout && timeoutMs) {
181
+ reject(createTimeoutError(executable, cmd, args, timeoutMs, finalExitCode, stdout, stderr));
182
+ return;
183
+ }
184
+ const failure = commandCloseFailure(abort, executable, cmd, args, finalExitCode, options.allowFailure, stdout, stderr);
185
+ if (failure) {
186
+ reject(failure);
187
+ return;
188
+ }
189
+ resolve({
190
+ stdout,
191
+ stderr,
192
+ exitCode: finalExitCode,
193
+ stdoutBuffer: stdoutChunks ? Buffer.concat(stdoutChunks) : void 0
194
+ });
195
+ }
196
+ const settlement = createCommandKillSettlement({
197
+ killProcessTree: () => killProcessTree(child, options),
198
+ settle
199
+ });
200
+ const abort = watchCommandAbort(options, settlement.requestKill);
201
+ timeoutHandle = timeoutMs ? setTimeout(() => {
202
+ didTimeout = true;
203
+ settlement.requestKill();
204
+ }, timeoutMs) : null;
205
+ if (!options.binaryStdout) child.stdout.setEncoding("utf8");
206
+ child.stderr.setEncoding("utf8");
207
+ writeChildStdin(child, options.stdin).catch((error) => {
208
+ if (abort.didAbort || didTimeout) return;
209
+ if (isEpipeError(error)) return;
210
+ fail(createStdinError(executable, cmd, args, error));
211
+ killProcessTree(child, options);
212
+ });
213
+ child.stdout.on("data", (chunk) => {
214
+ if (options.binaryStdout) {
215
+ stdoutChunks?.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
216
+ return;
217
+ }
218
+ const text = String(chunk);
219
+ stdout += text;
220
+ options.onStdoutChunk?.(text);
221
+ });
222
+ child.stderr.on("data", (chunk) => {
223
+ const text = String(chunk);
224
+ stderr += text;
225
+ options.onStderrChunk?.(text);
226
+ });
227
+ child.on("error", (err) => {
228
+ fail(spawnRejectionError(abort, executable, cmd, args, err));
229
+ });
230
+ child.once("exit", settlement.recordExit);
231
+ child.once("close", settle);
232
+ });
233
+ }
234
+ function runCmdSync(cmd, args, options = {}) {
235
+ const executable = normalizeExecutableCommand(cmd);
236
+ const result = spawnSync(executable, args, {
237
+ cwd: options.cwd,
238
+ env: resolveSpawnEnv(options.env),
239
+ stdio: [
240
+ "pipe",
241
+ "pipe",
242
+ "pipe"
243
+ ],
244
+ encoding: options.binaryStdout ? void 0 : "utf8",
245
+ input: options.stdin,
246
+ timeout: normalizeTimeoutMs(options.timeoutMs),
247
+ windowsHide: true,
248
+ shell: false,
249
+ ...options.maxBuffer !== void 0 ? { maxBuffer: options.maxBuffer } : {}
250
+ });
251
+ if (result.error) {
252
+ const code = result.error.code;
253
+ if (code === "ETIMEDOUT") throw new AppError("COMMAND_FAILED", `${executable} timed out after ${normalizeTimeoutMs(options.timeoutMs)}ms`, {
254
+ cmd,
255
+ args,
256
+ timeoutMs: normalizeTimeoutMs(options.timeoutMs)
257
+ }, result.error);
258
+ if (code === "ENOENT") throw createMissingToolError(executable, cmd, result.error);
259
+ throw createCommandFailedError(executable, cmd, args, result.error);
260
+ }
261
+ const stdoutBuffer = options.binaryStdout ? Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "") : void 0;
262
+ const stdout = options.binaryStdout ? "" : typeof result.stdout === "string" ? result.stdout : (result.stdout ?? "").toString();
263
+ const stderr = typeof result.stderr === "string" ? result.stderr : (result.stderr ?? "").toString();
264
+ const exitCode = result.status ?? 1;
265
+ if (exitCode !== 0 && !options.allowFailure) throw createExitError(executable, cmd, args, exitCode, stdout, stderr);
266
+ return {
267
+ stdout,
268
+ stderr,
269
+ exitCode,
270
+ stdoutBuffer
271
+ };
272
+ }
273
+ function createExecTraceContext() {
274
+ const diagnosticsMeta = getDiagnosticsMeta();
275
+ const diagnosticsDebugEnabled = diagnosticsMeta.debug === true;
276
+ const envTraceEnabled = parseBooleanLiteral(process.env.AGENT_DEVICE_EXEC_TRACE ?? "") === true;
277
+ if (!diagnosticsDebugEnabled && !envTraceEnabled) return createDisabledExecTraceContext();
278
+ if (envTraceEnabled && diagnosticsMeta.flushOnSuccess !== true) updateDiagnosticsScope({ flushOnSuccess: true });
279
+ const startedAtMs = Date.now();
280
+ let completionEmitted = false;
281
+ return {
282
+ emitForegroundCompletion: (cmd, args) => {
283
+ if (completionEmitted) return;
284
+ completionEmitted = true;
285
+ emitExecCommandDiagnostic({
286
+ cmd,
287
+ args,
288
+ startedAtMs
289
+ });
290
+ },
291
+ emitBackgroundSpawn: (cmd, args) => {
292
+ emitExecCommandDiagnostic({
293
+ cmd,
294
+ args,
295
+ data: { event: "spawn" }
296
+ });
297
+ },
298
+ emitBackgroundCompletion: (cmd, args, event) => {
299
+ if (completionEmitted) return;
300
+ completionEmitted = true;
301
+ emitExecCommandDiagnostic({
302
+ cmd,
303
+ args,
304
+ startedAtMs,
305
+ data: { event }
306
+ });
307
+ }
308
+ };
309
+ }
310
+ function createDisabledExecTraceContext() {
311
+ return {
312
+ emitForegroundCompletion: () => {},
313
+ emitBackgroundSpawn: () => {},
314
+ emitBackgroundCompletion: () => {}
315
+ };
316
+ }
317
+ function emitExecCommandDiagnostic(params) {
318
+ const argsPrefix = params.args.slice(0, EXEC_DIAGNOSTIC_ARG_LIMIT);
319
+ emitDiagnostic({
320
+ level: "debug",
321
+ phase: "exec_command",
322
+ durationMs: params.startedAtMs === void 0 ? void 0 : Math.max(0, Date.now() - params.startedAtMs),
323
+ data: {
324
+ command: params.cmd,
325
+ argsPrefix,
326
+ ...params.args.length > argsPrefix.length ? { omittedArgCount: params.args.length - argsPrefix.length } : {},
327
+ ...params.data ?? {}
328
+ }
329
+ });
330
+ }
331
+ function normalizeExecutableCommand(cmd) {
332
+ const candidate = normalizeExecutableLookup(cmd);
333
+ if (!candidate) throw new AppError("INVALID_ARGS", `Invalid executable command: ${JSON.stringify(cmd)}`, {
334
+ cmd,
335
+ hint: "Use a bare command name from PATH or an absolute executable path."
336
+ });
337
+ return candidate;
338
+ }
339
+ function createSpawnError(executable, cmd, args, err) {
340
+ if (err.code === "ENOENT") return createMissingToolError(executable, cmd, err);
341
+ return createCommandFailedError(executable, cmd, args, err);
342
+ }
343
+ function createMissingToolError(executable, cmd, cause) {
344
+ return new AppError("TOOL_MISSING", `${executable} not found in PATH`, { cmd }, cause);
345
+ }
346
+ function createCommandFailedError(executable, cmd, args, cause) {
347
+ return new AppError("COMMAND_FAILED", `Failed to run ${executable}`, {
348
+ cmd,
349
+ args
350
+ }, cause);
351
+ }
352
+ function createStdinError(executable, cmd, args, cause) {
353
+ return new AppError("COMMAND_FAILED", `Failed to write stdin for ${executable}`, {
354
+ cmd,
355
+ args
356
+ }, cause instanceof Error ? cause : void 0);
357
+ }
358
+ function createTimeoutError(executable, cmd, args, timeoutMs, exitCode, stdout, stderr) {
359
+ return new AppError("COMMAND_FAILED", `${executable} timed out after ${timeoutMs}ms`, {
360
+ cmd,
361
+ args,
362
+ stdout,
363
+ stderr,
364
+ exitCode,
365
+ timeoutMs
366
+ });
367
+ }
368
+ function createExitError(executable, cmd, args, exitCode, stdout, stderr) {
369
+ return new AppError("COMMAND_FAILED", `${executable} exited with code ${exitCode}`, execFailureDetails({
370
+ stdout,
371
+ stderr,
372
+ exitCode
373
+ }, {
374
+ cmd,
375
+ args
376
+ }));
377
+ }
378
+ const XCRUN_TOOL_NOT_FOUND_STDERR = /xcrun: error: unable to find utility "([^"]+)"/;
379
+ function xcrunToolNotFoundDetails(stderr) {
380
+ const tool = XCRUN_TOOL_NOT_FOUND_STDERR.exec(stderr)?.[1];
381
+ if (!tool) return {};
382
+ const developerDir = commandDeveloperDir();
383
+ const source = developerDir ? `DEVELOPER_DIR ${developerDir}` : "the developer dir selected by xcode-select (see xcode-select -p)";
384
+ return {
385
+ reason: "xcrun-tool-not-found",
386
+ developerDir: developerDir ?? null,
387
+ hint: `xcrun could not find ${tool} in ${source}. Export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer (or your Xcode's path) before running agent-device, or run sudo xcode-select -s /Applications/Xcode.app.`
388
+ };
389
+ }
390
+ /**
391
+ * Exit-failure details for normalizeError, including null exit codes from signalled processes.
392
+ */
393
+ function execFailureDetails(result, extra) {
394
+ return {
395
+ stdout: result.stdout,
396
+ stderr: result.stderr,
397
+ exitCode: result.exitCode,
398
+ processExitError: true,
399
+ ...extra,
400
+ ...xcrunToolNotFoundDetails(result.stderr)
401
+ };
402
+ }
403
+ function spawnRejectionError(abort, executable, cmd, args, err) {
404
+ return abort.didAbort ? createRequestCanceledError({
405
+ cmd,
406
+ args,
407
+ executable
408
+ }) : createSpawnError(executable, cmd, args, err);
409
+ }
410
+ function commandCloseFailure(abort, executable, cmd, args, exitCode, allowFailure, stdout, stderr) {
411
+ if (abort.didAbort) return createRequestCanceledError({
412
+ cmd,
413
+ args,
414
+ executable
415
+ });
416
+ if (exitCode !== 0 && !allowFailure) return createExitError(executable, cmd, args, exitCode, stdout, stderr);
417
+ return null;
418
+ }
419
+ function normalizeExecutableLookup(cmd) {
420
+ const candidate = cmd.trim();
421
+ if (!candidate || candidate.includes("\0")) return null;
422
+ if (path.isAbsolute(candidate)) return candidate;
423
+ if (candidate.includes("/") || candidate.includes("\\")) return null;
424
+ return BARE_COMMAND_RE.test(candidate) ? candidate : null;
425
+ }
426
+ function normalizeTimeoutMs(value) {
427
+ if (!Number.isFinite(value)) return void 0;
428
+ const timeout = Math.floor(value);
429
+ if (timeout <= 0) return void 0;
430
+ return timeout;
431
+ }
432
+ function createCommandKillSettlement(input) {
433
+ let killRequested = false;
434
+ let exited = false;
435
+ let exitCode = null;
436
+ const settleIfKilledAndGone = () => {
437
+ if (killRequested && exited) input.settle(exitCode);
438
+ };
439
+ return {
440
+ requestKill: () => {
441
+ killRequested = true;
442
+ input.killProcessTree();
443
+ settleIfKilledAndGone();
444
+ },
445
+ recordExit: (code) => {
446
+ exited = true;
447
+ exitCode = code ?? 1;
448
+ settleIfKilledAndGone();
449
+ }
450
+ };
451
+ }
452
+ function watchCommandAbort(options, onKill) {
453
+ let didAbort = false;
454
+ const onAbort = () => {
455
+ didAbort = true;
456
+ onKill();
457
+ };
458
+ if (options.signal?.aborted) onAbort();
459
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
460
+ return {
461
+ get didAbort() {
462
+ return didAbort;
463
+ },
464
+ dispose: () => {
465
+ options.signal?.removeEventListener("abort", onAbort);
466
+ }
467
+ };
468
+ }
469
+ /**
470
+ * Best-effort process-group signalling, shared by host-kit callers. This lives below
471
+ * host-process.ts to avoid an import cycle. Only positive integer pids are accepted:
472
+ * zero targets this process's group, and negative values can target unrelated processes.
473
+ */
474
+ function signalProcessGroupBestEffort(pid, signal) {
475
+ if (!Number.isInteger(pid) || pid <= 0) return false;
476
+ try {
477
+ process.kill(-pid, signal);
478
+ return true;
479
+ } catch {
480
+ return false;
481
+ }
482
+ }
483
+ function killProcessTree(child, options) {
484
+ if (!options.kill) {
485
+ signalProcessTree(child, options.detached, "SIGKILL");
486
+ return;
487
+ }
488
+ signalProcessTree(child, options.detached, options.kill.signal);
489
+ const escalation = setTimeout(() => signalProcessTree(child, options.detached, "SIGKILL"), options.kill.graceMs);
490
+ escalation.unref();
491
+ child.once("exit", () => clearTimeout(escalation));
492
+ }
493
+ function signalProcessTree(child, detached, signal) {
494
+ if (detached && child.pid && process.platform !== "win32") {
495
+ signalProcessGroupBestEffort(child.pid, signal);
496
+ return;
497
+ }
498
+ if (child.exitCode !== null || child.signalCode !== null) return;
499
+ child.kill(signal);
500
+ }
501
+ /**
502
+ * A kill that cannot reach an inherited-pipe holder must at least stop this process
503
+ * from holding the other end of those pipes open after it has settled.
504
+ */
505
+ function destroyCommandStreams(child) {
506
+ child.stdin?.destroy();
507
+ child.stdout?.destroy();
508
+ child.stderr?.destroy();
509
+ }
510
+ async function writeChildStdin(child, stdin) {
511
+ if (!child.stdin) return;
512
+ if (stdin === void 0) {
513
+ child.stdin?.end();
514
+ return;
515
+ }
516
+ await pipeline(Readable.from([stdin]), child.stdin);
517
+ }
518
+ function isEpipeError(error) {
519
+ return error instanceof Error && "code" in error && error.code === "EPIPE";
520
+ }
521
+ //#endregion
522
+ //#region ../host-kit/src/internal/retry.ts
523
+ function sleep(ms, signal) {
524
+ return new Promise((resolve) => {
525
+ if (signal?.aborted) {
526
+ resolve();
527
+ return;
528
+ }
529
+ let settled = false;
530
+ const finish = () => {
531
+ if (settled) return;
532
+ settled = true;
533
+ if (signal) signal.removeEventListener("abort", onAbort);
534
+ resolve();
535
+ };
536
+ const timer = setTimeout(finish, ms);
537
+ function onAbort() {
538
+ clearTimeout(timer);
539
+ finish();
540
+ }
541
+ if (signal) signal.addEventListener("abort", onAbort, { once: true });
542
+ });
543
+ }
544
+ //#endregion
545
+ //#region ../host-kit/src/internal/host-process.ts
546
+ const PS_TIMEOUT_MS = 1e3;
547
+ const HOST_PS_COMMAND = "/bin/ps";
548
+ const WINDOWS_PS_TIMEOUT_MS = 2e3;
549
+ const WINDOWS_PROCESS_TOOL = "powershell.exe";
550
+ function hostEnvironment() {
551
+ return process.env;
552
+ }
553
+ function readHostEnvironmentVariable(name) {
554
+ return hostEnvironment()[name];
555
+ }
556
+ function writeHostStderr(value) {
557
+ process.stderr.write(value);
558
+ }
559
+ function hostCurrentWorkingDirectory() {
560
+ return process.cwd();
561
+ }
562
+ function hostNodeExecutablePath() {
563
+ return process.execPath;
564
+ }
565
+ function hostNodeVersion() {
566
+ return process.version;
567
+ }
568
+ function hostPlatform() {
569
+ return process.platform;
570
+ }
571
+ function hostProcessId() {
572
+ return process.pid;
573
+ }
574
+ function isProcessAlive(pid) {
575
+ if (!Number.isInteger(pid) || pid <= 0) return false;
576
+ try {
577
+ process.kill(pid, 0);
578
+ return true;
579
+ } catch (error) {
580
+ return error.code === "EPERM";
581
+ }
582
+ }
583
+ function isProcessGroupAlive(pid) {
584
+ if (!Number.isInteger(pid) || pid <= 0) return false;
585
+ try {
586
+ process.kill(-pid, 0);
587
+ return true;
588
+ } catch (error) {
589
+ return error.code === "EPERM";
590
+ }
591
+ }
592
+ /**
593
+ * Reads who owns a pid, off the event loop, with a budget the caller names. The three fields
594
+ * are read concurrently so a loaded host pays one budget for the set rather than one per
595
+ * field; on Windows one CIM query answers the whole set. Use this rather than the synchronous
596
+ * probes when the answer decides whether a process is signaled: waiting is reversible and
597
+ * signaling the wrong process is not.
598
+ */
599
+ async function readProcessIdentityFacts(pid, timeoutMs = PS_TIMEOUT_MS) {
600
+ if (isWindowsHostPlatform()) return await readWindowsProcessIdentityFacts(pid, timeoutMs);
601
+ const [startTime, command, state] = await Promise.all([
602
+ readProcessFieldAsync(pid, "lstart=", timeoutMs),
603
+ readProcessFieldAsync(pid, "command=", timeoutMs),
604
+ readProcessFieldAsync(pid, "state=", timeoutMs)
605
+ ]);
606
+ return {
607
+ startTime,
608
+ command,
609
+ zombie: state === null ? null : state.startsWith("Z")
610
+ };
611
+ }
612
+ function readProcessStartTime(pid) {
613
+ return readProcessField(pid, "lstart=");
614
+ }
615
+ function readProcessCommand(pid) {
616
+ return readProcessField(pid, "command=");
617
+ }
618
+ function isProcessZombie(pid) {
619
+ if (isWindowsHostPlatform()) return false;
620
+ return readProcessField(pid, "state=")?.startsWith("Z") ?? false;
621
+ }
622
+ function readHostProcessIdentityObservations(pids) {
623
+ const observations = /* @__PURE__ */ new Map();
624
+ const selected = uniquePositivePids(pids);
625
+ if (selected.length === 0) return observations;
626
+ if (isWindowsHostPlatform()) {
627
+ for (const row of readWindowsProcessRows(selected, WINDOWS_PS_TIMEOUT_MS)) {
628
+ if (row.startTime === null) continue;
629
+ observations.set(row.pid, {
630
+ state: "R",
631
+ startTime: row.startTime
632
+ });
633
+ }
634
+ return observations;
635
+ }
636
+ try {
637
+ const result = runCmdSync(HOST_PS_COMMAND, [
638
+ "-p",
639
+ selected.join(","),
640
+ "-o",
641
+ "pid=,state=,lstart="
642
+ ], {
643
+ allowFailure: true,
644
+ timeoutMs: PS_TIMEOUT_MS
645
+ });
646
+ if (result.exitCode !== 0) return observations;
647
+ for (const line of result.stdout.split("\n")) {
648
+ const match = /^\s*(\d+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
649
+ if (!match) continue;
650
+ const pid = Number.parseInt(match[1], 10);
651
+ if (!Number.isInteger(pid) || pid <= 0) continue;
652
+ observations.set(pid, {
653
+ state: match[2],
654
+ startTime: match[3]
655
+ });
656
+ }
657
+ } catch {}
658
+ return observations;
659
+ }
660
+ function readProcessField(pid, field) {
661
+ if (!Number.isInteger(pid) || pid <= 0) return null;
662
+ if (isWindowsHostPlatform()) return readWindowsProcessField(pid, field);
663
+ try {
664
+ return processFieldValue(runCmdSync(HOST_PS_COMMAND, [
665
+ "-p",
666
+ String(pid),
667
+ "-o",
668
+ field
669
+ ], {
670
+ allowFailure: true,
671
+ timeoutMs: PS_TIMEOUT_MS
672
+ }));
673
+ } catch {
674
+ return null;
675
+ }
676
+ }
677
+ async function readProcessFieldAsync(pid, field, timeoutMs) {
678
+ if (!Number.isInteger(pid) || pid <= 0) return null;
679
+ try {
680
+ return processFieldValue(await runCmd(HOST_PS_COMMAND, [
681
+ "-p",
682
+ String(pid),
683
+ "-o",
684
+ field
685
+ ], {
686
+ allowFailure: true,
687
+ timeoutMs
688
+ }));
689
+ } catch {
690
+ return null;
691
+ }
692
+ }
693
+ function processFieldValue(result) {
694
+ if (result.exitCode !== 0) return null;
695
+ const value = result.stdout.trim();
696
+ return value.length > 0 ? value : null;
697
+ }
698
+ /**
699
+ * Walks process-table stdout line by line, keeping rows whose first two
700
+ * capture groups are a positive pid and an optional positive ppid. Every
701
+ * process-table format (POSIX `ps`, Windows CIM) feeds its own row pattern
702
+ * and row builder through this single walk.
703
+ */
704
+ function parseProcessTableRows(stdout, rowPattern, build) {
705
+ const rows = [];
706
+ for (const line of stdout.replace(/^\uFEFF/, "").split("\n")) {
707
+ const match = rowPattern.exec(line);
708
+ if (!match) continue;
709
+ const pid = Number.parseInt(match[1], 10);
710
+ if (!Number.isInteger(pid) || pid <= 0) continue;
711
+ const ppid = Number.parseInt(match[2], 10);
712
+ rows.push(build(match, pid, Number.isInteger(ppid) && ppid > 0 ? ppid : void 0));
713
+ }
714
+ return rows;
715
+ }
716
+ function parseHostProcessList(stdout) {
717
+ return parseProcessTableRows(stdout, /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/, (match, pid, ppid) => ({
718
+ pid,
719
+ ppid,
720
+ command: match[3]
721
+ }));
722
+ }
723
+ function isWindowsHostPlatform() {
724
+ return hostPlatform() === "win32";
725
+ }
726
+ const WINDOWS_PROCESS_ROW_BODY = " | ForEach-Object { $birth = if ($_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('yyyyMMddHHmmssfffffff', [System.Globalization.CultureInfo]::InvariantCulture) } else { '' }; '{0}|{1}|{2}|{3}' -f $_.ProcessId, $_.ParentProcessId, $birth," + String.raw` ($_.CommandLine -replace '[\r\n]+', ' ') }`;
727
+ function windowsProcessQueryArgs(pids) {
728
+ return [
729
+ "-NoProfile",
730
+ "-NonInteractive",
731
+ "-Command",
732
+ `[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; ${pids.length > 0 ? `Get-CimInstance -ClassName Win32_Process -Filter '${pids.map((pid) => `ProcessId=${pid}`).join(" OR ")}'` : "Get-CimInstance -ClassName Win32_Process"}${WINDOWS_PROCESS_ROW_BODY}`
733
+ ];
734
+ }
735
+ function parseWindowsProcessRows(stdout) {
736
+ return parseProcessTableRows(stdout, /^(\d+)\|(\d+)\|([^|]*)\|(.*)\r?$/, (match, pid, ppid) => ({
737
+ pid,
738
+ ...ppid !== void 0 ? { ppid } : {},
739
+ startTime: match[3].length > 0 ? match[3] : null,
740
+ command: match[4].length > 0 ? match[4] : null
741
+ }));
742
+ }
743
+ function readWindowsProcessRows(pids, timeoutMs) {
744
+ try {
745
+ const result = runCmdSync(WINDOWS_PROCESS_TOOL, windowsProcessQueryArgs(pids), {
746
+ allowFailure: true,
747
+ timeoutMs
748
+ });
749
+ if (result.exitCode !== 0) return [];
750
+ return parseWindowsProcessRows(result.stdout);
751
+ } catch {
752
+ return [];
753
+ }
754
+ }
755
+ function readWindowsProcessField(pid, field) {
756
+ const row = readWindowsProcessRows([pid], WINDOWS_PS_TIMEOUT_MS).find((candidate) => candidate.pid === pid);
757
+ if (!row) return null;
758
+ if (field === "lstart=") return row.startTime;
759
+ if (field === "command=") return row.command;
760
+ return "R";
761
+ }
762
+ function windowsBudgetMs(timeoutMs) {
763
+ return Math.max(timeoutMs, WINDOWS_PS_TIMEOUT_MS);
764
+ }
765
+ async function readWindowsProcessIdentityFacts(pid, timeoutMs) {
766
+ if (!Number.isInteger(pid) || pid <= 0) return {
767
+ startTime: null,
768
+ command: null,
769
+ zombie: null
770
+ };
771
+ try {
772
+ const result = await runCmd(WINDOWS_PROCESS_TOOL, windowsProcessQueryArgs([pid]), {
773
+ allowFailure: true,
774
+ timeoutMs: windowsBudgetMs(timeoutMs)
775
+ });
776
+ if (result.exitCode !== 0) return {
777
+ startTime: null,
778
+ command: null,
779
+ zombie: null
780
+ };
781
+ const row = parseWindowsProcessRows(result.stdout).find((candidate) => candidate.pid === pid);
782
+ if (!row) return {
783
+ startTime: null,
784
+ command: null,
785
+ zombie: null
786
+ };
787
+ return {
788
+ startTime: row.startTime,
789
+ command: row.command,
790
+ zombie: false
791
+ };
792
+ } catch {
793
+ return {
794
+ startTime: null,
795
+ command: null,
796
+ zombie: null
797
+ };
798
+ }
799
+ }
800
+ async function listWindowsHostProcesses(options) {
801
+ const result = await (options.runCommand ?? runCmd)(WINDOWS_PROCESS_TOOL, windowsProcessQueryArgs([]), {
802
+ allowFailure: true,
803
+ timeoutMs: windowsBudgetMs(options.timeoutMs)
804
+ });
805
+ if (result.exitCode !== 0) return [];
806
+ return parseWindowsProcessRows(result.stdout).map((row) => ({
807
+ pid: row.pid,
808
+ ...row.ppid !== void 0 ? { ppid: row.ppid } : {},
809
+ command: row.command ?? ""
810
+ }));
811
+ }
812
+ async function listHostProcesses(options) {
813
+ if (isWindowsHostPlatform()) return await listWindowsHostProcesses(options);
814
+ const result = await (options.runCommand ?? runCmd)(options.runCommand ? "ps" : HOST_PS_COMMAND, [
815
+ "-ax",
816
+ "-o",
817
+ "pid=,ppid=,command="
818
+ ], {
819
+ allowFailure: true,
820
+ timeoutMs: options.timeoutMs
821
+ });
822
+ if (result.exitCode !== 0) return [];
823
+ return parseHostProcessList(result.stdout);
824
+ }
825
+ function expandProcessTree(rootPids, processes) {
826
+ const selected = new Set(uniquePositivePids(rootPids));
827
+ let changed = true;
828
+ while (changed) {
829
+ changed = false;
830
+ for (const processInfo of processes) {
831
+ if (processInfo.ppid === void 0 || !selected.has(processInfo.ppid)) continue;
832
+ if (selected.has(processInfo.pid)) continue;
833
+ selected.add(processInfo.pid);
834
+ changed = true;
835
+ }
836
+ }
837
+ return processes.filter((processInfo) => selected.has(processInfo.pid));
838
+ }
839
+ function uniquePositivePids(values, options = {}) {
840
+ return [...new Set(values)].filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== options.excludePid);
841
+ }
842
+ function signalPidsBestEffort(pidsToSignal, signal) {
843
+ const pids = uniquePositivePids(pidsToSignal, { excludePid: process.pid });
844
+ let signaled = 0;
845
+ for (const pid of pids) try {
846
+ process.kill(pid, signal);
847
+ signaled += 1;
848
+ } catch {}
849
+ return signaled;
850
+ }
851
+ async function waitForProcessExit(pid, timeoutMs) {
852
+ if (!isProcessAlive(pid)) return true;
853
+ const start = Date.now();
854
+ while (Date.now() - start < timeoutMs) {
855
+ await sleep(50);
856
+ if (!isProcessAlive(pid)) return true;
857
+ }
858
+ return !isProcessAlive(pid);
859
+ }
860
+ async function stopPidsWithEscalation(options) {
861
+ const pids = uniquePositivePids(options.pids, { excludePid: process.pid });
862
+ if (pids.length === 0) return;
863
+ signalPidsBestEffort(pids, "SIGTERM");
864
+ await Promise.all(pids.map(async (pid) => await waitForProcessExit(pid, options.termTimeoutMs)));
865
+ signalPidsBestEffort(pids.filter((pid) => isProcessAlive(pid)), "SIGKILL");
866
+ await Promise.all(pids.map(async (pid) => await waitForProcessExit(pid, options.killTimeoutMs)));
867
+ }
868
+ const SYSCTL_TIMEOUT_MS = 1e3;
869
+ const APPLE_SILICON_SYSCTL = ["-n", "hw.optional.arm64"];
870
+ let pendingHostCpuArch;
871
+ let settledHostCpuArch;
872
+ /**
873
+ * The machine's native CPU architecture in Apple naming (`arm64`, `x86_64`), which is what
874
+ * simulators on a Mac run by default. Other CPUs keep Node's `process.arch` name. Resolved once
875
+ * per process.
876
+ */
877
+ function readHostCpuArch() {
878
+ if (settledHostCpuArch !== void 0) return Promise.resolve(settledHostCpuArch);
879
+ pendingHostCpuArch ??= resolveHostCpuArch(process.platform, process.arch).then(settleHostCpuArch);
880
+ return pendingHostCpuArch;
881
+ }
882
+ /**
883
+ * {@link readHostCpuArch} for a caller that cannot await. It shares the same per-process value,
884
+ * resolving it with a blocking `sysctl` only when nothing has resolved it yet.
885
+ */
886
+ function readHostCpuArchSync() {
887
+ return settledHostCpuArch ?? settleHostCpuArch(hostCpuArchName(process.platform === "darwin" && isAppleSiliconMacSync(), process.arch));
888
+ }
889
+ function settleHostCpuArch(arch) {
890
+ settledHostCpuArch ??= arch;
891
+ return settledHostCpuArch;
892
+ }
893
+ async function resolveHostCpuArch(platform, nodeArch) {
894
+ return hostCpuArchName(platform === "darwin" && await isAppleSiliconMac(), nodeArch);
895
+ }
896
+ function hostCpuArchName(appleSilicon, nodeArch) {
897
+ if (appleSilicon) return "arm64";
898
+ return nodeArch === "x64" ? "x86_64" : nodeArch;
899
+ }
900
+ async function isAppleSiliconMac() {
901
+ try {
902
+ const result = await runCmd("/usr/sbin/sysctl", APPLE_SILICON_SYSCTL, {
903
+ allowFailure: true,
904
+ timeoutMs: SYSCTL_TIMEOUT_MS
905
+ });
906
+ return result.exitCode === 0 && result.stdout.trim() === "1";
907
+ } catch {
908
+ return false;
909
+ }
910
+ }
911
+ function isAppleSiliconMacSync() {
912
+ try {
913
+ const result = runCmdSync("/usr/sbin/sysctl", APPLE_SILICON_SYSCTL, {
914
+ allowFailure: true,
915
+ timeoutMs: SYSCTL_TIMEOUT_MS
916
+ });
917
+ return result.exitCode === 0 && result.stdout.trim() === "1";
918
+ } catch {
919
+ return false;
920
+ }
921
+ }
922
+ //#endregion
923
+ //#region ../host-kit/src/internal/owned-process-reaper.ts
924
+ const DEFAULT_TERM_TIMEOUT_MS = 1500;
925
+ const DEFAULT_KILL_TIMEOUT_MS = 1e3;
926
+ async function reapOwnedProcessRecordsAtStartup(store, options = {}) {
927
+ const summary = createReapSummary();
928
+ const policy = createReapPolicy(options);
929
+ for (const entry of store.read()) {
930
+ if (!isDecodedRecord(entry)) continue;
931
+ const retained = await reapDecodedEntry(entry, policy, summary);
932
+ if (retained.length === 0) store.clear(entry.scope);
933
+ else if (retained.length !== entry.records.length) store.replace(entry.scope, retained);
934
+ }
935
+ return Object.freeze(summary);
936
+ }
937
+ function isDecodedRecord(entry) {
938
+ return entry.status === "decoded";
939
+ }
940
+ function createReapSummary() {
941
+ return {
942
+ inspected: 0,
943
+ inspectedPids: [],
944
+ terminated: 0,
945
+ terminatedPids: [],
946
+ missing: 0,
947
+ missingPids: [],
948
+ ownershipLost: 0,
949
+ ownershipLostPids: [],
950
+ skippedForOpenWebSession: false
951
+ };
952
+ }
953
+ function createReapPolicy(options) {
954
+ return {
955
+ openWebSession: (options.openWebSessionNames ?? []).length > 0,
956
+ purposes: options.purposes ? new Set(options.purposes) : void 0,
957
+ termTimeoutMs: options.termTimeoutMs ?? DEFAULT_TERM_TIMEOUT_MS,
958
+ killTimeoutMs: options.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS
959
+ };
960
+ }
961
+ async function reapDecodedEntry(entry, policy, summary) {
962
+ const retained = [];
963
+ for (const record of entry.records) {
964
+ if (shouldRetainWithoutInspection(entry.scope.kind, record, policy)) {
965
+ retained.push(record);
966
+ if (isOpenWebSessionSkip(entry.scope.kind, record, policy)) summary.skippedForOpenWebSession = true;
967
+ continue;
968
+ }
969
+ summary.inspected += 1;
970
+ summary.inspectedPids.push(record.pid);
971
+ appendReapOutcome(summary, record, await reapOwnedProcess(record, policy), retained);
972
+ }
973
+ return retained;
974
+ }
975
+ function shouldRetainWithoutInspection(scope, record, policy) {
976
+ return !isSelectedPurpose(record, policy) || isOpenWebSessionSkip(scope, record, policy);
977
+ }
978
+ function isSelectedPurpose(record, policy) {
979
+ return policy.purposes === void 0 || policy.purposes.has(record.purpose);
980
+ }
981
+ function isOpenWebSessionSkip(scope, record, policy) {
982
+ return scope === "daemon" && record.purpose === "managed-web-browser" && policy.openWebSession;
983
+ }
984
+ function appendReapOutcome(summary, record, outcome, retained) {
985
+ if (outcome === "terminated") {
986
+ summary.terminated += 1;
987
+ summary.terminatedPids.push(record.pid);
988
+ return;
989
+ }
990
+ if (outcome === "missing") {
991
+ summary.missing += 1;
992
+ summary.missingPids.push(record.pid);
993
+ return;
994
+ }
995
+ summary.ownershipLost += 1;
996
+ summary.ownershipLostPids.push(record.pid);
997
+ retained.push(record);
998
+ }
999
+ async function reapOwnedProcess(record, policy) {
1000
+ const initial = await inspectOwnedProcess(record);
1001
+ if (initial !== void 0) return initial;
1002
+ signalBestEffort(record.pid, initialSignalFor(record));
1003
+ return await escalateOwnedProcess(record, policy);
1004
+ }
1005
+ async function inspectOwnedProcess(record) {
1006
+ if (record.pid === process.pid || !isProcessAlive(record.pid)) return "missing";
1007
+ const facts = await readProcessIdentityFacts(record.pid);
1008
+ if (facts.zombie) return "missing";
1009
+ return matchesFacts(record, facts) ? void 0 : "ownership-lost";
1010
+ }
1011
+ function initialSignalFor(record) {
1012
+ return record.purpose === "simctl-screen-recording" ? "SIGINT" : "SIGTERM";
1013
+ }
1014
+ async function escalateOwnedProcess(record, policy) {
1015
+ const termOutcome = await waitForOwnedProcessExit(record, policy.termTimeoutMs);
1016
+ if (termOutcome === "exited") return "terminated";
1017
+ if (termOutcome === "ownership-lost") return "ownership-lost";
1018
+ signalBestEffort(record.pid, "SIGKILL");
1019
+ return await waitForOwnedProcessExit(record, policy.killTimeoutMs) === "exited" ? "terminated" : "ownership-lost";
1020
+ }
1021
+ async function waitForOwnedProcessExit(record, timeoutMs) {
1022
+ await waitForProcessExit(record.pid, timeoutMs);
1023
+ if (!isProcessAlive(record.pid)) return "exited";
1024
+ const facts = await readProcessIdentityFacts(record.pid);
1025
+ if (facts.zombie) return "exited";
1026
+ return matchesFacts(record, facts) ? "owned-alive" : "ownership-lost";
1027
+ }
1028
+ function matchesFacts(record, facts) {
1029
+ return facts.startTime === record.startTime && facts.command === record.command;
1030
+ }
1031
+ function signalBestEffort(pid, signal) {
1032
+ try {
1033
+ process.kill(pid, signal);
1034
+ } catch {}
1035
+ }
1036
+ //#endregion
1037
+ //#region ../host-kit/src/internal/atomic-file.ts
1038
+ /**
1039
+ * Gives a specialized durable publisher a canonical temp path and cleanup
1040
+ * ownership while it performs its own open/fsync/safety protocol.
1041
+ */
1042
+ function withAtomicPublishTempPathSync(destination, publish) {
1043
+ const temporaryPath = createAtomicPublishTempPath(destination);
1044
+ try {
1045
+ return publish(temporaryPath);
1046
+ } finally {
1047
+ try {
1048
+ fs.rmSync(temporaryPath, { force: true });
1049
+ } catch (error) {
1050
+ emitDiagnostic({
1051
+ level: "warn",
1052
+ phase: "atomic_file_cleanup_failed",
1053
+ data: {
1054
+ destination,
1055
+ temporaryPath,
1056
+ error: error instanceof Error ? error.message : String(error)
1057
+ }
1058
+ });
1059
+ }
1060
+ }
1061
+ }
1062
+ /** Returns the canonical same-directory temp path used by atomic publishers. */
1063
+ function createAtomicPublishTempPath(destination) {
1064
+ return path.join(path.dirname(destination), `.${path.basename(destination)}.${process.pid}-${crypto.randomUUID()}.tmp`);
1065
+ }
1066
+ //#endregion
1067
+ //#region ../host-kit/src/internal/owned-process-record.ts
1068
+ const RECORD_FILE_NAME = "owned-processes.json";
1069
+ const RECORD_VERSION = 1;
1070
+ function createOwnedProcessRecordStore(options) {
1071
+ const stateDir = path.resolve(options.stateDir);
1072
+ const sessionsDir = path.resolve(options.sessionsDir);
1073
+ const daemonPath = path.join(stateDir, RECORD_FILE_NAME);
1074
+ const resolvePath = (scope) => {
1075
+ if (scope.kind === "daemon") return daemonPath;
1076
+ const sessionDir = path.resolve(options.resolveSessionDir(scope.sessionId));
1077
+ if (path.dirname(sessionDir) !== sessionsDir) throw new TypeError("owned process session scope must resolve directly under the sessions dir");
1078
+ return path.join(sessionDir, RECORD_FILE_NAME);
1079
+ };
1080
+ return Object.freeze({
1081
+ replace(scope, records) {
1082
+ const normalized = normalizeRecords(records);
1083
+ const recordPath = resolvePath(scope);
1084
+ if (normalized.length === 0) {
1085
+ clearRecordPath(recordPath);
1086
+ return;
1087
+ }
1088
+ const directory = path.dirname(recordPath);
1089
+ fs.mkdirSync(directory, { recursive: true });
1090
+ withAtomicPublishTempPathSync(recordPath, (temporaryPath) => {
1091
+ const descriptor = fs.openSync(temporaryPath, "wx", 384);
1092
+ try {
1093
+ fs.writeFileSync(descriptor, `${JSON.stringify({
1094
+ version: RECORD_VERSION,
1095
+ processes: normalized
1096
+ })}\n`, "utf8");
1097
+ fs.fsyncSync(descriptor);
1098
+ } finally {
1099
+ fs.closeSync(descriptor);
1100
+ }
1101
+ assertSafeRecordDestination(recordPath);
1102
+ fs.renameSync(temporaryPath, recordPath);
1103
+ });
1104
+ },
1105
+ clear(scope) {
1106
+ clearRecordPath(resolvePath(scope));
1107
+ },
1108
+ read() {
1109
+ return readRecordFiles(sessionsDir, daemonPath);
1110
+ }
1111
+ });
1112
+ }
1113
+ function readOwnedProcessRecordFile(recordPath, scope) {
1114
+ let value;
1115
+ try {
1116
+ assertSafeRecordDestination(recordPath);
1117
+ value = JSON.parse(fs.readFileSync(recordPath, "utf8"));
1118
+ } catch (error) {
1119
+ return {
1120
+ scope,
1121
+ path: recordPath,
1122
+ status: "invalid",
1123
+ records: [],
1124
+ message: errorMessage(error)
1125
+ };
1126
+ }
1127
+ if (!isRecordFile(value)) return {
1128
+ scope,
1129
+ path: recordPath,
1130
+ status: "invalid",
1131
+ records: [],
1132
+ message: "owned process record must contain version 1 and a processes array"
1133
+ };
1134
+ try {
1135
+ return {
1136
+ scope,
1137
+ path: recordPath,
1138
+ status: "decoded",
1139
+ records: normalizeRecords(value.processes)
1140
+ };
1141
+ } catch (error) {
1142
+ return {
1143
+ scope,
1144
+ path: recordPath,
1145
+ status: "invalid",
1146
+ records: [],
1147
+ message: errorMessage(error)
1148
+ };
1149
+ }
1150
+ }
1151
+ function readRecordFiles(sessionsDir, daemonPath) {
1152
+ const entries = [];
1153
+ if (fileExists(daemonPath)) entries.push(readOwnedProcessRecordFile(daemonPath, { kind: "daemon" }));
1154
+ if (!fs.existsSync(sessionsDir)) return entries;
1155
+ for (const entry of fs.readdirSync(sessionsDir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
1156
+ if (!entry.isDirectory()) continue;
1157
+ const recordPath = path.join(sessionsDir, entry.name, RECORD_FILE_NAME);
1158
+ if (fileExists(recordPath)) entries.push(readOwnedProcessRecordFile(recordPath, {
1159
+ kind: "session",
1160
+ sessionId: entry.name
1161
+ }));
1162
+ }
1163
+ return entries;
1164
+ }
1165
+ function normalizeRecords(records) {
1166
+ const seen = /* @__PURE__ */ new Set();
1167
+ const normalized = [];
1168
+ for (const record of records) {
1169
+ if (!isValidOwnedProcessRecord(record)) throw new TypeError("owned process record has an invalid identity or purpose");
1170
+ if (seen.has(record.pid)) throw new TypeError(`duplicate owned process pid ${record.pid}`);
1171
+ seen.add(record.pid);
1172
+ normalized.push(Object.freeze({
1173
+ pid: record.pid,
1174
+ startTime: record.startTime,
1175
+ command: record.command,
1176
+ purpose: record.purpose
1177
+ }));
1178
+ }
1179
+ return normalized.sort((left, right) => left.pid - right.pid);
1180
+ }
1181
+ function isValidOwnedProcessRecord(record) {
1182
+ return Number.isInteger(record.pid) && record.pid > 0 && typeof record.startTime === "string" && record.startTime.length > 0 && typeof record.command === "string" && record.command.length > 0 && typeof record.purpose === "string" && record.purpose.length > 0;
1183
+ }
1184
+ function isRecordFile(value) {
1185
+ return typeof value === "object" && value !== null && "version" in value && value.version === RECORD_VERSION && "processes" in value && Array.isArray(value.processes);
1186
+ }
1187
+ function clearRecordPath(recordPath) {
1188
+ try {
1189
+ assertSafeRecordDestination(recordPath);
1190
+ fs.unlinkSync(recordPath);
1191
+ } catch (error) {
1192
+ if (!isMissingFile(error)) throw error;
1193
+ }
1194
+ }
1195
+ function assertSafeRecordDestination(recordPath) {
1196
+ try {
1197
+ const stats = fs.lstatSync(recordPath);
1198
+ if (stats.isSymbolicLink()) throw new Error("Refusing to follow an owned process record symlink");
1199
+ if (!stats.isFile()) throw new Error("Owned process record is not a regular file");
1200
+ } catch (error) {
1201
+ if (!isMissingFile(error)) throw error;
1202
+ }
1203
+ }
1204
+ function fileExists(filePath) {
1205
+ try {
1206
+ return fs.lstatSync(filePath).isFile();
1207
+ } catch {
1208
+ return false;
1209
+ }
1210
+ }
1211
+ function isMissingFile(error) {
1212
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
1213
+ }
1214
+ function errorMessage(error) {
1215
+ return error instanceof Error ? error.message : "owned process record is invalid";
1216
+ }
1217
+ //#endregion
1218
+ //#region ../host-kit/src/internal/owner-identity.ts
1219
+ /** A process id accepted by Node's native signal API. */
1220
+ function isProcessPid(value) {
1221
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 2147483647;
1222
+ }
1223
+ function readCurrentOwnerIdentity() {
1224
+ return {
1225
+ pid: process.pid,
1226
+ startTime: readProcessStartTime(process.pid)
1227
+ };
1228
+ }
1229
+ function ownerIdentityMatches(left, right) {
1230
+ return left.pid === right.pid && left.startTime === right.startTime;
1231
+ }
1232
+ /**
1233
+ * Positive proof that two records name DIFFERENT processes. Deliberately not
1234
+ * the negation of {@link ownerIdentityMatches}: equal pids with an unreadable
1235
+ * start time on either side are unproven rather than different, so a caller
1236
+ * that acts on this can never mistake one owner for two and hand its resource
1237
+ * away.
1238
+ */
1239
+ function ownerIdentityDiffers(left, right) {
1240
+ if (left.pid !== right.pid) return true;
1241
+ return Boolean(left.startTime && right.startTime && left.startTime !== right.startTime);
1242
+ }
1243
+ /**
1244
+ * This is deliberately proof-oriented, in both directions. A filesystem read
1245
+ * error is not proof that an owner state directory disappeared, so callers
1246
+ * must surface it as an unknown owner rather than treating the resource as
1247
+ * free. Likewise a failed `ps` read (it shells out with a short timeout and
1248
+ * loses under CPU contention) is not proof the owner died, so it never
1249
+ * condemns a pid that kill(pid, 0) says is alive. Death is only concluded
1250
+ * from positive evidence: the pid is gone or the process is a zombie (already
1251
+ * terminated, merely unreaped). A different readable start time proves PID
1252
+ * reuse, but remains a distinct result so each ownership policy must decide
1253
+ * explicitly whether that proof permits recovery. An unreadable start time is
1254
+ * not proof of reuse. An owner recorded without a start time stays fail-closed
1255
+ * while its pid is alive: there is no same-clock-domain proof of birth order (wall-clock
1256
+ * arithmetic over `ps etime` shifts under clock steps), and misreading a live
1257
+ * owner as recycled would let a waiter steal a held resource.
1258
+ */
1259
+ function classifyOwnerLiveness(params) {
1260
+ const guarded = guardOwnerPid(params.owner);
1261
+ if (guarded) return guarded;
1262
+ return classifyLiveOwnerFromObservation(params, readHostProcessIdentityObservations([params.owner.pid]).get(params.owner.pid) ?? null);
1263
+ }
1264
+ function classifyOwnerLivenessFromObservation(params, observation) {
1265
+ const guarded = guardOwnerPid(params.owner);
1266
+ if (guarded) return guarded;
1267
+ return classifyLiveOwnerFromObservation(params, observation);
1268
+ }
1269
+ /** The verdicts provable from the pid alone; undefined means the caller may probe on. */
1270
+ function guardOwnerPid(owner) {
1271
+ if (!isProcessPid(owner.pid)) return "unknown";
1272
+ if (!isProcessAlive(owner.pid)) return "owner-process-dead";
1273
+ }
1274
+ /** Judges an owner the caller has established as a live, valid pid; probes no liveness. */
1275
+ function classifyLiveOwnerFromObservation(params, observation) {
1276
+ const { owner, stateDir } = params;
1277
+ if (hostSaysZombie(owner, observation)) return "owner-process-dead";
1278
+ if (owner.startTime && provesPidReused(owner, observation)) return "owner-process-reused";
1279
+ return stateDir ? classifyOwnerStateDirectory(stateDir) : "live";
1280
+ }
1281
+ /**
1282
+ * Whether the host calls this owner a zombie. An `undefined` observation means
1283
+ * no snapshot was taken and the field probe answers; a `null` observation is a
1284
+ * completed snapshot that did not list the pid, which is unknown evidence and
1285
+ * never a zombie claim.
1286
+ */
1287
+ function hostSaysZombie(owner, observation) {
1288
+ if (observation === void 0) return isProcessZombie(owner.pid);
1289
+ return observation?.state.startsWith("Z") ?? false;
1290
+ }
1291
+ /**
1292
+ * Whether the recorded birth time proves the pid changed hands. Only a
1293
+ * readable current start time that differs from the record proves reuse; an
1294
+ * `undefined` observation falls back to the field probe, and a host that did
1295
+ * not answer is unknown evidence, never proof.
1296
+ */
1297
+ function provesPidReused(owner, observation) {
1298
+ const currentStartTime = observation === void 0 ? readProcessStartTime(owner.pid) : observation?.startTime ?? null;
1299
+ return currentStartTime !== null && currentStartTime !== owner.startTime;
1300
+ }
1301
+ function classifyOwnerStateDirectory(stateDir) {
1302
+ try {
1303
+ fs.statSync(stateDir);
1304
+ return "live";
1305
+ } catch (error) {
1306
+ const code = error?.code;
1307
+ if (code === "ENOENT" || code === "ENOTDIR") return "owner-state-dir-gone";
1308
+ return "unknown";
1309
+ }
1310
+ }
1311
+ //#endregion
1312
+ export { classifyOwnerLiveness, classifyOwnerLivenessFromObservation, createOwnedProcessRecordStore, expandProcessTree, hostCurrentWorkingDirectory, hostEnvironment, hostNodeExecutablePath, hostNodeVersion, hostPlatform, hostProcessId, isProcessAlive, isProcessGroupAlive, isProcessPid, isProcessZombie, listHostProcesses, ownerIdentityDiffers, ownerIdentityMatches, readCurrentOwnerIdentity, readHostCpuArch, readHostCpuArchSync, readHostEnvironmentVariable, readHostProcessIdentityObservations, readOwnedProcessRecordFile, readProcessCommand, readProcessIdentityFacts, readProcessStartTime, reapOwnedProcessRecordsAtStartup, signalPidsBestEffort, signalProcessGroupBestEffort, stopPidsWithEscalation, uniquePositivePids, waitForProcessExit, writeHostStderr };