@agent-device/proxy 0.21.23 → 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.
- package/dist/errors.mjs +7 -1
- package/dist/index.mjs +1 -0
- package/dist/process.mjs +208 -43
- package/dist/version.mjs +1 -1
- package/package.json +1 -1
package/dist/errors.mjs
CHANGED
|
@@ -123,12 +123,18 @@ const KNOWN_APP_ERROR_CODES = [
|
|
|
123
123
|
"REPAIR_COMMIT_FAILED",
|
|
124
124
|
"UNKNOWN"
|
|
125
125
|
];
|
|
126
|
-
|
|
126
|
+
const APP_ERROR_BRAND = Symbol.for("agent-device.AppError");
|
|
127
|
+
var AppError = class AppError extends Error {
|
|
128
|
+
static [Symbol.hasInstance](value) {
|
|
129
|
+
if (this !== AppError) return Function.prototype[Symbol.hasInstance].call(this, value);
|
|
130
|
+
return typeof value === "object" && value !== null && value[APP_ERROR_BRAND] === true;
|
|
131
|
+
}
|
|
127
132
|
code;
|
|
128
133
|
details;
|
|
129
134
|
cause;
|
|
130
135
|
constructor(code, message, details, cause) {
|
|
131
136
|
super(message);
|
|
137
|
+
Object.defineProperty(this, APP_ERROR_BRAND, { value: true });
|
|
132
138
|
this.code = code;
|
|
133
139
|
this.details = details;
|
|
134
140
|
this.cause = cause;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { r as normalizeError, t as AppError } from "./errors.mjs";
|
|
2
2
|
import crypto, { randomUUID } from "node:crypto";
|
|
3
3
|
import http from "node:http";
|
|
4
|
+
import "node:path";
|
|
4
5
|
//#region ../host-kit/src/internal/transport.ts
|
|
5
6
|
/**
|
|
6
7
|
* Compares two secret strings in constant time. Hashing both inputs first
|
package/dist/process.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { i as redactDiagnosticData, n as createRequestCanceledError, t as AppError } from "./errors.mjs";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
5
|
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
@@ -355,13 +355,6 @@ function createStdinError(executable, cmd, args, cause) {
|
|
|
355
355
|
args
|
|
356
356
|
}, cause instanceof Error ? cause : void 0);
|
|
357
357
|
}
|
|
358
|
-
function createCommandCanceledError(executable, cmd, args) {
|
|
359
|
-
return createRequestCanceledError({
|
|
360
|
-
cmd,
|
|
361
|
-
args,
|
|
362
|
-
executable
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
358
|
function createTimeoutError(executable, cmd, args, timeoutMs, exitCode, stdout, stderr) {
|
|
366
359
|
return new AppError("COMMAND_FAILED", `${executable} timed out after ${timeoutMs}ms`, {
|
|
367
360
|
cmd,
|
|
@@ -408,10 +401,18 @@ function execFailureDetails(result, extra) {
|
|
|
408
401
|
};
|
|
409
402
|
}
|
|
410
403
|
function spawnRejectionError(abort, executable, cmd, args, err) {
|
|
411
|
-
return abort.didAbort ?
|
|
404
|
+
return abort.didAbort ? createRequestCanceledError({
|
|
405
|
+
cmd,
|
|
406
|
+
args,
|
|
407
|
+
executable
|
|
408
|
+
}) : createSpawnError(executable, cmd, args, err);
|
|
412
409
|
}
|
|
413
410
|
function commandCloseFailure(abort, executable, cmd, args, exitCode, allowFailure, stdout, stderr) {
|
|
414
|
-
if (abort.didAbort) return
|
|
411
|
+
if (abort.didAbort) return createRequestCanceledError({
|
|
412
|
+
cmd,
|
|
413
|
+
args,
|
|
414
|
+
executable
|
|
415
|
+
});
|
|
415
416
|
if (exitCode !== 0 && !allowFailure) return createExitError(executable, cmd, args, exitCode, stdout, stderr);
|
|
416
417
|
return null;
|
|
417
418
|
}
|
|
@@ -518,14 +519,34 @@ function isEpipeError(error) {
|
|
|
518
519
|
return error instanceof Error && "code" in error && error.code === "EPIPE";
|
|
519
520
|
}
|
|
520
521
|
//#endregion
|
|
521
|
-
//#region ../host-kit/src/internal/
|
|
522
|
-
function sleep(ms) {
|
|
523
|
-
return new Promise((resolve) =>
|
|
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
|
+
});
|
|
524
543
|
}
|
|
525
544
|
//#endregion
|
|
526
545
|
//#region ../host-kit/src/internal/host-process.ts
|
|
527
546
|
const PS_TIMEOUT_MS = 1e3;
|
|
528
|
-
const HOST_PS_COMMAND =
|
|
547
|
+
const HOST_PS_COMMAND = "/bin/ps";
|
|
548
|
+
const WINDOWS_PS_TIMEOUT_MS = 2e3;
|
|
549
|
+
const WINDOWS_PROCESS_TOOL = "powershell.exe";
|
|
529
550
|
function hostEnvironment() {
|
|
530
551
|
return process.env;
|
|
531
552
|
}
|
|
@@ -571,10 +592,12 @@ function isProcessGroupAlive(pid) {
|
|
|
571
592
|
/**
|
|
572
593
|
* Reads who owns a pid, off the event loop, with a budget the caller names. The three fields
|
|
573
594
|
* are read concurrently so a loaded host pays one budget for the set rather than one per
|
|
574
|
-
* field
|
|
575
|
-
* process is signaled: waiting is reversible and
|
|
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.
|
|
576
598
|
*/
|
|
577
599
|
async function readProcessIdentityFacts(pid, timeoutMs = PS_TIMEOUT_MS) {
|
|
600
|
+
if (isWindowsHostPlatform()) return await readWindowsProcessIdentityFacts(pid, timeoutMs);
|
|
578
601
|
const [startTime, command, state] = await Promise.all([
|
|
579
602
|
readProcessFieldAsync(pid, "lstart=", timeoutMs),
|
|
580
603
|
readProcessFieldAsync(pid, "command=", timeoutMs),
|
|
@@ -593,14 +616,25 @@ function readProcessCommand(pid) {
|
|
|
593
616
|
return readProcessField(pid, "command=");
|
|
594
617
|
}
|
|
595
618
|
function isProcessZombie(pid) {
|
|
619
|
+
if (isWindowsHostPlatform()) return false;
|
|
596
620
|
return readProcessField(pid, "state=")?.startsWith("Z") ?? false;
|
|
597
621
|
}
|
|
598
622
|
function readHostProcessIdentityObservations(pids) {
|
|
599
623
|
const observations = /* @__PURE__ */ new Map();
|
|
600
624
|
const selected = uniquePositivePids(pids);
|
|
601
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
|
+
}
|
|
602
636
|
try {
|
|
603
|
-
const result = runCmdSync(
|
|
637
|
+
const result = runCmdSync(HOST_PS_COMMAND, [
|
|
604
638
|
"-p",
|
|
605
639
|
selected.join(","),
|
|
606
640
|
"-o",
|
|
@@ -625,6 +659,7 @@ function readHostProcessIdentityObservations(pids) {
|
|
|
625
659
|
}
|
|
626
660
|
function readProcessField(pid, field) {
|
|
627
661
|
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
662
|
+
if (isWindowsHostPlatform()) return readWindowsProcessField(pid, field);
|
|
628
663
|
try {
|
|
629
664
|
return processFieldValue(runCmdSync(HOST_PS_COMMAND, [
|
|
630
665
|
"-p",
|
|
@@ -660,24 +695,122 @@ function processFieldValue(result) {
|
|
|
660
695
|
const value = result.stdout.trim();
|
|
661
696
|
return value.length > 0 ? value : null;
|
|
662
697
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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);
|
|
667
708
|
if (!match) continue;
|
|
668
709
|
const pid = Number.parseInt(match[1], 10);
|
|
669
|
-
const ppid = Number.parseInt(match[2], 10);
|
|
670
|
-
const command = match[3];
|
|
671
710
|
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
676
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
|
+
};
|
|
677
798
|
}
|
|
678
|
-
|
|
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
|
+
}));
|
|
679
811
|
}
|
|
680
812
|
async function listHostProcesses(options) {
|
|
813
|
+
if (isWindowsHostPlatform()) return await listWindowsHostProcesses(options);
|
|
681
814
|
const result = await (options.runCommand ?? runCmd)(options.runCommand ? "ps" : HOST_PS_COMMAND, [
|
|
682
815
|
"-ax",
|
|
683
816
|
"-o",
|
|
@@ -864,14 +997,16 @@ function appendReapOutcome(summary, record, outcome, retained) {
|
|
|
864
997
|
retained.push(record);
|
|
865
998
|
}
|
|
866
999
|
async function reapOwnedProcess(record, policy) {
|
|
867
|
-
const initial = inspectOwnedProcess(record);
|
|
1000
|
+
const initial = await inspectOwnedProcess(record);
|
|
868
1001
|
if (initial !== void 0) return initial;
|
|
869
1002
|
signalBestEffort(record.pid, initialSignalFor(record));
|
|
870
1003
|
return await escalateOwnedProcess(record, policy);
|
|
871
1004
|
}
|
|
872
|
-
function inspectOwnedProcess(record) {
|
|
873
|
-
if (record.pid === process.pid || !isProcessAlive(record.pid)
|
|
874
|
-
|
|
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";
|
|
875
1010
|
}
|
|
876
1011
|
function initialSignalFor(record) {
|
|
877
1012
|
return record.purpose === "simctl-screen-recording" ? "SIGINT" : "SIGTERM";
|
|
@@ -885,11 +1020,13 @@ async function escalateOwnedProcess(record, policy) {
|
|
|
885
1020
|
}
|
|
886
1021
|
async function waitForOwnedProcessExit(record, timeoutMs) {
|
|
887
1022
|
await waitForProcessExit(record.pid, timeoutMs);
|
|
888
|
-
if (!isProcessAlive(record.pid)
|
|
889
|
-
|
|
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";
|
|
890
1027
|
}
|
|
891
|
-
function
|
|
892
|
-
return
|
|
1028
|
+
function matchesFacts(record, facts) {
|
|
1029
|
+
return facts.startTime === record.startTime && facts.command === record.command;
|
|
893
1030
|
}
|
|
894
1031
|
function signalBestEffort(pid, signal) {
|
|
895
1032
|
try {
|
|
@@ -1120,19 +1257,47 @@ function ownerIdentityDiffers(left, right) {
|
|
|
1120
1257
|
* owner as recycled would let a waiter steal a held resource.
|
|
1121
1258
|
*/
|
|
1122
1259
|
function classifyOwnerLiveness(params) {
|
|
1123
|
-
|
|
1260
|
+
const guarded = guardOwnerPid(params.owner);
|
|
1261
|
+
if (guarded) return guarded;
|
|
1262
|
+
return classifyLiveOwnerFromObservation(params, readHostProcessIdentityObservations([params.owner.pid]).get(params.owner.pid) ?? null);
|
|
1124
1263
|
}
|
|
1125
1264
|
function classifyOwnerLivenessFromObservation(params, observation) {
|
|
1126
|
-
const
|
|
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) {
|
|
1127
1271
|
if (!isProcessPid(owner.pid)) return "unknown";
|
|
1128
1272
|
if (!isProcessAlive(owner.pid)) return "owner-process-dead";
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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";
|
|
1134
1279
|
return stateDir ? classifyOwnerStateDirectory(stateDir) : "live";
|
|
1135
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
|
+
}
|
|
1136
1301
|
function classifyOwnerStateDirectory(stateDir) {
|
|
1137
1302
|
try {
|
|
1138
1303
|
fs.statSync(stateDir);
|
package/dist/version.mjs
CHANGED