@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 CHANGED
@@ -123,12 +123,18 @@ const KNOWN_APP_ERROR_CODES = [
123
123
  "REPAIR_COMMIT_FAILED",
124
124
  "UNKNOWN"
125
125
  ];
126
- var AppError = class extends Error {
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 ? createCommandCanceledError(executable, cmd, args) : createSpawnError(executable, cmd, args, err);
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 createCommandCanceledError(executable, cmd, args);
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/timeouts.ts
522
- function sleep(ms) {
523
- return new Promise((resolve) => setTimeout(resolve, ms));
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 = process.platform === "win32" ? "ps" : "/bin/ps";
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. Use this rather than the synchronous probes when the answer decides whether a
575
- * process is signaled: waiting is reversible and signaling the wrong process is not.
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("ps", [
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
- function parseHostProcessList(stdout) {
664
- const processes = [];
665
- for (const line of stdout.split("\n")) {
666
- const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
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
- processes.push({
673
- pid,
674
- ppid: Number.isInteger(ppid) && ppid > 0 ? ppid : void 0,
675
- command
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
- return processes;
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) || isProcessZombie(record.pid)) return "missing";
874
- return matchesRecord(record) ? void 0 : "ownership-lost";
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) || isProcessZombie(record.pid)) return "exited";
889
- return matchesRecord(record) ? "owned-alive" : "ownership-lost";
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 matchesRecord(record) {
892
- return readProcessStartTime(record.pid) === record.startTime && readProcessCommand(record.pid) === record.command;
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
- return classifyOwnerLivenessFromObservation(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);
1124
1263
  }
1125
1264
  function classifyOwnerLivenessFromObservation(params, observation) {
1126
- const { owner, stateDir } = params;
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
- if (observation !== void 0 ? observation?.state.startsWith("Z") : isProcessZombie(owner.pid)) return "owner-process-dead";
1130
- if (owner.startTime) {
1131
- const currentStartTime = observation !== void 0 ? observation?.startTime ?? null : readProcessStartTime(owner.pid);
1132
- if (currentStartTime !== null && currentStartTime !== owner.startTime) return "owner-process-reused";
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
@@ -1,5 +1,5 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
2
+ import fs from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
4
  //#region ../host-kit/src/internal/project-root.ts
5
5
  function resolveAgentDeviceProjectRoot(startDirectory) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-device/proxy",
3
- "version": "0.21.23",
3
+ "version": "0.21.24",
4
4
  "type": "module",
5
5
  "description": "Transport-neutral authenticating proxy in front of an agent-device daemon.",
6
6
  "license": "MIT",