@phnx-labs/agents-cli 1.22.84 → 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 (257) hide show
  1. package/CHANGELOG.md +57 -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/setup-accounts.d.ts +3 -2
  28. package/dist/commands/setup-accounts.js +4 -4
  29. package/dist/commands/setup-secrets.d.ts +16 -21
  30. package/dist/commands/setup-secrets.js +43 -224
  31. package/dist/commands/ssh.js +2 -2
  32. package/dist/commands/sync.js +1 -1
  33. package/dist/commands/update.d.ts +10 -0
  34. package/dist/commands/update.js +43 -40
  35. package/dist/commands/versions.d.ts +11 -11
  36. package/dist/commands/versions.js +131 -83
  37. package/dist/commands/view.d.ts +1 -9
  38. package/dist/commands/view.js +41 -55
  39. package/dist/commands/webhook.js +5 -5
  40. package/dist/commands/workflows.js +2 -1
  41. package/dist/index.d.ts +12 -12
  42. package/dist/index.js +13 -42
  43. package/dist/lib/account-capabilities.d.ts +1 -1
  44. package/dist/lib/account-capabilities.js +1 -1
  45. package/dist/lib/account-catalog.d.ts +121 -6
  46. package/dist/lib/account-catalog.js +421 -7
  47. package/dist/lib/account-registry.d.ts +8 -30
  48. package/dist/lib/account-registry.js +36 -79
  49. package/dist/lib/account-schema.d.ts +1 -1
  50. package/dist/lib/account-schema.js +1 -1
  51. package/dist/lib/accounting/account-pool-collect.js +16 -4
  52. package/dist/lib/accounting/rotate.d.ts +20 -7
  53. package/dist/lib/accounting/rotate.js +89 -13
  54. package/dist/lib/accounting/usage.js +11 -16
  55. package/dist/lib/accounts/add.d.ts +138 -0
  56. package/dist/lib/accounts/add.js +651 -0
  57. package/dist/lib/accounts/migrate.d.ts +117 -0
  58. package/dist/lib/accounts/migrate.js +536 -0
  59. package/dist/lib/accounts/slots.d.ts +13 -0
  60. package/dist/lib/accounts/slots.js +100 -0
  61. package/dist/lib/agent-spec/agents.d.ts +16 -0
  62. package/dist/lib/agent-spec/agents.js +32 -4
  63. package/dist/lib/app-bundle-install.js +5 -4
  64. package/dist/lib/auth-health.d.ts +3 -0
  65. package/dist/lib/auth-health.js +11 -0
  66. package/dist/lib/auth-mint.d.ts +52 -16
  67. package/dist/lib/auth-mint.js +117 -39
  68. package/dist/lib/browser/chrome.d.ts +1 -1
  69. package/dist/lib/browser/chrome.js +5 -5
  70. package/dist/lib/byok-usage.js +3 -3
  71. package/dist/lib/claude-account-token.d.ts +47 -3
  72. package/dist/lib/claude-account-token.js +172 -50
  73. package/dist/lib/cloud/antigravity.js +4 -4
  74. package/dist/lib/cloud/cursor.js +4 -4
  75. package/dist/lib/crabbox/cli.d.ts +1 -1
  76. package/dist/lib/crabbox/cli.js +6 -6
  77. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  78. package/dist/lib/crabbox/runtimes.js +5 -5
  79. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  80. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  81. package/dist/lib/daemon/auth-sync-service.js +28 -1
  82. package/dist/lib/daemon/daemon.d.ts +6 -16
  83. package/dist/lib/daemon/daemon.js +52 -97
  84. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  85. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  86. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  87. package/dist/lib/daemon/harness-update-service.js +1 -1
  88. package/dist/lib/daemon/runner.d.ts +15 -3
  89. package/dist/lib/daemon/runner.js +49 -24
  90. package/dist/lib/daemon-health.d.ts +1 -2
  91. package/dist/lib/daemon-health.js +7 -6
  92. package/dist/lib/daemon-services.d.ts +1 -1
  93. package/dist/lib/daemon-services.js +1 -11
  94. package/dist/lib/daemon-webhooks.d.ts +8 -7
  95. package/dist/lib/daemon-webhooks.js +10 -9
  96. package/dist/lib/device-config.js +1 -1
  97. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  98. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  99. package/dist/lib/devices/harness-inventory.js +126 -4
  100. package/dist/lib/doctor-diff.js +2 -1
  101. package/dist/lib/exec-account-home.d.ts +38 -0
  102. package/dist/lib/exec-account-home.js +164 -0
  103. package/dist/lib/exec.d.ts +30 -0
  104. package/dist/lib/exec.js +101 -31
  105. package/dist/lib/fleet/apply.d.ts +2 -2
  106. package/dist/lib/fleet/apply.js +4 -4
  107. package/dist/lib/fleet/auth-sync.js +4 -3
  108. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  109. package/dist/lib/harness/adapter.d.ts +7 -7
  110. package/dist/lib/harness/adapter.js +10 -6
  111. package/dist/lib/harness/adapters/grok.js +8 -3
  112. package/dist/lib/harness/adapters/muse.js +1 -1
  113. package/dist/lib/harness/adapters/opencode.js +12 -3
  114. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  115. package/dist/lib/harness-auth-capabilities.js +52 -0
  116. package/dist/lib/helper-versions.d.ts +6 -4
  117. package/dist/lib/helper-versions.js +5 -4
  118. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  119. package/dist/lib/hosts/credential-transport.js +37 -0
  120. package/dist/lib/hosts/dispatch.d.ts +7 -0
  121. package/dist/lib/hosts/dispatch.js +13 -6
  122. package/dist/lib/identity/client.d.ts +3 -3
  123. package/dist/lib/identity/client.js +3 -3
  124. package/dist/lib/installations/index.d.ts +1 -1
  125. package/dist/lib/installations/index.js +1 -1
  126. package/dist/lib/installations/migrate.js +9 -0
  127. package/dist/lib/installations/resolve.js +1 -1
  128. package/dist/lib/installations/shims.d.ts +15 -0
  129. package/dist/lib/installations/shims.js +69 -0
  130. package/dist/lib/installations/store.d.ts +43 -0
  131. package/dist/lib/installations/store.js +75 -7
  132. package/dist/lib/installations/versions.js +1 -1
  133. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  134. package/dist/lib/menubar/install-menubar.js +3 -3
  135. package/dist/lib/native-accounts.d.ts +35 -0
  136. package/dist/lib/native-accounts.js +41 -0
  137. package/dist/lib/net-close.d.ts +11 -0
  138. package/dist/lib/net-close.js +25 -0
  139. package/dist/lib/openclaw-keychain.js +27 -1
  140. package/dist/lib/profiles.js +22 -9
  141. package/dist/lib/project-resources.js +1 -1
  142. package/dist/lib/reserved-stores.d.ts +53 -0
  143. package/dist/lib/reserved-stores.js +120 -0
  144. package/dist/lib/secrets-client.d.ts +191 -0
  145. package/dist/lib/secrets-client.js +710 -0
  146. package/dist/lib/secrets-policy.d.ts +222 -0
  147. package/dist/lib/secrets-policy.js +483 -0
  148. package/dist/lib/secrets-types.d.ts +188 -0
  149. package/dist/lib/secrets-types.js +24 -0
  150. package/dist/lib/service-manifest.js +3 -4
  151. package/dist/lib/session/db.d.ts +18 -0
  152. package/dist/lib/session/db.js +47 -14
  153. package/dist/lib/session/sync/config.js +2 -2
  154. package/dist/lib/sha256-asset.d.ts +11 -16
  155. package/dist/lib/sha256-asset.js +11 -16
  156. package/dist/lib/share/config.js +14 -5
  157. package/dist/lib/signin-badge.d.ts +26 -0
  158. package/dist/lib/signin-badge.js +42 -0
  159. package/dist/lib/staleness/detectors/workflows.js +3 -105
  160. package/dist/lib/staleness/writers/workflows.js +2 -1
  161. package/dist/lib/state.d.ts +7 -4
  162. package/dist/lib/state.js +18 -7
  163. package/dist/lib/sync-umbrella.js +2 -2
  164. package/dist/lib/teams/agents.js +1 -1
  165. package/dist/lib/types.d.ts +74 -33
  166. package/dist/lib/view-types.d.ts +6 -3
  167. package/dist/lib/workflows-registry.d.ts +71 -0
  168. package/dist/lib/workflows-registry.js +280 -0
  169. package/dist/lib/workflows.d.ts +21 -21
  170. package/dist/lib/workflows.js +12 -327
  171. package/package.json +2 -3
  172. package/scripts/postinstall.js +4 -29
  173. package/dist/commands/secrets-import.d.ts +0 -18
  174. package/dist/commands/secrets-import.js +0 -74
  175. package/dist/commands/secrets-migrate.d.ts +0 -25
  176. package/dist/commands/secrets-migrate.js +0 -334
  177. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  178. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  179. package/dist/commands/secrets-sync.d.ts +0 -11
  180. package/dist/commands/secrets-sync.js +0 -153
  181. package/dist/commands/secrets-vault.d.ts +0 -10
  182. package/dist/commands/secrets-vault.js +0 -130
  183. package/dist/commands/secrets.d.ts +0 -212
  184. package/dist/commands/secrets.js +0 -3030
  185. package/dist/lib/accounts/connect.d.ts +0 -176
  186. package/dist/lib/accounts/connect.js +0 -453
  187. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  188. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  189. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  190. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  191. package/dist/lib/secrets/agent.d.ts +0 -358
  192. package/dist/lib/secrets/agent.js +0 -1291
  193. package/dist/lib/secrets/audit.d.ts +0 -46
  194. package/dist/lib/secrets/audit.js +0 -101
  195. package/dist/lib/secrets/bundles.d.ts +0 -290
  196. package/dist/lib/secrets/bundles.js +0 -1547
  197. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  198. package/dist/lib/secrets/download-keychain.js +0 -70
  199. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  200. package/dist/lib/secrets/drivers/rush.js +0 -90
  201. package/dist/lib/secrets/fallback.d.ts +0 -48
  202. package/dist/lib/secrets/fallback.js +0 -48
  203. package/dist/lib/secrets/filestore.d.ts +0 -222
  204. package/dist/lib/secrets/filestore.js +0 -1099
  205. package/dist/lib/secrets/headless.d.ts +0 -43
  206. package/dist/lib/secrets/headless.js +0 -56
  207. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  208. package/dist/lib/secrets/icloud-import.js +0 -206
  209. package/dist/lib/secrets/index.d.ts +0 -451
  210. package/dist/lib/secrets/index.js +0 -1568
  211. package/dist/lib/secrets/install-helper.d.ts +0 -72
  212. package/dist/lib/secrets/install-helper.js +0 -245
  213. package/dist/lib/secrets/lease.d.ts +0 -25
  214. package/dist/lib/secrets/lease.js +0 -44
  215. package/dist/lib/secrets/linux.d.ts +0 -77
  216. package/dist/lib/secrets/linux.js +0 -393
  217. package/dist/lib/secrets/list-filter.d.ts +0 -109
  218. package/dist/lib/secrets/list-filter.js +0 -261
  219. package/dist/lib/secrets/mcp.d.ts +0 -93
  220. package/dist/lib/secrets/mcp.js +0 -211
  221. package/dist/lib/secrets/profiles.d.ts +0 -10
  222. package/dist/lib/secrets/profiles.js +0 -13
  223. package/dist/lib/secrets/push.d.ts +0 -133
  224. package/dist/lib/secrets/push.js +0 -273
  225. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  226. package/dist/lib/secrets/rc-hygiene.js +0 -143
  227. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  228. package/dist/lib/secrets/read-backoff.js +0 -64
  229. package/dist/lib/secrets/reaper.d.ts +0 -90
  230. package/dist/lib/secrets/reaper.js +0 -243
  231. package/dist/lib/secrets/remote.d.ts +0 -152
  232. package/dist/lib/secrets/remote.js +0 -344
  233. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  234. package/dist/lib/secrets/reserved-sync.js +0 -147
  235. package/dist/lib/secrets/scope.d.ts +0 -26
  236. package/dist/lib/secrets/scope.js +0 -29
  237. package/dist/lib/secrets/session-store.d.ts +0 -107
  238. package/dist/lib/secrets/session-store.js +0 -342
  239. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  240. package/dist/lib/secrets/sync-backend.js +0 -13
  241. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  242. package/dist/lib/secrets/sync-commands.js +0 -21
  243. package/dist/lib/secrets/sync.d.ts +0 -48
  244. package/dist/lib/secrets/sync.js +0 -237
  245. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  246. package/dist/lib/secrets/unlock-hints.js +0 -36
  247. package/dist/lib/secrets/usage-db.d.ts +0 -46
  248. package/dist/lib/secrets/usage-db.js +0 -96
  249. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  250. package/dist/lib/secrets/vault-age-helper.js +0 -34
  251. package/dist/lib/secrets/vault.d.ts +0 -49
  252. package/dist/lib/secrets/vault.js +0 -399
  253. package/dist/lib/secrets/windows.d.ts +0 -81
  254. package/dist/lib/secrets/windows.js +0 -556
  255. package/scripts/install-helper.js +0 -97
  256. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  257. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -24,10 +24,11 @@ import chalk from 'chalk';
24
24
  import { atomicWriteFileSync } from './fs-atomic.js';
25
25
  import { getUserAgentsDir, readMeta, updateMeta } from './state.js';
26
26
  import { isAgentId } from './types.js';
27
- import { deleteKeychainToken, getKeychainToken, hasKeychainToken } from './secrets/index.js';
28
- import { bundleExists, deleteBundle, listBundles, readAndResolveBundleEnv, readBundle, renameBundle, writeBundleWithItems } from './secrets/bundles.js';
27
+ import { bundleExistsSync, deleteBundleSync, deleteKeychainTokenSync, getKeychainTokenSync, hasKeychainTokenSync, listBundlesSync, readAndResolveBundleEnvSync, readBundleSync, renameBundleSync, writeBundleWithItemsSync, } from './secrets-client.js';
29
28
  import { getAccountProvider } from './account-provider-registry.js';
30
29
  import { accountSecretItem, buildAccountBundle, parseAccountBundle, secretVarFor } from './account-schema.js';
30
+ export { recordSlot, readSlots } from './accounts/slots.js';
31
+ export { registeredNativeAccountForEmail, parseNativeIdentityKey } from './native-accounts.js';
31
32
  const NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
32
33
  const NATIVE_LABEL = /^[a-zA-Z0-9][a-zA-Z0-9@._+-]*$/;
33
34
  const AUTH_KINDS = ['api-key', 'setup-token', 'bearer-token'];
@@ -56,7 +57,7 @@ function toCredentialAccount(record) {
56
57
  /** Every account bundle currently on this device, keyed by stable id. */
57
58
  function readAccountBundles() {
58
59
  const out = [];
59
- for (const bundle of listBundles()) {
60
+ for (const bundle of listBundlesSync()) {
60
61
  const record = parseAccountBundle(bundle);
61
62
  if (record)
62
63
  out.push(toCredentialAccount(record));
@@ -100,8 +101,8 @@ function migrateLegacyRegistryFile(base) {
100
101
  const provider = String(item.provider ?? '');
101
102
  const legacySecretRef = String(item.secretRef ?? `agents-cli.accounts.${id}.credential`);
102
103
  retiredSecretItems.push(legacySecretRef);
103
- if (bundleExists(name)) {
104
- const existing = parseAccountBundle(readBundle(name));
104
+ if (bundleExistsSync(name)) {
105
+ const existing = parseAccountBundle(readBundleSync(name));
105
106
  if (!existing || existing.id !== id) {
106
107
  throw new Error(`Cannot migrate account '${name}': a different secrets bundle already uses that name.`);
107
108
  }
@@ -109,17 +110,17 @@ function migrateLegacyRegistryFile(base) {
109
110
  }
110
111
  const baseUrl = item.baseUrl ? String(item.baseUrl) : undefined;
111
112
  const record = { id, name, provider, auth, baseUrl };
112
- const secret = hasKeychainToken(legacySecretRef) ? getKeychainToken(legacySecretRef) : '';
113
+ const secret = hasKeychainTokenSync(legacySecretRef) ? getKeychainTokenSync(legacySecretRef) : '';
113
114
  const { bundle, items } = buildAccountBundle(record, secret || 'x');
114
115
  if (!secret)
115
116
  items.clear(); // no device-local secret: write metadata only
116
- writeBundleWithItems(bundle, items);
117
+ writeBundleWithItemsSync(bundle, items);
117
118
  }
118
119
  // Success: the file's accounts all exist as bundles now. Archive it and drop
119
120
  // the superseded per-account keychain items.
120
121
  archiveLegacyFile(file, 'accounts.migrated.yaml');
121
122
  for (const legacyItem of retiredSecretItems)
122
- deleteKeychainToken(legacyItem);
123
+ deleteKeychainTokenSync(legacyItem);
123
124
  }
124
125
  function archiveLegacyFile(file, archiveName) {
125
126
  const archived = path.join(path.dirname(file), archiveName);
@@ -275,7 +276,7 @@ function assertUniqueUnifiedName(name, meta, doc, exceptIds, agent) {
275
276
  }
276
277
  /**
277
278
  * Validate a native account NAME (charset + per-harness uniqueness) WITHOUT a
278
- * known identity — the pre-flight `agents accounts connect` runs before it
279
+ * known identity — the pre-flight `agents accounts add` runs before it
279
280
  * installs a home and drives a login, so a bad/colliding name fails before any
280
281
  * side effect instead of orphaning a freshly-minted home (PHNX-3940).
281
282
  */
@@ -335,71 +336,24 @@ export function labelNativeAccount(agent, identityKey, identityLabel, label, sco
335
336
  if (rowScope === 'device') {
336
337
  const native = { ...current.deviceAccounts?.native };
337
338
  for (const row of matches)
338
- native[row.id] = { id: row.id, name: resolvedLabel, agent, identityKey, identityLabel, scope: rowScope };
339
+ native[row.id] = { ...native[row.id], name: resolvedLabel, identityLabel, scope: rowScope };
339
340
  return { ...current, deviceAccounts: { ...current.deviceAccounts, native } };
340
341
  }
341
342
  const native = { ...current.accounts?.native };
342
343
  for (const row of matches)
343
- native[row.id] = { id: row.id, name: resolvedLabel, agent, identityKey, identityLabel, scope: rowScope };
344
+ native[row.id] = { ...native[row.id], name: resolvedLabel, identityLabel, scope: rowScope };
344
345
  return { ...current, accounts: { ...current.accounts, native } };
345
346
  });
346
347
  return { ...matches[0], name: resolvedLabel, identityLabel, scope: rowScope };
347
348
  }
348
349
  /**
349
- * Record THIS box's connect home for an account (PHNX-3940). Device-scoped: the
350
- * home a box minted for an account is not assumed to exist on any other box, so
351
- * it lives in the device doc keyed by the stable account id, never on the
352
- * fleet-synced central identity row. Idempotent. A reconnect reads it back via
353
- * {@link nativeAccountHome} to reuse the exact home even when the credential has
354
- * expired and local signed-in discovery can no longer find it.
350
+ * This box's recorded home LABEL for an account, or null.
351
+ * Reads the leftover device-scoped `homes` map so legacy `acct-*` installation
352
+ * labels still resolve (T5/T7); spawn-time HOME is {@link readSlots}[id].slotDir.
355
353
  */
356
- export function setNativeAccountHome(accountId, installationLabel) {
357
- updateMeta(current => ({
358
- ...current,
359
- deviceAccounts: {
360
- ...current.deviceAccounts,
361
- homes: { ...current.deviceAccounts?.homes, [accountId]: installationLabel },
362
- },
363
- }));
364
- }
365
- /** This box's recorded connect home for an account, or null. */
366
354
  export function nativeAccountHome(accountId, meta) {
367
355
  return meta.deviceAccounts?.homes?.[accountId] ?? null;
368
356
  }
369
- /**
370
- * Every installation label THIS box has recorded as SOME account's connect home
371
- * (PHNX-3940). These are identity-bearing and MUST NEVER be re-minted for a new
372
- * account — the safe-allocation invariant that stops a new connect from
373
- * overwriting another account's login.
374
- */
375
- export function ownedConnectHomeLabels(meta) {
376
- return new Set(Object.values(meta.deviceAccounts?.homes ?? {}));
377
- }
378
- function pendingConnectKey(agent, name) {
379
- return `${agent}:${name.toLowerCase()}`;
380
- }
381
- /** This box's in-flight connect slot for `(agent, name)`, or null. */
382
- export function pendingConnectSlot(agent, name, meta) {
383
- return meta.deviceAccounts?.pendingConnects?.[pendingConnectKey(agent, name)] ?? null;
384
- }
385
- /** Record an in-flight connect slot for `(agent, name)` (device-scoped). */
386
- export function setPendingConnectSlot(agent, name, slot) {
387
- updateMeta(current => ({
388
- ...current,
389
- deviceAccounts: {
390
- ...current.deviceAccounts,
391
- pendingConnects: { ...current.deviceAccounts?.pendingConnects, [pendingConnectKey(agent, name)]: slot },
392
- },
393
- }));
394
- }
395
- /** Clear the in-flight connect slot for `(agent, name)` once the account lands. */
396
- export function clearPendingConnectSlot(agent, name) {
397
- updateMeta(current => {
398
- const pendingConnects = { ...current.deviceAccounts?.pendingConnects };
399
- delete pendingConnects[pendingConnectKey(agent, name)];
400
- return { ...current, deviceAccounts: { ...current.deviceAccounts, pendingConnects } };
401
- });
402
- }
403
357
  /**
404
358
  * Set the per-harness default account by NAME only when none is configured
405
359
  * (PHNX-3940). Never overrides an existing choice — a first connect selecting a
@@ -528,11 +482,11 @@ export function addAccount(name, provider, auth, secret, base = getUserAgentsDir
528
482
  assertUniqueUnifiedName(name, readMeta(), readAccountRegistry(base));
529
483
  const adapter = getAccountProvider(provider);
530
484
  adapter.validate(auth, secret);
531
- if (bundleExists(name))
485
+ if (bundleExistsSync(name))
532
486
  throw new Error(`Secrets bundle '${name}' already exists. Choose a different account name.`);
533
487
  const record = { id: crypto.randomUUID(), name, provider: adapter.provider, auth, baseUrl: opts.baseUrl };
534
488
  const { bundle, items } = buildAccountBundle(record, secret);
535
- writeBundleWithItems(bundle, items);
489
+ writeBundleWithItemsSync(bundle, items);
536
490
  return toCredentialAccount(record);
537
491
  }
538
492
  export function setAccountSecret(name, secret, base = getUserAgentsDir()) {
@@ -542,8 +496,8 @@ export function setAccountSecret(name, secret, base = getUserAgentsDir()) {
542
496
  getAccountProvider(account.provider).validate(account.auth, secret);
543
497
  const record = { id: account.id, name: account.name, provider: account.provider, auth: account.auth, baseUrl: account.baseUrl };
544
498
  const { bundle, items } = buildAccountBundle(record, secret);
545
- bundle.created_at = readBundle(account.name).created_at; // rotate the secret, keep the bundle's birth time
546
- writeBundleWithItems(bundle, items);
499
+ bundle.created_at = readBundleSync(account.name).created_at; // rotate the secret, keep the bundle's birth time
500
+ writeBundleWithItemsSync(bundle, items);
547
501
  }
548
502
  /**
549
503
  * `oldSelector` is a bare name, a row id, or `<harness>#<name>`. A native row
@@ -606,7 +560,7 @@ export function renameAccount(oldSelector, newName, base = getUserAgentsDir()) {
606
560
  }
607
561
  return { ...current, accounts: { ...current.accounts, defaults } };
608
562
  });
609
- renameBundle(account.name, newName); // moves metadata + secret, preserves ACCOUNT_ID
563
+ renameBundleSync(account.name, newName); // moves metadata + secret, preserves ACCOUNT_ID
610
564
  renameProfileConsumers(account.name, newName, base);
611
565
  }
612
566
  /** `selector` is a bare name, a row id, or `<harness>#<name>` (see {@link renameAccount}). */
@@ -622,20 +576,23 @@ export function removeAccount(selector, base = getUserAgentsDir()) {
622
576
  // Sweep every row for the identity (PHNX-3206) from its owning store (PHNX-3315).
623
577
  const rowScope = rows[0].scope;
624
578
  updateMeta(current => {
625
- // Drop this box's connect-home record for the removed account (PHNX-3940).
579
+ // Drop this box's connect-home and slot records for the removed account (PHNX-3940).
626
580
  const homes = { ...current.deviceAccounts?.homes };
627
- for (const id of ids)
581
+ const slots = { ...current.deviceAccounts?.slots };
582
+ for (const id of ids) {
628
583
  delete homes[id];
584
+ delete slots[id];
585
+ }
629
586
  if (rowScope === 'device') {
630
587
  const accounts = { ...current.deviceAccounts?.native };
631
588
  for (const id of ids)
632
589
  delete accounts[id];
633
- return { ...current, deviceAccounts: { ...current.deviceAccounts, native: accounts, homes } };
590
+ return { ...current, deviceAccounts: { ...current.deviceAccounts, native: accounts, homes, slots } };
634
591
  }
635
592
  const accounts = { ...current.accounts?.native };
636
593
  for (const id of ids)
637
594
  delete accounts[id];
638
- return { ...current, accounts: { ...current.accounts, native: accounts }, deviceAccounts: { ...current.deviceAccounts, homes } };
595
+ return { ...current, accounts: { ...current.accounts, native: accounts }, deviceAccounts: { ...current.deviceAccounts, homes, slots } };
639
596
  });
640
597
  return;
641
598
  }
@@ -654,17 +611,17 @@ export function removeAccount(selector, base = getUserAgentsDir()) {
654
611
  const consumers = [...new Set([...profileConsumers(account.name, base), ...profileConsumers(account.id, base)])].sort();
655
612
  if (consumers.length)
656
613
  throw new Error(`Account '${account.name}' is used by harness${consumers.length === 1 ? '' : 'es'}: ${consumers.join(', ')}. Reassign them before removing it.`);
657
- deleteKeychainToken(account.secretRef);
658
- deleteBundle(account.name);
614
+ deleteKeychainTokenSync(account.secretRef);
615
+ deleteBundleSync(account.name);
659
616
  }
660
617
  export function inspectAccount(name, base = getUserAgentsDir()) {
661
618
  const account = findAccount(name, readAccountRegistry(base));
662
619
  if (!account)
663
620
  throw new Error(`Unknown account '${name}'.`);
664
- const bundle = readBundle(account.name);
621
+ const bundle = readBundleSync(account.name);
665
622
  if (bundle.policy !== 'never')
666
623
  throw new Error(`Account bundle '${account.name}' must use secrets policy 'never'.`);
667
- return { ...account, secretPresent: hasKeychainToken(account.secretRef), policy: bundle.policy };
624
+ return { ...account, secretPresent: hasKeychainTokenSync(account.secretRef), policy: bundle.policy };
668
625
  }
669
626
  export function resolveCredentialAccount(name, host, expectedProvider, base = getUserAgentsDir()) {
670
627
  const account = findAccount(name, readAccountRegistry(base));
@@ -677,15 +634,15 @@ export function resolveCredentialAccount(name, host, expectedProvider, base = ge
677
634
  throw new Error(`Provider '${account.provider}' cannot use a setup-token with the ${host} harness.`);
678
635
  }
679
636
  const envVar = account.auth === 'setup-token' ? 'CLAUDE_CODE_OAUTH_TOKEN' : adapter.envFor(host, account.auth);
680
- if (!hasKeychainToken(account.secretRef))
637
+ if (!hasKeychainTokenSync(account.secretRef))
681
638
  throw new Error(`Credential for account '${account.name}' is missing on this device. Add it with 'agents accounts set-key ${account.name}'.`);
682
639
  const secretVar = secretVarFor(account.auth);
683
640
  // Account bundles are policy `never`, so their value items carry no biometry
684
641
  // ACL. Resolve through the bundle path that verifies that policy and attests
685
- // `silentNoAcl` to the headless keychain guard. Calling getKeychainToken()
642
+ // `silentNoAcl` to the headless keychain guard. Calling getKeychainTokenSync()
686
643
  // directly makes a headless --account launch reject the prompt-free item as
687
644
  // if it required Touch ID before the helper ever reads it (PHNX-2939).
688
- const secret = readAndResolveBundleEnv(account.name, {
645
+ const secret = readAndResolveBundleEnvSync(account.name, {
689
646
  keys: [secretVar],
690
647
  keyMode: 'storage',
691
648
  agentOnly: true,
@@ -742,8 +699,8 @@ export function resolveSpawnAccount(explicit, agent, version, meta, opts = {}) {
742
699
  return null;
743
700
  }
744
701
  const remedy = selection.source === 'binding'
745
- ? `Detach the stale binding with: agents accounts detach ${selection.id} ${target}`
746
- : `Add an account named '${selection.id}' or remove the --account override.`;
702
+ ? `The binding for ${target} points at an account that no longer exists.`
703
+ : `Add an account named '${selection.id}' with: agents accounts add ${agent} ${selection.id}`;
747
704
  throw new Error(`Unknown account '${selection.id}' for ${agent} harness. ${remedy}`);
748
705
  }
749
706
  if (unified.kind === 'native') {
@@ -1,4 +1,4 @@
1
- import type { SecretsBundle } from './secrets/bundles.js';
1
+ import type { SecretsBundle } from './secrets-types.js';
2
2
  import type { AccountAuthKind } from './account-provider-registry.js';
3
3
  /** Canonical bundle variable names for a provider-account bundle. */
4
4
  export declare const ACCOUNT_VARS: {
@@ -20,7 +20,7 @@
20
20
  * record back out, with no I/O. The registry ([[account-registry]]) owns
21
21
  * reads and writes; the catalog ([[account-catalog]]) owns discovery.
22
22
  */
23
- import { parseBundleValue, secretsKeychainItem } from './secrets/index.js';
23
+ import { parseBundleValue, secretsKeychainItem } from './secrets-client.js';
24
24
  /** Canonical bundle variable names for a provider-account bundle. */
25
25
  export const ACCOUNT_VARS = {
26
26
  id: 'ACCOUNT_ID',
@@ -1,5 +1,5 @@
1
1
  import { readAccountRegistry } from '../account-registry.js';
2
- import { hasKeychainToken } from '../secrets/index.js';
2
+ import { hasKeychainTokenSync, isSecretsTransportError } from '../secrets-client.js';
3
3
  import { getGlobalDefault, listInstalledVersions } from '../installations/versions.js';
4
4
  import { collectRunCandidates } from './rotate.js';
5
5
  import { registryPoolCandidates } from './account-pool.js';
@@ -11,9 +11,21 @@ import { registryPoolCandidates } from './account-pool.js';
11
11
  * rotating to a healthy one.
12
12
  */
13
13
  function localRegistryRecords() {
14
- return Object.values(readAccountRegistry().accounts)
15
- .filter((a) => hasKeychainToken(a.secretRef))
16
- .map((a) => ({ name: a.name, provider: a.provider, auth: a.auth, secretPresent: true }));
14
+ // Read-only launch input: an unreachable standalone `secrets` (missing binary,
15
+ // timeout, non-JSON) must not abort the run — it just means no provider
16
+ // accounts are foldable here, so the run proceeds on the native login rather
17
+ // than hanging or crashing (PHNX-3989, honoring the ≤3s sync bound). A genuine
18
+ // non-transport error still throws. DIST-1 holds: no embedded-engine fallback.
19
+ try {
20
+ return Object.values(readAccountRegistry().accounts)
21
+ .filter((a) => hasKeychainTokenSync(a.secretRef))
22
+ .map((a) => ({ name: a.name, provider: a.provider, auth: a.auth, secretPresent: true }));
23
+ }
24
+ catch (err) {
25
+ if (isSecretsTransportError(err))
26
+ return [];
27
+ throw err;
28
+ }
17
29
  }
18
30
  /**
19
31
  * PURE: fold provider-account candidates into the native version-home list.
@@ -62,6 +62,16 @@ export interface RotateCandidate {
62
62
  * authenticates through the existing provider-account injection.
63
63
  */
64
64
  providerAccount?: string;
65
+ /**
66
+ * Native account name when this candidate is a slot (PHNX-3940 T5). The
67
+ * picker and `--account` / `#name` selector use this; `version` is the
68
+ * binary (managed install) and is no longer the account identity.
69
+ */
70
+ nativeAccount?: string;
71
+ /** Slot dir when this candidate is a slot — the spawn HOME. */
72
+ slotDir?: string;
73
+ /** True when this row came from `deviceAccounts.slots`, not a version home. */
74
+ fromSlot?: boolean;
65
75
  }
66
76
  export interface RotateResult {
67
77
  /** The version picked for this run. */
@@ -402,16 +412,19 @@ export declare function collectHarnessCandidates(agentIds?: AgentId[]): Promise<
402
412
  * (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
403
413
  * Returns null when nothing matches, so the caller can fall back and warn.
404
414
  */
415
+ export declare function matchAccountCandidate(candidates: RotateCandidate[], account: string, preferredLabel?: string | null): RotateCandidate | null;
405
416
  export declare function matchAccountVersion(candidates: RotateCandidate[], account: string, preferredLabel?: string | null): string | null;
406
417
  /**
407
- * Resolve a routine's `account:` pin (login email or account key) to the
408
- * installed version currently holding that account. Thin I/O wrapper over
409
- * {@link collectRunCandidates} + {@link matchAccountVersion}; returns null when
410
- * no signed-in version matches. Pinning a routine to a distinct account is the
411
- * durable cure for the shared-single-use-refresh-token revocation storm
412
- * (RUSH-1957): the pinned run never rotates and never lands on another
413
- * routine's credential.
418
+ * Resolve a routine's `account:` pin (login email, account key, or native
419
+ * account name) to the candidate currently holding that account. Thin I/O
420
+ * wrapper over {@link collectRunCandidates} + {@link matchAccountCandidate};
421
+ * returns null when no signed-in candidate matches. Pinning a routine to a
422
+ * distinct account is the durable cure for the shared-single-use-refresh-token
423
+ * revocation storm (RUSH-1957): the pinned run never rotates and never lands
424
+ * on another routine's credential. Post-T5 the candidate's `nativeAccount`
425
+ * (not just `version`) is what disambiguates two slots on one managed install.
414
426
  */
427
+ export declare function resolveAccountCandidate(agent: AgentId, account: string, preferredLabel?: string | null): Promise<RotateCandidate | null>;
415
428
  export declare function resolveAccountVersion(agent: AgentId, account: string, preferredLabel?: string | null): Promise<string | null>;
416
429
  /**
417
430
  * Pick a healthy version for `agent` using weighted random by remaining
@@ -10,6 +10,9 @@ import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
10
10
  import { accountDisplayLabel, getAccountInfo, credentialPresence, ALL_AGENT_IDS, } from '../agents.js';
11
11
  import { readMeta, writeMeta, getHelpersDir } from '../state.js';
12
12
  import { listInstalledVersions, getVersionHomePath, resolveVersion } from '../installations/versions.js';
13
+ import { resolveManagedInstallation } from '../installations/store.js';
14
+ import { listNativeAccounts } from '../account-registry.js';
15
+ import { readSlots } from '../accounts/slots.js';
13
16
  import { getProjectRunConfigs } from '../run-config.js';
14
17
  import { emit } from '../feed/events.js';
15
18
  import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, } from './usage.js';
@@ -74,7 +77,14 @@ export function setGlobalRunStrategy(agent, strategy) {
74
77
  * warning can never disagree: an account is eligible iff its readiness is
75
78
  * `ready` — signed in, not server-revoked, and not out of usage.
76
79
  */
80
+ /** Slot verdicts balanced/available will launch (PHNX-3940 T5). */
81
+ const LAUNCHABLE_SLOT_VERDICTS = new Set(['live', 'unverified']);
82
+ function isLaunchableSlotVerdict(verdict) {
83
+ return verdict !== null && LAUNCHABLE_SLOT_VERDICTS.has(verdict);
84
+ }
77
85
  function isRotationEligible(candidate) {
86
+ if (candidate.fromSlot && !isLaunchableSlotVerdict(candidate.authVerdict))
87
+ return false;
78
88
  return readinessFromCandidate(candidate).ready;
79
89
  }
80
90
  /**
@@ -684,9 +694,65 @@ export async function collectRunCandidates(agent) {
684
694
  // LOCAL host — routing decides which local version to launch.
685
695
  const authCache = readAuthHealthCache();
686
696
  const localHost = machineId();
687
- const rows = await Promise.all(versions.map(async (version) => {
697
+ const meta = readMeta();
698
+ const binaryLabel = resolveManagedInstallation(agent)?.label ?? versions[0];
699
+ const slotRows = [];
700
+ const slotIdentities = new Set();
701
+ const slotDirs = new Set();
702
+ if (binaryLabel) {
703
+ const slots = readSlots(meta);
704
+ const natives = listNativeAccounts(meta).filter((row) => row.agent === agent);
705
+ const probed = await Promise.all(natives.map(async (account) => {
706
+ const slot = slots[account.id];
707
+ if (!slot || !fs.existsSync(slot.slotDir))
708
+ return null;
709
+ const home = slot.slotDir;
710
+ const info = await getAccountInfo(agent, home);
711
+ const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
712
+ const slotOk = isLaunchableSlotVerdict(slot.verdict);
713
+ return {
714
+ agent,
715
+ version: binaryLabel,
716
+ home,
717
+ info,
718
+ accountKey: launchable ? info.accountKey : null,
719
+ accountLabel: account.name || (launchable ? accountDisplayLabel(info) : ''),
720
+ email: launchable ? info.email : null,
721
+ usageStatus: launchable ? info.usageStatus : null,
722
+ plan: launchable ? info.plan : null,
723
+ signedIn: launchable && slotOk,
724
+ authVerdict: slot.verdict,
725
+ authCheckedAt: (() => {
726
+ const ts = slot.checkedAt ? Date.parse(slot.checkedAt) : NaN;
727
+ return Number.isFinite(ts) ? ts : null;
728
+ })(),
729
+ lastActive: info.lastActive,
730
+ nativeAccount: account.name,
731
+ slotDir: home,
732
+ fromSlot: true,
733
+ };
734
+ }));
735
+ for (const row of probed) {
736
+ if (!row)
737
+ continue;
738
+ slotRows.push(row);
739
+ slotDirs.add(path.resolve(row.home));
740
+ if (row.info.accountKey)
741
+ slotIdentities.add(row.info.accountKey);
742
+ if (row.email)
743
+ slotIdentities.add(row.email.toLowerCase());
744
+ }
745
+ }
746
+ // Legacy per-account installations (`acct-*` homes) until T7 migrates them.
747
+ const versionRows = await Promise.all(versions.map(async (version) => {
688
748
  const home = getVersionHomePath(agent, version);
749
+ if (slotDirs.has(path.resolve(home)))
750
+ return null;
689
751
  const info = await getAccountInfo(agent, home);
752
+ if (info.accountKey && slotIdentities.has(info.accountKey))
753
+ return null;
754
+ if (info.email && slotIdentities.has(info.email.toLowerCase()))
755
+ return null;
690
756
  // We used to additionally call isClaudeAuthValid(home), which reads
691
757
  // "Claude Code-credentials-<hash>" from the system keychain. That item is
692
758
  // written by Claude Code itself with its own process in the ACL, so our
@@ -717,6 +783,7 @@ export async function collectRunCandidates(agent) {
717
783
  lastActive: info.lastActive,
718
784
  };
719
785
  }));
786
+ const rows = [...slotRows, ...versionRows.filter((row) => row !== null)];
720
787
  // These candidates feed a routing decision on the `agents run` hot path, so
721
788
  // this read is CACHE-ONLY (`readOnly`): it never blocks on a live provider
722
789
  // fetch. A snapshot older than USAGE_DECISION_MAX_AGE_MS is not trusted for
@@ -775,26 +842,35 @@ export async function collectHarnessCandidates(agentIds = ALL_AGENT_IDS) {
775
842
  * (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
776
843
  * Returns null when nothing matches, so the caller can fall back and warn.
777
844
  */
778
- export function matchAccountVersion(candidates, account, preferredLabel) {
845
+ export function matchAccountCandidate(candidates, account, preferredLabel) {
779
846
  const needle = account.trim().toLowerCase();
780
847
  if (!needle)
781
848
  return null;
782
849
  const matching = candidates.filter((c) => c.signedIn &&
783
- (c.email?.toLowerCase() === needle || c.accountKey?.toLowerCase() === needle));
784
- return matching.find(candidate => candidate.version === preferredLabel)?.version ?? matching[0]?.version ?? null;
850
+ (c.email?.toLowerCase() === needle
851
+ || c.accountKey?.toLowerCase() === needle
852
+ || c.nativeAccount?.toLowerCase() === needle));
853
+ return matching.find(candidate => candidate.version === preferredLabel) ?? matching[0] ?? null;
854
+ }
855
+ export function matchAccountVersion(candidates, account, preferredLabel) {
856
+ return matchAccountCandidate(candidates, account, preferredLabel)?.version ?? null;
785
857
  }
786
858
  /**
787
- * Resolve a routine's `account:` pin (login email or account key) to the
788
- * installed version currently holding that account. Thin I/O wrapper over
789
- * {@link collectRunCandidates} + {@link matchAccountVersion}; returns null when
790
- * no signed-in version matches. Pinning a routine to a distinct account is the
791
- * durable cure for the shared-single-use-refresh-token revocation storm
792
- * (RUSH-1957): the pinned run never rotates and never lands on another
793
- * routine's credential.
859
+ * Resolve a routine's `account:` pin (login email, account key, or native
860
+ * account name) to the candidate currently holding that account. Thin I/O
861
+ * wrapper over {@link collectRunCandidates} + {@link matchAccountCandidate};
862
+ * returns null when no signed-in candidate matches. Pinning a routine to a
863
+ * distinct account is the durable cure for the shared-single-use-refresh-token
864
+ * revocation storm (RUSH-1957): the pinned run never rotates and never lands
865
+ * on another routine's credential. Post-T5 the candidate's `nativeAccount`
866
+ * (not just `version`) is what disambiguates two slots on one managed install.
794
867
  */
795
- export async function resolveAccountVersion(agent, account, preferredLabel) {
868
+ export async function resolveAccountCandidate(agent, account, preferredLabel) {
796
869
  const candidates = await collectRunCandidates(agent);
797
- return matchAccountVersion(candidates, account, preferredLabel);
870
+ return matchAccountCandidate(candidates, account, preferredLabel);
871
+ }
872
+ export async function resolveAccountVersion(agent, account, preferredLabel) {
873
+ return (await resolveAccountCandidate(agent, account, preferredLabel))?.version ?? null;
798
874
  }
799
875
  /**
800
876
  * Pick a healthy version for `agent` using weighted random by remaining
@@ -19,7 +19,7 @@ import { promisify } from 'util';
19
19
  import chalk from 'chalk';
20
20
  import { decodeJwtPayload, decryptDroidAuthPayload } from '../agents.js';
21
21
  import { walkForFiles } from '../fs-walk.js';
22
- import { getKeychainToken, setKeychainToken, deleteKeychainToken, isKeychainBackendOverridden, } from '../secrets/index.js';
22
+ import { deleteKeychainToken, deleteKeychainTokenSync, getKeychainTokenSync, setKeychainToken, } from '../secrets-client.js';
23
23
  import { resolveClaudeSetupToken } from '../claude-account-token.js';
24
24
  import { formatBackoffRemaining, noteUsageRateLimited, usageRateLimitedUntil, } from '../usage-backoff.js';
25
25
  import { getCacheDir } from '../state.js';
@@ -1547,7 +1547,7 @@ function claudeOauthCacheItem(service) {
1547
1547
  */
1548
1548
  function deleteCachedClaudeOauth(service) {
1549
1549
  try {
1550
- deleteKeychainToken(claudeOauthCacheItem(service));
1550
+ deleteKeychainTokenSync(claudeOauthCacheItem(service));
1551
1551
  }
1552
1552
  catch {
1553
1553
  /* best-effort — cache is an optimization */
@@ -1627,13 +1627,11 @@ export async function loadClaudeOauth(home, opts) {
1627
1627
  // dispatch does not (SING-1b / RUSH-2359). The OS keychain/keyring step is
1628
1628
  // macOS/Linux-only; Windows and any
1629
1629
  // fileOnly caller skip to the .credentials.json read below (the Claude CLI
1630
- // stores its OAuth token in that file too). An injected test backend makes the
1631
- // keychain path exercisable anywhere, so the platform check yields to it.
1632
- if (!opts?.fileOnly
1633
- && (process.platform === 'darwin' || process.platform === 'linux' || isKeychainBackendOverridden())) {
1630
+ // stores its OAuth token in that file too).
1631
+ if (!opts?.fileOnly && (process.platform === 'darwin' || process.platform === 'linux')) {
1634
1632
  const service = getClaudeKeychainService(home);
1635
1633
  try {
1636
- const fromKeychain = parseClaudeOauthPayload(getKeychainToken(service));
1634
+ const fromKeychain = parseClaudeOauthPayload(getKeychainTokenSync(service));
1637
1635
  if (fromKeychain)
1638
1636
  return fromKeychain;
1639
1637
  }
@@ -1658,12 +1656,9 @@ export async function loadClaudeOauth(home, opts) {
1658
1656
  * Exported for regression tests; not part of the public command surface.
1659
1657
  */
1660
1658
  export async function saveClaudeOauth(home, credentials) {
1661
- // Windows not yet supported. An injected test backend is the exception, for
1662
- // the same reason as the loadClaudeOauth guard above: it makes the keychain
1663
- // path exercisable anywhere, and without it this returns before the rotated
1664
- // credential is written OR a stale no-ACL cache item an earlier version wrote
1665
- // is evicted (deleteCachedClaudeOauth) — leaving that stale item behind.
1666
- if (process.platform !== 'darwin' && process.platform !== 'linux' && !isKeychainBackendOverridden()) {
1659
+ // Windows not yet supported: Claude Code keeps its credential in the file
1660
+ // there, so the rotated credential is written by the harness, not here.
1661
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
1667
1662
  return false;
1668
1663
  }
1669
1664
  try {
@@ -1671,7 +1666,7 @@ export async function saveClaudeOauth(home, credentials) {
1671
1666
  // Read existing payload to preserve other fields
1672
1667
  let existingPayload = {};
1673
1668
  try {
1674
- const stdout = getKeychainToken(service);
1669
+ const stdout = getKeychainTokenSync(service);
1675
1670
  existingPayload = JSON.parse(stdout.trim());
1676
1671
  }
1677
1672
  catch {
@@ -1691,12 +1686,12 @@ export async function saveClaudeOauth(home, credentials) {
1691
1686
  const payloadJson = JSON.stringify(newPayload);
1692
1687
  // Delete existing entry first, then add updated entry
1693
1688
  try {
1694
- deleteKeychainToken(service);
1689
+ await deleteKeychainToken(service);
1695
1690
  }
1696
1691
  catch {
1697
1692
  // Entry might not exist, ignore
1698
1693
  }
1699
- setKeychainToken(service, payloadJson);
1694
+ await setKeychainToken(service, payloadJson);
1700
1695
  // A new credential rotation means any cached access token is stale.
1701
1696
  deleteCachedClaudeOauth(service);
1702
1697
  return true;