@agent-device/proxy 0.21.27 → 0.21.28-nightly.20261014.57
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/process.mjs +255 -31
- package/package.json +3 -3
package/dist/process.mjs
CHANGED
|
@@ -98,6 +98,9 @@ function parseBooleanLiteral(value) {
|
|
|
98
98
|
const BARE_COMMAND_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
99
99
|
const EXEC_DIAGNOSTIC_ARG_LIMIT = 6;
|
|
100
100
|
const commandExecutorOverrideScope = new AsyncLocalStorage();
|
|
101
|
+
async function withoutCommandExecutorOverride(fn) {
|
|
102
|
+
return await commandExecutorOverrideScope.run(void 0, fn);
|
|
103
|
+
}
|
|
101
104
|
const requestCommandEnvScope = new AsyncLocalStorage();
|
|
102
105
|
/** Effective toolchain selection; an explicitly empty request value selects `xcode-select`. */
|
|
103
106
|
function commandDeveloperDir() {
|
|
@@ -619,43 +622,95 @@ function isProcessZombie(pid) {
|
|
|
619
622
|
if (isWindowsHostPlatform()) return false;
|
|
620
623
|
return readProcessField(pid, "state=")?.startsWith("Z") ?? false;
|
|
621
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* What the host says about each pid named, from one process-tool snapshot. An unanswered pid is
|
|
627
|
+
* missing from the map, which is unknown evidence rather than absence: a caller that signals on
|
|
628
|
+
* the answer must stay fail-closed. Use {@link readHostProcessIdentityObservationsAsync} from a
|
|
629
|
+
* path that is already waiting; this one blocks the event loop for the whole snapshot.
|
|
630
|
+
*/
|
|
622
631
|
function readHostProcessIdentityObservations(pids) {
|
|
623
|
-
const observations = /* @__PURE__ */ new Map();
|
|
624
632
|
const selected = uniquePositivePids(pids);
|
|
625
|
-
if (selected.length === 0) return
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
633
|
+
if (selected.length === 0) return /* @__PURE__ */ new Map();
|
|
634
|
+
return readHostProcessIdentitySnapshot(selected, readHostProcessIdentityRowsSync(selected));
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* {@link readHostProcessIdentityObservations} off the event loop, with the same one-snapshot
|
|
638
|
+
* guarantee and the same fail-closed answer. A daemon request path that only decides whether to
|
|
639
|
+
* pay a cost must not stall every other request, timer, and abort handler behind a
|
|
640
|
+
* `spawnSync` of the host process tool.
|
|
641
|
+
*/
|
|
642
|
+
async function readHostProcessIdentityObservationsAsync(pids) {
|
|
643
|
+
const selected = uniquePositivePids(pids);
|
|
644
|
+
if (selected.length === 0) return /* @__PURE__ */ new Map();
|
|
645
|
+
return readHostProcessIdentitySnapshot(selected, await withoutCommandExecutorOverride(() => readHostProcessIdentityRowsAsync(selected)));
|
|
646
|
+
}
|
|
647
|
+
function readHostProcessIdentitySnapshot(pids, rows) {
|
|
648
|
+
const selected = uniquePositivePids(pids);
|
|
649
|
+
const observations = /* @__PURE__ */ new Map();
|
|
650
|
+
if (selected.length === 0 || rows.length === 0) return observations;
|
|
651
|
+
const named = new Set(selected);
|
|
652
|
+
for (const row of rows) {
|
|
653
|
+
if (!named.has(row.pid)) continue;
|
|
654
|
+
observations.set(row.pid, {
|
|
655
|
+
state: row.state,
|
|
656
|
+
startTime: row.startTime
|
|
657
|
+
});
|
|
635
658
|
}
|
|
659
|
+
return observations;
|
|
660
|
+
}
|
|
661
|
+
const HOST_PROCESS_IDENTITY_PS_ARGS = (pids) => [
|
|
662
|
+
"-p",
|
|
663
|
+
pids.join(","),
|
|
664
|
+
"-o",
|
|
665
|
+
"pid=,state=,lstart="
|
|
666
|
+
];
|
|
667
|
+
function readHostProcessIdentityRowsSync(pids) {
|
|
668
|
+
if (isWindowsHostPlatform()) return windowsProcessIdentityRows(readWindowsProcessRows(pids, WINDOWS_PS_TIMEOUT_MS));
|
|
636
669
|
try {
|
|
637
|
-
const result = runCmdSync(HOST_PS_COMMAND,
|
|
638
|
-
"-p",
|
|
639
|
-
selected.join(","),
|
|
640
|
-
"-o",
|
|
641
|
-
"pid=,state=,lstart="
|
|
642
|
-
], {
|
|
670
|
+
const result = runCmdSync(HOST_PS_COMMAND, HOST_PROCESS_IDENTITY_PS_ARGS(pids), {
|
|
643
671
|
allowFailure: true,
|
|
644
672
|
timeoutMs: PS_TIMEOUT_MS
|
|
645
673
|
});
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
674
|
+
return result.exitCode !== 0 ? [] : parseHostProcessIdentityRows(result.stdout);
|
|
675
|
+
} catch {
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function readHostProcessIdentityRowsAsync(pids) {
|
|
680
|
+
const selected = uniquePositivePids(pids);
|
|
681
|
+
if (selected.length === 0) return [];
|
|
682
|
+
if (isWindowsHostPlatform()) return windowsProcessIdentityRows(await readWindowsProcessRowsAsync(selected, WINDOWS_PS_TIMEOUT_MS));
|
|
683
|
+
try {
|
|
684
|
+
const result = await runCmd(HOST_PS_COMMAND, HOST_PROCESS_IDENTITY_PS_ARGS(selected), {
|
|
685
|
+
allowFailure: true,
|
|
686
|
+
timeoutMs: PS_TIMEOUT_MS
|
|
687
|
+
});
|
|
688
|
+
return result.exitCode !== 0 ? [] : parseHostProcessIdentityRows(result.stdout);
|
|
689
|
+
} catch {
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function parseHostProcessIdentityRows(stdout) {
|
|
694
|
+
const rows = [];
|
|
695
|
+
for (const line of stdout.split("\n")) {
|
|
696
|
+
const match = /^\s*(\d+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
|
|
697
|
+
if (!match) continue;
|
|
698
|
+
const pid = Number.parseInt(match[1], 10);
|
|
699
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
700
|
+
rows.push({
|
|
701
|
+
pid,
|
|
702
|
+
state: match[2],
|
|
703
|
+
startTime: match[3]
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return rows;
|
|
707
|
+
}
|
|
708
|
+
function windowsProcessIdentityRows(rows) {
|
|
709
|
+
return rows.flatMap((row) => row.startTime === null ? [] : [{
|
|
710
|
+
pid: row.pid,
|
|
711
|
+
state: "R",
|
|
712
|
+
startTime: row.startTime
|
|
713
|
+
}]);
|
|
659
714
|
}
|
|
660
715
|
function readProcessField(pid, field) {
|
|
661
716
|
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
@@ -752,6 +807,18 @@ function readWindowsProcessRows(pids, timeoutMs) {
|
|
|
752
807
|
return [];
|
|
753
808
|
}
|
|
754
809
|
}
|
|
810
|
+
async function readWindowsProcessRowsAsync(pids, timeoutMs) {
|
|
811
|
+
try {
|
|
812
|
+
const result = await runCmd(WINDOWS_PROCESS_TOOL, windowsProcessQueryArgs(pids), {
|
|
813
|
+
allowFailure: true,
|
|
814
|
+
timeoutMs
|
|
815
|
+
});
|
|
816
|
+
if (result.exitCode !== 0) return [];
|
|
817
|
+
return parseWindowsProcessRows(result.stdout);
|
|
818
|
+
} catch {
|
|
819
|
+
return [];
|
|
820
|
+
}
|
|
821
|
+
}
|
|
755
822
|
function readWindowsProcessField(pid, field) {
|
|
756
823
|
const row = readWindowsProcessRows([pid], WINDOWS_PS_TIMEOUT_MS).find((candidate) => candidate.pid === pid);
|
|
757
824
|
if (!row) return null;
|
|
@@ -857,6 +924,163 @@ async function waitForProcessExit(pid, timeoutMs) {
|
|
|
857
924
|
}
|
|
858
925
|
return !isProcessAlive(pid);
|
|
859
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* Projects a retained verdict onto the keys a caller may publish. A caller adds its own artifact
|
|
929
|
+
* paths beside these; a layer that forwards the verdict forwards only these, so an inner surface's
|
|
930
|
+
* file paths and advice cannot ride along under an outer surface's message.
|
|
931
|
+
*/
|
|
932
|
+
function confirmedProcessStopDetails(retained, pid) {
|
|
933
|
+
return {
|
|
934
|
+
reason: retained.reason,
|
|
935
|
+
signal: "signal" in retained ? retained.signal : void 0,
|
|
936
|
+
errno: retained.reason === "signal-failed" ? retained.errno : void 0,
|
|
937
|
+
pid
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Names a pid the stop left alone. This is the one verdict that must not say the process is still
|
|
942
|
+
* running: the host refused to describe it, so nothing was ever signaled to it.
|
|
943
|
+
*/
|
|
944
|
+
function describeUnconfirmedProcessStop(pid, claimedAs) {
|
|
945
|
+
return `pid ${pid} could not be confirmed as ${claimedAs}, so no signal was sent to it`;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Whether `reason` names a retained stop verdict. Forwarding gates on this rather than on error text,
|
|
949
|
+
* so an unrelated failure's details cannot be forwarded as one.
|
|
950
|
+
*/
|
|
951
|
+
function isConfirmedProcessStopReason(value) {
|
|
952
|
+
return value === "signal-failed" || value === "exit-timeout" || value === "identity-unverified";
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* The verdict a nested layer published, or nothing when the error carries no stop verdict. A wrapper
|
|
956
|
+
* that flattens the inner message keeps the verdict in `details` and drops the inner surface's file
|
|
957
|
+
* paths and advice, which describe a file the caller of the wrapper was not asked about.
|
|
958
|
+
*/
|
|
959
|
+
function readConfirmedProcessStopDetails(details) {
|
|
960
|
+
const reason = details?.reason;
|
|
961
|
+
const pid = details?.pid;
|
|
962
|
+
if (!isConfirmedProcessStopReason(reason) || typeof pid !== "number") return void 0;
|
|
963
|
+
const signal = details?.signal;
|
|
964
|
+
const errno = details?.errno;
|
|
965
|
+
return {
|
|
966
|
+
reason,
|
|
967
|
+
signal: signal === "SIGTERM" || signal === "SIGKILL" ? signal : void 0,
|
|
968
|
+
errno: reason === "signal-failed" && typeof errno === "string" ? errno : void 0,
|
|
969
|
+
pid
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Signals one process TERM then KILL and proves which way the recorded process ended, so a caller
|
|
974
|
+
* only forgets one it watched leave. Identity settles it both ways: unreadable identity retains
|
|
975
|
+
* rather than signaling a process this caller cannot name, while a released pid, a zombie, or a
|
|
976
|
+
* start time the host contradicts proves it gone. A refusal is not absence, so each stage waits.
|
|
977
|
+
*/
|
|
978
|
+
async function stopProcessAndConfirmExit(target, budget) {
|
|
979
|
+
if (target.pid === process.pid) throw new RangeError("Cannot stop the current process");
|
|
980
|
+
const identity = await proveStopTargetIdentity(target);
|
|
981
|
+
if (identity === "gone") return { status: "stopped" };
|
|
982
|
+
if (identity === "unverified") return {
|
|
983
|
+
status: "retained",
|
|
984
|
+
reason: "identity-unverified"
|
|
985
|
+
};
|
|
986
|
+
const term = await proveSignalStage(target, "SIGTERM", budget.termTimeoutMs);
|
|
987
|
+
if (term.proof === "exited") return { status: "stopped" };
|
|
988
|
+
if (term.proof === "refused") return retainedSignalFailure("SIGTERM", term.errno);
|
|
989
|
+
const kill = await proveSignalStage(target, "SIGKILL", budget.killTimeoutMs);
|
|
990
|
+
if (kill.proof === "exited") return { status: "stopped" };
|
|
991
|
+
if (kill.proof === "refused") return retainedSignalFailure("SIGKILL", kill.errno);
|
|
992
|
+
return {
|
|
993
|
+
status: "retained",
|
|
994
|
+
reason: "exit-timeout",
|
|
995
|
+
signal: "SIGKILL"
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Whether the live pid still names the process the caller recorded, read once before anything is
|
|
1000
|
+
* signaled. `ours` needs a claim the host confirmed: two recorded facts that no readable fact
|
|
1001
|
+
* contradicts only prove the host is silent, and silence about a pid this caller may not own is
|
|
1002
|
+
* exactly what must not become permission to signal it.
|
|
1003
|
+
*/
|
|
1004
|
+
async function proveStopTargetIdentity(target) {
|
|
1005
|
+
if (!isStopPidLive(target.pid)) return "gone";
|
|
1006
|
+
const facts = await readProcessIdentityFacts(target.pid);
|
|
1007
|
+
if (facts.zombie) return "gone";
|
|
1008
|
+
if (target.identity.startTime && facts.startTime && facts.startTime !== target.identity.startTime) return "gone";
|
|
1009
|
+
if (target.identity.matches && facts.command && !target.identity.matches(facts.command)) return "gone";
|
|
1010
|
+
const confirmedStartTime = Boolean(target.identity.startTime && facts.startTime);
|
|
1011
|
+
const confirmedCommand = Boolean(target.identity.matches && facts.command);
|
|
1012
|
+
return confirmedStartTime || confirmedCommand ? "ours" : "unverified";
|
|
1013
|
+
}
|
|
1014
|
+
function retainedSignalFailure(signal, errno) {
|
|
1015
|
+
return {
|
|
1016
|
+
status: "retained",
|
|
1017
|
+
reason: "signal-failed",
|
|
1018
|
+
signal,
|
|
1019
|
+
errno
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
async function proveSignalStage(target, signal, timeoutMs) {
|
|
1023
|
+
const errno = signalProcessForStop(target.pid, signal);
|
|
1024
|
+
if (errno === "ESRCH") return { proof: "exited" };
|
|
1025
|
+
if (await waitForStopPidRelease(target.pid, timeoutMs)) return { proof: "exited" };
|
|
1026
|
+
if (await proveRecordedProcessGone(target)) return { proof: "exited" };
|
|
1027
|
+
return errno === void 0 ? { proof: "survived" } : {
|
|
1028
|
+
proof: "refused",
|
|
1029
|
+
errno
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
const STOP_LIVENESS_POLL_MS = 50;
|
|
1033
|
+
/**
|
|
1034
|
+
* Whether `kill(pid, 0)` finds the process. `isProcessAlive` reads every answer but `EPERM` as
|
|
1035
|
+
* absence, which suits a best-effort sweep and not a stop: here only `ESRCH` releases a pid, and an
|
|
1036
|
+
* answer that is not about the process at all must not become permission to forget it.
|
|
1037
|
+
*/
|
|
1038
|
+
function isStopPidLive(pid) {
|
|
1039
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1040
|
+
try {
|
|
1041
|
+
process.kill(pid, 0);
|
|
1042
|
+
return true;
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
const code = error.code;
|
|
1045
|
+
if (code === "ESRCH") return false;
|
|
1046
|
+
if (code === "EPERM") return true;
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async function waitForStopPidRelease(pid, timeoutMs) {
|
|
1051
|
+
const deadline = Date.now() + timeoutMs;
|
|
1052
|
+
for (;;) {
|
|
1053
|
+
if (!isStopPidLive(pid)) return true;
|
|
1054
|
+
if (Date.now() >= deadline) return false;
|
|
1055
|
+
await sleep(Math.min(STOP_LIVENESS_POLL_MS, Math.max(0, deadline - Date.now())));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Whether the pid outliving its budget still proves the recorded process is gone, which the answer to
|
|
1060
|
+
* `kill(pid, 0)` cannot: `EPERM` also comes back for a zombie awaiting its parent, and on Windows for
|
|
1061
|
+
* a process that exited while its object still had handles. A start time the host contradicts proves
|
|
1062
|
+
* the pid changed hands; the process table holding no row does not, because a hidden or unreadable
|
|
1063
|
+
* process is missing from it too.
|
|
1064
|
+
*/
|
|
1065
|
+
async function proveRecordedProcessGone(target) {
|
|
1066
|
+
const facts = await readProcessIdentityFacts(target.pid);
|
|
1067
|
+
if (facts.zombie) return true;
|
|
1068
|
+
const startTime = target.identity.startTime;
|
|
1069
|
+
return Boolean(startTime && facts.startTime && facts.startTime !== startTime);
|
|
1070
|
+
}
|
|
1071
|
+
/** The errno that refused the signal, or `undefined` when the host accepted it. */
|
|
1072
|
+
function signalProcessForStop(pid, signal) {
|
|
1073
|
+
if (!Number.isInteger(pid) || pid <= 0) return "ESRCH";
|
|
1074
|
+
try {
|
|
1075
|
+
process.kill(pid, signal);
|
|
1076
|
+
return;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
const code = error.code;
|
|
1079
|
+
if (typeof code === "string" && ERRNO_CODE.test(code)) return code;
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const ERRNO_CODE = /^E[A-Z0-9]+$/;
|
|
860
1084
|
async function stopPidsWithEscalation(options) {
|
|
861
1085
|
const pids = uniquePositivePids(options.pids, { excludePid: process.pid });
|
|
862
1086
|
if (pids.length === 0) return;
|
|
@@ -1309,4 +1533,4 @@ function classifyOwnerStateDirectory(stateDir) {
|
|
|
1309
1533
|
}
|
|
1310
1534
|
}
|
|
1311
1535
|
//#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 };
|
|
1536
|
+
export { classifyOwnerLiveness, classifyOwnerLivenessFromObservation, confirmedProcessStopDetails, createOwnedProcessRecordStore, describeUnconfirmedProcessStop, expandProcessTree, hostCurrentWorkingDirectory, hostEnvironment, hostNodeExecutablePath, hostNodeVersion, hostPlatform, hostProcessId, isProcessAlive, isProcessGroupAlive, isProcessPid, isProcessZombie, listHostProcesses, ownerIdentityDiffers, ownerIdentityMatches, readConfirmedProcessStopDetails, readCurrentOwnerIdentity, readHostCpuArch, readHostCpuArchSync, readHostEnvironmentVariable, readHostProcessIdentityObservations, readHostProcessIdentityObservationsAsync, readOwnedProcessRecordFile, readProcessCommand, readProcessIdentityFacts, readProcessStartTime, reapOwnedProcessRecordsAtStartup, signalPidsBestEffort, signalProcessGroupBestEffort, stopPidsWithEscalation, stopProcessAndConfirmExit, uniquePositivePids, waitForProcessExit, writeHostStderr };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-device/proxy",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.28-nightly.20261014.57",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Transport-neutral authenticating proxy in front of an agent-device daemon.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@agent-device/contracts": "0.0.0",
|
|
29
28
|
"@agent-device/host-kit": "0.0.0",
|
|
30
|
-
"@agent-device/kernel": "0.0.0"
|
|
29
|
+
"@agent-device/kernel": "0.0.0",
|
|
30
|
+
"@agent-device/contracts": "0.0.0"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsdown --config tsdown.config.ts"
|