@phnx-labs/agents-cli 1.22.83 → 1.22.85

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 (291) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +80 -53
  3. package/dist/bootstrap.d.ts +7 -6
  4. package/dist/bootstrap.js +20 -27
  5. package/dist/cli/command-registry.js +5 -1
  6. package/dist/commands/accounts.d.ts +6 -13
  7. package/dist/commands/accounts.js +330 -143
  8. package/dist/commands/apply.js +1 -1
  9. package/dist/commands/auth-mint.d.ts +7 -3
  10. package/dist/commands/auth-mint.js +21 -9
  11. package/dist/commands/auth.js +2 -3
  12. package/dist/commands/browser.js +10 -10
  13. package/dist/commands/daemon.d.ts +7 -3
  14. package/dist/commands/daemon.js +40 -25
  15. package/dist/commands/doctor.js +28 -2
  16. package/dist/commands/exec.js +87 -39
  17. package/dist/commands/fleet-capture.js +21 -2
  18. package/dist/commands/harness-wizard.js +2 -2
  19. package/dist/commands/lease.js +7 -12
  20. package/dist/commands/profiles.d.ts +2 -2
  21. package/dist/commands/profiles.js +20 -13
  22. package/dist/commands/repo.js +3 -3
  23. package/dist/commands/run-account-picker.d.ts +3 -3
  24. package/dist/commands/run-account-picker.js +29 -30
  25. package/dist/commands/secrets-passthrough.d.ts +20 -0
  26. package/dist/commands/secrets-passthrough.js +43 -0
  27. package/dist/commands/sessions-trace.d.ts +11 -0
  28. package/dist/commands/sessions-trace.js +71 -0
  29. package/dist/commands/setup-accounts.d.ts +3 -2
  30. package/dist/commands/setup-accounts.js +4 -4
  31. package/dist/commands/setup-secrets.d.ts +16 -21
  32. package/dist/commands/setup-secrets.js +43 -224
  33. package/dist/commands/ssh.js +2 -2
  34. package/dist/commands/sync.js +1 -1
  35. package/dist/commands/update.d.ts +10 -0
  36. package/dist/commands/update.js +43 -40
  37. package/dist/commands/versions.d.ts +11 -11
  38. package/dist/commands/versions.js +131 -83
  39. package/dist/commands/view.d.ts +1 -9
  40. package/dist/commands/view.js +41 -55
  41. package/dist/commands/webhook.js +5 -5
  42. package/dist/commands/workflows.js +2 -1
  43. package/dist/index.d.ts +12 -12
  44. package/dist/index.js +13 -42
  45. package/dist/lib/account-capabilities.d.ts +1 -1
  46. package/dist/lib/account-capabilities.js +1 -1
  47. package/dist/lib/account-catalog.d.ts +121 -6
  48. package/dist/lib/account-catalog.js +421 -7
  49. package/dist/lib/account-registry.d.ts +8 -30
  50. package/dist/lib/account-registry.js +36 -79
  51. package/dist/lib/account-schema.d.ts +1 -1
  52. package/dist/lib/account-schema.js +1 -1
  53. package/dist/lib/accounting/account-pool-collect.js +16 -4
  54. package/dist/lib/accounting/rotate.d.ts +20 -7
  55. package/dist/lib/accounting/rotate.js +89 -13
  56. package/dist/lib/accounting/usage.js +11 -16
  57. package/dist/lib/accounts/add.d.ts +138 -0
  58. package/dist/lib/accounts/add.js +651 -0
  59. package/dist/lib/accounts/migrate.d.ts +117 -0
  60. package/dist/lib/accounts/migrate.js +536 -0
  61. package/dist/lib/accounts/slots.d.ts +13 -0
  62. package/dist/lib/accounts/slots.js +100 -0
  63. package/dist/lib/agent-spec/agents.d.ts +16 -0
  64. package/dist/lib/agent-spec/agents.js +32 -4
  65. package/dist/lib/app-bundle-install.js +5 -4
  66. package/dist/lib/auth-health.d.ts +3 -0
  67. package/dist/lib/auth-health.js +11 -0
  68. package/dist/lib/auth-mint.d.ts +52 -16
  69. package/dist/lib/auth-mint.js +117 -39
  70. package/dist/lib/browser/chrome.d.ts +1 -1
  71. package/dist/lib/browser/chrome.js +5 -5
  72. package/dist/lib/byok-usage.js +3 -3
  73. package/dist/lib/claude-account-token.d.ts +47 -3
  74. package/dist/lib/claude-account-token.js +172 -50
  75. package/dist/lib/cloud/antigravity.js +4 -4
  76. package/dist/lib/cloud/cursor.js +4 -4
  77. package/dist/lib/crabbox/cli.d.ts +1 -1
  78. package/dist/lib/crabbox/cli.js +6 -6
  79. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  80. package/dist/lib/crabbox/runtimes.js +5 -5
  81. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  82. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  83. package/dist/lib/daemon/auth-sync-service.js +28 -1
  84. package/dist/lib/daemon/daemon.d.ts +6 -16
  85. package/dist/lib/daemon/daemon.js +52 -97
  86. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  87. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  88. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  89. package/dist/lib/daemon/harness-update-service.js +1 -1
  90. package/dist/lib/daemon/runner.d.ts +15 -3
  91. package/dist/lib/daemon/runner.js +49 -24
  92. package/dist/lib/daemon-health.d.ts +1 -2
  93. package/dist/lib/daemon-health.js +7 -6
  94. package/dist/lib/daemon-services.d.ts +1 -1
  95. package/dist/lib/daemon-services.js +1 -11
  96. package/dist/lib/daemon-ticks.js +23 -2
  97. package/dist/lib/daemon-webhooks.d.ts +8 -7
  98. package/dist/lib/daemon-webhooks.js +10 -9
  99. package/dist/lib/device-config.js +1 -1
  100. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  101. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  102. package/dist/lib/devices/harness-inventory.js +126 -4
  103. package/dist/lib/doctor-diff.js +2 -1
  104. package/dist/lib/exec-account-home.d.ts +38 -0
  105. package/dist/lib/exec-account-home.js +164 -0
  106. package/dist/lib/exec.d.ts +30 -0
  107. package/dist/lib/exec.js +101 -31
  108. package/dist/lib/feed/activity-stream.d.ts +60 -0
  109. package/dist/lib/feed/activity-stream.js +271 -0
  110. package/dist/lib/feed/activity.d.ts +7 -0
  111. package/dist/lib/feed/activity.js +10 -4
  112. package/dist/lib/feed/watch.d.ts +9 -3
  113. package/dist/lib/feed/watch.js +72 -36
  114. package/dist/lib/fleet/apply.d.ts +2 -2
  115. package/dist/lib/fleet/apply.js +4 -4
  116. package/dist/lib/fleet/auth-sync.js +4 -3
  117. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  118. package/dist/lib/fleet-shared-state.d.ts +6 -0
  119. package/dist/lib/harness/adapter.d.ts +7 -7
  120. package/dist/lib/harness/adapter.js +10 -6
  121. package/dist/lib/harness/adapters/grok.js +8 -3
  122. package/dist/lib/harness/adapters/muse.js +1 -1
  123. package/dist/lib/harness/adapters/opencode.js +12 -3
  124. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  125. package/dist/lib/harness-auth-capabilities.js +52 -0
  126. package/dist/lib/helper-versions.d.ts +6 -4
  127. package/dist/lib/helper-versions.js +5 -4
  128. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  129. package/dist/lib/hosts/credential-transport.js +37 -0
  130. package/dist/lib/hosts/dispatch.d.ts +7 -0
  131. package/dist/lib/hosts/dispatch.js +13 -6
  132. package/dist/lib/identity/client.d.ts +3 -3
  133. package/dist/lib/identity/client.js +3 -3
  134. package/dist/lib/installations/index.d.ts +1 -1
  135. package/dist/lib/installations/index.js +1 -1
  136. package/dist/lib/installations/migrate.js +9 -0
  137. package/dist/lib/installations/resolve.js +1 -1
  138. package/dist/lib/installations/shims.d.ts +15 -0
  139. package/dist/lib/installations/shims.js +69 -0
  140. package/dist/lib/installations/store.d.ts +43 -0
  141. package/dist/lib/installations/store.js +75 -7
  142. package/dist/lib/installations/versions.js +1 -1
  143. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  144. package/dist/lib/menubar/install-menubar.js +3 -3
  145. package/dist/lib/native-accounts.d.ts +35 -0
  146. package/dist/lib/native-accounts.js +41 -0
  147. package/dist/lib/net-close.d.ts +11 -0
  148. package/dist/lib/net-close.js +25 -0
  149. package/dist/lib/openclaw-keychain.js +27 -1
  150. package/dist/lib/profiles.js +22 -9
  151. package/dist/lib/project-resources.js +1 -1
  152. package/dist/lib/reserved-stores.d.ts +53 -0
  153. package/dist/lib/reserved-stores.js +120 -0
  154. package/dist/lib/secrets-client.d.ts +191 -0
  155. package/dist/lib/secrets-client.js +710 -0
  156. package/dist/lib/secrets-policy.d.ts +222 -0
  157. package/dist/lib/secrets-policy.js +483 -0
  158. package/dist/lib/secrets-types.d.ts +188 -0
  159. package/dist/lib/secrets-types.js +24 -0
  160. package/dist/lib/service-manifest.js +3 -4
  161. package/dist/lib/session/active.d.ts +38 -4
  162. package/dist/lib/session/active.js +36 -4
  163. package/dist/lib/session/bash-command.js +10 -0
  164. package/dist/lib/session/db.d.ts +65 -2
  165. package/dist/lib/session/db.js +178 -18
  166. package/dist/lib/session/mirror.d.ts +5 -0
  167. package/dist/lib/session/mirror.js +166 -0
  168. package/dist/lib/session/parse.d.ts +49 -0
  169. package/dist/lib/session/parse.js +324 -30
  170. package/dist/lib/session/prompt.d.ts +93 -2
  171. package/dist/lib/session/prompt.js +266 -15
  172. package/dist/lib/session/remote/peer-stream.d.ts +47 -0
  173. package/dist/lib/session/remote/peer-stream.js +142 -0
  174. package/dist/lib/session/remote/watch.d.ts +1 -1
  175. package/dist/lib/session/remote/watch.js +41 -42
  176. package/dist/lib/session/session-cache.d.ts +9 -0
  177. package/dist/lib/session/session-cache.js +40 -2
  178. package/dist/lib/session/sync/config.js +2 -2
  179. package/dist/lib/session/timeline-pass.d.ts +129 -0
  180. package/dist/lib/session/timeline-pass.js +323 -0
  181. package/dist/lib/session/timeline.d.ts +182 -0
  182. package/dist/lib/session/timeline.js +636 -0
  183. package/dist/lib/session/types.d.ts +155 -1
  184. package/dist/lib/sha256-asset.d.ts +11 -16
  185. package/dist/lib/sha256-asset.js +11 -16
  186. package/dist/lib/share/config.js +14 -5
  187. package/dist/lib/signin-badge.d.ts +26 -0
  188. package/dist/lib/signin-badge.js +42 -0
  189. package/dist/lib/staleness/detectors/workflows.js +3 -105
  190. package/dist/lib/staleness/writers/workflows.js +2 -1
  191. package/dist/lib/state.d.ts +7 -4
  192. package/dist/lib/state.js +18 -7
  193. package/dist/lib/summarizer/pass.d.ts +2 -0
  194. package/dist/lib/summarizer/pass.js +14 -1
  195. package/dist/lib/summarizer/summarize.d.ts +7 -0
  196. package/dist/lib/summarizer/summarize.js +3 -0
  197. package/dist/lib/sync-umbrella.js +2 -2
  198. package/dist/lib/teams/agents.js +1 -1
  199. package/dist/lib/types.d.ts +74 -33
  200. package/dist/lib/view-types.d.ts +6 -3
  201. package/dist/lib/workflows-registry.d.ts +71 -0
  202. package/dist/lib/workflows-registry.js +280 -0
  203. package/dist/lib/workflows.d.ts +21 -21
  204. package/dist/lib/workflows.js +12 -327
  205. package/package.json +2 -3
  206. package/scripts/postinstall.js +4 -29
  207. package/dist/commands/secrets-import.d.ts +0 -18
  208. package/dist/commands/secrets-import.js +0 -74
  209. package/dist/commands/secrets-migrate.d.ts +0 -25
  210. package/dist/commands/secrets-migrate.js +0 -334
  211. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  212. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  213. package/dist/commands/secrets-sync.d.ts +0 -11
  214. package/dist/commands/secrets-sync.js +0 -153
  215. package/dist/commands/secrets-vault.d.ts +0 -10
  216. package/dist/commands/secrets-vault.js +0 -130
  217. package/dist/commands/secrets.d.ts +0 -212
  218. package/dist/commands/secrets.js +0 -3030
  219. package/dist/lib/accounts/connect.d.ts +0 -170
  220. package/dist/lib/accounts/connect.js +0 -383
  221. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  222. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  223. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  224. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  225. package/dist/lib/secrets/agent.d.ts +0 -358
  226. package/dist/lib/secrets/agent.js +0 -1291
  227. package/dist/lib/secrets/audit.d.ts +0 -46
  228. package/dist/lib/secrets/audit.js +0 -101
  229. package/dist/lib/secrets/bundles.d.ts +0 -290
  230. package/dist/lib/secrets/bundles.js +0 -1547
  231. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  232. package/dist/lib/secrets/download-keychain.js +0 -70
  233. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  234. package/dist/lib/secrets/drivers/rush.js +0 -90
  235. package/dist/lib/secrets/fallback.d.ts +0 -48
  236. package/dist/lib/secrets/fallback.js +0 -48
  237. package/dist/lib/secrets/filestore.d.ts +0 -222
  238. package/dist/lib/secrets/filestore.js +0 -1099
  239. package/dist/lib/secrets/headless.d.ts +0 -43
  240. package/dist/lib/secrets/headless.js +0 -56
  241. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  242. package/dist/lib/secrets/icloud-import.js +0 -206
  243. package/dist/lib/secrets/index.d.ts +0 -451
  244. package/dist/lib/secrets/index.js +0 -1568
  245. package/dist/lib/secrets/install-helper.d.ts +0 -72
  246. package/dist/lib/secrets/install-helper.js +0 -245
  247. package/dist/lib/secrets/lease.d.ts +0 -25
  248. package/dist/lib/secrets/lease.js +0 -44
  249. package/dist/lib/secrets/linux.d.ts +0 -77
  250. package/dist/lib/secrets/linux.js +0 -393
  251. package/dist/lib/secrets/list-filter.d.ts +0 -109
  252. package/dist/lib/secrets/list-filter.js +0 -261
  253. package/dist/lib/secrets/mcp.d.ts +0 -93
  254. package/dist/lib/secrets/mcp.js +0 -211
  255. package/dist/lib/secrets/profiles.d.ts +0 -10
  256. package/dist/lib/secrets/profiles.js +0 -13
  257. package/dist/lib/secrets/push.d.ts +0 -133
  258. package/dist/lib/secrets/push.js +0 -273
  259. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  260. package/dist/lib/secrets/rc-hygiene.js +0 -143
  261. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  262. package/dist/lib/secrets/read-backoff.js +0 -64
  263. package/dist/lib/secrets/reaper.d.ts +0 -90
  264. package/dist/lib/secrets/reaper.js +0 -243
  265. package/dist/lib/secrets/remote.d.ts +0 -152
  266. package/dist/lib/secrets/remote.js +0 -344
  267. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  268. package/dist/lib/secrets/reserved-sync.js +0 -147
  269. package/dist/lib/secrets/scope.d.ts +0 -26
  270. package/dist/lib/secrets/scope.js +0 -29
  271. package/dist/lib/secrets/session-store.d.ts +0 -107
  272. package/dist/lib/secrets/session-store.js +0 -342
  273. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  274. package/dist/lib/secrets/sync-backend.js +0 -13
  275. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  276. package/dist/lib/secrets/sync-commands.js +0 -21
  277. package/dist/lib/secrets/sync.d.ts +0 -48
  278. package/dist/lib/secrets/sync.js +0 -237
  279. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  280. package/dist/lib/secrets/unlock-hints.js +0 -36
  281. package/dist/lib/secrets/usage-db.d.ts +0 -46
  282. package/dist/lib/secrets/usage-db.js +0 -96
  283. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  284. package/dist/lib/secrets/vault-age-helper.js +0 -34
  285. package/dist/lib/secrets/vault.d.ts +0 -49
  286. package/dist/lib/secrets/vault.js +0 -399
  287. package/dist/lib/secrets/windows.d.ts +0 -81
  288. package/dist/lib/secrets/windows.js +0 -556
  289. package/scripts/install-helper.js +0 -97
  290. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  291. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -224,6 +224,62 @@ export function listInstallations(agent) {
224
224
  }
225
225
  return out;
226
226
  }
227
+ /**
228
+ * The label of the single managed installation every harness gets under the
229
+ * account model v2 (PHNX-3940). One managed install per harness per device;
230
+ * accounts live in credential slots beside it, and a second home is an expert
231
+ * `--isolated` copy, never another managed install.
232
+ */
233
+ export const MANAGED_INSTALLATION_LABEL = 'main';
234
+ /**
235
+ * Resolve the ONE managed installation of a harness, or null when the harness
236
+ * has no managed install yet. The winner is deterministic: the `main` label
237
+ * first, then the recorded global default (the install the user actually
238
+ * launches), then the first remaining non-isolated installation by label.
239
+ * Isolated copies (`agents add --isolated`) never count — they are expert
240
+ * second homes, not the managed install.
241
+ */
242
+ export function resolveManagedInstallation(agent) {
243
+ const managed = listInstallations(agent).filter((i) => !isVersionIsolated(agent, i.label));
244
+ if (managed.length === 0)
245
+ return null;
246
+ const main = managed.find((i) => i.label === MANAGED_INSTALLATION_LABEL);
247
+ if (main)
248
+ return main;
249
+ const globalDefault = getGlobalDefault(agent);
250
+ const def = globalDefault ? managed.find((i) => i.label === globalDefault) : undefined;
251
+ return def ?? managed[0];
252
+ }
253
+ /**
254
+ * The taught entry point for "make this harness runnable" (PHNX-3940): return
255
+ * the harness's single managed installation, installing the current release
256
+ * into the `main` label when the harness is not installed yet. `release` pins
257
+ * the fresh install to a concrete release (recorded as a pinned update policy
258
+ * by `installVersion`); it has no effect when an installation already exists —
259
+ * moving an existing install is `agents update <agent> --to <release>`'s job.
260
+ *
261
+ * Throws (fail loud) when the install fails; never returns a half-installed
262
+ * record.
263
+ */
264
+ export async function ensureHarnessInstallation(agent, opts = {}) {
265
+ const existing = resolveManagedInstallation(agent);
266
+ if (existing)
267
+ return { installation: existing, installed: false };
268
+ const release = opts.release ?? 'latest';
269
+ // installVersion lives in ./versions.js, which imports this file. A dynamic
270
+ // import keeps the static module graph acyclic (see the docblock at the top
271
+ // of this file) while the call itself runs after both modules are loaded.
272
+ const { installVersion } = await import('./versions.js');
273
+ const result = await installVersion(agent, release, opts.onProgress, { installationLabel: MANAGED_INSTALLATION_LABEL });
274
+ if (!result.success) {
275
+ throw new Error(result.error || `Failed to install ${agent}@${release}.`);
276
+ }
277
+ const record = readInstallation(agent, MANAGED_INSTALLATION_LABEL);
278
+ if (!record) {
279
+ throw new Error(`Install of ${agent}@${release} reported success but left no installation record at ${MANAGED_INSTALLATION_LABEL}.`);
280
+ }
281
+ return { installation: record, installed: true };
282
+ }
227
283
  /**
228
284
  * Get the directory where a specific version is installed.
229
285
  */
@@ -534,6 +590,13 @@ function collapseGlobalBinaryVersions(agent, versions) {
534
590
  * For a self-updating global-binary agent (droid) every version dir resolves to
535
591
  * the SAME binary, so this collapses them to a single canonical entry — one
536
592
  * install, one row in `agents view`, never the phantom set of semver dir names.
593
+ *
594
+ * A directory is an installation only when it has a working launch binary.
595
+ * Account credential slots (PHNX-3940, `~/.agents/.history/accounts/**`) are
596
+ * HOME-shaped dirs with no binary and no `installation.json`, so they can
597
+ * never appear here. Record-less pre-frozen installs are reported as-is; their
598
+ * migration belongs to the record-based paths (`listInstallations`), never to
599
+ * this read surface.
537
600
  */
538
601
  export function listInstalledVersions(agent) {
539
602
  const agentVersionsDir = path.join(getVersionsDir(), agent);
@@ -554,13 +617,18 @@ export function listInstalledVersions(agent) {
554
617
  const entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
555
618
  const versions = [];
556
619
  for (const entry of entries) {
557
- if (entry.isDirectory()) {
558
- // Probe the real launch binary (isVersionInstalled), not just the
559
- // node_modules/.bin wrapper — a gutted install must not count as healthy
560
- // in the balanced account/version picker.
561
- if (isVersionInstalled(agent, entry.name)) {
562
- versions.push(entry.name);
563
- }
620
+ if (!entry.isDirectory())
621
+ continue;
622
+ // Probe the real launch binary (isVersionInstalled), not just the
623
+ // node_modules/.bin wrapper — a gutted install must not count as healthy
624
+ // in the balanced account/version picker. A dir with no working binary
625
+ // (a slot, a partial download) is not an installation at all. Read-only:
626
+ // `agents view` enumerates through here and must leave every version home
627
+ // byte-identical (issue #2058), so a record-less pre-frozen install is
628
+ // reported as-is — its migration belongs to the record-based paths
629
+ // (listInstallations), never to a read surface.
630
+ if (isVersionInstalled(agent, entry.name)) {
631
+ versions.push(entry.name);
564
632
  }
565
633
  }
566
634
  versions.sort(compareVersions);
@@ -1044,7 +1044,7 @@ export async function installVersion(agent, version, onProgress, opts) {
1044
1044
  throw new Error(`Invalid version: ${JSON.stringify(version)}`);
1045
1045
  }
1046
1046
  // A caller-supplied installation label decouples the addressable version-dir
1047
- // name from the vendor release (PHNX-3940): `agents accounts connect` mints an
1047
+ // name from the vendor release (PHNX-3940): `agents accounts add` mints an
1048
1048
  // opaque stable label so ten accounts can share the same upstream `latest`
1049
1049
  // under distinct isolated homes, each with its own native login. It follows
1050
1050
  // the same identity/release split the installScript branch already uses via
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Install + lifecycle for the macOS menu-bar helper (`MenubarHelper.app`).
3
3
  *
4
- * Mirrors `src/lib/secrets/install-helper.ts` (stable Application Support path,
5
- * survives npm re-sign) and the secrets-agent launchd pattern in
6
- * `src/lib/secrets/agent.ts` (RunAtLoad + KeepAlive user service).
4
+ * Mirrors the stable-Application-Support-path-survives-npm-re-sign pattern and
5
+ * a RunAtLoad + KeepAlive user service, the same shape the deleted embedded
6
+ * secrets-agent used before it moved to the standalone secrets-cli (PHNX-3989).
7
7
  *
8
8
  * The helper is a no-Dock `.accessory` status-bar app. It reads live agent
9
9
  * state directly from disk and shells `agents` only for actions, so the plist
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Install + lifecycle for the macOS menu-bar helper (`MenubarHelper.app`).
3
3
  *
4
- * Mirrors `src/lib/secrets/install-helper.ts` (stable Application Support path,
5
- * survives npm re-sign) and the secrets-agent launchd pattern in
6
- * `src/lib/secrets/agent.ts` (RunAtLoad + KeepAlive user service).
4
+ * Mirrors the stable-Application-Support-path-survives-npm-re-sign pattern and
5
+ * a RunAtLoad + KeepAlive user service, the same shape the deleted embedded
6
+ * secrets-agent used before it moved to the standalone secrets-cli (PHNX-3989).
7
7
  *
8
8
  * The helper is a no-Dock `.accessory` status-bar app. It reads live agent
9
9
  * state directly from disk and shells `agents` only for actions, so the plist
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Native-account lookup leaf (PHNX-3940).
3
+ *
4
+ * A PURE, types-only module: it answers "which registered account is this
5
+ * email / what does its identity key decode to" from a `Meta` snapshot passed
6
+ * in by the caller — it never reads state, the keychain, or the network, and it
7
+ * imports nothing from `account-registry` (which imports the agent-spec engine).
8
+ * That import-graph constraint is load-bearing: `agent-spec/agents.ts`
9
+ * completes an email-only worker home from the registry row here, and importing
10
+ * `account-registry` there would be a cycle. `account-registry` re-exports both
11
+ * functions so existing callers keep one import site.
12
+ *
13
+ * The merge of central (`accounts.native`, `scope:'version'`) and device-scoped
14
+ * (`deviceAccounts.native`) rows mirrors `listNativeAccounts`, inlined so this
15
+ * module stays a leaf.
16
+ */
17
+ import type { AgentId, Meta, NativeAccountRecord } from './types.js';
18
+ /**
19
+ * The one native row for `agent` whose `identityLabel` (email) matches, or null.
20
+ *
21
+ * Returns null when zero OR several rows match — never guesses. Several rows can
22
+ * share one email (a Team seat AND a personal Max under the same address are two
23
+ * accounts, two orgs), and picking one would attribute a worker home to the
24
+ * wrong org. The read-side caller falls closed to email-only identity in that
25
+ * case, which is correct: an ambiguous identity is better left unresolved than
26
+ * resolved wrong.
27
+ */
28
+ export declare function registeredNativeAccountForEmail(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, agent: AgentId, email: string): NativeAccountRecord | null;
29
+ /**
30
+ * Decode an `<agent>:<label>=<value>[:<label>=<value>…]` identity key into its
31
+ * parts, or null when it does not belong to `agent` or is malformed. The key
32
+ * shape is what `buildIdentityKey` in `agent-spec/agents.ts` writes — e.g.
33
+ * `claude:account=<uuid>:org=<uuid>` → `{ account, org }`.
34
+ */
35
+ export declare function parseNativeIdentityKey(agent: AgentId, identityKey: string): Record<string, string> | null;
@@ -0,0 +1,41 @@
1
+ /** Central + device-scoped native rows, device winning on a shared id. */
2
+ function mergedNativeAccounts(meta) {
3
+ const merged = { ...meta.accounts?.native, ...meta.deviceAccounts?.native };
4
+ return Object.values(merged);
5
+ }
6
+ /**
7
+ * The one native row for `agent` whose `identityLabel` (email) matches, or null.
8
+ *
9
+ * Returns null when zero OR several rows match — never guesses. Several rows can
10
+ * share one email (a Team seat AND a personal Max under the same address are two
11
+ * accounts, two orgs), and picking one would attribute a worker home to the
12
+ * wrong org. The read-side caller falls closed to email-only identity in that
13
+ * case, which is correct: an ambiguous identity is better left unresolved than
14
+ * resolved wrong.
15
+ */
16
+ export function registeredNativeAccountForEmail(meta, agent, email) {
17
+ const needle = email.trim().toLowerCase();
18
+ if (!needle)
19
+ return null;
20
+ const matches = mergedNativeAccounts(meta).filter((account) => account.agent === agent && account.identityLabel?.trim().toLowerCase() === needle);
21
+ return matches.length === 1 ? matches[0] : null;
22
+ }
23
+ /**
24
+ * Decode an `<agent>:<label>=<value>[:<label>=<value>…]` identity key into its
25
+ * parts, or null when it does not belong to `agent` or is malformed. The key
26
+ * shape is what `buildIdentityKey` in `agent-spec/agents.ts` writes — e.g.
27
+ * `claude:account=<uuid>:org=<uuid>` → `{ account, org }`.
28
+ */
29
+ export function parseNativeIdentityKey(agent, identityKey) {
30
+ const prefix = `${agent}:`;
31
+ if (!identityKey.startsWith(prefix))
32
+ return null;
33
+ const parts = {};
34
+ for (const segment of identityKey.slice(prefix.length).split(':')) {
35
+ const eq = segment.indexOf('=');
36
+ if (eq < 1 || eq === segment.length - 1)
37
+ return null;
38
+ parts[segment.slice(0, eq)] = segment.slice(eq + 1);
39
+ }
40
+ return Object.keys(parts).length > 0 ? parts : null;
41
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Bounded-close helper for a local `net.Server`. Generic — no secrets/engine
3
+ * dependency — so consumers that only needed it for that reason (the webhook
4
+ * receiver, the daemon's webhook service) no longer import `lib/secrets/*`.
5
+ */
6
+ import type * as net from 'node:net';
7
+ /**
8
+ * Close `server`, resolving once closed or after `timeoutMs`, whichever comes
9
+ * first — a shutdown must never hang on a client that never disconnects.
10
+ */
11
+ export declare function closeServerBounded(server: net.Server, timeoutMs?: number): Promise<void>;
@@ -0,0 +1,25 @@
1
+ const SERVER_CLOSE_TIMEOUT_MS = 5000;
2
+ /**
3
+ * Close `server`, resolving once closed or after `timeoutMs`, whichever comes
4
+ * first — a shutdown must never hang on a client that never disconnects.
5
+ */
6
+ export function closeServerBounded(server, timeoutMs = SERVER_CLOSE_TIMEOUT_MS) {
7
+ return new Promise((resolve) => {
8
+ let settled = false;
9
+ const finish = () => {
10
+ if (settled)
11
+ return;
12
+ settled = true;
13
+ clearTimeout(timer);
14
+ resolve();
15
+ };
16
+ const timer = setTimeout(finish, timeoutMs);
17
+ try {
18
+ server.close(() => finish());
19
+ }
20
+ catch {
21
+ // Already closed / not listening — treat as released.
22
+ finish();
23
+ }
24
+ });
25
+ }
@@ -1,6 +1,32 @@
1
1
  import { execFileSync, spawnSync } from 'child_process';
2
2
  import { getCliLaunch } from './cli-entry.js';
3
- import { buildAddGenericPasswordArgs, buildAddGenericPasswordSpawnOptions, } from './secrets/index.js';
3
+ /**
4
+ * argv for writing a bare (non-`agents-cli.`) keychain item via
5
+ * `/usr/bin/security add-generic-password`, deliberately WITHOUT the value: the
6
+ * secret travels over stdin (see below) so it never lands in argv or a `ps`
7
+ * snapshot. Pure — no agents-cli secrets engine involved (this migration writes
8
+ * directly to the OpenClaw account's OWN macOS keychain entries, bypassing the
9
+ * `agents secrets` bundle store entirely), so it's a local helper rather than a
10
+ * process-client call.
11
+ */
12
+ function buildAddGenericPasswordArgs(account, item) {
13
+ return ['add-generic-password', '-U', '-a', account, '-s', item, '-w'];
14
+ }
15
+ /**
16
+ * spawnSync options for the bare `-w` keychain write. `input` pipes the value
17
+ * TWICE (bare `-w` prompts enter+confirm; one line fails the confirm and stores
18
+ * an empty secret). `detached: true` runs `security` in a new session with no
19
+ * controlling terminal, so readpassphrase(3) falls back to our piped stdin
20
+ * instead of prompting the user's `/dev/tty` in an interactive shell.
21
+ */
22
+ function buildAddGenericPasswordSpawnOptions(value) {
23
+ return {
24
+ input: `${value}\n${value}\n`,
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ timeout: 10_000,
27
+ detached: true,
28
+ };
29
+ }
4
30
  export const OPENCLAW_KEYCHAIN_ACCOUNT = 'openclaw';
5
31
  export const OPENCLAW_KEYCHAIN_PROVIDER = 'agents_keychain';
6
32
  export const OPENCLAW_KEYCHAIN_ENV_SERVICES = {
@@ -11,7 +11,7 @@ import * as crypto from 'node:crypto';
11
11
  import * as yaml from 'yaml';
12
12
  import { ALL_AGENT_IDS } from './agents.js';
13
13
  import { getUserAgentsDir } from './state.js';
14
- import { deleteKeychainToken, getKeychainToken, hasKeychainToken, keychainItemName } from './secrets/profiles.js';
14
+ import { deleteKeychainTokenSync, getKeychainTokenSync, hasKeychainTokenSync, isSecretsClientError, profileKeychainItem } from './secrets-client.js';
15
15
  import { getPreset } from './profiles-presets.js';
16
16
  import { MODEL_TIERS, isTierToken } from './model-tiers.js';
17
17
  import { addAccount, findAccount, resolveCredentialAccount } from './account-registry.js';
@@ -85,7 +85,7 @@ function migrateLegacyProfileAuth(profile, file) {
85
85
  const accountName = `legacy-${profile.provider}-${suffix}`;
86
86
  if (!findAccount(accountName)) {
87
87
  const kind = profile.auth.envVar.includes('BEARER_TOKEN') ? 'bearer-token' : 'api-key';
88
- addAccount(accountName, provider, kind, getKeychainToken(profile.auth.keychainItem));
88
+ addAccount(accountName, provider, kind, getKeychainTokenSync(profile.auth.keychainItem));
89
89
  }
90
90
  const migratedAccount = findAccount(accountName);
91
91
  const oldItem = profile.auth.keychainItem;
@@ -110,7 +110,7 @@ function migrateLegacyProfileAuth(profile, file) {
110
110
  }
111
111
  });
112
112
  if (!stillReferenced)
113
- deleteKeychainToken(oldItem);
113
+ deleteKeychainTokenSync(oldItem);
114
114
  }
115
115
  /** Write a profile to disk atomically (write-to-tmp then rename). */
116
116
  export function writeProfile(profile) {
@@ -250,7 +250,7 @@ export function profileAuthLabel(profile) {
250
250
  return `${provider} ${maskToken(token)}`;
251
251
  }
252
252
  if (profile.auth) {
253
- return `${provider} ${hasKeychainToken(profile.auth.keychainItem) ? 'stored' : 'missing'}`;
253
+ return `${provider} ${hasKeychainTokenSync(profile.auth.keychainItem) ? 'stored' : 'missing'}`;
254
254
  }
255
255
  return provider;
256
256
  }
@@ -324,7 +324,7 @@ export function profileFromPreset(profileName, preset, version) {
324
324
  env: { ...preset.env },
325
325
  auth: {
326
326
  envVar: preset.authEnvVar,
327
- keychainItem: keychainItemName(preset.provider),
327
+ keychainItem: profileKeychainItem(preset.provider),
328
328
  },
329
329
  authOptional: preset.authOptional,
330
330
  description: preset.description,
@@ -392,7 +392,7 @@ export function profileFromHostModel(name, host, model, opts = {}) {
392
392
  forkedFrom: host,
393
393
  };
394
394
  if (opts.provider && opts.authEnvVar) {
395
- profile.auth = { envVar: opts.authEnvVar, keychainItem: keychainItemName(opts.provider) };
395
+ profile.auth = { envVar: opts.authEnvVar, keychainItem: profileKeychainItem(opts.provider) };
396
396
  profile.authOptional = false;
397
397
  }
398
398
  return profile;
@@ -455,7 +455,7 @@ export function forkProfile(source, name, opts = {}) {
455
455
  throw new Error(`Host '${host}' has no known auth env var; --provider cannot be attached to this fork.`);
456
456
  }
457
457
  forked.provider = opts.provider;
458
- forked.auth = { envVar, keychainItem: keychainItemName(opts.provider) };
458
+ forked.auth = { envVar, keychainItem: profileKeychainItem(opts.provider) };
459
459
  forked.authOptional = source.authOptional ?? false;
460
460
  }
461
461
  return forked;
@@ -529,10 +529,23 @@ export function resolveProfileEnv(profile) {
529
529
  // Optional auth (host manages its own login) with no stored token: inject
530
530
  // nothing and let the host use its own credentials. Only required auth
531
531
  // hard-fails on a missing keychain item.
532
- if (profile.authOptional && !hasKeychainToken(profile.auth.keychainItem)) {
532
+ if (profile.authOptional && !hasKeychainTokenSync(profile.auth.keychainItem)) {
533
533
  return env;
534
534
  }
535
- const token = getKeychainToken(profile.auth.keychainItem);
535
+ let token;
536
+ try {
537
+ token = getKeychainTokenSync(profile.auth.keychainItem);
538
+ }
539
+ catch (err) {
540
+ // The standalone deliberately reports only a code, so name the item and
541
+ // the repair here — a bare `NOT_FOUND` gives the user nothing to act on.
542
+ if (!isSecretsClientError(err, 'NOT_FOUND'))
543
+ throw err;
544
+ throw new Error(`Harness '${profile.name}' needs a ${profile.provider ?? 'provider'} key, but its keychain item ` +
545
+ `'${profile.auth.keychainItem}' is missing on this device. Store one with ` +
546
+ `'agents harness edit ${profile.name} --auth-provider ${profile.provider ?? '<provider>'}' ` +
547
+ `(or --from-secrets <bundle>:<key>), or bind an account with 'agents harness edit ${profile.name} --account <name>'.`);
548
+ }
536
549
  env[profile.auth.envVar] = token;
537
550
  }
538
551
  return env;
@@ -9,7 +9,7 @@ import { markdownToToml } from './convert.js';
9
9
  import { safeJoin } from './paths.js';
10
10
  import { subagentTarget } from './subagents-registry.js';
11
11
  import { parseSubagentFrontmatter } from './subagents.js';
12
- import { syncWorkflowToVersion } from './workflows.js';
12
+ import { syncWorkflowToVersion } from './workflows-registry.js';
13
13
  const MANIFEST_FILE = '.agents-managed.json';
14
14
  const MANIFEST_VERSION = 1;
15
15
  const COPY_IGNORE = new Set(['.DS_Store', '.git', '.gitignore', '.venv', '__pycache__', 'node_modules']);
@@ -0,0 +1,53 @@
1
+ import type { SecretsBackend } from './secrets-types.js';
2
+ import { type AgentId } from './types.js';
3
+ /** Legacy readable alias for `__claude__`. Not migrated in this track. */
4
+ export declare const AUTH_STORE_ALIAS = "auth";
5
+ /** The one reserved bundle name most callers actually need. */
6
+ export declare const AUTH_BUNDLE_NAME = "auth";
7
+ /**
8
+ * The reserved `auth` bundle holds per-account Claude setup-tokens for
9
+ * unattended usage/probe and MUST be file-backed (headless, fleet-shareable —
10
+ * credential-management.md invariant 7). The standalone enforces the same
11
+ * rule on its write path (`WRONG_BACKEND`); agents-cli asserts it on every
12
+ * read so a keychain/vault-backed `auth` left over from an older layout fails
13
+ * loud instead of silently being ignored by usage/probe (SEC-GAP-3).
14
+ */
15
+ export declare const AUTH_BUNDLE_BACKEND: SecretsBackend;
16
+ export declare const RESERVED_BUNDLE_NAMES: Set<string>;
17
+ export declare function isReservedBundleName(name: string): boolean;
18
+ /** Thrown when the reserved `auth` bundle is found on the wrong backend. */
19
+ export declare class ReservedBundleWrongBackendError extends Error {
20
+ readonly bundle: string;
21
+ readonly backend: SecretsBackend;
22
+ constructor(bundle: string, backend: SecretsBackend);
23
+ }
24
+ /** Fail loud when the reserved `auth` bundle is on any backend but `file`. */
25
+ export declare function assertReservedAuthBackend(backend: SecretsBackend): void;
26
+ /**
27
+ * True for the wrong-backend refusal in either shape: raised here from a
28
+ * read-side check, or returned by the standalone (`WRONG_BACKEND`) when its
29
+ * own write/resolve guard fired.
30
+ */
31
+ export declare function isReservedBundleBackendError(error: unknown): boolean;
32
+ export declare const RESERVED_STORES: Record<AgentId, string>;
33
+ export declare function reservedStoreName(harness: AgentId): string;
34
+ export declare function isReservedStoreName(name: string): boolean;
35
+ export type StorableCredentialKind = 'setup-token' | 'api-key';
36
+ /**
37
+ * Fail loud at the write boundary: only a setup-token or an API key may enter
38
+ * a reserved store. Rotating OAuth/session credentials stay in their slot on
39
+ * the device that minted them.
40
+ */
41
+ export declare function assertStorableCredentialKind(kind: string, harness?: AgentId): asserts kind is StorableCredentialKind;
42
+ /**
43
+ * Whether the local reserved `auth` bundle exists and is on the expected
44
+ * (file) backend. `ok: true` on a missing bundle — nothing to be wrong about.
45
+ * Read-only status probe (`agents doctor`, `agents fleet apply`); an
46
+ * unreachable standalone is treated the same as "bundle absent" so a
47
+ * diagnostic never crashes over one optional finding.
48
+ */
49
+ export declare function inspectReservedAuthBundle(): {
50
+ exists: boolean;
51
+ backend: SecretsBackend | null;
52
+ ok: boolean;
53
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Reserved per-harness credential stores (PHNX-3940 / PHNX-3989).
3
+ *
4
+ * One store name per `ALL_AGENT_IDS` entry: `__<harness>__`. A user-created
5
+ * bundle can never collide (the standalone rejects `__`-prefixed names unless
6
+ * explicitly allowed). The legacy `auth` bundle remains a readable alias for
7
+ * `__claude__` — this module does not migrate data.
8
+ *
9
+ * Deliberately a LEAF module (no import of `claude-account-token.ts` or
10
+ * `secrets-policy.ts`): both of those need these constants, and
11
+ * `claude-account-token.ts` is itself a dependency of `secrets-policy.ts`'s
12
+ * fleet-sync helpers — importing this module's exports from `secrets-policy.js`
13
+ * instead would form a real circular import that throws
14
+ * `ReferenceError: Cannot access '...' before initialization` under real ESM
15
+ * evaluation order (reproduced running this suite outside vitest's
16
+ * mock-tolerant transform). Keep it a leaf; `secrets-policy.ts` re-exports it
17
+ * for the existing public surface.
18
+ */
19
+ import { bundleExistsSync, bundleBackendSync, isSecretsClientError } from './secrets-client.js';
20
+ import { AGENT_IDS } from './types.js';
21
+ /** Legacy readable alias for `__claude__`. Not migrated in this track. */
22
+ export const AUTH_STORE_ALIAS = 'auth';
23
+ /** The one reserved bundle name most callers actually need. */
24
+ export const AUTH_BUNDLE_NAME = AUTH_STORE_ALIAS;
25
+ /**
26
+ * The reserved `auth` bundle holds per-account Claude setup-tokens for
27
+ * unattended usage/probe and MUST be file-backed (headless, fleet-shareable —
28
+ * credential-management.md invariant 7). The standalone enforces the same
29
+ * rule on its write path (`WRONG_BACKEND`); agents-cli asserts it on every
30
+ * read so a keychain/vault-backed `auth` left over from an older layout fails
31
+ * loud instead of silently being ignored by usage/probe (SEC-GAP-3).
32
+ */
33
+ export const AUTH_BUNDLE_BACKEND = 'file';
34
+ export const RESERVED_BUNDLE_NAMES = new Set([AUTH_BUNDLE_NAME]);
35
+ export function isReservedBundleName(name) {
36
+ return RESERVED_BUNDLE_NAMES.has(name.trim().toLowerCase());
37
+ }
38
+ /** Thrown when the reserved `auth` bundle is found on the wrong backend. */
39
+ export class ReservedBundleWrongBackendError extends Error {
40
+ bundle;
41
+ backend;
42
+ constructor(bundle, backend) {
43
+ super(`Bundle '${bundle}' is reserved for file-backed setup-tokens (headless, fleet-shareable). ` +
44
+ `A ${backend}-backed '${bundle}' bundle is ignored by usage/probe instead of authenticating. ` +
45
+ `Recreate it as file-backed: agents secrets delete ${bundle} --yes && agents secrets create ${bundle} --backend file`);
46
+ this.name = 'ReservedBundleWrongBackendError';
47
+ this.bundle = bundle;
48
+ this.backend = backend;
49
+ }
50
+ }
51
+ /** Fail loud when the reserved `auth` bundle is on any backend but `file`. */
52
+ export function assertReservedAuthBackend(backend) {
53
+ if (backend !== AUTH_BUNDLE_BACKEND)
54
+ throw new ReservedBundleWrongBackendError(AUTH_STORE_ALIAS, backend);
55
+ }
56
+ /**
57
+ * True for the wrong-backend refusal in either shape: raised here from a
58
+ * read-side check, or returned by the standalone (`WRONG_BACKEND`) when its
59
+ * own write/resolve guard fired.
60
+ */
61
+ export function isReservedBundleBackendError(error) {
62
+ return error instanceof ReservedBundleWrongBackendError || isSecretsClientError(error, 'WRONG_BACKEND');
63
+ }
64
+ export const RESERVED_STORES = Object.fromEntries(AGENT_IDS.map((id) => [id, `__${id}__`]));
65
+ export function reservedStoreName(harness) {
66
+ const name = RESERVED_STORES[harness];
67
+ if (!name)
68
+ throw new Error(`No reserved store for '${harness}'.`);
69
+ return name;
70
+ }
71
+ export function isReservedStoreName(name) {
72
+ const n = name.trim().toLowerCase();
73
+ if (n === AUTH_STORE_ALIAS)
74
+ return true;
75
+ return AGENT_IDS.some((id) => RESERVED_STORES[id].toLowerCase() === n);
76
+ }
77
+ /**
78
+ * Fail loud at the write boundary: only a setup-token or an API key may enter
79
+ * a reserved store. Rotating OAuth/session credentials stay in their slot on
80
+ * the device that minted them.
81
+ */
82
+ export function assertStorableCredentialKind(kind, harness) {
83
+ if (kind === 'setup-token' || kind === 'api-key')
84
+ return;
85
+ throw new Error(storableCredentialRefusal(kind, harness));
86
+ }
87
+ function storableCredentialRefusal(kind, harness) {
88
+ if (harness === 'codex')
89
+ return 'codex: auth.json is a rotating session; add an API key instead';
90
+ if (harness === 'droid')
91
+ return 'droid: refresh tokens are single-use and collapse the fleet when copied; add a FACTORY_API_KEY instead';
92
+ if (harness === 'claude')
93
+ return 'claude: native OAuth (.credentials.json) is a rotating session; mint a setup-token instead';
94
+ if (harness === 'grok')
95
+ return 'grok: auth.json is a rotating session; add an XAI_API_KEY instead';
96
+ if (harness === 'kimi' || harness === 'antigravity')
97
+ return `${harness}: no portable worker credential; log in per device`;
98
+ const who = harness ?? 'this harness';
99
+ return `${who}: reserved stores accept only a setup-token or an API key, not '${kind}'`;
100
+ }
101
+ /**
102
+ * Whether the local reserved `auth` bundle exists and is on the expected
103
+ * (file) backend. `ok: true` on a missing bundle — nothing to be wrong about.
104
+ * Read-only status probe (`agents doctor`, `agents fleet apply`); an
105
+ * unreachable standalone is treated the same as "bundle absent" so a
106
+ * diagnostic never crashes over one optional finding.
107
+ */
108
+ export function inspectReservedAuthBundle() {
109
+ let exists;
110
+ try {
111
+ exists = bundleExistsSync(AUTH_BUNDLE_NAME);
112
+ }
113
+ catch {
114
+ return { exists: false, backend: null, ok: true };
115
+ }
116
+ if (!exists)
117
+ return { exists: false, backend: null, ok: true };
118
+ const backend = bundleBackendSync(AUTH_BUNDLE_NAME);
119
+ return { exists: true, backend, ok: backend === AUTH_BUNDLE_BACKEND };
120
+ }