@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
@@ -49,7 +49,7 @@ export interface LaunchResult {
49
49
  * needs them can `agents secrets unlock <bundle>`). Exported so the no-prompt
50
50
  * behavior is testable on the real keychain path without spawning a browser.
51
51
  */
52
- export declare function resolveProfileSecretsEnv(secrets?: string): Record<string, string>;
52
+ export declare function resolveProfileSecretsEnv(secrets?: string): Promise<Record<string, string>>;
53
53
  export declare function launchBrowser(profileName: string, browserType: BrowserType, port: number, options?: ChromeOptions, secrets?: string, customBinary?: string, isElectron?: boolean): Promise<LaunchResult>;
54
54
  export declare function attachToChrome(port: number): Promise<string>;
55
55
  export declare function killChrome(pid: number): void;
@@ -4,7 +4,7 @@ import * as path from 'path';
4
4
  import * as os from 'os';
5
5
  import { getProfileRuntimeDir } from './profiles.js';
6
6
  import { discoverBrowserWsUrl, registerPipeTransport } from './cdp.js';
7
- import { readAndResolveBundleEnv, bundleExists } from '../secrets/bundles.js';
7
+ import { readAndResolveBundleEnv, bundleExists } from '../secrets-client.js';
8
8
  import { writeProfileRuntime, readProfileRuntime } from './runtime-state.js';
9
9
  // Windows install roots. Resolve from the environment (fall back to the usual
10
10
  // defaults) so per-user installs under %LOCALAPPDATA% and 64-bit Program Files
@@ -247,11 +247,11 @@ export function listInstalledBrowsers(platform = os.platform()) {
247
247
  * needs them can `agents secrets unlock <bundle>`). Exported so the no-prompt
248
248
  * behavior is testable on the real keychain path without spawning a browser.
249
249
  */
250
- export function resolveProfileSecretsEnv(secrets) {
251
- if (!secrets || !bundleExists(secrets))
250
+ export async function resolveProfileSecretsEnv(secrets) {
251
+ if (!secrets || !(await bundleExists(secrets)))
252
252
  return {};
253
253
  try {
254
- const { env } = readAndResolveBundleEnv(secrets, { caller: 'browser profile', agentOnly: true });
254
+ const { env } = await readAndResolveBundleEnv(secrets, { caller: 'browser profile', agentOnly: true });
255
255
  return env;
256
256
  }
257
257
  catch {
@@ -321,7 +321,7 @@ isElectron = false) {
321
321
  ];
322
322
  // Profile secrets: agentOnly (SEC-13) — a locked bundle resolves to an empty map
323
323
  // (launch proceeds without them), never a Touch ID prompt. See resolveProfileSecretsEnv.
324
- const env = { ...process.env, ...resolveProfileSecretsEnv(secrets) };
324
+ const env = { ...process.env, ...(await resolveProfileSecretsEnv(secrets)) };
325
325
  const child = spawn(browserPath, args, {
326
326
  detached: true,
327
327
  stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
@@ -7,7 +7,7 @@
7
7
  * same bar style the native usage system uses, just sourced from the provider
8
8
  * API rather than the CLI's own usage tracking.
9
9
  */
10
- import { hasKeychainToken, getKeychainToken } from './secrets/profiles.js';
10
+ import { getKeychainTokenSync, hasKeychainTokenSync } from './secrets-client.js';
11
11
  import { renderBar, getUsageColor } from './accounting/usage.js';
12
12
  import * as fs from 'node:fs';
13
13
  import * as path from 'node:path';
@@ -74,9 +74,9 @@ function resolveByokToken(profile) {
74
74
  if (!keychainItem)
75
75
  return null;
76
76
  try {
77
- if (!hasKeychainToken(keychainItem))
77
+ if (!hasKeychainTokenSync(keychainItem))
78
78
  return null;
79
- return getKeychainToken(keychainItem);
79
+ return getKeychainTokenSync(keychainItem);
80
80
  }
81
81
  catch {
82
82
  return null;
@@ -1,3 +1,5 @@
1
+ import { ReservedBundleWrongBackendError } from './reserved-stores.js';
2
+ import type { DeviceAccountSlot, NativeAccountRecord } from './types.js';
1
3
  /**
2
4
  * Reserved FILE-BASED secrets bundle holding long-lived, non-rotating Claude
3
5
  * setup-tokens. Usage/probe reads authenticate with these instead of Claude
@@ -6,8 +8,11 @@
6
8
  * bare key, so one account's token can't be misapplied to another in a
7
9
  * multi-account fleet.
8
10
  */
9
- /** Alias of the secrets-layer reserved name so mint/seed shares one source of truth. */
11
+ /** Alias of the reserved-store name so mint/seed shares one source of truth. */
10
12
  export declare const AUTH_BUNDLE = "auth";
13
+ export { ReservedBundleWrongBackendError };
14
+ /** Drop every memoized setup-token; called after this process seeds or rotates one. */
15
+ export declare function invalidateClaudeSetupTokenCache(): void;
11
16
  /** True only for a clean, single-line `sk-ant-oat01-…` token — see {@link SETUP_TOKEN_RE}. */
12
17
  export declare function isValidClaudeSetupToken(value: string): boolean;
13
18
  /** The per-account key an email maps to inside the `auth` bundle. */
@@ -24,8 +29,8 @@ export declare function readClaudeAccountEmail(home?: string): string | null;
24
29
  export declare function resolveClaudeSetupToken(home?: string): string | null;
25
30
  /**
26
31
  * Resolve a long-lived setup-token for an EXPLICIT account email, independent of
27
- * any version home's `.claude.json`. This is what lets `agents accounts attach`
28
- * provision a headless worker home that has never had an interactive login: the
32
+ * any version home's `.claude.json`. This is what lets worker-slot provisioning
33
+ * seed a headless worker home that has never had an interactive login: the
29
34
  * account's non-rotating setup-token is already fleet-synced in the file-based
30
35
  * `auth` bundle, keyed by email ({@link claudeAccountTokenKey}), so we can write
31
36
  * the home's `.oauth_token` from it without the circular
@@ -47,4 +52,43 @@ export declare function resolveClaudeSetupTokenForEmail(email: string, cacheKey?
47
52
  * It never copies a rotating OAuth credential (`.credentials.json`) — the setup-token
48
53
  * stays the credential of record.
49
54
  */
55
+ /**
56
+ * Write a Claude worker slot's `.oauth_token` (0600) from a durable setup-token,
57
+ * the way the pre-slot worker home was provisioned. Refuses a malformed token so
58
+ * a corrupt bundle entry can never reach the auth header (see {@link SETUP_TOKEN_RE}).
59
+ * `home` is the slot dir; the claude adapter shim reads `$CLAUDE_CONFIG_DIR/.oauth_token`
60
+ * where `CLAUDE_CONFIG_DIR` is `<home>/.claude`. Returns the written path.
61
+ */
62
+ export declare function writeClaudeWorkerOauthToken(home: string, token: string): string;
63
+ /**
64
+ * Read one non-rotating credential value from a reserved store by its storage
65
+ * key. Returns null when the bundle or key is absent. Used to materialize a
66
+ * worker slot from a synced durable credential (setup-token / API key).
67
+ *
68
+ * A reserved `__<harness>__` store is file-backed by design (headless, fleet-
69
+ * shareable) and its item is read directly, symmetric with how it is written —
70
+ * `readAndResolveBundleEnv`'s name validation does not yet accept a reserved
71
+ * name on the READ path (a secrets-track seam; see the PR body). A non-reserved
72
+ * bundle (the legacy `auth` alias, a provider bundle) goes through the normal
73
+ * resolver so refs/expiry/lease gates still apply.
74
+ */
75
+ export declare function readReservedCredential(bundle: string, key: string): string | null;
76
+ /**
77
+ * Materialize a worker slot for a portable account from its synced durable
78
+ * credential (PHNX-3940 T6 — the generalization of the pre-slot Claude worker-home
79
+ * provisioning). Creates the HOME-shaped slot dir (T1 `ensureSlot`), then, for a
80
+ * durable harness, writes the credential into it the way the Claude worker home is
81
+ * provisioned today: for `claude`, the setup-token → `.oauth_token` (0600) + the
82
+ * seeded identity email (the read-side join in agent-spec then completes the uuids
83
+ * from the registry row). API-key harnesses need no file — the key is injected at
84
+ * spawn from the reserved store (T5) — so their slot is created and recorded
85
+ * `durable` with no write. A per-device harness (`worker: 'none'`) gets a
86
+ * `per-device` slot and no credential — it logs in per box.
87
+ *
88
+ * This is worker-side reconciliation: it runs on the box where the key landed and
89
+ * NEVER transports anything (the SSH push that delivered the key is the daemon's
90
+ * job — invariant 1). Fails loud when a durable claude account has no resolvable
91
+ * token on this device rather than recording a slot that cannot authenticate.
92
+ */
93
+ export declare function provisionWorkerSlot(account: NativeAccountRecord): DeviceAccountSlot;
50
94
  export declare function seedClaudeWorkerHomeIdentity(versionHome: string, email: string): void;
@@ -1,9 +1,10 @@
1
1
  import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
- import { AUTH_BUNDLE_NAME, ReservedBundleWrongBackendError, bundleBackend, bundleExists, readAndResolveBundleEnv, } from './secrets/bundles.js';
5
- import { fileStoreItemPath } from './secrets/filestore.js';
6
- import { secretsKeychainItem } from './secrets/index.js';
4
+ import { bundleBackendSync, bundleExistsSync, isSecretsClientError, readAndResolveBundleEnvSync, secretsKeychainItem, storeGetSync, storeHasSync, } from './secrets-client.js';
5
+ import { AUTH_STORE_ALIAS, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, isReservedStoreName, } from './reserved-stores.js';
6
+ import { ensureSlot, recordSlot } from './accounts/slots.js';
7
+ import { harnessWorkerKinds } from './harness-auth-capabilities.js';
7
8
  /**
8
9
  * Reserved FILE-BASED secrets bundle holding long-lived, non-rotating Claude
9
10
  * setup-tokens. Usage/probe reads authenticate with these instead of Claude
@@ -12,8 +13,9 @@ import { secretsKeychainItem } from './secrets/index.js';
12
13
  * bare key, so one account's token can't be misapplied to another in a
13
14
  * multi-account fleet.
14
15
  */
15
- /** Alias of the secrets-layer reserved name so mint/seed shares one source of truth. */
16
- export const AUTH_BUNDLE = AUTH_BUNDLE_NAME;
16
+ /** Alias of the reserved-store name so mint/seed shares one source of truth. */
17
+ export const AUTH_BUNDLE = AUTH_STORE_ALIAS;
18
+ export { ReservedBundleWrongBackendError };
17
19
  /**
18
20
  * A well-formed Claude OAuth setup-token: the `sk-ant-oat01-` prefix followed by
19
21
  * token-safe characters only, on a single line. `claude setup-token` mints exactly
@@ -28,8 +30,49 @@ export const AUTH_BUNDLE = AUTH_BUNDLE_NAME;
28
30
  * where a corrupt bundle entry must be caught before it reaches the auth header.
29
31
  */
30
32
  const SETUP_TOKEN_RE = /^sk-ant-oat01-[A-Za-z0-9_-]+$/;
31
- /** Process-local only: plaintext setup-tokens are never written to another cache. */
33
+ /**
34
+ * Process-local memo of resolved setup-tokens, keyed by the caller's cache key
35
+ * AND the per-account token key — a version home that switches accounts must
36
+ * miss, never be served the previous account's token. Plaintext setup-tokens
37
+ * are never written to another cache. The bundle now
38
+ * lives behind the standalone `secrets` process (one spawn per read), so the
39
+ * memo is what keeps a probe loop over many accounts from re-decrypting the
40
+ * whole `auth` bundle per account; it is bounded by {@link SETUP_TOKEN_MEMO_TTL_MS}
41
+ * so a rotation performed by another process is observed within seconds, and
42
+ * an in-process writer clears it outright ({@link invalidateClaudeSetupTokenCache}).
43
+ */
32
44
  const setupTokenCache = new Map();
45
+ const SETUP_TOKEN_MEMO_TTL_MS = 10_000;
46
+ /** Drop every memoized setup-token; called after this process seeds or rotates one. */
47
+ export function invalidateClaudeSetupTokenCache() {
48
+ setupTokenCache.clear();
49
+ }
50
+ /**
51
+ * Read the reserved `auth` bundle through the standalone. Returns null when it
52
+ * does not exist — checked explicitly, because a prompt-free (`agentOnly`) read
53
+ * of a bundle that does not exist reports LOCKED, and a genuinely locked store
54
+ * must stay distinguishable from an absent bundle. A keychain- or vault-backed
55
+ * `auth` fails loud with {@link ReservedBundleWrongBackendError} — whether the
56
+ * standalone refused the resolve (`WRONG_BACKEND`) or the returned metadata
57
+ * names the wrong backend — so usage/probe never silently ignores a seeded
58
+ * token (SEC-GAP-3).
59
+ */
60
+ function readReservedAuthBundle(caller) {
61
+ if (!bundleExistsSync(AUTH_BUNDLE))
62
+ return null;
63
+ let resolved;
64
+ try {
65
+ resolved = readAndResolveBundleEnvSync(AUTH_BUNDLE, { caller, agentOnly: true });
66
+ }
67
+ catch (err) {
68
+ if (isSecretsClientError(err, 'WRONG_BACKEND')) {
69
+ throw new ReservedBundleWrongBackendError(AUTH_BUNDLE, bundleBackendSync(AUTH_BUNDLE));
70
+ }
71
+ throw err;
72
+ }
73
+ assertReservedAuthBackend(resolved.bundle.backend ?? 'keychain');
74
+ return resolved;
75
+ }
33
76
  function credentialFingerprint(credentialPath) {
34
77
  try {
35
78
  const stat = fs.statSync(credentialPath, { bigint: true });
@@ -117,7 +160,7 @@ function discoverClaudeAccountEmailFromOauthToken(home) {
117
160
  return email;
118
161
  }
119
162
  catch (err) {
120
- if (err instanceof ReservedBundleWrongBackendError)
163
+ if (isReservedBundleBackendError(err))
121
164
  throw err;
122
165
  return null;
123
166
  }
@@ -132,13 +175,10 @@ function discoverEmailUncached(home, tokenPath) {
132
175
  }
133
176
  if (!isValidClaudeSetupToken(token))
134
177
  return null;
135
- if (!bundleExists(AUTH_BUNDLE))
178
+ const resolved = readReservedAuthBundle('usage');
179
+ if (!resolved)
136
180
  return null;
137
- if (bundleBackend(AUTH_BUNDLE) !== 'file') {
138
- throw new ReservedBundleWrongBackendError(AUTH_BUNDLE, bundleBackend(AUTH_BUNDLE));
139
- }
140
- const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
141
- for (const [key, value] of Object.entries(env)) {
181
+ for (const [key, value] of Object.entries(resolved.env)) {
142
182
  if (value.trim() !== token)
143
183
  continue;
144
184
  const email = emailFromTokenKey(key);
@@ -175,8 +215,8 @@ function emailFromTokenKey(key) {
175
215
  }
176
216
  /**
177
217
  * Resolve a long-lived setup-token for an EXPLICIT account email, independent of
178
- * any version home's `.claude.json`. This is what lets `agents accounts attach`
179
- * provision a headless worker home that has never had an interactive login: the
218
+ * any version home's `.claude.json`. This is what lets worker-slot provisioning
219
+ * seed a headless worker home that has never had an interactive login: the
180
220
  * account's non-rotating setup-token is already fleet-synced in the file-based
181
221
  * `auth` bundle, keyed by email ({@link claudeAccountTokenKey}), so we can write
182
222
  * the home's `.oauth_token` from it without the circular
@@ -192,44 +232,23 @@ export function resolveClaudeSetupTokenForEmail(email, cacheKey) {
192
232
  const trimmed = email.trim();
193
233
  if (!trimmed)
194
234
  return null;
195
- if (!bundleExists(AUTH_BUNDLE))
196
- return null;
197
- const backend = bundleBackend(AUTH_BUNDLE);
198
- if (backend !== 'file') {
199
- // SEC-GAP-3: a keychain/vault-backed `auth` used to return null here, so
200
- // usage/probe fell through to the interactive login (Touch ID) with no
201
- // hint that the seeded setup-token was being ignored.
202
- throw new ReservedBundleWrongBackendError(AUTH_BUNDLE, backend);
203
- }
204
235
  const key = claudeAccountTokenKey(trimmed);
205
- const ck = cacheKey ?? `email:${trimmed}`;
206
- const item = secretsKeychainItem(AUTH_BUNDLE, key);
207
- const credentialPath = fileStoreItemPath(item);
208
- for (let attempt = 0; attempt < 2; attempt++) {
209
- const before = credentialFingerprint(credentialPath);
210
- const cached = setupTokenCache.get(ck);
211
- if (cached?.credentialPath === credentialPath && cached.fingerprint === before) {
212
- return cached.token;
213
- }
214
- if (before === 'missing') {
215
- setupTokenCache.set(ck, { credentialPath, fingerprint: before, token: null });
216
- return null;
217
- }
218
- const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
219
- const v = (env[key] ?? '').trim();
220
- const token = v.length > 0 && isValidClaudeSetupToken(v) ? v : null;
221
- const after = credentialFingerprint(credentialPath);
222
- if (before === after) {
223
- setupTokenCache.set(ck, { credentialPath, fingerprint: after, token });
224
- return token;
225
- }
226
- }
227
- // The credential changed during both decrypt attempts. Fail closed rather
228
- // than return a token whose current file fingerprint was never observed.
229
- return null;
236
+ const ck = `${cacheKey ?? `email:${trimmed}`}\0${key}`;
237
+ const cached = setupTokenCache.get(ck);
238
+ if (cached && Date.now() - cached.readAt < SETUP_TOKEN_MEMO_TTL_MS)
239
+ return cached.token;
240
+ // SEC-GAP-3: a keychain/vault-backed `auth` used to return null here, so
241
+ // usage/probe fell through to the interactive login (Touch ID) with no
242
+ // hint that the seeded setup-token was being ignored. readReservedAuthBundle
243
+ // throws ReservedBundleWrongBackendError instead, which propagates.
244
+ const resolved = readReservedAuthBundle('usage');
245
+ const v = (resolved?.env[key] ?? '').trim();
246
+ const token = v.length > 0 && isValidClaudeSetupToken(v) ? v : null;
247
+ setupTokenCache.set(ck, { readAt: Date.now(), token });
248
+ return token;
230
249
  }
231
250
  catch (err) {
232
- if (err instanceof ReservedBundleWrongBackendError)
251
+ if (isReservedBundleBackendError(err))
233
252
  throw err;
234
253
  return null;
235
254
  }
@@ -244,6 +263,109 @@ export function resolveClaudeSetupTokenForEmail(email, cacheKey) {
244
263
  * It never copies a rotating OAuth credential (`.credentials.json`) — the setup-token
245
264
  * stays the credential of record.
246
265
  */
266
+ /**
267
+ * Write a Claude worker slot's `.oauth_token` (0600) from a durable setup-token,
268
+ * the way the pre-slot worker home was provisioned. Refuses a malformed token so
269
+ * a corrupt bundle entry can never reach the auth header (see {@link SETUP_TOKEN_RE}).
270
+ * `home` is the slot dir; the claude adapter shim reads `$CLAUDE_CONFIG_DIR/.oauth_token`
271
+ * where `CLAUDE_CONFIG_DIR` is `<home>/.claude`. Returns the written path.
272
+ */
273
+ export function writeClaudeWorkerOauthToken(home, token) {
274
+ if (!isValidClaudeSetupToken(token)) {
275
+ throw new Error('Refusing to write a malformed Claude setup-token to a worker slot.');
276
+ }
277
+ const tokenPath = path.join(home, '.claude', '.oauth_token');
278
+ fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
279
+ fs.writeFileSync(tokenPath, token, { mode: 0o600 });
280
+ return tokenPath;
281
+ }
282
+ /**
283
+ * Read one non-rotating credential value from a reserved store by its storage
284
+ * key. Returns null when the bundle or key is absent. Used to materialize a
285
+ * worker slot from a synced durable credential (setup-token / API key).
286
+ *
287
+ * A reserved `__<harness>__` store is file-backed by design (headless, fleet-
288
+ * shareable) and its item is read directly, symmetric with how it is written —
289
+ * `readAndResolveBundleEnv`'s name validation does not yet accept a reserved
290
+ * name on the READ path (a secrets-track seam; see the PR body). A non-reserved
291
+ * bundle (the legacy `auth` alias, a provider bundle) goes through the normal
292
+ * resolver so refs/expiry/lease gates still apply.
293
+ */
294
+ export function readReservedCredential(bundle, key) {
295
+ try {
296
+ if (isReservedStoreName(bundle) && bundle.startsWith('__')) {
297
+ const item = secretsKeychainItem(bundle, key);
298
+ if (!storeHasSync('file', item))
299
+ return null;
300
+ const value = storeGetSync('file', item).trim();
301
+ return value.length > 0 ? value : null;
302
+ }
303
+ const { env } = readAndResolveBundleEnvSync(bundle, {
304
+ keys: [key],
305
+ keyMode: 'storage',
306
+ agentOnly: true,
307
+ caller: 'provision-worker-slot',
308
+ });
309
+ const value = (env[key] ?? '').trim();
310
+ return value.length > 0 ? value : null;
311
+ }
312
+ catch (err) {
313
+ if (isReservedBundleBackendError(err))
314
+ throw err;
315
+ return null;
316
+ }
317
+ }
318
+ /**
319
+ * Materialize a worker slot for a portable account from its synced durable
320
+ * credential (PHNX-3940 T6 — the generalization of the pre-slot Claude worker-home
321
+ * provisioning). Creates the HOME-shaped slot dir (T1 `ensureSlot`), then, for a
322
+ * durable harness, writes the credential into it the way the Claude worker home is
323
+ * provisioned today: for `claude`, the setup-token → `.oauth_token` (0600) + the
324
+ * seeded identity email (the read-side join in agent-spec then completes the uuids
325
+ * from the registry row). API-key harnesses need no file — the key is injected at
326
+ * spawn from the reserved store (T5) — so their slot is created and recorded
327
+ * `durable` with no write. A per-device harness (`worker: 'none'`) gets a
328
+ * `per-device` slot and no credential — it logs in per box.
329
+ *
330
+ * This is worker-side reconciliation: it runs on the box where the key landed and
331
+ * NEVER transports anything (the SSH push that delivered the key is the daemon's
332
+ * job — invariant 1). Fails loud when a durable claude account has no resolvable
333
+ * token on this device rather than recording a slot that cannot authenticate.
334
+ */
335
+ export function provisionWorkerSlot(account) {
336
+ const harness = account.agent;
337
+ const durable = harnessWorkerKinds(harness).some((kind) => kind === 'setup-token' || kind.startsWith('api-key'));
338
+ const slot = ensureSlot(harness, account.id);
339
+ const checkedAt = new Date().toISOString();
340
+ if (!durable) {
341
+ const record = { ...slot, authMode: 'per-device', verdict: 'unconfigured', checkedAt };
342
+ recordSlot(account.id, record);
343
+ return record;
344
+ }
345
+ if (harness === 'claude') {
346
+ const cred = account.workerCredential;
347
+ const token = cred
348
+ ? readReservedCredential(cred.bundle, cred.key)
349
+ // Claude row predating T1 (no workerCredential): the legacy `auth` bundle
350
+ // keys the token by the account email.
351
+ : account.identityLabel
352
+ ? resolveClaudeSetupTokenForEmail(account.identityLabel, slot.slotDir)
353
+ : null;
354
+ if (!token) {
355
+ const where = cred ? `${cred.bundle}:${cred.key}` : `'${AUTH_BUNDLE}' keyed by ${account.identityLabel ?? '(no email)'}`;
356
+ throw new Error(`No durable Claude setup-token for account ${account.id} on this device (${where}). `
357
+ + `Mint and sync it from the headed device.`);
358
+ }
359
+ writeClaudeWorkerOauthToken(slot.slotDir, token);
360
+ if (account.identityLabel)
361
+ seedClaudeWorkerHomeIdentity(slot.slotDir, account.identityLabel);
362
+ }
363
+ // API-key harnesses: the key rides the reserved store and is injected at spawn
364
+ // (T5); nothing is written into the slot here.
365
+ const record = { ...slot, authMode: 'durable', verdict: 'unverified', checkedAt };
366
+ recordSlot(account.id, record);
367
+ return record;
368
+ }
247
369
  export function seedClaudeWorkerHomeIdentity(versionHome, email) {
248
370
  const trimmed = email.trim();
249
371
  if (!trimmed)
@@ -20,7 +20,7 @@
20
20
  * back to GEMINI_API_KEY / GOOGLE_API_KEY in the environment.
21
21
  */
22
22
  import { resolveDispatchRepos, normalizeProviderStatus } from './types.js';
23
- import { readAndResolveBundleEnv } from '../secrets/bundles.js';
23
+ import { readAndResolveBundleEnv } from '../secrets-client.js';
24
24
  const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions';
25
25
  const DEFAULT_MODEL = 'antigravity-preview-05-2026';
26
26
  const KEY_NAMES = ['GEMINI_API_KEY', 'GOOGLE_API_KEY'];
@@ -62,7 +62,7 @@ export class AntigravityCloudProvider {
62
62
  return KEY_NAMES.some((k) => Boolean(process.env[k]));
63
63
  }
64
64
  /** Resolve the Gemini API key from the configured bundle or the environment. */
65
- resolveApiKey() {
65
+ async resolveApiKey() {
66
66
  if (this.secretsBundle) {
67
67
  try {
68
68
  // Cloud dispatch resolves the key on its own (no human at a sheet), so the
@@ -71,7 +71,7 @@ export class AntigravityCloudProvider {
71
71
  // catch below re-raises verbatim — dispatch genuinely needs the key, so it
72
72
  // fails LOUD with the unlock hint rather than swallowing it into a wrong
73
73
  // path. A `never`/no-ACL or broker-held bundle resolves silently.
74
- const { env } = readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:antigravity', agentOnly: true });
74
+ const { env } = await readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:antigravity', agentOnly: true });
75
75
  for (const k of KEY_NAMES) {
76
76
  if (env[k])
77
77
  return env[k];
@@ -112,7 +112,7 @@ export class AntigravityCloudProvider {
112
112
  throw new Error(`Antigravity cloud is a raw sandbox with no GitHub repo → PR flow. Got repo(s): ${repos.join(', ')}. ` +
113
113
  `Use --provider rush for repo-backed dispatch.`);
114
114
  }
115
- const apiKey = this.resolveApiKey();
115
+ const apiKey = await this.resolveApiKey();
116
116
  const model = options.model ?? this.model;
117
117
  const body = buildInteractionBody(options.prompt, model);
118
118
  let resp;
@@ -1,6 +1,6 @@
1
1
  /** Cursor Cloud Agents provider backed by the public v1 REST API. */
2
2
  import { normalizeProviderStatus, resolveDispatchRepos } from './types.js';
3
- import { readAndResolveBundleEnv } from '../secrets/bundles.js';
3
+ import { readAndResolveBundleEnv } from '../secrets-client.js';
4
4
  export const CURSOR_API_BASE_URL = 'https://api.cursor.com/v1';
5
5
  const KEY_NAME = 'CURSOR_API_KEY';
6
6
  /** Translate unified dispatch options into Cursor's documented v1 create shape. */
@@ -111,10 +111,10 @@ export class CursorCloudProvider {
111
111
  hasKeySource() {
112
112
  return Boolean(this.secretsBundle || process.env[KEY_NAME]);
113
113
  }
114
- resolveApiKey() {
114
+ async resolveApiKey() {
115
115
  if (this.secretsBundle) {
116
116
  try {
117
- const { env } = readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:cursor', agentOnly: true });
117
+ const { env } = await readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:cursor', agentOnly: true });
118
118
  if (env[KEY_NAME])
119
119
  return env[KEY_NAME];
120
120
  throw new Error(`Secrets bundle '${this.secretsBundle}' has no ${KEY_NAME}. Add one: agents secrets add ${this.secretsBundle} ${KEY_NAME}`);
@@ -143,7 +143,7 @@ export class CursorCloudProvider {
143
143
  };
144
144
  }
145
145
  async request(path, init) {
146
- const apiKey = this.resolveApiKey();
146
+ const apiKey = await this.resolveApiKey();
147
147
  let response;
148
148
  try {
149
149
  response = await fetch(`${CURSOR_API_BASE_URL}${path}`, {
@@ -11,7 +11,7 @@
11
11
  * needs a provider token (e.g. HCLOUD_TOKEN) in the environment. We inject it
12
12
  * from a secrets bundle when one is configured (see `crabboxEnv`).
13
13
  */
14
- import { type SecretsBundle } from '../secrets/bundles.js';
14
+ import type { SecretsBundle } from '../secrets-types.js';
15
15
  /** A crabbox machine as reported by `crabbox list --json`. */
16
16
  export interface CrabboxBox {
17
17
  /** Provider machine name, e.g. `crabbox-blue-hermit-1039689b`. */
@@ -12,7 +12,7 @@
12
12
  * from a secrets bundle when one is configured (see `crabboxEnv`).
13
13
  */
14
14
  import { spawn, spawnSync } from 'child_process';
15
- import { readAndResolveBundleEnv, listBundles, bundleExists } from '../secrets/bundles.js';
15
+ import { readAndResolveBundleEnvSync, listBundlesSync, bundleExistsSync } from '../secrets-client.js';
16
16
  import { readMeta, writeMeta } from '../state.js';
17
17
  import { DEFAULT_CRABBOX_PROFILE } from './config.js';
18
18
  /** Locate the crabbox binary, or throw an actionable error. */
@@ -59,7 +59,7 @@ function resolveTailscaleBundleMemo() {
59
59
  if (!tailscaleBundleMemo) {
60
60
  let value;
61
61
  try {
62
- value = pickTailscaleBundleFromList(listBundles());
62
+ value = pickTailscaleBundleFromList(listBundlesSync());
63
63
  }
64
64
  catch {
65
65
  /* secrets unreadable — no auto-detect */
@@ -88,7 +88,7 @@ function resolveTailscaleKeyValueMemo(ts) {
88
88
  if (!tailscaleValueMemo) {
89
89
  let value;
90
90
  try {
91
- const { env } = readAndResolveBundleEnv(ts.name, {
91
+ const { env } = readAndResolveBundleEnvSync(ts.name, {
92
92
  caller: 'agents run --lease (crabbox tailscale)',
93
93
  keys: [ts.key],
94
94
  agentOnly: true,
@@ -129,14 +129,14 @@ export function resolveLeaseBundle() {
129
129
  return { name: env };
130
130
  try {
131
131
  const configured = readMeta().lease?.secretsBundle;
132
- if (configured && bundleExists(configured))
132
+ if (configured && bundleExistsSync(configured))
133
133
  return { name: configured };
134
134
  }
135
135
  catch {
136
136
  /* config unreadable — fall through to auto-detect */
137
137
  }
138
138
  try {
139
- const bundles = listBundles();
139
+ const bundles = listBundlesSync();
140
140
  const name = pickLeaseBundleFromList(bundles);
141
141
  if (name) {
142
142
  const b = bundles.find((x) => x.name === name);
@@ -187,7 +187,7 @@ function resolveLeaseEnvMemo(explicitBundle) {
187
187
  // privilege; an unrelated bundle can't leak its other secrets into crabbox).
188
188
  // An explicitly-named bundle (env/config or `opts.secretsBundle`) injects
189
189
  // whole — the user chose it. Same resolver `agents secrets exec` uses.
190
- const { env } = readAndResolveBundleEnv(resolved.name, {
190
+ const { env } = readAndResolveBundleEnvSync(resolved.name, {
191
191
  caller: 'agents run --lease (crabbox)',
192
192
  keys: resolved.keys,
193
193
  // --lease is headless by contract and a locked bundle must fail loud with
@@ -78,9 +78,9 @@ export declare const CLAUDE_TOKEN_REMOTE = ".claude/.credentials.json";
78
78
  * The RAW wrapped Claude credential payload (`{"claudeAiOauth":{…}}`) to write to
79
79
  * the box's `~/.claude/.credentials.json`, or null if no signed-in token is found.
80
80
  *
81
- * On macOS the token is in the login Keychain, read SILENTLY via
82
- * `getKeychainToken` (the `/usr/bin/security … -w` path — Claude's item trusts it,
83
- * no Touch ID). A default native install uses the bare `Claude Code-credentials`
81
+ * On macOS the token is in the login Keychain, read SILENTLY via the standalone
82
+ * `secrets` client's `getKeychainTokenSync` (the `/usr/bin/security … -w` path —
83
+ * Claude's item trusts it, no Touch ID). A default native install uses the bare `Claude Code-credentials`
84
84
  * service; an agents-cli managed install (where `~/.claude` symlinks into a
85
85
  * versioned home) uses a hash-suffixed service, so we try the bare service first,
86
86
  * then enumerate installed version homes (preferring the account whose email
@@ -16,7 +16,7 @@ import * as os from 'os';
16
16
  import * as path from 'path';
17
17
  import * as fs from 'fs';
18
18
  import { getAccountInfo } from '../agents.js';
19
- import { getKeychainToken } from '../secrets/index.js';
19
+ import { getKeychainTokenSync } from '../secrets-client.js';
20
20
  import { getClaudeKeychainService } from '../accounting/usage.js';
21
21
  import { listInstalledVersions, getVersionHomePath } from '../installations/versions.js';
22
22
  export const LEASE_RUNTIMES = [
@@ -185,9 +185,9 @@ function isClaudeCredentialsBlob(s) {
185
185
  * The RAW wrapped Claude credential payload (`{"claudeAiOauth":{…}}`) to write to
186
186
  * the box's `~/.claude/.credentials.json`, or null if no signed-in token is found.
187
187
  *
188
- * On macOS the token is in the login Keychain, read SILENTLY via
189
- * `getKeychainToken` (the `/usr/bin/security … -w` path — Claude's item trusts it,
190
- * no Touch ID). A default native install uses the bare `Claude Code-credentials`
188
+ * On macOS the token is in the login Keychain, read SILENTLY via the standalone
189
+ * `secrets` client's `getKeychainTokenSync` (the `/usr/bin/security … -w` path —
190
+ * Claude's item trusts it, no Touch ID). A default native install uses the bare `Claude Code-credentials`
191
191
  * service; an agents-cli managed install (where `~/.claude` symlinks into a
192
192
  * versioned home) uses a hash-suffixed service, so we try the bare service first,
193
193
  * then enumerate installed version homes (preferring the account whose email
@@ -203,7 +203,7 @@ function isClaudeCredentialsBlob(s) {
203
203
  * real Keychain.
204
204
  */
205
205
  export async function resolveClaudeCredentialsBlob(opts) {
206
- const readItem = opts?.readItem ?? getKeychainToken;
206
+ const readItem = opts?.readItem ?? getKeychainTokenSync;
207
207
  const service = opts?.service ?? getClaudeKeychainService;
208
208
  const listVersions = opts?.listVersions ?? (() => listInstalledVersions('claude'));
209
209
  const versionHome = opts?.versionHome ?? ((v) => getVersionHomePath('claude', v));