@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.
Files changed (2) hide show
  1. package/dist/process.mjs +255 -31
  2. 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 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;
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
- 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;
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.27",
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"