@phnx-labs/agents-cli 1.22.57 → 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 (101) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/bootstrap.js +8 -1
  3. package/dist/commands/accounts.js +7 -3
  4. package/dist/commands/apply.js +10 -2
  5. package/dist/commands/fork.d.ts +23 -10
  6. package/dist/commands/fork.js +115 -58
  7. package/dist/commands/monitors.js +11 -0
  8. package/dist/commands/prune.js +5 -3
  9. package/dist/commands/routines.d.ts +8 -0
  10. package/dist/commands/routines.js +57 -3
  11. package/dist/commands/sessions-picker.d.ts +11 -0
  12. package/dist/commands/sessions-picker.js +16 -0
  13. package/dist/commands/sessions.js +1 -0
  14. package/dist/commands/share.d.ts +14 -0
  15. package/dist/commands/share.js +43 -2
  16. package/dist/commands/status.js +1 -1
  17. package/dist/commands/sync.js +83 -7
  18. package/dist/commands/traces.js +7 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +6 -1
  21. package/dist/lib/account-registry.d.ts +5 -1
  22. package/dist/lib/account-registry.js +47 -14
  23. package/dist/lib/accounting/capacity.d.ts +18 -7
  24. package/dist/lib/accounting/capacity.js +19 -8
  25. package/dist/lib/accounting/usage-sync.d.ts +29 -1
  26. package/dist/lib/accounting/usage-sync.js +76 -2
  27. package/dist/lib/accounting/usage.js +7 -1
  28. package/dist/lib/auth-mint.d.ts +11 -1
  29. package/dist/lib/auth-mint.js +21 -6
  30. package/dist/lib/browser/ipc.d.ts +8 -0
  31. package/dist/lib/browser/ipc.js +87 -0
  32. package/dist/lib/browser/service.d.ts +19 -0
  33. package/dist/lib/browser/service.js +96 -11
  34. package/dist/lib/browser/sessions-list.js +10 -1
  35. package/dist/lib/daemon/runner.d.ts +3 -0
  36. package/dist/lib/daemon/runner.js +86 -45
  37. package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
  38. package/dist/lib/daemon/usage-sync-service.js +14 -8
  39. package/dist/lib/daemon-services.js +1 -1
  40. package/dist/lib/devices/connect.d.ts +17 -8
  41. package/dist/lib/devices/connect.js +31 -14
  42. package/dist/lib/doctor-diff.js +77 -7
  43. package/dist/lib/fleet/manifest.d.ts +17 -0
  44. package/dist/lib/fleet/manifest.js +26 -0
  45. package/dist/lib/hooks/install.d.ts +27 -11
  46. package/dist/lib/hooks/install.js +42 -17
  47. package/dist/lib/hosts/reconnect.d.ts +52 -203
  48. package/dist/lib/hosts/reconnect.js +64 -284
  49. package/dist/lib/installations/migrate.d.ts +6 -120
  50. package/dist/lib/installations/migrate.js +27 -259
  51. package/dist/lib/installations/shims.d.ts +13 -95
  52. package/dist/lib/installations/shims.js +22 -139
  53. package/dist/lib/installations/store.js +1 -1
  54. package/dist/lib/installations/versions.d.ts +26 -133
  55. package/dist/lib/installations/versions.js +41 -204
  56. package/dist/lib/plugins/skills.d.ts +8 -1
  57. package/dist/lib/plugins/skills.js +18 -2
  58. package/dist/lib/refresh.d.ts +9 -0
  59. package/dist/lib/refresh.js +3 -1
  60. package/dist/lib/routine-readiness.d.ts +15 -1
  61. package/dist/lib/routine-readiness.js +41 -0
  62. package/dist/lib/sandbox.d.ts +4 -1
  63. package/dist/lib/sandbox.js +30 -1
  64. package/dist/lib/secrets/agent.d.ts +80 -225
  65. package/dist/lib/secrets/agent.js +139 -401
  66. package/dist/lib/secrets/bundles.d.ts +73 -222
  67. package/dist/lib/secrets/bundles.js +168 -467
  68. package/dist/lib/secrets/reaper.d.ts +28 -70
  69. package/dist/lib/secrets/reaper.js +30 -85
  70. package/dist/lib/secrets/remote.d.ts +42 -129
  71. package/dist/lib/secrets/remote.js +55 -173
  72. package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
  73. package/dist/lib/self-heal/checks/install-staging.js +96 -0
  74. package/dist/lib/self-heal/registry.js +2 -0
  75. package/dist/lib/self-heal/types.d.ts +1 -1
  76. package/dist/lib/self-update.d.ts +23 -0
  77. package/dist/lib/self-update.js +50 -0
  78. package/dist/lib/session/active.d.ts +13 -1
  79. package/dist/lib/session/active.js +2 -0
  80. package/dist/lib/session/db.d.ts +20 -1
  81. package/dist/lib/session/db.js +139 -9
  82. package/dist/lib/session/fork.d.ts +45 -26
  83. package/dist/lib/session/fork.js +32 -95
  84. package/dist/lib/session/tool-calls.d.ts +43 -1
  85. package/dist/lib/session/tool-calls.js +74 -44
  86. package/dist/lib/session/tool-store.d.ts +33 -2
  87. package/dist/lib/session/tool-store.js +56 -3
  88. package/dist/lib/staleness/writers/sources.d.ts +5 -0
  89. package/dist/lib/staleness/writers/sources.js +2 -1
  90. package/dist/lib/sync-status.d.ts +22 -0
  91. package/dist/lib/sync-status.js +27 -0
  92. package/dist/lib/sync-umbrella.d.ts +9 -0
  93. package/dist/lib/sync-umbrella.js +21 -2
  94. package/dist/lib/traces/insights.d.ts +47 -14
  95. package/dist/lib/traces/insights.js +92 -21
  96. package/dist/lib/traces/phenotype.d.ts +23 -3
  97. package/dist/lib/traces/phenotype.js +72 -24
  98. package/dist/lib/traces/sync.d.ts +15 -0
  99. package/dist/lib/traces/sync.js +104 -19
  100. package/dist/lib/traces/worker-template.js +154 -1
  101. package/package.json +1 -1
@@ -58,6 +58,7 @@ export async function refresh(options = {}) {
58
58
  // Resources this pass refused to write, surfaced by the caller. An empty
59
59
  // synced list cannot also mean "declined and here is why" (RUSH-2700).
60
60
  const declined = [];
61
+ const reconciled = [];
61
62
  migratePromptcutsToRoot(agentsDir, quiet);
62
63
  const manifest = readManifest(agentsDir);
63
64
  if (!manifest) {
@@ -176,6 +177,7 @@ export async function refresh(options = {}) {
176
177
  // Pass the already-built `available` so each version does not re-scan
177
178
  // resource trees (RUSH-2320 #5).
178
179
  const syncResult = syncResourcesToVersion(agentId, ver, selection, { available, ...(forceFullSync ? { force: true } : {}) });
180
+ reconciled.push({ agent: agentId, version: ver });
179
181
  if (syncResult.commands)
180
182
  kinds.add('commands');
181
183
  if (syncResult.skills)
@@ -353,5 +355,5 @@ export async function refresh(options = {}) {
353
355
  for (const reason of declined)
354
356
  log(` ${chalk.yellow(reason)}`);
355
357
  }
356
- return { declined };
358
+ return { declined, reconciled };
357
359
  }
@@ -10,7 +10,21 @@
10
10
  * authentication and Codex's native workspace-trust record before activation.
11
11
  */
12
12
  import type { JobConfig } from './scheduling/routines.js';
13
- import { type RoutineReadinessResult } from './routine-context.js';
13
+ import { type RoutineReadinessResult, type RoutineReadiness } from './routine-context.js';
14
+ /**
15
+ * Fire-time auth preflight: read the daemon-warmed auth-health cache for the
16
+ * exact (agent, version) a routine has resolved to run, and return an
17
+ * `agent_auth_failed` blocker when that account is provably signed out — so the
18
+ * daemon records a terminal `blocked` run (with the re-login repair) instead of
19
+ * spawning a doomed run that 401s and burns a session (PHNX-3415).
20
+ *
21
+ * Cache-only (no network, no prompt — the daemon refreshes the cache
22
+ * periodically and `fleet ping` writes it), and fails OPEN on a missing or
23
+ * non-blocking verdict so a stale/absent probe never wedges a routine. It is
24
+ * checked AFTER version rotation resolves the account (`launch.chain[0]`), so it
25
+ * judges the identity the run will actually use, never a rotated-past dead pin.
26
+ */
27
+ export declare function fireTimeAuthReadiness(agent: string, version: string): RoutineReadiness | null;
14
28
  /**
15
29
  * Evaluate whether a routine is ready to activate on this box. `probeAgent`
16
30
  * defaults to "is a version of the routine's agent resolvable" via
@@ -14,6 +14,8 @@ import * as path from 'path';
14
14
  import * as TOML from 'smol-toml';
15
15
  import { resolveJobExecutionContext, resolveHostStrategy } from './scheduling/routines.js';
16
16
  import { evaluateRoutineReadiness } from './routine-context.js';
17
+ import { readAuthHealth } from './auth-health.js';
18
+ import { machineId } from './machine-id.js';
17
19
  import { getVersionHomePath, isVersionInstalled, resolveVersion } from './installations/versions.js';
18
20
  import { probeLocalFleetAuth } from './auth-health.js';
19
21
  import { resolveHostRunTarget } from './hosts/run-target.js';
@@ -21,6 +23,45 @@ import { hostIdentityArgs, sshTargetFor } from './hosts/types.js';
21
23
  import { probeHost } from './hosts/ready.js';
22
24
  import { sshExec, shellQuote } from './ssh-exec.js';
23
25
  import { encodePowershell, powershellQuote, POWERSHELL_PROGRESS_SILENCE } from './hosts/remote-cmd.js';
26
+ /**
27
+ * Verdicts that make a FIRE-TIME auth preflight block the run: the last live
28
+ * probe found the account either server-rejected (`revoked`) or with no
29
+ * credential at all (`unconfigured` — the "Please run /login" / "no account
30
+ * signed in" case, which is the most common way a routine's dispatch account
31
+ * goes dead). Everything else fails OPEN: `rate_limited` is still authenticated,
32
+ * `expired` self-heals on the next refresh, `unverified` means signed-in but no
33
+ * live probe endpoint (codex/grok), and `error` is indeterminate — none of those
34
+ * should stop a fire. This is deliberately BROADER than {@link isDeadVerdict}
35
+ * (display-only, `revoked` alone): a signed-out account must block a fire, not
36
+ * just paint a red cell.
37
+ */
38
+ function fireBlockingAuthVerdict(verdict) {
39
+ return verdict === 'revoked' || verdict === 'unconfigured';
40
+ }
41
+ /**
42
+ * Fire-time auth preflight: read the daemon-warmed auth-health cache for the
43
+ * exact (agent, version) a routine has resolved to run, and return an
44
+ * `agent_auth_failed` blocker when that account is provably signed out — so the
45
+ * daemon records a terminal `blocked` run (with the re-login repair) instead of
46
+ * spawning a doomed run that 401s and burns a session (PHNX-3415).
47
+ *
48
+ * Cache-only (no network, no prompt — the daemon refreshes the cache
49
+ * periodically and `fleet ping` writes it), and fails OPEN on a missing or
50
+ * non-blocking verdict so a stale/absent probe never wedges a routine. It is
51
+ * checked AFTER version rotation resolves the account (`launch.chain[0]`), so it
52
+ * judges the identity the run will actually use, never a rotated-past dead pin.
53
+ */
54
+ export function fireTimeAuthReadiness(agent, version) {
55
+ const health = readAuthHealth(machineId(), agent, version);
56
+ if (!health || !fireBlockingAuthVerdict(health.verdict))
57
+ return null;
58
+ const who = health.account ? ` (${health.account})` : '';
59
+ return {
60
+ code: 'agent_auth_failed',
61
+ message: `the ${agent}${who} account is signed out — the last auth probe returned '${health.verdict}', so this run would fail authentication`,
62
+ repair: `agents run ${agent}@${version} -- login`,
63
+ };
64
+ }
24
65
  /**
25
66
  * Evaluate whether a routine is ready to activate on this box. `probeAgent`
26
67
  * defaults to "is a version of the routine's agent resolvable" via
@@ -7,6 +7,7 @@
7
7
  * access to explicitly allowed paths.
8
8
  */
9
9
  import type { JobConfig } from './scheduling/routines.js';
10
+ import type { AgentId } from './types.js';
10
11
  /**
11
12
  * Absolute path to this host's `gh` config directory (`hosts.yml` + `config.yml`),
12
13
  * or null when the host has never run `gh auth login`. Prefer `GH_CONFIG_DIR` when
@@ -28,7 +29,9 @@ export declare function buildSpawnEnv(overlayHome: string, extraEnv?: Record<str
28
29
  */
29
30
  export declare function getJobHomePath(name: string): string;
30
31
  /** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
31
- export declare function prepareJobHome(config: JobConfig): string;
32
+ export declare function prepareJobHome(config: JobConfig, version?: string): string;
33
+ /** Link only the selected harness login into the disposable routine HOME. */
34
+ export declare function linkVersionAuth(overlayHome: string, agent: AgentId, version?: string): void;
32
35
  /**
33
36
  * Link this host's `gh` config directory into the disposable overlay so
34
37
  * `$HOME/.config/gh` resolves even when `GH_CONFIG_DIR` is unset. Mirrors
@@ -12,6 +12,7 @@ import * as os from 'os';
12
12
  import { getRoutinesDir, getUserAgentsDir } from './state.js';
13
13
  import { safeJoin } from './paths.js';
14
14
  import { createLink } from './platform/index.js';
15
+ import { getVersionHomePath } from './installations/versions.js';
15
16
  function resolveRealHome() {
16
17
  const home = os.homedir();
17
18
  try {
@@ -132,7 +133,7 @@ export function getJobHomePath(name) {
132
133
  return path.join(safeJoin(getRoutinesDir(), name), 'home');
133
134
  }
134
135
  /** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
135
- export function prepareJobHome(config) {
136
+ export function prepareJobHome(config, version) {
136
137
  const overlayHome = getJobHomePath(config.name);
137
138
  cleanJobHome(config.name);
138
139
  fs.mkdirSync(overlayHome, { recursive: true });
@@ -143,6 +144,7 @@ export function prepareJobHome(config) {
143
144
  }
144
145
  else if (config.agent === 'codex') {
145
146
  generateCodexConfig(overlayHome, config);
147
+ linkVersionAuth(overlayHome, 'codex', version);
146
148
  }
147
149
  else if (config.agent === 'cursor') {
148
150
  generateCursorConfig(overlayHome);
@@ -165,6 +167,33 @@ export function prepareJobHome(config) {
165
167
  }
166
168
  return overlayHome;
167
169
  }
170
+ /** Link only the selected harness login into the disposable routine HOME. */
171
+ export function linkVersionAuth(overlayHome, agent, version) {
172
+ if (!version)
173
+ return;
174
+ const versionHome = getVersionHomePath(agent, version);
175
+ const pairs = agent === 'claude'
176
+ ? [
177
+ [path.join(versionHome, '.claude', '.claude.json'), path.join(overlayHome, '.claude', '.claude.json')],
178
+ [path.join(versionHome, '.claude', '.credentials.json'), path.join(overlayHome, '.claude', '.credentials.json')],
179
+ ]
180
+ : agent === 'codex'
181
+ ? [[path.join(versionHome, '.codex', 'auth.json'), path.join(overlayHome, '.codex', 'auth.json')]]
182
+ : [];
183
+ for (const [source, target] of pairs) {
184
+ if (!fs.existsSync(source))
185
+ continue;
186
+ fs.mkdirSync(path.dirname(target), { recursive: true });
187
+ try {
188
+ fs.rmSync(target, { force: true });
189
+ }
190
+ catch { /* absent */ }
191
+ try {
192
+ createLink(source, target);
193
+ }
194
+ catch { /* the harness fails auth loudly if linking is unavailable */ }
195
+ }
196
+ }
168
197
  /**
169
198
  * Link this host's `gh` config directory into the disposable overlay so
170
199
  * `$HOME/.config/gh` resolves even when `GH_CONFIG_DIR` is unset. Mirrors
@@ -1,27 +1,11 @@
1
1
  /**
2
- * The secrets-agent: a local broker that holds resolved bundle env in memory
3
- * after a single Touch ID unlock, so concurrent agent processes don't each pop
4
- * their own prompt.
2
+ * The secrets-agent: an in-memory broker that holds resolved bundle env after one
3
+ * Touch ID unlock, so concurrent agents don't each prompt.
5
4
  *
6
- * Why this exists: every secret item carries a biometry access control, and
7
- * macOS refuses to cache that across processes — N concurrent `agents run`
8
- * spawns = N Touch ID prompts (see src/lib/secrets/bundles.ts). The Swift
9
- * helper's LAContext only deduplicates reads *within one process*. This broker
10
- * is the ssh-agent answer: `agents secrets unlock <bundle>` decrypts the bundle
11
- * once (one prompt), ships the resolved env here, and every later read returns
12
- * from memory over a user-only Unix socket — no prompt.
13
- *
14
- * Security model (deliberate): while a bundle is unlocked, any same-user
15
- * process that can reach the socket reads it silently. That's strictly the same
16
- * trust boundary the keychain already concedes (docs/secrets.md: the ACL is
17
- * user-presence, not code-identity — any same-user process can pop the prompt
18
- * and read), minus the visible prompt. We bound it with: explicit per-bundle
19
- * opt-in (nothing is held unless you `unlock` it), an absolute TTL (~7d), an
20
- * auto-wipe on sleep / logout, and `agents secrets lock`. A bare screen-lock is
21
- * NOT a wipe (the login password already gates it). Nothing ever touches disk.
22
- *
23
- * macOS only: Linux libsecret has no biometry prompt, so there's nothing to
24
- * deduplicate — every entry point here no-ops off darwin.
5
+ * Security model: while unlocked, any same-user process reaching the socket can
6
+ * read it silently — the same trust boundary the keychain already concedes. We
7
+ * bound it with per-bundle opt-in, a TTL (~7d), auto-wipe on sleep/logout, and
8
+ * explicit lock. Nothing touches disk. Off-darwin the broker is unused.
25
9
  */
26
10
  import * as net from 'net';
27
11
  import type { SecretsBundle } from './bundles.js';
@@ -31,63 +15,33 @@ export { GLOBAL_HARNESS, bundleScopeChain };
31
15
  /** Default lifetime of an unlocked bundle when `--ttl` is not given. */
32
16
  export declare const DEFAULT_TTL_MS: number;
33
17
  /**
34
- * Whether the secrets broker service is enabled in the daemon service config.
35
- * Off-darwin this is irrelevant (the broker is never used), but the helper is
36
- * kept synchronous and safe everywhere so callers can fail loud without a prompt.
18
+ * Whether the secrets broker service is enabled in daemon service config.
37
19
  */
38
20
  export declare function isSecretsBrokerEnabled(): boolean;
39
21
  /**
40
22
  * Reserved store-key prefix for the `secrets list` metadata snapshot cache.
41
- * The broker holds the resolved bundle-metadata array (names/policy/timestamps,
42
- * NO resolved secret values beyond the literals already in metadata) keyed by a
43
- * hash of the current keychain bundle name-set, so the second and later
44
- * `secrets list` within the hold window read metadata without a Touch ID
45
- * prompt. Keyed by the name-set hash so adding/removing/renaming a bundle
46
- * changes the key and misses the cache automatically — no active invalidation.
47
- * The '!' sentinel can never collide with a real bundle name
48
- * (BUNDLE_NAME_PATTERN requires an alphanumeric first char) and is safe as
49
- * spawnSync argv (unlike a NUL byte); `status` hides these entries.
23
+ * Keyed by a hash of the keychain name-set; adding/removing/renaming a bundle
24
+ * changes the key and invalidates passively. The '!' sentinel cannot collide
25
+ * with a real bundle name and is safe as argv.
50
26
  */
51
27
  export declare const META_CACHE_PREFIX = "!meta:";
52
28
  export { SYNC_GET_CMD, SYNC_PING_CMD, SYNC_LOCK_CMD } from './sync-commands.js';
53
29
  /**
54
- * Decide whether a persistent broker should self-heal onto freshly-installed
55
- * code (exit so launchd relaunches it). Only when the store is EMPTY: exiting
56
- * with bundles still unlocked wipes them from memory, so the next reader falls
57
- * back to a direct keychain read and re-prompts for Touch ID. Deferring the
58
- * restart until the cache is idle (TTL-expired / slept) means an
59
- * in-place `npm i -g` never wipes a hot cache — the new code is adopted at the
60
- * next quiet moment instead. See #435: rapid repeated upgrades wiped a hot
61
- * cache on every bump and produced a recurring Touch ID storm.
30
+ * Decide whether a persistent broker should exit so launchd relaunches it on
31
+ * freshly-installed code. Only when the store is empty: exiting with held
32
+ * bundles would force a re-prompt. Deferring protects against rapid upgrades
33
+ * wiping the hot cache (#435).
62
34
  */
63
35
  export declare function shouldSelfHealForUpgrade(persistent: boolean, storeSize: number, runningVersion: string, onDiskVersion: string): boolean;
64
36
  /**
65
- * Client-side twin of shouldSelfHealForUpgrade: whether ensureAgentRunning may
66
- * tear down a reachable broker whose running version differs from the client's
67
- * on-disk version. Only while it holds NO real unlocks — tearing down a hot
68
- * broker wipes every held bundle, so the next read of each one re-prompts for
69
- * Touch ID. On a machine where installed versions churn (dev builds stamp a
70
- * fresh 0.0.0-dev.<sha> on every install; an npm copy and a dev copy invoke in
71
- * turn), an unguarded teardown produced a rolling Touch ID storm — the exact
72
- * failure #435 fixed on the server side. A hot, protocol-compatible broker
73
- * keeps serving; its own sweep adopts the new code at the next quiet moment.
37
+ * Client-side twin: may only tear down a version-skewed broker when it holds
38
+ * no real unlocks, otherwise every held bundle re-prompts (#435).
74
39
  */
75
40
  export declare function shouldTeardownVersionSkewedBroker(realHeldBundles: number): boolean;
76
41
  /**
77
- * Whether a version-skewed client may evict the reachable broker at all. The
78
- * held-bundle gate above is necessary but not sufficient: a broker the always-on
79
- * daemon is hosting must NEVER be client-evicted, even when it holds zero
80
- * unlocks. teardownStaleBroker() recognizes only the standalone broker's
81
- * pidPath() O_EXCL claim (the daemon writes ownerPath(), never pidPath()), so
82
- * evicting a daemon-hosted broker unlinks its socket WITHOUT stopping the daemon;
83
- * the daemon then keeps hostedBroker != null and shouldTakeOverBroker() refuses
84
- * to re-host, orphaning its broker until the daemon restarts while every reader
85
- * falls onto cold one-off brokers that re-prompt Touch ID — the storm. Deferring
86
- * is safe: daemon code-version upgrades are handled by postinstall.js restarting
87
- * it, and agentPing() already gated on PROTOCOL_VERSION, so a code-skewed daemon
88
- * broker is still wire-compatible. Only when NO daemon owns the broker (churning
89
- * dev installs with a dead/absent daemon — the case #435's client twin was built
90
- * for) does the zero-held-bundles teardown apply, exactly as before.
42
+ * Whether a client may evict a version-skewed broker. A daemon-hosted broker
43
+ * must never be client-evicted: the daemon owns the socket via ownerPath(), so
44
+ * unlinking it would orphan the broker until daemon restart (#435).
91
45
  */
92
46
  export declare function shouldClientEvictSkewedBroker(daemonRunning: boolean, realHeldBundles: number): boolean;
93
47
  export interface StoredBundle {
@@ -110,28 +64,21 @@ export interface AgentStatusEntry {
110
64
  /** Public accessor for the broker's socket path — `agents daemon status`/`services` reads it for display. */
111
65
  export declare function secretsBrokerSocketPath(): string;
112
66
  /**
113
- * Read the current broker capability token, or null if none is present. Clients
114
- * read it fresh per request and attach it to every non-ping command; a broker
115
- * restart mints a new token, so a stale read simply fails authorization and the
116
- * caller falls back to a direct keychain read (soft, never a hard error).
67
+ * Read the current broker capability token. A restart mints a new token, so a
68
+ * stale read fails soft and the caller falls back to a direct keychain read.
117
69
  */
118
70
  export declare function readAgentToken(): string | null;
119
71
  /** True if a legacy standalone-broker launchd plist is still installed. */
120
72
  export declare function secretsAgentServiceInstalled(): boolean;
121
73
  /**
122
- * Retire the legacy standalone secrets-agent launchd service: bootout the job
123
- * (falling back to the legacy `unload`) and remove its plist so the always-on
124
- * daemon owns the broker socket. Idempotent and best-effort — a no-op when no
125
- * legacy plist is present. Does NOT wipe held bundles: the booted-out process's
126
- * memory is gone anyway, and the daemon-hosted broker starts fresh.
74
+ * Retire the legacy standalone launchd service so the daemon owns the socket.
75
+ * Idempotent; does not wipe held bundles.
127
76
  */
128
77
  export declare function retireLegacySecretsAgentService(): void;
129
78
  /**
130
- * Stop the persistent broker for `agents secrets stop`: wipe whatever the broker
131
- * holds (forces Touch ID again on the next read), then retire any legacy
132
- * standalone service. The daemon-hosted broker itself is left running — it is
133
- * the always-on backbone, and stopping it would take down unrelated background
134
- * work (routines, browser IPC, session-sync).
79
+ * Stop the persistent broker for `agents secrets stop`: wipe held bundles, then
80
+ * retire any legacy standalone service. The daemon-hosted broker itself is left
81
+ * running because it backs unrelated background work.
135
82
  */
136
83
  export declare function uninstallSecretsAgentService(): Promise<void>;
137
84
  export type Request = {
@@ -199,73 +146,47 @@ export type Response = {
199
146
  error: string;
200
147
  };
201
148
  /**
202
- * Pure request handler over the in-memory store. Extracted so the store
203
- * semantics (lazy expiry on get/status, lock-one vs lock-all, load TTL) are
204
- * unit-testable with a controlled `now`, without a socket or a spawned process.
205
- * Mutates `store` in place; returns the wire response.
206
- */
207
- /**
208
- * Count of real unlocked bundles in the store, excluding the internal
209
- * `secrets list` metadata cache. Used to decide broker "warmth" for self-heal
210
- * and idle-exit: a metadata-only store must read as empty so a disposable list
211
- * cache never blocks an upgrade restart (#435) or an idle one-off broker from
212
- * exiting. Pure + exported for unit testing.
149
+ * Count of real unlocked bundles, excluding the internal `secrets list` metadata
150
+ * cache. A metadata-only store must read as empty so it doesn't block upgrade
151
+ * self-heal or idle-exit (#435). Exported for tests.
213
152
  */
214
153
  export declare function realBundleCount(store: Map<string, StoredBundle>): number;
215
154
  export declare function scopedBundleKey(name: string, harness: string): string;
155
+ /** Pure request handler over the in-memory store. Exported for tests. */
216
156
  export declare function handleAgentRequest(store: Map<string, StoredBundle>, req: Request, now?: number, evictedAt?: Map<string, number>): Response;
217
157
  /**
218
- * Decide whether a `watch-lock` helper line should wipe the in-memory store.
219
- * The helper emits `LOCK` on screen-lock / screensaver and `SLEEP` on system
220
- * sleep. We wipe on SLEEP only: a bare screen-lock is already gated by the login
221
- * password, and with the ~7d hold, re-authing after every lock would defeat the
222
- * point. Logout needs no line — it tears down the launchd session and kills the
223
- * broker outright. Pure + exported so the LOCK-survives / SLEEP-wipes contract
224
- * has direct regression coverage (the inline stdout handler isn't unit-testable).
158
+ * Wipe the in-memory store only on SLEEP, not screen-lock (already gated by the
159
+ * login password). Exported for regression coverage of the LOCK-survives /
160
+ * SLEEP-wipes contract.
225
161
  */
226
162
  export declare function shouldWipeOnWatchEvent(chunk: string): boolean;
227
163
  /**
228
- * Authorization gate applied to every request BEFORE handleAgentRequest touches
229
- * the store (RUSH-1760). A bare same-UID socket connection is no longer trusted
230
- * to load/get arbitrary bundle env: each command except the liveness `ping` must
231
- * carry the per-broker capability token, which lives in a 0600 file inside the
232
- * 0700 agent dir and so is readable only by the UID that owns the broker.
233
- *
234
- * Fail closed: a missing/empty expected token (no token file) rejects every
235
- * command but ping. `ping` stays unauthenticated — it exposes only the protocol
236
- * and cli version, and clients need it to detect a reachable broker before they
237
- * have any reason to read the token. Pure + exported for direct unit testing.
164
+ * Authorization gate: every request except `ping` must carry the per-broker
165
+ * capability token from the 0600 token file. `ping` stays unauthenticated so
166
+ * clients can probe reachability before reading the token. Fail closed. Exported
167
+ * for tests.
238
168
  */
239
169
  export declare function isRequestAuthorized(req: Request, expectedToken: string | null): boolean;
240
170
  type BrokerConnectionHandler = (conn: net.Socket) => void;
241
171
  /**
242
- * Build the socket `connection` handler shared by both brokers (standalone and
243
- * daemon-hosted): newline-framed JSON in, one response line out, with the
244
- * authorization gate (isRequestAuthorized) applied before `handle`. `token`
245
- * resolves the currently-expected capability token per request so a token
246
- * rotation on broker restart is picked up without rebuilding the handler.
172
+ * Build the socket `connection` handler shared by standalone and daemon-hosted
173
+ * brokers. Newline-framed JSON in, one response line out, with per-request token
174
+ * lookup so rotation on restart is picked up.
247
175
  */
248
176
  export declare function makeConnectionHandler(handle: (req: Request) => Response, token: () => string | null): BrokerConnectionHandler;
249
177
  /**
250
- * Whether a live process still owns the broker pid file.
251
- *
252
- * A single missed `agentPing` is NOT proof the owner is dead — the broker is
253
- * single-threaded, so a big `get` or the rehydrate at startup can blow the
254
- * 700ms ping budget while the process is perfectly healthy. Treating that as
255
- * death let a starting broker unlink a live socket and rebind, leaving the old
256
- * process alive holding every unlocked bundle in RAM that no client could reach
257
- * any more (observed live: two brokers, one socket path, two kernel sockets).
258
- * The pid file is the second, independent liveness signal that makes the reclaim
178
+ * Whether a live process still owns the broker pid file. A single missed ping
179
+ * isn't proof of death: the single-threaded broker can blow the ping budget
180
+ * while healthy. The pid file is the second liveness signal that makes reclaim
259
181
  * safe.
260
182
  */
261
- /** Drop our ownership record, but only if it is still ours — never clobber a
262
- * successor that already claimed the socket. */
183
+ /** Drop our ownership record only if it is still ours. */
263
184
  export declare function releaseBrokerPid(): void;
264
185
  export declare function brokerPidAlive(): boolean;
265
186
  /**
266
- * Run the broker in the foreground. Spawned detached by ensureAgentRunning via
267
- * `agents secrets _agent-run`. Holds the store in memory, serves the socket,
268
- * sweeps expired entries, wipes on sleep, and self-exits when idle.
187
+ * Run the standalone broker in the foreground. Spawned by ensureAgentRunning via
188
+ * `agents secrets _agent-run`. Serves the socket, sweeps expired entries, wipes
189
+ * on sleep, and self-exits when idle.
269
190
  */
270
191
  export declare function runSecretsAgent(opts?: {
271
192
  service?: boolean;
@@ -273,72 +194,35 @@ export declare function runSecretsAgent(opts?: {
273
194
  close(): void | Promise<void>;
274
195
  } | null>;
275
196
  /**
276
- * Host the secrets broker inside the always-on daemon (#416).
277
- *
278
- * Serves the SAME socket and wire protocol as the standalone `runSecretsAgent`
279
- * — so every existing client (`agentGetSync`, `agentPing`, `agentAutoLoadSync`)
280
- * keeps working through the versioned protocol — but it is daemon-safe:
281
- *
282
- * - no pid-file single-instance guard (the daemon owns the instance);
283
- * - no `process.exit`, no SIGTERM/SIGINT handlers, no self-heal/idle-exit
284
- * (those would kill the daemon — the daemon is the always-on backbone and
285
- * manages its own version/lifecycle). The sweep only TTL-evicts.
286
- *
287
- * The caller (`runDaemon`) normally invokes this only when no broker answers
288
- * its initial ping. Binding still arbitrates ownership through the same shared
289
- * path as the standalone service: a live owner wins, while only an unreachable
290
- * stale socket is reclaimed. Returns a handle the daemon closes on shutdown,
291
- * or null off-darwin (nothing to broker without biometry).
197
+ * Host the secrets broker inside the always-on daemon (#416). Serves the same
198
+ * socket/protocol as the standalone broker, but daemon-safe: no pid-file guard,
199
+ * no process.exit/SIG handlers, no self-heal/idle-exit (the daemon owns the
200
+ * lifecycle). Returns null off-darwin.
292
201
  */
293
202
  export declare function startHostedBroker(): Promise<{
294
203
  close(): void | Promise<void>;
295
204
  } | null>;
296
205
  /**
297
- * Call Node's net.Server.close() and wait for the 'close' event (or a bounded
298
- * timeout). Without this, close() returned while the listen socket could still
299
- * be held — a successor bind could race EADDRINUSE against a half-closed server
300
- * (RUSH-2421). Pure side-effect helper; never throws.
206
+ * Wait for net.Server.close()'s 'close' event (or a bounded timeout) so a
207
+ * successor bind doesn't race a half-closed socket (RUSH-2421).
301
208
  */
302
209
  export declare function closeServerBounded(server: net.Server, timeoutMs?: number): Promise<void>;
303
- /** True if a broker socket exists at all. Cheap; gates the sync read so the
304
- * never-unlocked path stays a single stat. */
210
+ /** Cheap socket-existence check so the never-unlocked path stays a single stat. */
305
211
  export declare function agentSocketExists(): boolean;
306
212
  /**
307
- * Spawn one of the `__secrets-*` sync clients and return its result.
308
- *
309
- * These three call sites used to hand-roll `spawnSync(process.execPath, ['-e',
310
- * <inline node program>, …])`. That is correct only for a JS install, where
311
- * `process.execPath` is `node`. Since 1.20.53 the shipped macOS `agents` is a
312
- * **bun-compiled Mach-O**, where `process.execPath` is the CLI binary itself —
313
- * so the spawn became `agents -e <program> …`, which commander rejects with
314
- * `error: unknown option '-e'` and a non-zero exit. Every sync client then took
315
- * its own failure path (`agentGetSync` → null, `agentReachableSync` → false,
316
- * `agentEvictSync` → no-op), which reads as "broker down" and falls through to
317
- * a real keychain read. Net effect on the standalone binary: the broker cache
318
- * was never hit, so the `daily` policy's one-prompt-per-7d never applied and
319
- * every bundle read re-popped Touch ID.
320
- *
321
- * Same defect class as the broker-launch bug fixed in 1.20.56 (see cliSpawn's
322
- * doc comment); these three sites were simply never converted. Routing them
323
- * through `getCliLaunch` fixes both install shapes at once and keeps a single
324
- * code path.
325
- *
326
- * The subcommands are top-level `__secrets-*` tokens intercepted in index.ts
327
- * BEFORE commander and before the CLI's startup work, exactly like
328
- * `__daemon-run` and `__vault-age-helper`. That is load-bearing, not cosmetic:
329
- * a normal command path runs `checkForUpdates()` and `spawnDetachedSync()` on
330
- * every invocation (index.ts), so registering these as ordinary hidden
331
- * subcommands would fire an update check and spawn a detached background sync
332
- * on every cache hit — turning the hot read path into a process fork storm.
213
+ * Spawn one of the `__secrets-*` sync clients via `getCliLaunch`. The old
214
+ * `process.execPath -e` pattern broke on the bun-compiled Mach-O binary
215
+ * (1.20.53); routing through getCliLaunch fixes both install shapes. The
216
+ * subcommands are intercepted in index.ts before commander/startup so a cache
217
+ * hit doesn't fork detached syncs or run update checks.
333
218
  */
334
219
  export declare function syncClientLaunch(sub: string[], agentsBin?: string): {
335
220
  command: string;
336
221
  args: string[];
337
222
  };
338
223
  /**
339
- * Synchronous read for the hot path. Returns the cached resolved bundle, or
340
- * null if the agent isn't running / doesn't hold this bundle / anything fails
341
- * (soft — caller falls through to the real keychain). macOS only.
224
+ * Synchronous read for the hot path. Returns null on any failure so the caller
225
+ * falls through to the real keychain. macOS only.
342
226
  */
343
227
  export declare function agentGetSync(name: string, harness?: string): {
344
228
  bundle: SecretsBundle;
@@ -346,70 +230,41 @@ export declare function agentGetSync(name: string, harness?: string): {
346
230
  lease?: SecretLease;
347
231
  } | null;
348
232
  /**
349
- * Last non-empty line of a child's stdout — the payload line.
350
- *
351
- * The inline `node -e` client this replaced was a bare node process that could
352
- * only ever emit its own JSON. The replacement boots the real CLI, so anything
353
- * the CLI prints on the way up shares that stream. Today the known chatter (the
354
- * `~/.agents/ is N commits behind` notice) correctly goes to stderr, but a
355
- * single future stdout write anywhere in startup would make `JSON.parse` throw
356
- * and turn every cache hit back into a silent miss — i.e. quietly reintroduce
357
- * the Touch-ID-on-every-read bug this fix closes, with no failing test to catch
358
- * it. Anchoring on the last line makes the payload the terminator of the
359
- * stream rather than the whole of it, so preceding chatter is inert.
233
+ * Last non-empty line of a child's stdout — the payload line. Anchors on the
234
+ * terminator so any future CLI startup chatter on stdout doesn't break JSON.parse
235
+ * and silently turn cache hits into misses.
360
236
  */
361
237
  export declare function lastLine(stdout: string): string;
362
238
  /**
363
- * Synchronous liveness check: is a broker actually LISTENING and answering (not
364
- * just a lingering socket file)? Used to decide whether the auto-cache may take
365
- * the synchronous warm path — a dead broker whose socket outlived it (crash,
366
- * OOM, version-skew teardown) must NOT drag a foreground read through the
367
- * worker's 20s cold-start budget. A stale socket refuses instantly, so this is
368
- * fast in both the alive and dead cases. macOS only.
239
+ * Synchronous liveness check: is a broker actually listening? Gates the
240
+ * synchronous warm path so a stale socket doesn't drag a foreground read
241
+ * through a cold-start. macOS only.
369
242
  */
370
243
  export declare function agentReachableSync(): boolean;
371
244
  /**
372
- * Synchronously evict one bundle from the broker. Called after a mutating
373
- * keychain write (add / rotate / remove / rename / delete) so the broker never
374
- * keeps serving the pre-write snapshot for up to the ~7d hold — the next read
375
- * re-resolves from the keychain (one prompt) and re-caches fresh values.
376
- * Best-effort: no broker, no socket, or any failure is a silent no-op.
377
- * macOS only.
245
+ * Synchronously evict one bundle after a mutating write so the broker doesn't
246
+ * serve a stale snapshot for the hold window. Best-effort silent no-op. macOS only.
378
247
  */
379
248
  export declare function agentEvictSync(name: string): void;
380
- /** Body of `__secrets-get <name>`. Prints `{bundle, env}` as JSON on a
381
- * cache hit. Exit 0 = hit, 3 = miss or broker down. */
249
+ /** Body of `__secrets-get <name>`. Exit 0 = hit, 3 = miss/down. */
382
250
  export declare function runAgentGetSync(name: string, harness?: string): Promise<number>;
383
- /** Body of `__secrets-ping`. Exit 0 = a broker is listening and speaking
384
- * our protocol, 3 = nothing there. Deliberately does NOT gate on
385
- * PROTOCOL_VERSION: this only decides whether the auto-cache may take the
386
- * synchronous warm path, and a version-skewed broker still answers reads. That
387
- * matches the inline program this replaced. */
251
+ /** Body of `__secrets-ping`. Exit 0 = listening broker, 3 = nothing there.
252
+ * Does not gate on PROTOCOL_VERSION so version-skewed brokers still answer reads. */
388
253
  export declare function runAgentPingSync(): Promise<number>;
389
254
  /** Body of `__secrets-lock <name>`. Best-effort evict; exit 0 even when no
390
- * broker answers, so a missing broker never fails the mutating write that
391
- * triggered it (agentEvictSync discards this either way).
392
- *
393
- * An empty name is refused rather than forwarded: the broker treats a nameless
394
- * lock as lock-ALL (handleAgentRequest), so a bare `__secrets-lock` would wipe
395
- * every held bundle and re-prompt Touch ID for each. That was unreachable while
396
- * this was an inline `node -e` string; as a top-level argv token it is one typo
397
- * away, and agentEvictSync only ever locks by name. */
255
+ * broker answers. An empty name is refused to avoid accidentally locking ALL
256
+ * bundles. */
398
257
  export declare function runAgentLockSync(name: string): Promise<number>;
399
258
  /**
400
- * Read the cached `secrets list` metadata snapshot for the given keychain
401
- * name-set hash, or null on miss / no broker / off-darwin. Reuses the value
402
- * fast-path socket read (agentGetSync) — no prompt, no wire change. The hash is
403
- * the cache key: a changed name-set (bundle added/removed/renamed) yields a
404
- * different key and therefore a clean miss, so the stale set is never served.
259
+ * Read the cached `secrets list` metadata snapshot for a keychain name-set hash.
260
+ * Reuses agentGetSync. The name-set hash is the cache key, so add/remove/rename
261
+ * yields a clean miss.
405
262
  */
406
263
  export declare function agentGetMetaSync(nameSetHash: string): SecretsBundle[] | null;
407
264
  /**
408
- * Fire-and-forget: populate the broker with a freshly-read metadata snapshot so
409
- * the next `secrets list` within the hold window renders without a prompt.
410
- * Stored as an ordinary entry (placeholder bundle, snapshot in env) under the
411
- * reserved META_CACHE_PREFIX key; the snapshot travels over stdin to the
412
- * detached worker (never argv/disk), same as value caching. macOS only.
265
+ * Fire-and-forget: populate the broker with a metadata snapshot so the next
266
+ * `secrets list` within the hold window is prompt-free. Stored under a reserved
267
+ * META_CACHE_PREFIX key; snapshot travels over stdin. macOS only.
413
268
  */
414
269
  export declare function agentAutoLoadMetaSync(nameSetHash: string, bundles: SecretsBundle[], ttlMs: number): void;
415
270
  /** True unless `secrets.agent.auto` is explicitly disabled in agents.yaml. The