@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
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Per-device account slots (PHNX-3940).
3
+ *
4
+ * An account is a credential slot, not an installation. The binary lives in
5
+ * the one managed harness install; each account gets a HOME-shaped dir under
6
+ * `~/.agents/.history/accounts/<harness>/<accountId>/` with no binary in it.
7
+ * Native OAuth files stay in this dir on the device that minted them and are
8
+ * never copied. Settings and resources are projected through the same writers
9
+ * version homes use (`carryForwardSettings`, `getWriter`) — credentials are
10
+ * excluded by those writers.
11
+ *
12
+ * Slot records live in the device doc (`deviceAccounts.slots`), never central.
13
+ */
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { agentConfigDirName } from '../agents.js';
17
+ import { harnessAuth, harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
18
+ import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../installations/store.js';
19
+ import { carryForwardSettings } from '../settings-manifest.js';
20
+ import { getHistoryDir, updateMeta } from '../state.js';
21
+ import { ALL_RESOURCE_KINDS, getDetector, getWriter, kindToCapability } from '../staleness/registry.js';
22
+ import { supports } from '../capabilities.js';
23
+ import { isAgentId } from '../types.js';
24
+ const AUTH_MODES = ['native', 'durable', 'per-device'];
25
+ export function slotDir(harness, accountId) {
26
+ if (!isAgentId(harness))
27
+ throw new Error(`Unknown harness '${harness}' for a slot dir.`);
28
+ if (!accountId || /[\\/]|\.\./.test(accountId)) {
29
+ throw new Error(`Invalid account id '${accountId}' for a slot dir.`);
30
+ }
31
+ return path.join(getHistoryDir(), 'accounts', harness, accountId);
32
+ }
33
+ export function readSlots(meta) {
34
+ return { ...(meta.deviceAccounts?.slots ?? {}) };
35
+ }
36
+ export function recordSlot(accountId, slot) {
37
+ if (slot.accountId !== accountId) {
38
+ throw new Error(`recordSlot accountId mismatch: key '${accountId}' vs slot.accountId '${slot.accountId}'.`);
39
+ }
40
+ if (!AUTH_MODES.includes(slot.authMode)) {
41
+ throw new Error(`recordSlot: unknown authMode '${slot.authMode}'.`);
42
+ }
43
+ updateMeta((current) => ({
44
+ ...current,
45
+ deviceAccounts: {
46
+ ...current.deviceAccounts,
47
+ slots: { ...current.deviceAccounts?.slots, [accountId]: slot },
48
+ },
49
+ }));
50
+ }
51
+ function defaultAuthMode(harness) {
52
+ return harnessWorkerIsPerDevice(harness) ? 'per-device' : 'native';
53
+ }
54
+ function sourceVersion(harness) {
55
+ return getGlobalDefault(harness) ?? listInstalledVersions(harness)[0] ?? null;
56
+ }
57
+ function projectResources(harness, version, destHome, fromHome) {
58
+ const cwd = process.cwd();
59
+ for (const kind of ALL_RESOURCE_KINDS) {
60
+ if (!supports(harness, kindToCapability(kind), version).ok)
61
+ continue;
62
+ const writer = getWriter(kind, harness);
63
+ if (!writer)
64
+ continue;
65
+ if (kind === 'rules') {
66
+ writer.write({ version, versionHome: destHome, selection: { preset: 'default' }, cwd });
67
+ continue;
68
+ }
69
+ const names = getDetector(kind, harness)?.list({ version, versionHome: fromHome, cwd }) ?? [];
70
+ if (names.length === 0)
71
+ continue;
72
+ writer.write({ version, versionHome: destHome, selection: names, cwd });
73
+ }
74
+ }
75
+ /**
76
+ * Create the HOME-shaped slot dir and project settings/resources from the
77
+ * managed install. Does not copy credentials (carryForwardSettings excludes
78
+ * them; resource writers write skills/hooks/commands, never OAuth files).
79
+ * Does not persist the slot record — call {@link recordSlot} after identity
80
+ * is captured.
81
+ */
82
+ export function ensureSlot(harness, accountId) {
83
+ harnessAuth(harness);
84
+ const dir = slotDir(harness, accountId);
85
+ fs.mkdirSync(path.join(dir, agentConfigDirName(harness)), { recursive: true });
86
+ const version = sourceVersion(harness);
87
+ if (version) {
88
+ const fromHome = getVersionHomePath(harness, version);
89
+ if (fs.existsSync(fromHome)) {
90
+ carryForwardSettings(harness, fromHome, dir);
91
+ projectResources(harness, version, dir, fromHome);
92
+ }
93
+ }
94
+ return {
95
+ accountId,
96
+ slotDir: dir,
97
+ authMode: defaultAuthMode(harness),
98
+ verdict: 'unconfigured',
99
+ };
100
+ }
@@ -236,6 +236,22 @@ export declare function supportsAccountInspection(agentId: AgentId): boolean;
236
236
  export declare function accountDisplayLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
237
237
  /** Return the email address associated with the agent's auth config, or null. */
238
238
  export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
239
+ /**
240
+ * Extract full account information (identity, plan, usage status, credits) from
241
+ * the agent's local auth/config files. Supports Claude, Codex, and Gemini.
242
+ */
243
+ /**
244
+ * Resolve a file-auth agent's credential file. Sign-in is account-global, but
245
+ * each installed version gets an isolated home; the credential physically lives
246
+ * only in the home the user logged in under (the one the `~/.<config>` symlink
247
+ * targets). Check the per-version `base` first, then fall back to the active
248
+ * config location under the real HOME so every installed version reflects the
249
+ * true account state (droid/antigravity/kimi all stored login per-version-home
250
+ * and showed non-active versions as "not signed in"). Returns the first
251
+ * existing path, or null.
252
+ */
253
+ /** True when `dir` is under the per-account slot root (`~/.agents/.history/accounts/`). */
254
+ export declare function isAccountSlotDir(dir: string): boolean;
239
255
  /** Where an agent's credential file lives, split into the per-version copy and
240
256
  * the active/global copy under the real HOME. */
241
257
  export interface CredentialPresence {
@@ -22,7 +22,8 @@ import { execFileShellSpec } from '../platform/index.js';
22
22
  import { latestFileMtimeMs } from '../fs-walk.js';
23
23
  import { damerauLevenshtein } from '../fuzzy.js';
24
24
  import { probeCapture } from '../probe.js';
25
- import { getCacheDir, getVersionsDir, getShimsDir, getHistoryDir, getCliVersionCachePath } from '../state.js';
25
+ import { getCacheDir, getVersionsDir, getShimsDir, getHistoryDir, getCliVersionCachePath, readMeta } from '../state.js';
26
+ import { registeredNativeAccountForEmail, parseNativeIdentityKey } from '../native-accounts.js';
26
27
  import { resolveVersion, getVersionHomePath, getBinaryPath } from '../installations/versions.js';
27
28
  import { supports } from '../capabilities.js';
28
29
  import { MCP_TARGETS } from '../mcp-registry.js';
@@ -1384,6 +1385,12 @@ export async function getAccountEmail(agentId, home) {
1384
1385
  * and showed non-active versions as "not signed in"). Returns the first
1385
1386
  * existing path, or null.
1386
1387
  */
1388
+ /** True when `dir` is under the per-account slot root (`~/.agents/.history/accounts/`). */
1389
+ export function isAccountSlotDir(dir) {
1390
+ const root = path.resolve(getHistoryDir(), 'accounts');
1391
+ const resolved = path.resolve(dir);
1392
+ return resolved === root || resolved.startsWith(root + path.sep);
1393
+ }
1387
1394
  function resolveAccountCredentialPath(base, ...segments) {
1388
1395
  const perVersion = path.join(base, ...segments);
1389
1396
  try {
@@ -1391,6 +1398,10 @@ function resolveAccountCredentialPath(base, ...segments) {
1391
1398
  return perVersion;
1392
1399
  }
1393
1400
  catch { /* unreadable */ }
1401
+ // A slot is the account's own HOME (PHNX-3940 T5). Never inherit another
1402
+ // account's adopted ~/.<config> — that is the cross-account leak.
1403
+ if (isAccountSlotDir(base))
1404
+ return null;
1394
1405
  const active = path.join(process.env.AGENTS_REAL_HOME || os.homedir(), ...segments);
1395
1406
  if (active !== perVersion) {
1396
1407
  try {
@@ -1900,13 +1911,30 @@ export function readClaudeHomeConfig(base) {
1900
1911
  const oa = config.oauthAccount;
1901
1912
  if (!oa)
1902
1913
  return null;
1903
- const accountId = normalizeIdentityPart(oa.accountUuid);
1904
- const organizationId = normalizeIdentityPart(oa.organizationUuid);
1914
+ const email = normalizeIdentityPart(oa.emailAddress);
1915
+ let accountId = normalizeIdentityPart(oa.accountUuid);
1916
+ let organizationId = normalizeIdentityPart(oa.organizationUuid);
1917
+ // Worker slot provisioned from a durable setup-token: only the email was
1918
+ // seeded on disk (seedClaudeWorkerHomeIdentity), so accountUuid/organizationUuid
1919
+ // are absent and both accountKey and usageKey would be null — the row reads
1920
+ // "not connected here" (PHNX-3940, sibling worker-identity diagnosis). Complete
1921
+ // the identity from the fleet-synced registry row for this email, keyed exactly
1922
+ // as a headed login's home carries it. Guarded on email-only so a headed home
1923
+ // (uuids on disk) never pays the readMeta() read, and fails CLOSED to email-only
1924
+ // when zero or several rows match (registeredNativeAccountForEmail never guesses).
1925
+ if (email && !accountId && !organizationId) {
1926
+ const registered = registeredNativeAccountForEmail(readMeta(), 'claude', email);
1927
+ const parts = registered ? parseNativeIdentityKey('claude', registered.identityKey) : null;
1928
+ if (parts) {
1929
+ accountId = normalizeIdentityPart(parts.account);
1930
+ organizationId = normalizeIdentityPart(parts.org);
1931
+ }
1932
+ }
1905
1933
  return {
1906
1934
  path: activeFile,
1907
1935
  config,
1908
1936
  identity: {
1909
- email: oa.emailAddress || null,
1937
+ email: email ?? (oa.emailAddress || null),
1910
1938
  accountId,
1911
1939
  organizationId,
1912
1940
  organizationName: oa.organizationName ?? null,
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Atomic, serialized install of a macOS `.app` bundle to a stable user path.
3
3
  *
4
- * Shared by the two helpers agents-cli installs on darwin — the secrets keychain
5
- * helper (`lib/secrets/install-helper.ts`) and the menu-bar helper
6
- * (`lib/menubar/install-menubar.ts`) — both of which are (re)installed on the hot
7
- * path of ordinary `agents` invocations. Both previously did a non-atomic
4
+ * Used by the menu-bar helper (`lib/menubar/install-menubar.ts`), which is
5
+ * (re)installed on the hot path of ordinary `agents` invocations. (A second
6
+ * helper this module used to also serve, the secrets keychain broker, moved
7
+ * out of this repo entirely with the standalone `secrets` engine —
8
+ * PHNX-3989 — and installs itself now.) It previously did a non-atomic
8
9
  * `rm -rf dest` + `cp -R src dest` straight onto the live bundle. That copy takes
9
10
  * long enough that a concurrent reader (Gatekeeper, or an exec of the bundle) sees
10
11
  * a half-written `.app` — a truncated Mach-O / mismatched `_CodeSignature` hash —
@@ -24,6 +24,8 @@ export interface AuthHealth {
24
24
  detail?: string;
25
25
  /** account label for display (email / id), when known. Never part of the key. */
26
26
  account?: string;
27
+ /** Stable registered account id. Display labels are never used as identity. */
28
+ accountId?: string;
27
29
  }
28
30
  /** Maximum age of an auth verdict used for automatic routing decisions. */
29
31
  export declare const AUTH_PROBE_MAX_AGE_MS: number;
@@ -173,6 +175,7 @@ export interface AuthProbeRow {
173
175
  agent: AgentId;
174
176
  version: string;
175
177
  account?: string;
178
+ accountId?: string;
176
179
  health: AuthHealth;
177
180
  }
178
181
  /** One installed (agent, version) home, tagged with its resolved account label. */
@@ -255,6 +255,7 @@ export function readFleetAuthRows(host) {
255
255
  agent: agent,
256
256
  version: identity.slice(separator + 1),
257
257
  account: health.account,
258
+ accountId: health.accountId,
258
259
  health,
259
260
  });
260
261
  }
@@ -422,6 +423,14 @@ export async function probeLocalFleetAuth(opts) {
422
423
  inst.info = await getAccountInfo(inst.agent, inst.home).catch(() => null);
423
424
  inst.account = authAccountLabel(inst.info);
424
425
  }));
426
+ const [{ readMeta }, { findNativeAccountByIdentity }] = await Promise.all([
427
+ import('./state.js'),
428
+ import('./account-registry.js'),
429
+ ]);
430
+ const meta = readMeta();
431
+ for (const inst of installs) {
432
+ inst.accountId = findNativeAccountByIdentity(meta, inst.agent, inst.info)?.id;
433
+ }
425
434
  // Probe once per (agent, account) — but only for the network-probing agents
426
435
  // that can actually 429; best-effort agents stay per-install (see
427
436
  // groupFleetAuthInstalls). Groups run in parallel: they target distinct
@@ -430,12 +439,14 @@ export async function probeLocalFleetAuth(opts) {
430
439
  const rep = group.probe;
431
440
  const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info, forceLive: opts?.forceLive, signal: opts?.signal });
432
441
  health.account = authAccountLabel(rep.info);
442
+ health.accountId = rep.accountId;
433
443
  if (health.verdict === 'unconfigured')
434
444
  return [];
435
445
  return group.members.map((inst) => ({
436
446
  agent: inst.agent,
437
447
  version: inst.version,
438
448
  account: health.account,
449
+ accountId: inst.accountId ?? health.accountId,
439
450
  // A distinct object per row so a later mutation of one can't bleed across.
440
451
  health: { ...health },
441
452
  }));
@@ -15,25 +15,34 @@
15
15
  */
16
16
  import type { AgentId } from './types.js';
17
17
  import { type CredentialAccount } from './account-registry.js';
18
+ import { type StorableCredentialKind } from './secrets-policy.js';
18
19
  import { type DriveOptions, type PtyDriver } from './fleet/remote-login.js';
19
20
  /** Well-formed Claude setup-token as it appears inside a TTY blob. */
20
21
  export declare const CLAUDE_SETUP_TOKEN_CAPTURE_RE: RegExp;
21
22
  export interface MintFlow {
22
23
  harness: AgentId;
23
24
  provider: string;
24
- auth: 'setup-token';
25
- /** Interactive mint argv after HOME=… <bin>. Null when stdin-seed only. */
25
+ auth: 'setup-token' | 'api-key';
26
+ /** Interactive mint argv after HOME=… <bin>. Null when stdin-seed only or an api-key collection flow. */
26
27
  mintArgs: string[] | null;
27
- verificationUrlRegex: RegExp;
28
- tokenCapture: RegExp;
28
+ /** api-key flows: the env var the collected key injects as on a worker (e.g. OPENAI_API_KEY). */
29
+ apiKeyEnv?: string;
30
+ /** setup-token flows: authorize-URL and token patterns scraped from the PTY. */
31
+ verificationUrlRegex?: RegExp;
32
+ tokenCapture?: RegExp;
29
33
  }
30
34
  /**
31
- * Harnesses that expose an interactive setup-token mint. Native device-code
32
- * login (codex/droid/kimi/grok) stays on `agents fleet login`; API keys stay
33
- * on `agents accounts add`. Adding a harness here without a real mint command
34
- * is a lying table — do not.
35
+ * Durable-credential flows per harness. Only Claude exposes an interactive
36
+ * setup-token MINT; every other harness's durable credential is a provider API
37
+ * key, COLLECTED via `--api-key` or a prompt by `accounts add` (never derived
38
+ * from OAuth). The api-key entries describe collection only — they have no
39
+ * mint argv, and `mintAndSeed` refuses them with the `accounts add` pointer.
40
+ * Native device-code login (kimi/antigravity) stays on `agents fleet login`.
41
+ * The apiKeyEnv values are pinned to HARNESS_AUTH's `api-key:<ENV>` worker
42
+ * kinds by auth-mint.test.ts — keep them in lockstep.
35
43
  */
36
44
  export declare const MINT_FLOWS: Record<string, MintFlow>;
45
+ /** Harnesses with an interactive setup-token mint (api-key flows are collection, not mint). */
37
46
  export declare function listMintableHarnesses(): AgentId[];
38
47
  export declare function getMintFlow(harnessRaw: string): MintFlow;
39
48
  export declare function unmintableMessage(harness: string): string;
@@ -75,6 +84,32 @@ export declare function resolveMintIdentity(input: ResolveMintIdentityInput): Re
75
84
  export declare function seedReservedAuthToken(email: string, token: string): {
76
85
  key: string;
77
86
  };
87
+ /**
88
+ * The env var a harness's durable worker credential injects as on a worker
89
+ * (claude's setup-token rides CLAUDE_CODE_OAUTH_TOKEN; api-key harnesses ride
90
+ * their MINT_FLOWS apiKeyEnv). Throws for a harness with no portable worker
91
+ * credential — kimi/antigravity log in per box.
92
+ */
93
+ export declare function workerCredentialEnv(harness: AgentId): string;
94
+ /**
95
+ * The reserved-store key for one account's worker credential:
96
+ * `<ENV>_<accountId>` — keyed by account id, never by name or email, so a
97
+ * rename or an email change never breaks the key. Hyphens are stripped because
98
+ * the bundle key grammar (`BUNDLE_KEY_PATTERN`) is env-var-shaped.
99
+ */
100
+ export declare function workerCredentialStoreKey(harness: AgentId, accountId: string): string;
101
+ /**
102
+ * Write (or rotate) one account's worker credential in the reserved
103
+ * `__<harness>__` store (PHNX-3940). Only non-rotating kinds may enter
104
+ * (`assertStorableCredentialKind` refuses a rotating OAuth/session credential at
105
+ * this boundary — RUSH-1958). FILE-backed, policy `never`, same contract as the
106
+ * legacy `auth` bundle: a keychain- or vault-backed store of this name fails
107
+ * loud, since worker provisioning reads the file backend.
108
+ */
109
+ export declare function seedReservedStoreKey(harness: AgentId, kind: StorableCredentialKind, key: string, value: string): {
110
+ bundle: string;
111
+ key: string;
112
+ };
78
113
  /**
79
114
  * Create or rotate the named provider account that `agents run --account` and
80
115
  * `agents accounts sync` consume. Existing account of a different kind fails
@@ -104,7 +139,7 @@ export interface DriveSetupTokenMintOpts extends MintDriveHooks {
104
139
  * Tears the session down on the way out (success, timeout, or throw).
105
140
  */
106
141
  export declare function driveSetupTokenMint(command: string, flow: MintFlow, opts?: DriveSetupTokenMintOpts): Promise<DriveMintResult>;
107
- export declare function buildMintCommand(flow: MintFlow, bin: string, home: string): string;
142
+ export declare function buildMintCommand(flow: MintFlow, bin: string, home: string, env?: Record<string, string>): string;
108
143
  export declare function resolveMintInstallation(harness: AgentId): {
109
144
  version: string;
110
145
  bin: string;
@@ -146,13 +181,14 @@ export declare function resolveSyncTargets(fleet: boolean, devices: string[]): P
146
181
  /**
147
182
  * True when a Claude setup-token is already seeded on this box (setup status).
148
183
  *
149
- * Read-only status probe (`agents setup`, `agents doctor`) — the reserved
150
- * auth-bundle check below MUST NOT crash the whole command because the
151
- * keychain could not be reached (e.g. the macOS Keychain helper source is
152
- * unavailable — PHNX-3385); `bundleExists()`/`hasKeychainToken()` document
153
- * themselves as failing loud, but that contract is for destructive-write
154
- * guards, not a diagnostic. An unreachable keychain here just means "cannot
155
- * confirm the reserved bundle," reported honestly rather than propagated.
184
+ * Read-only status probe (`agents setup`, `agents doctor`) — neither the
185
+ * account-registry read nor the reserved auth-bundle check MUST crash the whole
186
+ * command because the secret store could not be reached (the macOS Keychain
187
+ * helper source unavailable — PHNX-3385 — or the standalone `secrets` CLI
188
+ * missing / unspawnable); the client's calls fail loud by contract, but that
189
+ * contract is for destructive-write guards, not a diagnostic. An unreachable
190
+ * store here just means "cannot confirm," reported honestly rather than
191
+ * propagated.
156
192
  */
157
193
  export declare function hasMintedSetupToken(): {
158
194
  ready: boolean;
@@ -1,26 +1,29 @@
1
1
  import { resolveAgentName } from './agents.js';
2
- import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, } from './claude-account-token.js';
2
+ import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, invalidateClaudeSetupTokenCache, } from './claude-account-token.js';
3
3
  import { addAccount, findAccount, readAccountRegistry, setAccountSecret } from './account-registry.js';
4
- import { bundleBackend, bundleExists, keychainRef, readBundle, rotateBundleSecret, writeBundleWithItems, } from './secrets/bundles.js';
5
- import { secretsKeychainItem } from './secrets/index.js';
4
+ import { bundleBackendSync, bundleExists, bundleExistsSync, keychainRef, pushBundleToHost, readBundleSync, rotateBundleSecretSync, secretsKeychainItem, storeSetSync, writeBundleWithItemsSync, } from './secrets-client.js';
5
+ import { assertStorableCredentialKind, reservedStoreName, } from './secrets-policy.js';
6
6
  import { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } from './installations/versions.js';
7
7
  import { shellQuote } from './ssh-exec.js';
8
8
  import { defaultPtyDriver, } from './fleet/remote-login.js';
9
9
  import { showUrl } from './open-url.js';
10
10
  import { loadDevices } from './devices/registry.js';
11
11
  import { isSelfHost } from './devices/self-host.js';
12
- import { pushBundleToHost } from './secrets/push.js';
13
- import { assertCredentialTransportHostPinned, resolveHostSshTarget } from './secrets/remote.js';
12
+ import { assertCredentialTransportHostPinned, resolveHostSshTarget } from './hosts/credential-transport.js';
14
13
  import { resolveRemoteOsSync } from './hosts/remote-os.js';
15
14
  /** Well-formed Claude setup-token as it appears inside a TTY blob. */
16
15
  export const CLAUDE_SETUP_TOKEN_CAPTURE_RE = /sk-ant-oat01-[A-Za-z0-9_-]+/;
17
16
  const ACCOUNT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
18
17
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
19
18
  /**
20
- * Harnesses that expose an interactive setup-token mint. Native device-code
21
- * login (codex/droid/kimi/grok) stays on `agents fleet login`; API keys stay
22
- * on `agents accounts add`. Adding a harness here without a real mint command
23
- * is a lying table — do not.
19
+ * Durable-credential flows per harness. Only Claude exposes an interactive
20
+ * setup-token MINT; every other harness's durable credential is a provider API
21
+ * key, COLLECTED via `--api-key` or a prompt by `accounts add` (never derived
22
+ * from OAuth). The api-key entries describe collection only — they have no
23
+ * mint argv, and `mintAndSeed` refuses them with the `accounts add` pointer.
24
+ * Native device-code login (kimi/antigravity) stays on `agents fleet login`.
25
+ * The apiKeyEnv values are pinned to HARNESS_AUTH's `api-key:<ENV>` worker
26
+ * kinds by auth-mint.test.ts — keep them in lockstep.
24
27
  */
25
28
  export const MINT_FLOWS = {
26
29
  claude: {
@@ -32,9 +35,15 @@ export const MINT_FLOWS = {
32
35
  verificationUrlRegex: /(https:\/\/[^\s"'<>]+)/i,
33
36
  tokenCapture: CLAUDE_SETUP_TOKEN_CAPTURE_RE,
34
37
  },
38
+ codex: { harness: 'codex', provider: 'openai', auth: 'api-key', mintArgs: null, apiKeyEnv: 'OPENAI_API_KEY' },
39
+ grok: { harness: 'grok', provider: 'xai', auth: 'api-key', mintArgs: null, apiKeyEnv: 'XAI_API_KEY' },
40
+ opencode: { harness: 'opencode', provider: 'opencode', auth: 'api-key', mintArgs: null, apiKeyEnv: 'OPENCODE_API_KEY' },
41
+ cursor: { harness: 'cursor', provider: 'cursor', auth: 'api-key', mintArgs: null, apiKeyEnv: 'CURSOR_API_KEY' },
42
+ droid: { harness: 'droid', provider: 'factory', auth: 'api-key', mintArgs: null, apiKeyEnv: 'FACTORY_API_KEY' },
35
43
  };
44
+ /** Harnesses with an interactive setup-token mint (api-key flows are collection, not mint). */
36
45
  export function listMintableHarnesses() {
37
- return Object.keys(MINT_FLOWS);
46
+ return Object.values(MINT_FLOWS).filter((f) => f.auth === 'setup-token').map((f) => f.harness);
38
47
  }
39
48
  export function getMintFlow(harnessRaw) {
40
49
  const harness = resolveAgentName(harnessRaw);
@@ -42,8 +51,12 @@ export function getMintFlow(harnessRaw) {
42
51
  throw new Error(`Unknown harness '${harnessRaw}'. Interactive setup-token mint is implemented for: ${listMintableHarnesses().join(', ')}.`);
43
52
  }
44
53
  const flow = MINT_FLOWS[harness];
45
- if (flow)
54
+ if (flow?.auth === 'setup-token')
46
55
  return flow;
56
+ if (flow) {
57
+ throw new Error(`'${harness}' has no derivable token — its durable credential is an API key (${flow.apiKeyEnv}). `
58
+ + `Collect it with: agents accounts add ${harness} <name> --api-key <key>.`);
59
+ }
47
60
  throw new Error(unmintableMessage(harness));
48
61
  }
49
62
  export function unmintableMessage(harness) {
@@ -76,6 +89,8 @@ export function extractClaudeSetupToken(screen) {
76
89
  }
77
90
  /** First https URL on the screen, trailing punctuation stripped. */
78
91
  export function extractMintUrl(screen, flow) {
92
+ if (!flow.verificationUrlRegex)
93
+ return undefined;
79
94
  const text = stripAnsi(screen);
80
95
  const m = text.match(flow.verificationUrlRegex);
81
96
  if (!m)
@@ -154,14 +169,15 @@ export function resolveMintIdentity(input) {
154
169
  export function seedReservedAuthToken(email, token) {
155
170
  const cleaned = assertValidSetupToken(token);
156
171
  const key = claudeAccountTokenKey(email);
157
- if (bundleExists(AUTH_BUNDLE)) {
158
- const backend = bundleBackend(AUTH_BUNDLE);
172
+ if (bundleExistsSync(AUTH_BUNDLE)) {
173
+ const backend = bundleBackendSync(AUTH_BUNDLE);
159
174
  if (backend !== 'file') {
160
175
  throw new Error(`Reserved bundle '${AUTH_BUNDLE}' exists with backend '${backend}', but usage/probe only reads a FILE-backed auth bundle. Recreate it with: agents secrets create ${AUTH_BUNDLE} --backend file --policy never --i-understand --force`);
161
176
  }
162
- const bundle = readBundle(AUTH_BUNDLE);
177
+ const bundle = readBundleSync(AUTH_BUNDLE);
163
178
  if (key in bundle.vars) {
164
- rotateBundleSecret(bundle, key, { newValue: cleaned, meta: { type: 'token' } });
179
+ rotateBundleSecretSync(bundle, key, { newValue: cleaned, meta: { type: 'token' } });
180
+ invalidateClaudeSetupTokenCache();
165
181
  return { key };
166
182
  }
167
183
  const item = secretsKeychainItem(AUTH_BUNDLE, key);
@@ -169,7 +185,8 @@ export function seedReservedAuthToken(email, token) {
169
185
  if (!bundle.meta)
170
186
  bundle.meta = {};
171
187
  bundle.meta[key] = { type: 'token' };
172
- writeBundleWithItems(bundle, new Map([[item, cleaned]]));
188
+ writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
189
+ invalidateClaudeSetupTokenCache();
173
190
  return { key };
174
191
  }
175
192
  const item = secretsKeychainItem(AUTH_BUNDLE, key);
@@ -181,9 +198,59 @@ export function seedReservedAuthToken(email, token) {
181
198
  vars: { [key]: keychainRef(key) },
182
199
  meta: { [key]: { type: 'token' } },
183
200
  };
184
- writeBundleWithItems(bundle, new Map([[item, cleaned]]));
201
+ writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
202
+ invalidateClaudeSetupTokenCache();
185
203
  return { key };
186
204
  }
205
+ /**
206
+ * The env var a harness's durable worker credential injects as on a worker
207
+ * (claude's setup-token rides CLAUDE_CODE_OAUTH_TOKEN; api-key harnesses ride
208
+ * their MINT_FLOWS apiKeyEnv). Throws for a harness with no portable worker
209
+ * credential — kimi/antigravity log in per box.
210
+ */
211
+ export function workerCredentialEnv(harness) {
212
+ if (harness === 'claude')
213
+ return 'CLAUDE_CODE_OAUTH_TOKEN';
214
+ const flow = MINT_FLOWS[harness];
215
+ if (flow?.auth === 'api-key' && flow.apiKeyEnv)
216
+ return flow.apiKeyEnv;
217
+ throw new Error(`No portable worker credential env for '${harness}' — it logs in per box.`);
218
+ }
219
+ /**
220
+ * The reserved-store key for one account's worker credential:
221
+ * `<ENV>_<accountId>` — keyed by account id, never by name or email, so a
222
+ * rename or an email change never breaks the key. Hyphens are stripped because
223
+ * the bundle key grammar (`BUNDLE_KEY_PATTERN`) is env-var-shaped.
224
+ */
225
+ export function workerCredentialStoreKey(harness, accountId) {
226
+ const slug = accountId.replace(/-/g, '');
227
+ if (!/^[A-Za-z0-9_]+$/.test(slug) || !slug) {
228
+ throw new Error(`Invalid account id '${accountId}' for a worker credential key.`);
229
+ }
230
+ return `${workerCredentialEnv(harness)}_${slug}`;
231
+ }
232
+ /**
233
+ * Write (or rotate) one account's worker credential in the reserved
234
+ * `__<harness>__` store (PHNX-3940). Only non-rotating kinds may enter
235
+ * (`assertStorableCredentialKind` refuses a rotating OAuth/session credential at
236
+ * this boundary — RUSH-1958). FILE-backed, policy `never`, same contract as the
237
+ * legacy `auth` bundle: a keychain- or vault-backed store of this name fails
238
+ * loud, since worker provisioning reads the file backend.
239
+ */
240
+ export function seedReservedStoreKey(harness, kind, key, value) {
241
+ assertStorableCredentialKind(kind, harness);
242
+ const name = reservedStoreName(harness);
243
+ const cleaned = value.trim();
244
+ if (!cleaned)
245
+ throw new Error(`Empty ${kind} for reserved store '${name}' key ${key}.`);
246
+ // A `__<harness>__` reserved store is written and read as raw FILE-backed items,
247
+ // never as a standalone bundle: the standalone's bundle-name validation rejects
248
+ // the `__`-wrapped reserved name, so the worker credential rides the raw item
249
+ // directly, symmetric with how `readReservedCredential` reads it. A first write
250
+ // and a rotation are the same operation — `store.set` overwrites the item value.
251
+ storeSetSync('file', secretsKeychainItem(name, key), cleaned);
252
+ return { bundle: name, key };
253
+ }
187
254
  /**
188
255
  * Create or rotate the named provider account that `agents run --account` and
189
256
  * `agents accounts sync` consume. Existing account of a different kind fails
@@ -211,6 +278,9 @@ function emitMintProgress(line, json) {
211
278
  * Tears the session down on the way out (success, timeout, or throw).
212
279
  */
213
280
  export async function driveSetupTokenMint(command, flow, opts = {}) {
281
+ if (flow.auth !== 'setup-token' || !flow.tokenCapture) {
282
+ throw new Error(`Harness '${flow.harness}' has no interactive mint command.`);
283
+ }
214
284
  const driver = opts.driver ?? defaultPtyDriver();
215
285
  const initialDelayMs = opts.drive?.initialDelayMs ?? 1500;
216
286
  const pollMs = opts.drive?.pollMs ?? 500;
@@ -274,12 +344,16 @@ export async function driveSetupTokenMint(command, flow, opts = {}) {
274
344
  await driver.stop(id).catch(() => { });
275
345
  }
276
346
  }
277
- export function buildMintCommand(flow, bin, home) {
278
- if (!flow.mintArgs) {
347
+ export function buildMintCommand(flow, bin, home, env = {}) {
348
+ if (flow.auth !== 'setup-token' || !flow.mintArgs) {
279
349
  throw new Error(`Harness '${flow.harness}' has no interactive mint command.`);
280
350
  }
281
351
  const args = flow.mintArgs.map(shellQuote).join(' ');
282
- return `HOME=${shellQuote(home)} ${shellQuote(bin)} ${args}`;
352
+ const envPrefix = [
353
+ `HOME=${shellQuote(home)}`,
354
+ ...Object.entries(env).map(([k, v]) => `${k}=${shellQuote(v)}`),
355
+ ].join(' ');
356
+ return `${envPrefix} ${shellQuote(bin)} ${args}`;
283
357
  }
284
358
  export function resolveMintInstallation(harness) {
285
359
  const installed = listInstalledVersions(harness);
@@ -382,7 +456,7 @@ async function syncMintedBundles(accountName, device) {
382
456
  const account = findAccount(accountName);
383
457
  if (!account)
384
458
  throw new Error(`Unknown provider account '${accountName}'.`);
385
- const accountPush = pushBundleToHost(accountName, device, {
459
+ const accountPush = await pushBundleToHost(accountName, device, {
386
460
  remoteBackend,
387
461
  force: true,
388
462
  operation: 'accounts mint --fleet',
@@ -397,8 +471,8 @@ async function syncMintedBundles(accountName, device) {
397
471
  });
398
472
  if (!accountPush.ok)
399
473
  throw new Error(accountPush.message);
400
- if (bundleExists(AUTH_BUNDLE)) {
401
- const authPush = pushBundleToHost(AUTH_BUNDLE, device, {
474
+ if (await bundleExists(AUTH_BUNDLE)) {
475
+ const authPush = await pushBundleToHost(AUTH_BUNDLE, device, {
402
476
  remoteBackend: 'file',
403
477
  force: true,
404
478
  operation: 'accounts mint --fleet',
@@ -417,32 +491,36 @@ async function syncMintedBundles(accountName, device) {
417
491
  /**
418
492
  * True when a Claude setup-token is already seeded on this box (setup status).
419
493
  *
420
- * Read-only status probe (`agents setup`, `agents doctor`) — the reserved
421
- * auth-bundle check below MUST NOT crash the whole command because the
422
- * keychain could not be reached (e.g. the macOS Keychain helper source is
423
- * unavailable — PHNX-3385); `bundleExists()`/`hasKeychainToken()` document
424
- * themselves as failing loud, but that contract is for destructive-write
425
- * guards, not a diagnostic. An unreachable keychain here just means "cannot
426
- * confirm the reserved bundle," reported honestly rather than propagated.
494
+ * Read-only status probe (`agents setup`, `agents doctor`) — neither the
495
+ * account-registry read nor the reserved auth-bundle check MUST crash the whole
496
+ * command because the secret store could not be reached (the macOS Keychain
497
+ * helper source unavailable — PHNX-3385 — or the standalone `secrets` CLI
498
+ * missing / unspawnable); the client's calls fail loud by contract, but that
499
+ * contract is for destructive-write guards, not a diagnostic. An unreachable
500
+ * store here just means "cannot confirm," reported honestly rather than
501
+ * propagated.
427
502
  */
428
503
  export function hasMintedSetupToken() {
429
- const records = Object.values(readAccountRegistry().accounts);
430
- const setup = records.filter((a) => a.auth === 'setup-token' && a.provider === 'anthropic');
431
- if (setup.length) {
432
- return { ready: true, detail: `${setup.length} Claude setup-token account${setup.length === 1 ? '' : 's'}` };
433
- }
434
504
  try {
435
- if (bundleExists(AUTH_BUNDLE) && bundleBackend(AUTH_BUNDLE) === 'file') {
436
- const bundle = readBundle(AUTH_BUNDLE);
505
+ // The registry is the account bundles behind the standalone `secrets` CLI, so
506
+ // this read is as reachable — or as unreachable — as the auth-bundle read
507
+ // below and sits inside the same guard.
508
+ const records = Object.values(readAccountRegistry().accounts);
509
+ const setup = records.filter((a) => a.auth === 'setup-token' && a.provider === 'anthropic');
510
+ if (setup.length) {
511
+ return { ready: true, detail: `${setup.length} Claude setup-token account${setup.length === 1 ? '' : 's'}` };
512
+ }
513
+ if (bundleExistsSync(AUTH_BUNDLE) && bundleBackendSync(AUTH_BUNDLE) === 'file') {
514
+ const bundle = readBundleSync(AUTH_BUNDLE);
437
515
  const keys = Object.keys(bundle.vars).filter((k) => k.startsWith('CLAUDE_CODE_OAUTH_TOKEN_'));
438
516
  if (keys.length)
439
517
  return { ready: true, detail: `reserved auth bundle (${keys.length} account key${keys.length === 1 ? '' : 's'})` };
440
518
  }
441
519
  }
442
520
  catch (err) {
443
- return { ready: false, detail: `could not check the reserved auth bundle: ${err.message}` };
521
+ return { ready: false, detail: `could not check the secret store: ${err.message}` };
444
522
  }
445
- return { ready: false, detail: 'no Claude setup-token minted — agents accounts mint claude' };
523
+ return { ready: false, detail: 'no Claude setup-token minted — agents accounts add claude [name]' };
446
524
  }
447
525
  function sleep(ms) {
448
526
  return new Promise((resolve) => setTimeout(resolve, ms));