@phnx-labs/agents-cli 1.20.78 → 1.20.83

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 (183) hide show
  1. package/CHANGELOG.md +595 -0
  2. package/README.md +26 -26
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +66 -0
  12. package/dist/commands/doctor.js +596 -33
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/menubar.js +12 -0
  22. package/dist/commands/models.js +1 -1
  23. package/dist/commands/packages.js +6 -6
  24. package/dist/commands/permissions.js +1 -1
  25. package/dist/commands/profiles.js +1 -1
  26. package/dist/commands/pull.d.ts +1 -1
  27. package/dist/commands/pull.js +4 -4
  28. package/dist/commands/repo.d.ts +78 -0
  29. package/dist/commands/repo.js +187 -11
  30. package/dist/commands/routines.d.ts +8 -1
  31. package/dist/commands/routines.js +241 -77
  32. package/dist/commands/rules.js +13 -12
  33. package/dist/commands/secrets.d.ts +3 -1
  34. package/dist/commands/secrets.js +100 -32
  35. package/dist/commands/sessions-inject.d.ts +7 -4
  36. package/dist/commands/sessions-inject.js +18 -18
  37. package/dist/commands/sessions-picker.js +32 -3
  38. package/dist/commands/sessions-resume.d.ts +16 -0
  39. package/dist/commands/sessions-resume.js +14 -13
  40. package/dist/commands/sessions.d.ts +115 -0
  41. package/dist/commands/sessions.js +189 -35
  42. package/dist/commands/set.d.ts +15 -0
  43. package/dist/commands/set.js +79 -0
  44. package/dist/commands/setup-fleet.d.ts +21 -0
  45. package/dist/commands/setup-fleet.js +201 -0
  46. package/dist/commands/setup-secrets.d.ts +20 -0
  47. package/dist/commands/setup-secrets.js +223 -0
  48. package/dist/commands/setup.js +17 -3
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/sync.js +7 -2
  51. package/dist/commands/teams-picker.js +0 -1
  52. package/dist/commands/teams.d.ts +33 -1
  53. package/dist/commands/teams.js +208 -44
  54. package/dist/commands/versions.js +10 -5
  55. package/dist/commands/view.d.ts +1 -0
  56. package/dist/commands/view.js +11 -0
  57. package/dist/index.js +52 -8
  58. package/dist/lib/acp/harnesses.js +0 -7
  59. package/dist/lib/activity.d.ts +156 -0
  60. package/dist/lib/activity.js +282 -0
  61. package/dist/lib/agents.d.ts +9 -2
  62. package/dist/lib/agents.js +72 -63
  63. package/dist/lib/browser/service.js +3 -0
  64. package/dist/lib/browser/types.d.ts +7 -0
  65. package/dist/lib/capabilities.js +6 -2
  66. package/dist/lib/crabbox/lease.js +2 -2
  67. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  68. package/dist/lib/crabbox/setup-copy.js +4 -4
  69. package/dist/lib/daemon.d.ts +18 -0
  70. package/dist/lib/daemon.js +47 -6
  71. package/dist/lib/devices/fleet.d.ts +1 -1
  72. package/dist/lib/devices/fleet.js +1 -1
  73. package/dist/lib/doctor-diff.d.ts +20 -0
  74. package/dist/lib/doctor-diff.js +6 -1
  75. package/dist/lib/drift.d.ts +31 -11
  76. package/dist/lib/drift.js +58 -7
  77. package/dist/lib/events.d.ts +8 -1
  78. package/dist/lib/events.js +10 -1
  79. package/dist/lib/exec.d.ts +7 -0
  80. package/dist/lib/exec.js +69 -11
  81. package/dist/lib/feed-post.d.ts +28 -1
  82. package/dist/lib/feed-post.js +110 -2
  83. package/dist/lib/fleet/apply.js +8 -1
  84. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  85. package/dist/lib/fleet/auth-sync.js +25 -11
  86. package/dist/lib/fleet/remote-login.js +5 -0
  87. package/dist/lib/git.d.ts +14 -14
  88. package/dist/lib/git.js +41 -44
  89. package/dist/lib/hooks.d.ts +71 -0
  90. package/dist/lib/hooks.js +245 -95
  91. package/dist/lib/hosts/dispatch.d.ts +28 -1
  92. package/dist/lib/hosts/dispatch.js +33 -5
  93. package/dist/lib/hosts/option.js +2 -2
  94. package/dist/lib/hosts/passthrough.d.ts +22 -1
  95. package/dist/lib/hosts/passthrough.js +238 -13
  96. package/dist/lib/hosts/run-target.d.ts +2 -0
  97. package/dist/lib/hosts/run-target.js +1 -0
  98. package/dist/lib/hq/floor.d.ts +1 -1
  99. package/dist/lib/hq/floor.js +6 -1
  100. package/dist/lib/mcp.js +3 -71
  101. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  102. package/dist/lib/menubar/install-menubar.d.ts +24 -0
  103. package/dist/lib/menubar/install-menubar.js +52 -2
  104. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  105. package/dist/lib/menubar/notify-desktop.js +41 -5
  106. package/dist/lib/merged-resources.d.ts +11 -0
  107. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  108. package/dist/lib/permissions.d.ts +0 -32
  109. package/dist/lib/permissions.js +4 -157
  110. package/dist/lib/picker.js +1 -1
  111. package/dist/lib/platform/process.d.ts +16 -5
  112. package/dist/lib/platform/process.js +54 -0
  113. package/dist/lib/plugins.d.ts +0 -8
  114. package/dist/lib/plugins.js +4 -110
  115. package/dist/lib/project-resources.js +4 -1
  116. package/dist/lib/refresh.d.ts +5 -3
  117. package/dist/lib/refresh.js +7 -5
  118. package/dist/lib/resources/commands.js +4 -1
  119. package/dist/lib/resources/mcp.js +0 -4
  120. package/dist/lib/resources/permissions.d.ts +1 -1
  121. package/dist/lib/resources/permissions.js +1 -5
  122. package/dist/lib/resources/rules.js +4 -1
  123. package/dist/lib/resources/skills.js +4 -1
  124. package/dist/lib/resources/subagents.js +4 -0
  125. package/dist/lib/resources/types.d.ts +1 -1
  126. package/dist/lib/routines.d.ts +33 -0
  127. package/dist/lib/routines.js +122 -0
  128. package/dist/lib/run-defaults.d.ts +1 -0
  129. package/dist/lib/run-defaults.js +9 -0
  130. package/dist/lib/runner.d.ts +0 -12
  131. package/dist/lib/runner.js +49 -10
  132. package/dist/lib/scheduler.js +16 -4
  133. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  134. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  135. package/dist/lib/secrets/agent.d.ts +16 -0
  136. package/dist/lib/secrets/agent.js +105 -3
  137. package/dist/lib/secrets/audit.d.ts +46 -0
  138. package/dist/lib/secrets/audit.js +56 -0
  139. package/dist/lib/secrets/bundles.d.ts +11 -8
  140. package/dist/lib/secrets/bundles.js +31 -23
  141. package/dist/lib/secrets/index.d.ts +2 -1
  142. package/dist/lib/secrets/index.js +8 -2
  143. package/dist/lib/secrets/remote.d.ts +75 -2
  144. package/dist/lib/secrets/remote.js +139 -5
  145. package/dist/lib/secrets/sync.js +3 -3
  146. package/dist/lib/session/active.d.ts +128 -26
  147. package/dist/lib/session/active.js +192 -53
  148. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  149. package/dist/lib/session/actor-sidecar.js +101 -0
  150. package/dist/lib/session/db.d.ts +4 -1
  151. package/dist/lib/session/db.js +65 -8
  152. package/dist/lib/session/discover.d.ts +4 -0
  153. package/dist/lib/session/discover.js +13 -0
  154. package/dist/lib/session/hook-sessions.d.ts +9 -0
  155. package/dist/lib/session/hook-sessions.js +57 -8
  156. package/dist/lib/session/pid-registry.d.ts +13 -0
  157. package/dist/lib/session/render.d.ts +2 -0
  158. package/dist/lib/session/render.js +1 -1
  159. package/dist/lib/session/types.d.ts +16 -0
  160. package/dist/lib/staleness/detectors/commands.js +2 -2
  161. package/dist/lib/staleness/detectors/permissions.js +1 -45
  162. package/dist/lib/staleness/writers/commands.js +4 -5
  163. package/dist/lib/staleness/writers/hooks.js +1 -1
  164. package/dist/lib/startup/command-registry.d.ts +1 -2
  165. package/dist/lib/startup/command-registry.js +2 -4
  166. package/dist/lib/state.js +57 -1
  167. package/dist/lib/subagents-registry.js +5 -12
  168. package/dist/lib/subagents.d.ts +0 -10
  169. package/dist/lib/subagents.js +0 -12
  170. package/dist/lib/teams/agents.d.ts +0 -2
  171. package/dist/lib/teams/agents.js +10 -27
  172. package/dist/lib/teams/index.d.ts +1 -1
  173. package/dist/lib/teams/index.js +1 -1
  174. package/dist/lib/types.d.ts +17 -9
  175. package/dist/lib/usage.d.ts +28 -5
  176. package/dist/lib/usage.js +271 -8
  177. package/dist/lib/versions.js +7 -1
  178. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  179. package/dist/lib/watchdog/watchdog.js +25 -13
  180. package/package.json +1 -2
  181. package/dist/commands/check.d.ts +0 -15
  182. package/dist/commands/check.js +0 -180
  183. package/dist/commands/resources.d.ts +0 -5
@@ -206,6 +206,22 @@ type BrokerConnectionHandler = (conn: net.Socket) => void;
206
206
  * rotation on broker restart is picked up without rebuilding the handler.
207
207
  */
208
208
  export declare function makeConnectionHandler(handle: (req: Request) => Response, token: () => string | null): BrokerConnectionHandler;
209
+ /**
210
+ * Whether a live process still owns the broker pid file.
211
+ *
212
+ * A single missed `agentPing` is NOT proof the owner is dead — the broker is
213
+ * single-threaded, so a big `get` or the rehydrate at startup can blow the
214
+ * 700ms ping budget while the process is perfectly healthy. Treating that as
215
+ * death let a starting broker unlink a live socket and rebind, leaving the old
216
+ * process alive holding every unlocked bundle in RAM that no client could reach
217
+ * any more (observed live: two brokers, one socket path, two kernel sockets).
218
+ * The pid file is the second, independent liveness signal that makes the reclaim
219
+ * safe.
220
+ */
221
+ /** Drop our ownership record, but only if it is still ours — never clobber a
222
+ * successor that already claimed the socket. */
223
+ export declare function releaseBrokerPid(): void;
224
+ export declare function brokerPidAlive(): boolean;
209
225
  /**
210
226
  * Run the broker in the foreground. Spawned detached by ensureAgentRunning via
211
227
  * `agents secrets _agent-run`. Holds the store in memory, serves the socket,
@@ -30,7 +30,7 @@ import * as path from 'path';
30
30
  import { randomBytes } from 'crypto';
31
31
  import { spawn, spawnSync, execFileSync } from 'child_process';
32
32
  import { getHelpersDir, readMeta } from '../state.js';
33
- import { isAlive } from '../platform/process.js';
33
+ import { isAlive, waitForExit } from '../platform/process.js';
34
34
  import { getKeychainHelperPath } from './install-helper.js';
35
35
  import { getCliVersion, getCliVersionFresh } from '../version.js';
36
36
  import { getCliLaunch } from '../cli-entry.js';
@@ -65,6 +65,19 @@ export const META_CACHE_PREFIX = '!meta:';
65
65
  const IDLE_EXIT_MS = 5 * 60 * 1000; // 5m
66
66
  /** How often the broker sweeps expired entries. */
67
67
  const SWEEP_INTERVAL_MS = 30 * 1000;
68
+ /** How many times a starting broker probes an in-use socket before treating its
69
+ * owner as gone. One probe is not enough: the broker is single-threaded, so a
70
+ * large read or the startup rehydrate can outlast a single ping budget while the
71
+ * process is healthy. */
72
+ const BIND_PROBE_ATTEMPTS = 3;
73
+ /** How long a broker teardown waits for the old process to actually exit before
74
+ * clearing its socket + ownership record. */
75
+ const BROKER_STOP_GRACE_MS = 3000;
76
+ /** Pause between those probes. */
77
+ const BIND_PROBE_INTERVAL_MS = 250;
78
+ function delay(ms) {
79
+ return new Promise((resolve) => setTimeout(resolve, ms));
80
+ }
68
81
  /**
69
82
  * Timeouts for the three synchronous broker clients, split across the process
70
83
  * boundary they now straddle.
@@ -170,6 +183,20 @@ function pidPath() {
170
183
  function tokenPath() {
171
184
  return path.join(agentDir(), 'agent.token');
172
185
  }
186
+ /**
187
+ * Path of the CURRENT SOCKET OWNER's pid.
188
+ *
189
+ * Deliberately NOT `agent.pid`: that file is the standalone service's O_EXCL
190
+ * single-instance claim, and a standalone that loses the socket race stays alive
191
+ * and quiescent while holding it (so launchd's KeepAlive doesn't restart-loop
192
+ * it), ready to take over if the hosted broker stops. Overloading it as the
193
+ * socket-ownership record made the standalone see a live holder and exit
194
+ * immediately — the exact restart loop that guard exists to prevent. This file
195
+ * answers a different question: which pid is serving the socket right now.
196
+ */
197
+ function ownerPath() {
198
+ return path.join(agentDir(), 'agent.owner');
199
+ }
173
200
  /**
174
201
  * Read the current broker capability token, or null if none is present. Clients
175
202
  * read it fresh per request and attach it to every non-ping command; a broker
@@ -425,11 +452,45 @@ export function makeConnectionHandler(handle, token) {
425
452
  conn.on('error', () => { });
426
453
  };
427
454
  }
455
+ /**
456
+ * Whether a live process still owns the broker pid file.
457
+ *
458
+ * A single missed `agentPing` is NOT proof the owner is dead — the broker is
459
+ * single-threaded, so a big `get` or the rehydrate at startup can blow the
460
+ * 700ms ping budget while the process is perfectly healthy. Treating that as
461
+ * death let a starting broker unlink a live socket and rebind, leaving the old
462
+ * process alive holding every unlocked bundle in RAM that no client could reach
463
+ * any more (observed live: two brokers, one socket path, two kernel sockets).
464
+ * The pid file is the second, independent liveness signal that makes the reclaim
465
+ * safe.
466
+ */
467
+ /** Drop our ownership record, but only if it is still ours — never clobber a
468
+ * successor that already claimed the socket. */
469
+ export function releaseBrokerPid() {
470
+ try {
471
+ if (parseInt(fs.readFileSync(ownerPath(), 'utf-8').trim(), 10) === process.pid) {
472
+ fs.unlinkSync(ownerPath());
473
+ }
474
+ }
475
+ catch { /* absent or unreadable — nothing to release */ }
476
+ }
477
+ export function brokerPidAlive() {
478
+ try {
479
+ const holder = parseInt(fs.readFileSync(ownerPath(), 'utf-8').trim(), 10);
480
+ return !isNaN(holder) && holder !== process.pid && isAlive(holder);
481
+ }
482
+ catch {
483
+ return false; // no pid file → nothing claims ownership
484
+ }
485
+ }
428
486
  /**
429
487
  * Bind the shared broker socket without stealing it from another live owner.
430
488
  * Both the standalone service and daemon-hosted broker use this single path so
431
489
  * either startup order is safe: a reachable owner wins, while an unreachable
432
490
  * stale socket is reclaimed once.
491
+ *
492
+ * "Unreachable" is established with retries plus a pid-file liveness check, never
493
+ * a single ping — see {@link brokerPidAlive}.
433
494
  */
434
495
  async function bindBrokerSocket(sock, onConnection) {
435
496
  const listenOnce = () => new Promise((resolve, reject) => {
@@ -453,14 +514,38 @@ async function bindBrokerSocket(sock, onConnection) {
453
514
  writeAgentToken();
454
515
  }
455
516
  catch { /* dir 0700 gates the socket regardless */ }
517
+ // Record ownership at the same moment, for the same reason. The
518
+ // daemon-hosted broker deliberately skips the O_EXCL pid-file GUARD (the
519
+ // daemon owns its instance), but without an ownership RECORD
520
+ // brokerPidAlive() has nothing to read — so a later starter saw an
521
+ // ownerless socket and reclaimed the live hosted broker anyway, which is
522
+ // the primary configuration and the one that actually orphaned here.
523
+ // Writing it from the confirmed owner covers both broker flavours.
524
+ try {
525
+ fs.writeFileSync(ownerPath(), String(process.pid));
526
+ }
527
+ catch { /* dir 0700 gates it */ }
456
528
  resolve(server);
457
529
  });
458
530
  });
459
531
  let bound = await listenOnce();
460
532
  if (bound !== 'inuse')
461
533
  return bound;
462
- if ((await agentPing()).reachable)
463
- return null;
534
+ // Give a busy owner several chances before concluding it is gone. One slow
535
+ // ping used to be enough to evict a healthy broker and orphan its RAM.
536
+ for (let attempt = 0; attempt < BIND_PROBE_ATTEMPTS; attempt++) {
537
+ if ((await agentPing()).reachable)
538
+ return null;
539
+ if (attempt < BIND_PROBE_ATTEMPTS - 1)
540
+ await delay(BIND_PROBE_INTERVAL_MS);
541
+ }
542
+ // Silent but alive is still alive: refuse to steal the socket from a process
543
+ // that still owns the pid file, and let the caller surface the wedge instead
544
+ // of quietly starting a second broker on the same path.
545
+ if (brokerPidAlive()) {
546
+ throw new Error(`Secrets broker socket is held by a live process that is not answering: ${sock}. ` +
547
+ `Run 'agents secrets lock --all' then retry, or stop the stuck broker.`);
548
+ }
464
549
  try {
465
550
  fs.unlinkSync(sock);
466
551
  }
@@ -637,6 +722,7 @@ export async function runSecretsAgent(opts = {}) {
637
722
  fs.unlinkSync(sock);
638
723
  }
639
724
  catch { /* gone */ }
725
+ releaseBrokerPid();
640
726
  releasePid();
641
727
  };
642
728
  sweepTimer = setInterval(sweep, SWEEP_INTERVAL_MS);
@@ -746,6 +832,7 @@ export async function startHostedBroker() {
746
832
  fs.unlinkSync(sock);
747
833
  }
748
834
  catch { /* gone */ }
835
+ releaseBrokerPid();
749
836
  },
750
837
  };
751
838
  }
@@ -1248,6 +1335,12 @@ export async function ensureAgentRunning(timeoutMs = 5000) {
1248
1335
  process.kill(stalePid, 'SIGTERM');
1249
1336
  }
1250
1337
  catch { /* already dead */ }
1338
+ // Wait for it to actually go before clearing its socket and ownership
1339
+ // record. Unlinking them under a live broker is what leaves an orphan
1340
+ // holding every unlocked bundle in RAM with no way to reach it — and it also
1341
+ // destroys the very record brokerPidAlive() reads, so the successor sees an
1342
+ // ownerless socket and reclaims it regardless.
1343
+ waitForExit(stalePid, BROKER_STOP_GRACE_MS);
1251
1344
  }
1252
1345
  try {
1253
1346
  fs.unlinkSync(socketPath());
@@ -1257,6 +1350,10 @@ export async function ensureAgentRunning(timeoutMs = 5000) {
1257
1350
  fs.unlinkSync(pidPath());
1258
1351
  }
1259
1352
  catch { /* gone */ }
1353
+ try {
1354
+ fs.unlinkSync(ownerPath());
1355
+ }
1356
+ catch { /* gone */ }
1260
1357
  const { cmd, args } = brokerSpawn();
1261
1358
  spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
1262
1359
  const deadline = Date.now() + timeoutMs;
@@ -1286,6 +1383,7 @@ async function teardownStaleBroker() {
1286
1383
  process.kill(pid, 'SIGTERM');
1287
1384
  }
1288
1385
  catch { /* gone */ }
1386
+ waitForExit(pid, BROKER_STOP_GRACE_MS); // same reason as ensureAgentRunning
1289
1387
  }
1290
1388
  try {
1291
1389
  fs.unlinkSync(socketPath());
@@ -1295,4 +1393,8 @@ async function teardownStaleBroker() {
1295
1393
  fs.unlinkSync(pidPath());
1296
1394
  }
1297
1395
  catch { /* gone */ }
1396
+ try {
1397
+ fs.unlinkSync(ownerPath());
1398
+ }
1399
+ catch { /* gone */ }
1298
1400
  }
@@ -0,0 +1,46 @@
1
+ export type SecretAuditEvent = 'secrets.get' | 'secrets.unlocked';
2
+ export interface SecretAuditParams {
3
+ /** Which audit event this is — a read (`secrets.get`) or an unlock grant. */
4
+ event: SecretAuditEvent;
5
+ /** Bundle name (never a value). */
6
+ bundle?: string;
7
+ /** Raw keychain item/service name for `secrets get <item>` (never a value). */
8
+ item?: string;
9
+ /** Free-form operation/caller label, e.g. 'export to shell', 'unlock'. */
10
+ operation?: string;
11
+ /**
12
+ * Where the read/grant originated: agent | session | reveal | raw-item |
13
+ * remote | sync-push | broker | broker+durable.
14
+ */
15
+ source?: string;
16
+ status?: 'success' | 'error';
17
+ /** Env/key NAMES exposed — names only, never values. */
18
+ keys?: string[];
19
+ keyCount?: number;
20
+ /** Subset of `keys` backed by the OS keychain — names only. */
21
+ keychainKeys?: string[];
22
+ /** Count of resolved values by kind (literal / keychain / env / ...). */
23
+ kindCounts?: Record<string, number>;
24
+ /**
25
+ * Resolving agent/harness identity the access was scoped to (`*` = a global
26
+ * grant readable by every harness). Falls back to `$AGENTS_AGENT_NAME`.
27
+ */
28
+ agent?: string;
29
+ /** Remote host the value was pulled from, when applicable. */
30
+ host?: string;
31
+ /** Grant lifetime in ms (`secrets.unlocked` only). */
32
+ ttlMs?: number;
33
+ error?: string;
34
+ }
35
+ /**
36
+ * The agent/harness identity to attribute a secret access to. Explicit callers
37
+ * (the bundle reader knows the scope it resolved under) win; otherwise fall back
38
+ * to the ambient `$AGENTS_AGENT_NAME` set on every agent launch.
39
+ */
40
+ export declare function resolveAuditAgent(explicit?: string): string | undefined;
41
+ /**
42
+ * Emit one value-free secret audit event to the raw/operational event stream and
43
+ * the persisted audit log. Only ever pass metadata — the secret value must never
44
+ * reach this function.
45
+ */
46
+ export declare function emitSecretAudit(p: SecretAuditParams): void;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Canonical audit emitter for `agents secrets` value access and unlock grants.
3
+ *
4
+ * Every path that reads a secret VALUE or grants an unlock funnels its audit
5
+ * through here, so the operational event stream — `agents events`, backed by the
6
+ * append-only `~/.agents/events.jsonl` audit log — carries a uniform, value-free
7
+ * provenance record: bundle, key NAMES, the resolving agent/harness identity,
8
+ * operation, source, status. The ts / host / session / caller fields are filled
9
+ * in by `emit()` itself. The secret VALUE is never part of the payload; this
10
+ * helper only ever receives metadata, and `emit()`'s `sanitizePayload` is a
11
+ * second redaction layer.
12
+ *
13
+ * Two event types, both audit-level and non-milestone (so they surface in
14
+ * `agents events` and the persisted audit trail, but are NOT required in the
15
+ * curated `agents activity` / `agents feed` surfaces):
16
+ * - `secrets.get` — a value was READ (exec inject, export, `view --reveal`,
17
+ * raw `get <item>`, remote resolve, sync push,
18
+ * `run --secrets`, and every other bundle read).
19
+ * - `secrets.unlocked` — a bundle was GRANTED into the secrets broker / durable
20
+ * session by `agents secrets unlock`, then readable
21
+ * prompt-free for the grant TTL.
22
+ */
23
+ import { emit } from '../events.js';
24
+ /**
25
+ * The agent/harness identity to attribute a secret access to. Explicit callers
26
+ * (the bundle reader knows the scope it resolved under) win; otherwise fall back
27
+ * to the ambient `$AGENTS_AGENT_NAME` set on every agent launch.
28
+ */
29
+ export function resolveAuditAgent(explicit) {
30
+ const a = explicit || process.env.AGENTS_AGENT_NAME;
31
+ return a && a.length > 0 ? a : undefined;
32
+ }
33
+ /**
34
+ * Emit one value-free secret audit event to the raw/operational event stream and
35
+ * the persisted audit log. Only ever pass metadata — the secret value must never
36
+ * reach this function.
37
+ */
38
+ export function emitSecretAudit(p) {
39
+ const agent = resolveAuditAgent(p.agent);
40
+ emit(p.event, {
41
+ module: 'secrets',
42
+ ...(p.bundle !== undefined ? { bundle: p.bundle } : {}),
43
+ ...(p.item !== undefined ? { item: p.item } : {}),
44
+ ...(p.operation !== undefined ? { operation: p.operation } : {}),
45
+ ...(p.source !== undefined ? { source: p.source } : {}),
46
+ ...(p.status !== undefined ? { status: p.status } : {}),
47
+ ...(p.keys !== undefined ? { keys: p.keys } : {}),
48
+ ...(p.keyCount !== undefined ? { keyCount: p.keyCount } : {}),
49
+ ...(p.keychainKeys !== undefined ? { keychainKeys: p.keychainKeys } : {}),
50
+ ...(p.kindCounts !== undefined ? { kindCounts: p.kindCounts } : {}),
51
+ ...(agent !== undefined ? { agent } : {}),
52
+ ...(p.host !== undefined ? { host: p.host } : {}),
53
+ ...(p.ttlMs !== undefined ? { ttlMs: p.ttlMs } : {}),
54
+ ...(p.error !== undefined ? { error: p.error } : {}),
55
+ });
56
+ }
@@ -43,7 +43,9 @@ export interface VarMeta {
43
43
  }
44
44
  /**
45
45
  * A bundle's prompt policy — how often macOS asks for Touch ID to read it:
46
- * - `daily` (default): ask once, then hold it silently for up to ~7 days.
46
+ * - `hold` (default): ask once, then serve it silently for the configured hold
47
+ * duration (`secrets.agent.holdMs`, 7d by default). Named for what it does —
48
+ * it was called `daily`, which stated a period it never had.
47
49
  * (Historical name — the window is now a rolling ~1 week, not one calendar day.)
48
50
  * Eligible for the secrets-agent — the first real keychain read auto-loads it
49
51
  * (auto-cache is on by default) so concurrent runs read it silently, or `unlock`
@@ -61,12 +63,12 @@ export interface VarMeta {
61
63
  * it loudly rather than silently downgrading to `always`.
62
64
  *
63
65
  * The default is configurable via `secrets.policy` in agents.yaml. Stored on disk
64
- * under the legacy `tier` key (`session` == `daily`, `biometry` == explicit
66
+ * under the legacy `tier` key (`session` == `hold`, `biometry` == explicit
65
67
  * `always`, `none` == `never`, absent == inherit the default) so bundles stay
66
68
  * readable across mixed CLI versions on synced machines. The user-facing
67
- * vocabulary is `policy`/`always`/`daily`/`never`.
69
+ * vocabulary is `policy`/`always`/`hold`/`never`.
68
70
  */
69
- export type SecretsPolicy = 'always' | 'daily' | 'never';
71
+ export type SecretsPolicy = 'always' | 'hold' | 'never';
70
72
  /** A named set of environment variable definitions backed by various secret providers. */
71
73
  export interface SecretsBundle {
72
74
  name: string;
@@ -74,7 +76,7 @@ export interface SecretsBundle {
74
76
  allow_exec?: boolean;
75
77
  /** Which store carries this bundle's items. Absent ⇒ `keychain` (the default). */
76
78
  backend?: SecretsBackend;
77
- /** Prompt policy. Absent ⇒ the configured default (`daily`). Serialized under
79
+ /** Prompt policy. Absent ⇒ the configured default (`hold`). Serialized under
78
80
  * the legacy `tier` key — see SecretsPolicy. */
79
81
  policy?: SecretsPolicy;
80
82
  /** ISO 8601 UTC timestamp. Set once on the first writeBundle() for a bundle. */
@@ -142,9 +144,10 @@ export declare class BundleUndecryptableError extends Error {
142
144
  export declare function readBundleIfDecryptable(name: string): SecretsBundle | null;
143
145
  export declare function readBundle(name: string): SecretsBundle;
144
146
  /** The default prompt policy applied to bundles without an explicit per-bundle
145
- * policy. Configurable via `secrets.policy` in agents.yaml; `daily` (one Touch
146
- * ID per ~7d) unless the user explicitly opts back into prompt-every-time with
147
- * `always`. Best-effort: an unreadable config falls back to the `daily` default. */
147
+ * policy. Configurable via `secrets.policy` in agents.yaml; `hold` (one Touch ID
148
+ * per hold window `secrets.agent.holdMs`, 7d by default) unless the user
149
+ * explicitly opts back into prompt-every-time with `always`. Best-effort: an
150
+ * unreadable config falls back to the `hold` default. */
148
151
  export declare function secretsDefaultPolicy(): SecretsPolicy;
149
152
  /** The effective prompt policy of a bundle (absent ⇒ the configured default). */
150
153
  export declare function bundlePolicy(bundle: SecretsBundle): SecretsPolicy;
@@ -28,6 +28,7 @@ import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainTo
28
28
  import { fileStore } from './filestore.js';
29
29
  import { getVaultSession, vaultDeleteItem, vaultExists, vaultGetItems, vaultGetItem, vaultHasItem, vaultListItems, vaultSetItems, vaultSetItem, } from './vault.js';
30
30
  import { emit } from '../events.js';
31
+ import { emitSecretAudit } from './audit.js';
31
32
  import { readMeta, getHelpersDir } from '../state.js';
32
33
  import { assertNameActiveInResourceProfile, filterNamesForActiveResourceProfile } from '../resource-profiles.js';
33
34
  import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, agentEvictSync, secretsAgentAutoEnabled, secretsHoldMs } from './agent.js';
@@ -324,7 +325,7 @@ export function readBundle(name) {
324
325
  // Absent ⇒ keychain; only set when non-keychain so a keychain bundle
325
326
  // round-trips byte-for-byte.
326
327
  backend: backend === 'keychain' ? undefined : backend,
327
- // Legacy wire key: the policy is persisted under `tier` (`session` == `daily`).
328
+ // Legacy wire key: the policy is persisted under `tier` (`session` == `hold`).
328
329
  policy: parsePolicy(parsed.tier),
329
330
  vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
330
331
  };
@@ -343,14 +344,14 @@ export function readBundle(name) {
343
344
  return bundle;
344
345
  }
345
346
  /** Normalize the persisted prompt policy. The on-disk `tier` key uses legacy
346
- * tokens for cross-version compatibility: `session` ⇒ `daily`, `biometry` ⇒ an
347
+ * tokens for cross-version compatibility: `session` ⇒ `hold`, `biometry` ⇒ an
347
348
  * explicit `always`. An absent token ⇒ undefined, which resolves to the
348
- * configured default policy (`daily`). Persisting an explicit `always` as the
349
- * legacy `biometry` token keeps older CLIs correct — they don't know `daily`,
349
+ * configured default policy (`hold`). Persisting an explicit `always` as the
350
+ * legacy `biometry` token keeps older CLIs correct — they don't know `hold`,
350
351
  * read `biometry` as undefined, and fall back to their own always default. */
351
352
  function parsePolicy(raw) {
352
- if (raw === 'daily' || raw === 'session')
353
- return 'daily';
353
+ if (raw === 'hold' || raw === 'daily' || raw === 'session')
354
+ return 'hold';
354
355
  if (raw === 'always' || raw === 'biometry')
355
356
  return 'always';
356
357
  if (raw === 'never' || raw === 'none')
@@ -358,15 +359,16 @@ function parsePolicy(raw) {
358
359
  return undefined;
359
360
  }
360
361
  /** The default prompt policy applied to bundles without an explicit per-bundle
361
- * policy. Configurable via `secrets.policy` in agents.yaml; `daily` (one Touch
362
- * ID per ~7d) unless the user explicitly opts back into prompt-every-time with
363
- * `always`. Best-effort: an unreadable config falls back to the `daily` default. */
362
+ * policy. Configurable via `secrets.policy` in agents.yaml; `hold` (one Touch ID
363
+ * per hold window `secrets.agent.holdMs`, 7d by default) unless the user
364
+ * explicitly opts back into prompt-every-time with `always`. Best-effort: an
365
+ * unreadable config falls back to the `hold` default. */
364
366
  export function secretsDefaultPolicy() {
365
367
  try {
366
- return readMeta().secrets?.policy === 'always' ? 'always' : 'daily';
368
+ return readMeta().secrets?.policy === 'always' ? 'always' : 'hold';
367
369
  }
368
370
  catch {
369
- return 'daily';
371
+ return 'hold';
370
372
  }
371
373
  }
372
374
  /** The effective prompt policy of a bundle (absent ⇒ the configured default). */
@@ -434,12 +436,12 @@ function prepareBundleWrite(bundle) {
434
436
  allow_exec: bundle.allow_exec ? true : undefined,
435
437
  backend: backend === 'keychain' ? undefined : backend,
436
438
  // Wire format: persist the policy under the legacy `tier` token so older CLI
437
- // versions on other synced machines keep reading it — `daily`⇒`session`,
439
+ // versions on other synced machines keep reading it — `hold`⇒`session`,
438
440
  // explicit `always`⇒`biometry`, `never`⇒`none`. An absent policy omits the
439
- // token entirely and resolves to the configured default (`daily`) on read.
441
+ // token entirely and resolves to the configured default (`hold`) on read.
440
442
  // An older CLI that doesn't know `none` reads it as undefined and falls back
441
443
  // to its own default — safe, since it also lacks the no-ACL write path.
442
- tier: bundle.policy === 'daily' ? 'session'
444
+ tier: bundle.policy === 'hold' ? 'session'
443
445
  : bundle.policy === 'always' ? 'biometry'
444
446
  : bundle.policy === 'never' ? 'none'
445
447
  : undefined,
@@ -546,7 +548,7 @@ function parseBundleMeta(nameHint, json, backend) {
546
548
  description: parsed.description,
547
549
  allow_exec: Boolean(parsed.allow_exec),
548
550
  backend: backend === 'keychain' ? undefined : backend,
549
- // Legacy wire key: the policy is persisted under `tier` (`session` == `daily`).
551
+ // Legacy wire key: the policy is persisted under `tier` (`session` == `hold`).
550
552
  policy: parsePolicy(parsed.tier),
551
553
  vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
552
554
  };
@@ -1111,13 +1113,14 @@ export function readAndResolveBundleEnv(name, opts = {}) {
1111
1113
  // the first cache-populating run.
1112
1114
  const filtered = filterAgentHitBySubsetAndExpiry(hit, opts);
1113
1115
  stampLastUsed(filtered.bundle);
1114
- emit('secrets.get', {
1115
- module: 'secrets',
1116
+ emitSecretAudit({
1117
+ event: 'secrets.get',
1116
1118
  bundle: name,
1117
1119
  operation: opts.caller,
1118
1120
  status: 'success',
1119
1121
  source: 'agent',
1120
1122
  keyCount: Object.keys(filtered.env).length,
1123
+ agent: harness,
1121
1124
  });
1122
1125
  return filtered;
1123
1126
  }
@@ -1137,13 +1140,14 @@ export function readAndResolveBundleEnv(name, opts = {}) {
1137
1140
  // MADE in (resolved.harness), never the asking scope — re-warming a global
1138
1141
  // grant as `claude` would silently narrow it for every other harness.
1139
1142
  agentAutoLoadSync(name, session.bundle, session.env, Math.max(1, session.expiresAt - Date.now()), resolved.harness);
1140
- emit('secrets.get', {
1141
- module: 'secrets',
1143
+ emitSecretAudit({
1144
+ event: 'secrets.get',
1142
1145
  bundle: name,
1143
1146
  operation: opts.caller,
1144
1147
  status: 'success',
1145
1148
  source: 'session',
1146
1149
  keyCount: Object.keys(filtered.env).length,
1150
+ agent: harness,
1147
1151
  });
1148
1152
  return filtered;
1149
1153
  }
@@ -1198,6 +1202,9 @@ export function readAndResolveBundleEnv(name, opts = {}) {
1198
1202
  ? getKeychainTokens([...new Set([metaItem, ...secretItems])], {
1199
1203
  agent: opts.agent || process.env.AGENTS_AGENT_NAME || 'Agents CLI',
1200
1204
  bundle: name,
1205
+ // The session that triggered the read, so a Touch ID prompt is
1206
+ // attributable when several agents run at once. Exported by exec.ts.
1207
+ sessionId: process.env.AGENT_SESSION_ID || process.env.AGENTS_SESSION_ID,
1201
1208
  reason: opts.caller ? `to ${opts.caller}` : reason,
1202
1209
  duration: opts.duration || humanUnlockDuration(secretsHoldMs()),
1203
1210
  defaultPolicy: secretsDefaultPolicy(),
@@ -1233,7 +1240,7 @@ export function readAndResolveBundleEnv(name, opts = {}) {
1233
1240
  description: parsed.description,
1234
1241
  allow_exec: Boolean(parsed.allow_exec),
1235
1242
  backend: backend === 'keychain' ? undefined : backend,
1236
- // Legacy wire key: the policy is persisted under `tier` (`session` == `daily`).
1243
+ // Legacy wire key: the policy is persisted under `tier` (`session` == `hold`).
1237
1244
  policy: parsePolicy(parsed.tier),
1238
1245
  vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
1239
1246
  };
@@ -1269,8 +1276,8 @@ export function readAndResolveBundleEnv(name, opts = {}) {
1269
1276
  const keys = [...selectedKeys].sort();
1270
1277
  keychainKeys.sort();
1271
1278
  const emitReadAudit = (status, err) => {
1272
- emit('secrets.get', {
1273
- module: 'secrets',
1279
+ emitSecretAudit({
1280
+ event: 'secrets.get',
1274
1281
  bundle: bundle.name,
1275
1282
  operation: opts.caller,
1276
1283
  status,
@@ -1278,6 +1285,7 @@ export function readAndResolveBundleEnv(name, opts = {}) {
1278
1285
  keys,
1279
1286
  keychainKeys,
1280
1287
  kindCounts,
1288
+ agent: opts.agent,
1281
1289
  error: err instanceof Error ? err.message : (err ? String(err) : undefined),
1282
1290
  });
1283
1291
  };
@@ -1328,7 +1336,7 @@ export function readAndResolveBundleEnv(name, opts = {}) {
1328
1336
  if (backend === 'keychain' &&
1329
1337
  !opts.noAgent &&
1330
1338
  process.env.AGENTS_SECRETS_NO_AGENT !== '1' &&
1331
- bundlePolicy(bundle) === 'daily' &&
1339
+ bundlePolicy(bundle) === 'hold' &&
1332
1340
  secretsAgentAutoEnabled() &&
1333
1341
  canCacheResolvedEnv(bundle, selectedKeys, opts.keyMode)) {
1334
1342
  agentAutoLoadSync(name, bundle, env, secretsHoldMs(), opts.agent || process.env.AGENTS_AGENT_NAME || GLOBAL_HARNESS);
@@ -213,9 +213,10 @@ export declare function hasKeychainToken(item: string): boolean;
213
213
  export interface KeychainReadContext {
214
214
  agent?: string;
215
215
  bundle?: string;
216
+ sessionId?: string;
216
217
  reason?: string;
217
218
  duration?: string;
218
- defaultPolicy?: 'daily' | 'always' | 'never';
219
+ defaultPolicy?: 'hold' | 'always' | 'never';
219
220
  forceDuration?: boolean;
220
221
  }
221
222
  export declare function keychainOperationPrompt(context?: KeychainReadContext): string;
@@ -30,6 +30,7 @@ import * as path from 'path';
30
30
  import { linuxBackend, usesFileFallback as linuxUsesFileFallback, importNativeSecretToolItems } from './linux.js';
31
31
  import { windowsBackend, usesFileFallback as windowsUsesFileFallback, importNativeCredManItems } from './windows.js';
32
32
  import { getKeychainHelperPath } from './install-helper.js';
33
+ import { deriveShortId } from '../session/short-id.js';
33
34
  const SERVICE_PREFIX = 'agents-cli';
34
35
  export const SECRETS_ITEM_PREFIX = `${SERVICE_PREFIX}.secrets.`;
35
36
  const BUNDLES_ITEM_PREFIX = `${SERVICE_PREFIX}.bundles.`;
@@ -710,9 +711,12 @@ export function hasKeychainToken(item) {
710
711
  export function keychainOperationPrompt(context = {}) {
711
712
  const agent = context.agent || 'Agents CLI';
712
713
  const bundle = context.bundle ? ` the '${context.bundle}' bundle` : ' secrets';
714
+ // Which session triggered the read — the short-id disambiguates an unexpected
715
+ // prompt when several agents run at once (interactive + headless + exec).
716
+ const session = context.sessionId ? ` (session ${deriveShortId(context.sessionId)})` : '';
713
717
  const duration = context.duration ? ` for ${context.duration}` : '';
714
718
  const reason = context.reason ? ` ${context.reason}` : '';
715
- return `${agent} is requesting to unlock${bundle}${duration}${reason}.`;
719
+ return `${agent} is requesting to unlock${bundle}${session}${duration}${reason}.`;
716
720
  }
717
721
  export function getKeychainToken(item, context = {}) {
718
722
  // Errors keep the requested (human-readable) name; the storage name may be
@@ -820,7 +824,9 @@ export function getKeychainTokens(items, context = {}) {
820
824
  ...process.env,
821
825
  AGENTS_KEYCHAIN_PROMPT: keychainOperationPrompt(context),
822
826
  AGENTS_KEYCHAIN_PROMPT_BASE: keychainOperationPrompt({ ...context, duration: undefined }),
823
- AGENTS_KEYCHAIN_DEFAULT_POLICY: context.defaultPolicy || 'daily',
827
+ // The signed helper's own vocabulary is unchanged (it predates the rename
828
+ // and ships as a separately-versioned binary), so map to its legacy token.
829
+ AGENTS_KEYCHAIN_DEFAULT_POLICY: (context.defaultPolicy ?? 'hold') === 'hold' ? 'daily' : context.defaultPolicy,
824
830
  AGENTS_KEYCHAIN_FORCE_DURATION: context.forceDuration ? '1' : '0',
825
831
  },
826
832
  stdio: ['ignore', 'pipe', 'pipe'],