@phnx-labs/agents-cli 1.22.56 → 1.22.58

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 (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +4 -4
  3. package/dist/bootstrap.js +11 -2
  4. package/dist/cli/command-registry.d.ts +0 -1
  5. package/dist/cli/command-registry.js +0 -3
  6. package/dist/commands/accounts.js +7 -3
  7. package/dist/commands/apply.js +10 -2
  8. package/dist/commands/exec.js +1 -1
  9. package/dist/commands/fork.d.ts +23 -10
  10. package/dist/commands/fork.js +115 -58
  11. package/dist/commands/hooks.js +4 -4
  12. package/dist/commands/insights.d.ts +7 -5
  13. package/dist/commands/insights.js +16 -9
  14. package/dist/commands/monitors.js +11 -0
  15. package/dist/commands/perf.d.ts +16 -7
  16. package/dist/commands/perf.js +29 -20
  17. package/dist/commands/prune.js +5 -3
  18. package/dist/commands/routines.d.ts +8 -0
  19. package/dist/commands/routines.js +57 -3
  20. package/dist/commands/rules.js +1 -1
  21. package/dist/commands/sessions-picker.d.ts +11 -0
  22. package/dist/commands/sessions-picker.js +16 -0
  23. package/dist/commands/sessions.js +1 -0
  24. package/dist/commands/share.d.ts +14 -0
  25. package/dist/commands/share.js +43 -2
  26. package/dist/commands/ssh.js +24 -14
  27. package/dist/commands/status.js +1 -1
  28. package/dist/commands/sync.js +83 -7
  29. package/dist/commands/traces.js +7 -0
  30. package/dist/commands/trash.d.ts +2 -2
  31. package/dist/commands/trash.js +2 -6
  32. package/dist/commands/versions.d.ts +2 -2
  33. package/dist/commands/versions.js +1 -10
  34. package/dist/commands/view.d.ts +2 -2
  35. package/dist/commands/view.js +7 -6
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +14 -0
  38. package/dist/lib/account-registry.d.ts +5 -1
  39. package/dist/lib/account-registry.js +47 -14
  40. package/dist/lib/accounting/capacity.d.ts +18 -7
  41. package/dist/lib/accounting/capacity.js +19 -8
  42. package/dist/lib/accounting/usage-ingest.d.ts +1 -0
  43. package/dist/lib/accounting/usage-ingest.js +75 -0
  44. package/dist/lib/accounting/usage-sync.d.ts +97 -0
  45. package/dist/lib/accounting/usage-sync.js +203 -0
  46. package/dist/lib/accounting/usage.d.ts +48 -2
  47. package/dist/lib/accounting/usage.js +79 -2
  48. package/dist/lib/agent-spec/agents.js +1 -1
  49. package/dist/lib/analytics/mix-commands.d.ts +8 -7
  50. package/dist/lib/analytics/mix-commands.js +50 -73
  51. package/dist/lib/auth-mint.d.ts +11 -1
  52. package/dist/lib/auth-mint.js +21 -6
  53. package/dist/lib/browser/ipc.d.ts +8 -0
  54. package/dist/lib/browser/ipc.js +87 -0
  55. package/dist/lib/browser/service.d.ts +19 -0
  56. package/dist/lib/browser/service.js +96 -11
  57. package/dist/lib/browser/sessions-list.js +10 -1
  58. package/dist/lib/daemon/daemon.js +5 -0
  59. package/dist/lib/daemon/runner.d.ts +3 -0
  60. package/dist/lib/daemon/runner.js +95 -53
  61. package/dist/lib/daemon/usage-sync-service.d.ts +21 -0
  62. package/dist/lib/daemon/usage-sync-service.js +42 -0
  63. package/dist/lib/daemon-services.d.ts +1 -1
  64. package/dist/lib/daemon-services.js +5 -0
  65. package/dist/lib/device-config.d.ts +17 -6
  66. package/dist/lib/device-config.js +25 -11
  67. package/dist/lib/devices/connect.d.ts +17 -8
  68. package/dist/lib/devices/connect.js +31 -14
  69. package/dist/lib/devices/pool.d.ts +4 -3
  70. package/dist/lib/devices/pool.js +13 -5
  71. package/dist/lib/doctor-diff.js +77 -7
  72. package/dist/lib/exec.d.ts +6 -41
  73. package/dist/lib/exec.js +6 -41
  74. package/dist/lib/fleet/manifest.d.ts +17 -0
  75. package/dist/lib/fleet/manifest.js +26 -0
  76. package/dist/lib/git.d.ts +13 -1
  77. package/dist/lib/git.js +36 -7
  78. package/dist/lib/harness/adapter.d.ts +7 -7
  79. package/dist/lib/harness/adapters/claude.js +3 -2
  80. package/dist/lib/hooks/install.d.ts +27 -11
  81. package/dist/lib/hooks/install.js +42 -17
  82. package/dist/lib/hosts/reconnect.d.ts +52 -203
  83. package/dist/lib/hosts/reconnect.js +64 -284
  84. package/dist/lib/hosts/remote-cmd.d.ts +9 -0
  85. package/dist/lib/hosts/remote-cmd.js +22 -0
  86. package/dist/lib/installations/migrate.d.ts +6 -120
  87. package/dist/lib/installations/migrate.js +27 -259
  88. package/dist/lib/installations/shims.d.ts +13 -95
  89. package/dist/lib/installations/shims.js +22 -139
  90. package/dist/lib/installations/store.js +1 -1
  91. package/dist/lib/installations/versions.d.ts +26 -133
  92. package/dist/lib/installations/versions.js +41 -204
  93. package/dist/lib/perf/db.d.ts +1 -1
  94. package/dist/lib/perf/db.js +1 -1
  95. package/dist/lib/plugins/skills.d.ts +8 -1
  96. package/dist/lib/plugins/skills.js +18 -2
  97. package/dist/lib/refresh.d.ts +9 -0
  98. package/dist/lib/refresh.js +3 -1
  99. package/dist/lib/routine-readiness.d.ts +15 -1
  100. package/dist/lib/routine-readiness.js +41 -0
  101. package/dist/lib/sandbox.d.ts +4 -1
  102. package/dist/lib/sandbox.js +30 -1
  103. package/dist/lib/secrets/agent.d.ts +80 -225
  104. package/dist/lib/secrets/agent.js +139 -401
  105. package/dist/lib/secrets/bundles.d.ts +73 -222
  106. package/dist/lib/secrets/bundles.js +168 -467
  107. package/dist/lib/secrets/reaper.d.ts +28 -70
  108. package/dist/lib/secrets/reaper.js +30 -85
  109. package/dist/lib/secrets/remote.d.ts +42 -129
  110. package/dist/lib/secrets/remote.js +55 -173
  111. package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
  112. package/dist/lib/self-heal/checks/install-staging.js +96 -0
  113. package/dist/lib/self-heal/registry.js +2 -0
  114. package/dist/lib/self-heal/types.d.ts +1 -1
  115. package/dist/lib/self-update.d.ts +23 -0
  116. package/dist/lib/self-update.js +50 -0
  117. package/dist/lib/session/active.d.ts +16 -32
  118. package/dist/lib/session/active.js +10 -68
  119. package/dist/lib/session/db.d.ts +24 -36
  120. package/dist/lib/session/db.js +143 -44
  121. package/dist/lib/session/discover.d.ts +6 -58
  122. package/dist/lib/session/discover.js +5 -43
  123. package/dist/lib/session/fork.d.ts +45 -26
  124. package/dist/lib/session/fork.js +32 -95
  125. package/dist/lib/session/parse.d.ts +1 -19
  126. package/dist/lib/session/parse.js +2 -15
  127. package/dist/lib/session/tool-calls.d.ts +43 -1
  128. package/dist/lib/session/tool-calls.js +74 -44
  129. package/dist/lib/session/tool-store.d.ts +33 -2
  130. package/dist/lib/session/tool-store.js +56 -3
  131. package/dist/lib/staleness/writers/sources.d.ts +5 -0
  132. package/dist/lib/staleness/writers/sources.js +2 -1
  133. package/dist/lib/startup/command-registry.d.ts +8 -2
  134. package/dist/lib/startup/command-registry.js +12 -4
  135. package/dist/lib/sync-status.d.ts +22 -0
  136. package/dist/lib/sync-status.js +27 -0
  137. package/dist/lib/sync-umbrella.d.ts +9 -0
  138. package/dist/lib/sync-umbrella.js +21 -2
  139. package/dist/lib/traces/insights.d.ts +47 -14
  140. package/dist/lib/traces/insights.js +92 -21
  141. package/dist/lib/traces/phenotype.d.ts +23 -3
  142. package/dist/lib/traces/phenotype.js +72 -24
  143. package/dist/lib/traces/sync.d.ts +15 -0
  144. package/dist/lib/traces/sync.js +104 -19
  145. package/dist/lib/traces/worker-template.js +154 -1
  146. package/package.json +1 -1
@@ -1,20 +1,14 @@
1
1
  /**
2
2
  * Reaper for orphaned / wedged keychain-helper processes.
3
3
  *
4
- * macOS keychain-helper calls are synchronous spawnSync invocations. When
5
- * coreauthd / LocalAuthentication hangs, the helper process (and sometimes its
6
- * parent `agents` process) can pile up forever. This module detects two classes
7
- * of stale process and kills them.
8
- *
9
- * The design splits cleanly into:
10
- * - a pure planner ({@link planKeychainReap}) that is unit-testable without a
11
- * real `ps` shell, and
12
- * - an impure driver ({@link reapOrphanedKeychainProcesses}) that shells `ps`
13
- * once per tick and executes kills through {@link killTree}.
4
+ * coreauthd / LocalAuthentication hangs can leave helper (and parent `agents`)
5
+ * processes pile up. The pure planner ({@link planKeychainReap}) is unit-testable
6
+ * without a real `ps`; the driver ({@link reapOrphanedKeychainProcesses}) shells
7
+ * `ps` once per tick and kills through {@link killTree}.
14
8
  */
15
- /** Elapsed grace before a helper whose parent exited is considered an orphan. */
9
+ /** Grace before a helper whose parent exited is considered an orphan. */
16
10
  export declare const ORPHAN_GRACE_SEC = 30;
17
- /** Elapsed grace before a helper with a live parent is considered stuck. */
11
+ /** Grace before a helper with a live parent is considered stuck. */
18
12
  export declare const STUCK_GRACE_SEC = 90;
19
13
  /** Snapshot of one process row from `ps`, fed into the pure planner. */
20
14
  export interface KeychainProcessSnapshot {
@@ -22,32 +16,19 @@ export interface KeychainProcessSnapshot {
22
16
  ppid: number;
23
17
  /** Elapsed seconds since the process started. */
24
18
  elapsedSec: number;
25
- /**
26
- * Stable start-time fingerprint from {@link captureProcessStartTime}.
27
- * `null` means "could not capture" — the planner must fail closed.
28
- */
19
+ /** Stable start-time fingerprint from {@link captureProcessStartTime}; null fails closed. */
29
20
  startTime: string | null;
30
- /**
31
- * True when this process is a REAP-ELIGIBLE helper invocation — the installed
32
- * helper binary running a short-lived keychain verb. False for a non-helper
33
- * process AND for the long-lived `watch-lock` watcher (see
34
- * {@link isReapableHelperCommand}).
35
- */
21
+ /** True for a reap-eligible helper invocation (short-lived keychain verb). */
36
22
  isHelper: boolean;
37
23
  /**
38
- * True when this process is the deliberately long-lived `watch-lock` watcher
39
- * (auto-lock-on-sleep). Mutually exclusive with {@link isHelper}:
40
- * {@link isReapableHelperCommand} keeps live-parent watch-locks out of the
41
- * stuck-helper path, and a SEPARATE orphan path in {@link planKeychainReap}
42
- * reaps them only once the owning daemon is provably dead (RUSH-2419).
24
+ * True for the deliberately long-lived `watch-lock` watcher
25
+ * (auto-lock-on-sleep). Mutually exclusive with {@link isHelper}; killing it
26
+ * silently disables auto-lock-on-sleep, so it is reaped only once the owning
27
+ * daemon is provably dead (RUSH-2419).
43
28
  */
44
29
  isWatchLock?: boolean;
45
30
  }
46
- /**
47
- * Tracked state for a stuck `agents` parent that has a live helper child.
48
- * Keyed by parent PID. The two-sweep debounce avoids acting on a racy `ps`
49
- * snapshot; the `stage` field drives child-first kill then parent escalation.
50
- */
31
+ /** Tracked state for a stuck parent across two-sweep debounce. */
51
32
  export interface StuckParentCandidate {
52
33
  pid: number;
53
34
  startTime: string | null;
@@ -58,7 +39,7 @@ export interface StuckParentCandidate {
58
39
  }
59
40
  /** Result of one planning pass. */
60
41
  export interface ReapPlan {
61
- /** PIDs that should be killed this tick. */
42
+ /** PIDs to kill this tick. */
62
43
  kill: number[];
63
44
  /** Candidates to carry forward to the next sweep. */
64
45
  nextCandidates: Map<number, StuckParentCandidate>;
@@ -66,51 +47,31 @@ export interface ReapPlan {
66
47
  /**
67
48
  * Pure predicate: decide which processes to kill given a `ps`-like snapshot.
68
49
  *
69
- * Mirrors the shape of {@link isExpiredPoolStray} in `lib/crabbox/lease.ts`:
70
- * a side-effect-free classifier that the impure driver shells `ps` for. Three
71
- * conservative reap classes:
50
+ * Three conservative classes:
51
+ * 1. Orphaned helper: PPID == 1, helper path, alive longer than grace.
52
+ * 2. Stuck `agents` parent: helper child alive longer than stuck grace,
53
+ * recorded on first sight, child killed on second sweep, parent on third.
54
+ * 3. Orphaned `watch-lock`: long-lived watcher whose owning daemon is gone.
72
55
  *
73
- * 1. Orphaned helper: PPID == 1, path-matches the helper, alive longer than
74
- * {@link ORPHAN_GRACE_SEC}.
75
- * 2. Stuck `agents` parent: helper child alive longer than
76
- * {@link STUCK_GRACE_SEC}. Recorded on first sight, child killed on the
77
- * second consecutive sweep with the same PID + startTime, parent killed on
78
- * the third sweep if the helper child is still present.
79
- * 3. Orphaned `watch-lock` watcher (RUSH-2419): the long-lived auto-lock
80
- * child whose owning daemon is provably dead. {@link isReapableHelperCommand}
81
- * still excludes live-parent watch-locks from class 1/2; this path only
82
- * reaps when the parent is gone from the snapshot (Unix reparents to init
83
- * after daemon death) and the watcher itself has a start-time fingerprint.
84
- *
85
- * Never reaps a process whose start time could not be captured, whose path does
86
- * not match the helper, or (for stuck parents) whose parent is no longer in the
87
- * snapshot. A watch-lock whose parent IS still in the snapshot is never touched.
56
+ * Fails closed when start time can't be captured.
88
57
  */
89
58
  export declare function planKeychainReap(snapshots: KeychainProcessSnapshot[], now: number, prevCandidates: ReadonlyMap<number, StuckParentCandidate>): ReapPlan;
90
59
  /**
91
60
  * Parse macOS `ps -o etime=` elapsed time into whole seconds.
92
61
  *
93
- * BSD `etime` renders as `[[dd-]hh:]mm:ss` (e.g. `05:03`, `01:02:03`,
94
- * `14-04:10:52`). This is the portable keyword: `etimes` (raw seconds) is a
95
- * GNU/Linux procps extension that macOS `ps` rejects with a non-zero exit, so
96
- * the reaper — which only ever runs on darwin — must read `etime`.
97
- * Returns null for an unparseable value so the caller drops the row.
62
+ * BSD `etime` renders as `[[dd-]hh:]mm:ss`. `etimes` (raw seconds) is a GNU
63
+ * extension that macOS `ps` rejects, so darwin uses `etime`.
98
64
  */
99
65
  export declare function parseEtimeToSeconds(raw: string): number | null;
100
66
  /**
101
- * Whether a `ps` command line is a REAP-ELIGIBLE helper invocation: the installed
102
- * helper binary running a short-lived keychain verb (get/has/list/set/delete/
103
- * migrate-*) that a wedged `coreauthd` can hang. Returns false for a non-helper
104
- * command AND for the deliberately long-lived `watch-lock` watcher — matching by
105
- * the full argv (`ps … command=`), so a live-parent `watch-lock` child is never
106
- * mistaken for a stuck read and killed. Pure; unit-tested.
67
+ * Whether a `ps` command line is a reap-eligible helper invocation: the installed
68
+ * helper binary running a short-lived keychain verb. Returns false for the
69
+ * long-lived `watch-lock` watcher, which must never be mistaken for a stuck read.
107
70
  */
108
71
  export declare function isReapableHelperCommand(command: string, helperPath: string): boolean;
109
72
  /**
110
73
  * Whether a `ps` command line is the deliberately long-lived `watch-lock`
111
- * watcher (auto-lock-on-sleep). Inverse of {@link isReapableHelperCommand} for
112
- * the watch-lock verb only — used by the orphaned-watch-lock reaper path
113
- * (RUSH-2419). Pure; unit-tested.
74
+ * watcher. Inverse of {@link isReapableHelperCommand} for the watch-lock verb.
114
75
  */
115
76
  export declare function isWatchLockHelperCommand(command: string, helperPath: string): boolean;
116
77
  /** Test seam: reset the persisted candidate state. */
@@ -119,11 +80,8 @@ export declare function resetKeychainReaperCandidatesForTest(): void;
119
80
  * Impure driver: snapshot all processes once with `ps`, plan the reap, then
120
81
  * execute kills through {@link killTree}.
121
82
  *
122
- * Path-matching uses the full helper path (proc_pidpath-style), not just the
123
- * executable name, so an unrelated binary named "Agents CLI" is never targeted.
124
- *
125
- * Returns on non-darwin platforms without shelling anything — the helper only
126
- * exists on macOS.
83
+ * Path-matches the full helper path so an unrelated binary named "Agents CLI" is
84
+ * never targeted. Returns on non-darwin without shelling anything.
127
85
  */
128
86
  export declare function reapOrphanedKeychainProcesses(): {
129
87
  reaped: number;
@@ -1,46 +1,28 @@
1
1
  /**
2
2
  * Reaper for orphaned / wedged keychain-helper processes.
3
3
  *
4
- * macOS keychain-helper calls are synchronous spawnSync invocations. When
5
- * coreauthd / LocalAuthentication hangs, the helper process (and sometimes its
6
- * parent `agents` process) can pile up forever. This module detects two classes
7
- * of stale process and kills them.
8
- *
9
- * The design splits cleanly into:
10
- * - a pure planner ({@link planKeychainReap}) that is unit-testable without a
11
- * real `ps` shell, and
12
- * - an impure driver ({@link reapOrphanedKeychainProcesses}) that shells `ps`
13
- * once per tick and executes kills through {@link killTree}.
4
+ * coreauthd / LocalAuthentication hangs can leave helper (and parent `agents`)
5
+ * processes pile up. The pure planner ({@link planKeychainReap}) is unit-testable
6
+ * without a real `ps`; the driver ({@link reapOrphanedKeychainProcesses}) shells
7
+ * `ps` once per tick and kills through {@link killTree}.
14
8
  */
15
9
  import { execFileSync } from 'child_process';
16
10
  import { killTree, captureProcessStartTime } from '../platform/process.js';
17
11
  import { getKeychainHelperPath } from './install-helper.js';
18
- /** Elapsed grace before a helper whose parent exited is considered an orphan. */
12
+ /** Grace before a helper whose parent exited is considered an orphan. */
19
13
  export const ORPHAN_GRACE_SEC = 30;
20
- /** Elapsed grace before a helper with a live parent is considered stuck. */
14
+ /** Grace before a helper with a live parent is considered stuck. */
21
15
  export const STUCK_GRACE_SEC = 90;
22
16
  /**
23
17
  * Pure predicate: decide which processes to kill given a `ps`-like snapshot.
24
18
  *
25
- * Mirrors the shape of {@link isExpiredPoolStray} in `lib/crabbox/lease.ts`:
26
- * a side-effect-free classifier that the impure driver shells `ps` for. Three
27
- * conservative reap classes:
28
- *
29
- * 1. Orphaned helper: PPID == 1, path-matches the helper, alive longer than
30
- * {@link ORPHAN_GRACE_SEC}.
31
- * 2. Stuck `agents` parent: helper child alive longer than
32
- * {@link STUCK_GRACE_SEC}. Recorded on first sight, child killed on the
33
- * second consecutive sweep with the same PID + startTime, parent killed on
34
- * the third sweep if the helper child is still present.
35
- * 3. Orphaned `watch-lock` watcher (RUSH-2419): the long-lived auto-lock
36
- * child whose owning daemon is provably dead. {@link isReapableHelperCommand}
37
- * still excludes live-parent watch-locks from class 1/2; this path only
38
- * reaps when the parent is gone from the snapshot (Unix reparents to init
39
- * after daemon death) and the watcher itself has a start-time fingerprint.
19
+ * Three conservative classes:
20
+ * 1. Orphaned helper: PPID == 1, helper path, alive longer than grace.
21
+ * 2. Stuck `agents` parent: helper child alive longer than stuck grace,
22
+ * recorded on first sight, child killed on second sweep, parent on third.
23
+ * 3. Orphaned `watch-lock`: long-lived watcher whose owning daemon is gone.
40
24
  *
41
- * Never reaps a process whose start time could not be captured, whose path does
42
- * not match the helper, or (for stuck parents) whose parent is no longer in the
43
- * snapshot. A watch-lock whose parent IS still in the snapshot is never touched.
25
+ * Fails closed when start time can't be captured.
44
26
  */
45
27
  export function planKeychainReap(snapshots, now, prevCandidates) {
46
28
  const pidMap = new Map(snapshots.map((s) => [s.pid, s]));
@@ -50,8 +32,7 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
50
32
  if (!s.isHelper)
51
33
  continue;
52
34
  if (s.ppid === 1) {
53
- // Orphaned helper: parent is init/launchd. Fail closed if we can't prove
54
- // the process identity with a start-time fingerprint.
35
+ // Orphaned helper: fail closed without a start-time fingerprint.
55
36
  if (s.elapsedSec <= ORPHAN_GRACE_SEC)
56
37
  continue;
57
38
  if (s.startTime == null)
@@ -59,8 +40,7 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
59
40
  kill.push(s.pid);
60
41
  continue;
61
42
  }
62
- // Stuck agents: a helper with a live parent that has been wedged for longer
63
- // than the interactive timeout. The parent must still exist in the snapshot.
43
+ // Stuck agents: helper with a live parent wedged longer than the timeout.
64
44
  if (s.elapsedSec <= STUCK_GRACE_SEC)
65
45
  continue;
66
46
  const parent = pidMap.get(s.ppid);
@@ -74,13 +54,12 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
74
54
  prev.helperStartTime === s.startTime &&
75
55
  prev.startTime === parent.startTime) {
76
56
  if (prev.stage === 'watch') {
77
- // Second sweep: kill the child first so the parent's spawnSync returns.
78
- // Keep the parent candidate at stage 'escalate' for the next sweep.
57
+ // Second sweep: kill the child first so spawnSync returns; escalate next.
79
58
  kill.push(s.pid);
80
59
  nextCandidates.set(parent.pid, { ...prev, stage: 'escalate' });
81
60
  continue;
82
61
  }
83
- // Third sweep: child did not free the parent, so the parent itself is wedged.
62
+ // Third sweep: child did not free the parent, so the parent is wedged.
84
63
  kill.push(parent.pid);
85
64
  continue;
86
65
  }
@@ -94,32 +73,21 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
94
73
  stage: 'watch',
95
74
  });
96
75
  }
97
- // Separate path for orphaned watch-lock watchers (RUSH-2419). Does NOT
98
- // weaken {@link isReapableHelperCommand}: a watch-lock with a live parent
99
- // stays isHelper=false and is skipped above. Here we only kill when the
100
- // owning daemon is provably absent from the process table.
76
+ // Separate path for orphaned watch-lock watchers: only reap when the owning
77
+ // daemon is provably absent from the process table.
101
78
  for (const s of snapshots) {
102
79
  if (!s.isWatchLock)
103
80
  continue;
104
- // Fail closed: no start-time fingerprint → refuse to kill (pid-reuse guard
105
- // for the process we are about to target, same as orphan helpers).
106
81
  if (s.startTime == null)
107
82
  continue;
108
83
  if (s.elapsedSec <= ORPHAN_GRACE_SEC)
109
84
  continue;
110
- // Live parent in this snapshot → owning daemon is still up. Leave it alone
111
- // (this is the property isReapableHelperCommand protects for stuck-helper
112
- // reaping; re-assert it here so a mis-tagged row cannot be killed either).
113
85
  if (s.ppid !== 1) {
114
86
  const parent = pidMap.get(s.ppid);
115
87
  if (parent)
116
88
  continue;
117
- // Parent pid not listed: the process occupying that slot is gone. On Unix
118
- // a dead parent reparents the child to init; a missing parent with a
119
- // non-1 ppid is the race window before reparenting. Either way the owner
120
- // is dead — safe to reap after grace + fingerprint.
89
+ // Parent missing means the owner is dead (or reparenting race); safe to reap.
121
90
  }
122
- // ppid===1 (reparented to init/launchd) or parent missing → orphaned.
123
91
  kill.push(s.pid);
124
92
  }
125
93
  return { kill, nextCandidates };
@@ -127,11 +95,8 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
127
95
  /**
128
96
  * Parse macOS `ps -o etime=` elapsed time into whole seconds.
129
97
  *
130
- * BSD `etime` renders as `[[dd-]hh:]mm:ss` (e.g. `05:03`, `01:02:03`,
131
- * `14-04:10:52`). This is the portable keyword: `etimes` (raw seconds) is a
132
- * GNU/Linux procps extension that macOS `ps` rejects with a non-zero exit, so
133
- * the reaper — which only ever runs on darwin — must read `etime`.
134
- * Returns null for an unparseable value so the caller drops the row.
98
+ * BSD `etime` renders as `[[dd-]hh:]mm:ss`. `etimes` (raw seconds) is a GNU
99
+ * extension that macOS `ps` rejects, so darwin uses `etime`.
135
100
  */
136
101
  export function parseEtimeToSeconds(raw) {
137
102
  const m = raw.match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
@@ -150,8 +115,6 @@ export function parseEtimeToSeconds(raw) {
150
115
  *
151
116
  * Expected format from `ps -ax -o pid=,ppid=,etime=,command=`:
152
117
  * "<pid> <ppid> <etime> <command...>"
153
- * where `<etime>` is BSD elapsed time (`[[dd-]hh:]mm:ss`). The command field is
154
- * the remainder of the line and may contain spaces.
155
118
  */
156
119
  function parsePsLine(line) {
157
120
  const trimmed = line.trim();
@@ -168,22 +131,12 @@ function parsePsLine(line) {
168
131
  return null;
169
132
  return { pid, ppid, elapsedSec, command };
170
133
  }
171
- /**
172
- * The one helper verb that is DELIBERATELY long-lived: the broker's auto-lock
173
- * sleep/lock watcher (`spawn(getKeychainHelperPath(), ['watch-lock'], …)` in
174
- * `agent.ts`). It lives for the broker's whole hold — potentially days — as a
175
- * healthy child of the live broker/daemon, emitting LOCK/SLEEP lines that wipe
176
- * the in-memory secret store on sleep. It is NOT a stuck keychain read, so the
177
- * reaper must never target it: killing it silently disables auto-lock-on-sleep.
178
- */
134
+ /** The one helper verb that is deliberately long-lived: the auto-lock watcher. */
179
135
  const HELPER_WATCH_LOCK_VERB = 'watch-lock';
180
136
  /**
181
- * Whether a `ps` command line is a REAP-ELIGIBLE helper invocation: the installed
182
- * helper binary running a short-lived keychain verb (get/has/list/set/delete/
183
- * migrate-*) that a wedged `coreauthd` can hang. Returns false for a non-helper
184
- * command AND for the deliberately long-lived `watch-lock` watcher — matching by
185
- * the full argv (`ps … command=`), so a live-parent `watch-lock` child is never
186
- * mistaken for a stuck read and killed. Pure; unit-tested.
137
+ * Whether a `ps` command line is a reap-eligible helper invocation: the installed
138
+ * helper binary running a short-lived keychain verb. Returns false for the
139
+ * long-lived `watch-lock` watcher, which must never be mistaken for a stuck read.
187
140
  */
188
141
  export function isReapableHelperCommand(command, helperPath) {
189
142
  if (command === helperPath)
@@ -195,9 +148,7 @@ export function isReapableHelperCommand(command, helperPath) {
195
148
  }
196
149
  /**
197
150
  * Whether a `ps` command line is the deliberately long-lived `watch-lock`
198
- * watcher (auto-lock-on-sleep). Inverse of {@link isReapableHelperCommand} for
199
- * the watch-lock verb only — used by the orphaned-watch-lock reaper path
200
- * (RUSH-2419). Pure; unit-tested.
151
+ * watcher. Inverse of {@link isReapableHelperCommand} for the watch-lock verb.
201
152
  */
202
153
  export function isWatchLockHelperCommand(command, helperPath) {
203
154
  if (!command.startsWith(`${helperPath} `))
@@ -215,11 +166,8 @@ export function resetKeychainReaperCandidatesForTest() {
215
166
  * Impure driver: snapshot all processes once with `ps`, plan the reap, then
216
167
  * execute kills through {@link killTree}.
217
168
  *
218
- * Path-matching uses the full helper path (proc_pidpath-style), not just the
219
- * executable name, so an unrelated binary named "Agents CLI" is never targeted.
220
- *
221
- * Returns on non-darwin platforms without shelling anything — the helper only
222
- * exists on macOS.
169
+ * Path-matches the full helper path so an unrelated binary named "Agents CLI" is
170
+ * never targeted. Returns on non-darwin without shelling anything.
223
171
  */
224
172
  export function reapOrphanedKeychainProcesses() {
225
173
  const details = [];
@@ -249,11 +197,8 @@ export function reapOrphanedKeychainProcesses() {
249
197
  if (!parsed)
250
198
  continue;
251
199
  const { pid, ppid, elapsedSec, command } = parsed;
252
- // Reap-eligible = the helper binary running a short-lived keychain verb. The
253
- // full-argv match excludes the deliberately long-lived `watch-lock` watcher,
254
- // whose live-parent child would otherwise be killed as if it were stuck
255
- // (RUSH-2232 — that silently disabled auto-lock-on-sleep). Orphaned
256
- // watch-locks are tracked separately via isWatchLock (RUSH-2419).
200
+ // Match the full argv so the long-lived `watch-lock` watcher is never
201
+ // mistaken for a stuck read. Orphaned watch-locks are tracked separately.
257
202
  const isHelper = isReapableHelperCommand(command, helperPath);
258
203
  const isWatchLock = isWatchLockHelperCommand(command, helperPath);
259
204
  rows.push({ pid, ppid, elapsedSec, isHelper, isWatchLock, startTime: null });
@@ -1,37 +1,14 @@
1
1
  /**
2
2
  * Remote secrets — read and use `agents secrets` bundles that live on another
3
- * host, over the same hardened SSH path that `agents secrets export --device`
4
- * (the write inverse) already uses.
3
+ * host over the same SSH path that `agents secrets export --device` uses.
5
4
  *
6
- * This is the READ / USE direction:
7
- * - browse: drive the remote `agents secrets list|view` and stream its
8
- * stdout back verbatim (lossless, no parsing).
9
- * - use: resolve a remote bundle to an env map (JSON over ssh stdout) and
10
- * inject it ephemerally — never written to this machine's keychain.
11
- *
12
- * Trust model: relies on the operator's existing SSH access to the host (same
13
- * boundary as `export --device` / `run --device`). Bundle names are shell-quoted
14
- * into the remote command; resolved VALUES return over ssh stdout. File-backend
15
- * import never forwards AGENTS_SECRETS_PASSPHRASE (PHNX-2371). Nothing is
16
- * persisted locally.
5
+ * Browse streams remote stdout verbatim; use resolves a bundle to an env map
6
+ * and injects it ephemerally without persisting it locally.
17
7
  */
18
8
  import { type SshExecResult } from '../ssh-exec.js';
19
9
  /**
20
- * SSH options for a SECRET-carrying transport (RUSH-2527). Every `agents secrets
21
- * --host` operation moves credential bytes — over ssh stdin (push) or ssh stdout
22
- * (resolve/read-back) — so it MUST NOT ride the shared `accept-new` baseline
23
- * against the user's own `~/.ssh/known_hosts` with a reusable control socket.
24
- * Two hardenings over that baseline, matching the posture the `--copy-creds`
25
- * dispatch already uses (`hosts/dispatch.ts` -> `hostKeyCheckingOpts`):
26
- *
27
- * - **Managed pinned host keys.** Verify against the CLI-owned known_hosts
28
- * store (`known-hosts.ts`), not `~/.ssh/known_hosts`. A CHANGED key on a
29
- * known host is refused (`StrictHostKeyChecking` `yes` once pinned,
30
- * `accept-new` before that). Callers that copy durable provider credentials
31
- * must separately require an existing pin before invoking this transport.
32
- * - **No multiplex reuse.** `multiplex: false` — a credential channel never
33
- * leaves a persistent `ControlMaster` socket lingering (60s `ControlPersist`)
34
- * that any other `agents` invocation to that host would silently reuse.
10
+ * SSH options for secret-carrying transport. Pins the managed host key and
11
+ * disables multiplex so no reusable control socket lingers for other invocations.
35
12
  */
36
13
  export declare function credentialTransportSshOpts(target: string): {
37
14
  hostKeyOpts: string[];
@@ -40,37 +17,21 @@ export declare function credentialTransportSshOpts(target: string): {
40
17
  /** Refuse durable credential transfer until the destination SSH key is pinned. */
41
18
  export declare function assertCredentialTransportHostPinned(target: string, pinned?: boolean): void;
42
19
  /**
43
- * Trust boundary for a remote-resolved env map. A peer's `secrets export` output
44
- * is untrusted input: a compromised or misconfigured host could return keys that
45
- * silently reshape THIS process's behavior once merged into the agent env
46
- * (bundles.ts:251 `sanitizeProcessEnv` only strips loader vars from process.env,
47
- * never the remote bundle). Block the dangerous-override classes here — at the
48
- * source — so every consumer (`run --secrets b@host`, `secrets exec --host`) is
49
- * protected, not just one call site:
50
- * - LD_* / DYLD_* / NODE_OPTIONS and the other loader/interpreter injections
51
- * (reuses the canonical bundles.ts predicate);
52
- * - GIT_* — GIT_SSH_COMMAND et al. hijack every git subprocess;
53
- * - *_PROXY — HTTP(S)_PROXY / ALL_PROXY reroute outbound traffic (MITM);
54
- * - *_BASE_URL — ANTHROPIC_BASE_URL / OPENAI_BASE_URL redirect the model API.
55
- * These keys are already rejected on the ADD side (validateEnvKey for loaders),
56
- * so a legitimate bundle never carries them — only a hostile peer would.
20
+ * Trust boundary for a remote-resolved env map. A peer's export output is
21
+ * untrusted input that could reshape this process once merged into the env, so
22
+ * block dangerous override classes here for every consumer:
23
+ * loader/interpreter vars, GIT_*, *_PROXY, and *_BASE_URL.
57
24
  */
58
25
  export declare function isDangerousRemoteEnvKey(name: string): boolean;
59
26
  /**
60
- * Resolve a `--device` value to an ssh target STRING for the remote-secrets path.
61
- * Delegates to the single host/device resolver (`resolveHost`, RUSH-1967) so a
62
- * name here dials the exact same box `run --device` does; on a miss, treats the
63
- * value as a raw ssh target and validates it against injection. Named distinctly
64
- * from `../devices/resolve-target.ts` (which returns richer shapes) so importing
65
- * the wrong one can't silently change which machine you dial.
27
+ * Resolve a `--device` value to an ssh target string for the remote-secrets
28
+ * path. Delegates to the same resolver `run --device` uses; on a miss, treats
29
+ * the value as a raw ssh target and validates it.
66
30
  */
67
31
  export declare function resolveHostSshTarget(nameOrAlias: string): Promise<string>;
68
32
  /**
69
- * Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
70
- * aliases) into an ordered, de-duplicated list. All four flags compose; any alone
71
- * works. `--device`/`--devices` resolve identically to `--device`/`--hosts` so the
72
- * fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
73
- * works on the secrets remote commands too. Empty when none is set.
33
+ * Merge `--host` / `--hosts` (and their `--device` / `--devices` aliases) into
34
+ * an ordered, de-duplicated list. Empty when none is set.
74
35
  */
75
36
  export declare function parseHostsOption(opts: {
76
37
  host?: string;
@@ -79,11 +40,9 @@ export declare function parseHostsOption(opts: {
79
40
  devices?: string;
80
41
  }): string[];
81
42
  /**
82
- * Split a `bundle@host` reference. No `@` → a local bundle (host undefined).
83
- * Bundle names can't contain `@` (BUNDLE_NAME_PATTERN), so the FIRST `@`
84
- * separates the bundle from the ssh target — and the target itself may be a
85
- * `user@host` (e.g. `r2.backups@muqsit@box` → bundle `r2.backups`, host
86
- * `muqsit@box`).
43
+ * Split a `bundle@host` reference. No `@` → a local bundle. Bundle names can't
44
+ * contain `@`, so the FIRST `@` separates bundle from ssh target; the target
45
+ * itself may be `user@host` (e.g. `r2.backups@muqsit@box`).
87
46
  */
88
47
  export declare function splitBundleRef(ref: string): {
89
48
  bundle: string;
@@ -91,9 +50,8 @@ export declare function splitBundleRef(ref: string): {
91
50
  };
92
51
  /**
93
52
  * Run `agents secrets <args>` on a remote host over ssh and return the raw
94
- * result. Used by the browse commands — the remote's human-readable stdout is
95
- * streamed back unchanged. `tty` forces an interactive ssh session (`-tt`) so a
96
- * remote Touch-ID / passphrase prompt can surface (e.g. `view --reveal`).
53
+ * result. Used by browse commands; `tty` forces `-tt` so remote passphrase
54
+ * prompts can surface.
97
55
  */
98
56
  export declare function remoteSecretsRaw(target: string, args: string[], opts?: {
99
57
  tty?: boolean;
@@ -102,17 +60,9 @@ export declare function remoteSecretsRaw(target: string, args: string[], opts?:
102
60
  secret?: boolean;
103
61
  }): SshExecResult;
104
62
  /**
105
- * Run a remote `agents secrets <args>` FOREGROUND, with the local stdio wired
106
- * straight through (`stdio: 'inherit'` + `-tt`), and return its exit code.
107
- *
108
- * Unlike `remoteSecretsRaw` — which pipes stdin, so even with `-tt` the remote
109
- * process's `process.stdin.isTTY` is false and a passphrase prompt refuses to
110
- * appear (the macOS file-store guard then hard-errors "needs
111
- * AGENTS_SECRETS_PASSPHRASE") — this inherits the caller's real terminal, so the
112
- * remote sees a genuine TTY and its hidden passphrase prompt surfaces and reads
113
- * the keystrokes. This is the transport for `unlock --device`: you type the remote
114
- * bundle's passphrase at your own terminal. Output is NOT captured (it streams
115
- * to the terminal); only the exit code is returned.
63
+ * Run a remote `agents secrets <args>` foreground with local stdio inherited,
64
+ * so the remote sees a real TTY and its passphrase prompt surfaces and reads
65
+ * keystrokes. Output streams to the terminal; only the exit code is returned.
116
66
  */
117
67
  export declare function remoteSecretsStream(target: string, args: string[], opts?: {
118
68
  osLookupName?: string;
@@ -120,31 +70,20 @@ export declare function remoteSecretsStream(target: string, args: string[], opts
120
70
  /**
121
71
  * Resolve a remote bundle to a plaintext env map by driving the remote's
122
72
  * `agents secrets export <bundle> --plaintext --format json`. Values cross over
123
- * ssh stdout (encrypted in transit), parsed in memory, never persisted.
73
+ * ssh stdout, parsed in memory, never persisted.
124
74
  *
125
- * The remote unlocks the bundle with ITS OWN credentials — the owner host's
126
- * keychain/secrets-agent, or its own `AGENTS_SECRETS_PASSPHRASE` (in the login
127
- * env) for a file-backed bundle. We deliberately do NOT forward this machine's
128
- * passphrase: the remote bundle is encrypted with the remote's passphrase, so
129
- * overriding it would break the read. (A macOS remote under non-interactive
130
- * SSH will block on Touch-ID — use `view`/`exec` with a remote `file` bundle,
131
- * an already-unlocked remote secrets-agent, or an interactive `-tt` session.)
75
+ * The remote unlocks the bundle with its own credentials; this machine does NOT
76
+ * forward its passphrase because the remote bundle is encrypted with the
77
+ * remote's passphrase.
132
78
  */
133
79
  export declare function remoteResolveEnv(target: string, bundle: string, opts?: {
134
80
  osLookupName?: string;
135
81
  }): Promise<Record<string, string>>;
136
82
  /**
137
- * Outcome of a post-push read-back verification (see verifyRemoteKeychainPush).
138
- * - `ok` — the pushed keys materialized readably on the remote.
139
- * - `locked-keychain` — the read-back gave the SPECIFIC signal of a keychain
140
- * that didn't persist (the remote's headless "not unlocked
141
- * in the secrets agent" guard, or a "stored item … not
142
- * found" on read-back, or pushed keys simply absent). Only
143
- * this verdict earns the locked-login-keychain diagnosis +
144
- * `--remote-backend file` steer.
145
- * - `error` — a DIFFERENT failure (flaky SSH, timeout, unparseable
146
- * payload). The raw error is re-surfaced verbatim, never
147
- * mislabeled as a locked keychain.
83
+ * Outcome of a post-push read-back verification.
84
+ * - `ok` — the pushed keys materialized readably.
85
+ * - `locked-keychain` — read-back gave the specific locked-keychain signal.
86
+ * - `error` — a different failure (SSH, timeout, etc.).
148
87
  */
149
88
  export type RemoteKeychainWriteVerification = {
150
89
  ok: true;
@@ -158,24 +97,10 @@ export type RemoteKeychainWriteVerification = {
158
97
  reason: string;
159
98
  };
160
99
  /**
161
- * Decide whether a keychain-backed push to a remote actually PERSISTED its secret
162
- * value items, given the read-back of that bundle from the remote's own store.
163
- *
164
- * The silent-failure this guards: pushing `--remote-backend keychain` (default) to
165
- * a macOS host over headless SSH lands the bundle METADATA but not readable value
166
- * items — the remote login keychain is locked in the non-interactive SSH context,
167
- * so Security accepts the item WRITE at the DB level but the biometry-ACL'd item is
168
- * unreadable, and the remote `import` still reports success (values written first,
169
- * metadata `noAcl` last — bundles.ts writeBundleWithItems). The metadata-only bundle
170
- * then fails every later read with the confusing `Bundle '<b>' key '<k>': stored
171
- * item '<item>' not found` (bundles.ts resolveBundleEnv). We catch it by reading
172
- * the bundle back the same way a later `secrets exec`/resolve will (the marker-gated
173
- * json transport, driven headlessly on the remote so its `agentOnly` guard FAILS FAST
174
- * before any keychain read — no Touch ID prompt) and confirming every pushed key
175
- * returned.
176
- *
177
- * Pure so both branches are unit-testable without a real locked keychain: inject the
178
- * "read-back failed / key absent" condition through `readBack`.
100
+ * Decide whether a keychain-backed push to a remote actually persisted its
101
+ * secret value items by reading the bundle back the same way a later resolve
102
+ * will. Catches the silent failure where headless SSH writes metadata but not
103
+ * readable value items to a locked macOS login keychain.
179
104
  */
180
105
  export declare function evaluateKeychainWriteVerification(pushedKeys: string[], readBack: {
181
106
  ok: true;
@@ -185,38 +110,26 @@ export declare function evaluateKeychainWriteVerification(pushedKeys: string[],
185
110
  stderr: string;
186
111
  }): RemoteKeychainWriteVerification;
187
112
  /**
188
- * The actionable error message for a failed keychain-over-SSH push verification.
189
- * Names the cause (locked remote login keychain) and steers to the two real fixes:
190
- * re-run with the headless-readable file backend, or unlock the remote keychain.
191
- * Pure + exported so the exact guidance is asserted in tests.
113
+ * Actionable error message for a failed keychain-over-SSH push. Names the cause
114
+ * (locked remote login keychain) and steers to the two real fixes.
192
115
  */
193
116
  export declare function keychainWriteFailureMessage(host: string, bundle: string, reason: string): string;
194
117
  /**
195
- * Read a bundle back from a remote over SSH (headlessly, so it fails fast rather
196
- * than prompting Touch ID) and confirm the pushed keys materialized. Drives the
197
- * remote's marker-gated json transport (`secrets export <bundle> --plaintext
198
- * --format json` under AGENTS_SECRETS_REMOTE_TRANSPORT) but keeps only the KEY
199
- * NAMES; the plaintext values are dropped immediately and never retained or
200
- * logged. Returns a verification verdict; the caller renders
201
- * `keychainWriteFailureMessage` on failure.
118
+ * Read a bundle back from a remote over SSH and confirm the pushed keys
119
+ * materialized. Drives the marker-gated json transport but keeps only KEY NAMES;
120
+ * plaintext values are discarded immediately. Returns a verification verdict.
202
121
  */
203
122
  export declare function verifyRemoteKeychainPush(target: string, bundle: string, pushedKeys: string[], opts?: {
204
123
  osLookupName?: string;
205
124
  secret?: boolean;
206
125
  }): RemoteKeychainWriteVerification;
207
126
  /**
208
- * The `bash -lc` command + stdin payload that drives a **file-backed** remote
209
- * import for `secrets export --device … --remote-backend file`.
127
+ * `bash -lc` command + stdin payload for a file-backed remote import.
210
128
  *
211
- * The file store is passphrase-free: the remote `agents secrets import --backend
212
- * file` auto-provisions the remote's own machine-local key (0600 under
213
- * `~/.agents/.secrets-key/`), so its reads are HEADLESS — no passphrase, no
214
- * Touch ID. AGENTS_SECRETS_PASSPHRASE is a deprecated override and MUST NOT be
129
+ * The file store is passphrase-free: the remote auto-provisions its own machine-
130
+ * local key, so reads are headless. AGENTS_SECRETS_PASSPHRASE must NOT be
215
131
  * forwarded (PHNX-2371): a remote keyed to a secret its daemon does not hold
216
132
  * reports "Imported N key(s)" then fails every later decrypt.
217
- *
218
- * Pure — no I/O — so the exact command string and stdin ordering are unit-testable
219
- * against the SSH boundary the same way `remoteSecretsRaw` is.
220
133
  */
221
134
  export declare function buildRemoteFileImportCommand(bundle: string, dotenv: string, opts?: {
222
135
  force?: boolean;