@phnx-labs/agents-cli 1.22.84 → 1.22.86

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 (259) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +80 -53
  3. package/dist/bootstrap.d.ts +7 -6
  4. package/dist/bootstrap.js +20 -27
  5. package/dist/cli/command-registry.js +5 -1
  6. package/dist/commands/accounts.d.ts +6 -13
  7. package/dist/commands/accounts.js +330 -143
  8. package/dist/commands/alias.js +7 -3
  9. package/dist/commands/apply.js +1 -1
  10. package/dist/commands/auth-mint.d.ts +7 -3
  11. package/dist/commands/auth-mint.js +21 -9
  12. package/dist/commands/auth.js +2 -3
  13. package/dist/commands/browser.js +10 -10
  14. package/dist/commands/daemon.d.ts +7 -3
  15. package/dist/commands/daemon.js +40 -25
  16. package/dist/commands/doctor.js +28 -2
  17. package/dist/commands/exec.js +87 -39
  18. package/dist/commands/fleet-capture.js +21 -2
  19. package/dist/commands/harness-wizard.js +2 -2
  20. package/dist/commands/lease.js +7 -12
  21. package/dist/commands/profiles.d.ts +2 -2
  22. package/dist/commands/profiles.js +20 -13
  23. package/dist/commands/repo.js +3 -3
  24. package/dist/commands/run-account-picker.d.ts +3 -3
  25. package/dist/commands/run-account-picker.js +29 -30
  26. package/dist/commands/secrets-passthrough.d.ts +20 -0
  27. package/dist/commands/secrets-passthrough.js +43 -0
  28. package/dist/commands/setup-accounts.d.ts +3 -2
  29. package/dist/commands/setup-accounts.js +4 -4
  30. package/dist/commands/setup-secrets.d.ts +16 -21
  31. package/dist/commands/setup-secrets.js +43 -224
  32. package/dist/commands/ssh.js +2 -2
  33. package/dist/commands/sync.js +1 -1
  34. package/dist/commands/update.d.ts +10 -0
  35. package/dist/commands/update.js +43 -40
  36. package/dist/commands/versions.d.ts +11 -11
  37. package/dist/commands/versions.js +131 -83
  38. package/dist/commands/view.d.ts +1 -9
  39. package/dist/commands/view.js +41 -55
  40. package/dist/commands/webhook.js +5 -5
  41. package/dist/commands/workflows.js +2 -1
  42. package/dist/index.d.ts +12 -12
  43. package/dist/index.js +13 -42
  44. package/dist/lib/account-capabilities.d.ts +1 -1
  45. package/dist/lib/account-capabilities.js +1 -1
  46. package/dist/lib/account-catalog.d.ts +121 -6
  47. package/dist/lib/account-catalog.js +421 -7
  48. package/dist/lib/account-registry.d.ts +8 -30
  49. package/dist/lib/account-registry.js +36 -79
  50. package/dist/lib/account-schema.d.ts +1 -1
  51. package/dist/lib/account-schema.js +1 -1
  52. package/dist/lib/accounting/account-pool-collect.js +16 -4
  53. package/dist/lib/accounting/rotate.d.ts +20 -7
  54. package/dist/lib/accounting/rotate.js +89 -13
  55. package/dist/lib/accounting/usage.js +11 -16
  56. package/dist/lib/accounts/add.d.ts +138 -0
  57. package/dist/lib/accounts/add.js +651 -0
  58. package/dist/lib/accounts/migrate.d.ts +117 -0
  59. package/dist/lib/accounts/migrate.js +536 -0
  60. package/dist/lib/accounts/slots.d.ts +13 -0
  61. package/dist/lib/accounts/slots.js +100 -0
  62. package/dist/lib/agent-spec/agents.d.ts +16 -0
  63. package/dist/lib/agent-spec/agents.js +38 -4
  64. package/dist/lib/app-bundle-install.js +5 -4
  65. package/dist/lib/auth-health.d.ts +3 -0
  66. package/dist/lib/auth-health.js +11 -0
  67. package/dist/lib/auth-mint.d.ts +52 -16
  68. package/dist/lib/auth-mint.js +117 -39
  69. package/dist/lib/browser/chrome.d.ts +1 -1
  70. package/dist/lib/browser/chrome.js +5 -5
  71. package/dist/lib/byok-usage.js +3 -3
  72. package/dist/lib/claude-account-token.d.ts +47 -3
  73. package/dist/lib/claude-account-token.js +172 -50
  74. package/dist/lib/cloud/antigravity.js +4 -4
  75. package/dist/lib/cloud/cursor.js +4 -4
  76. package/dist/lib/crabbox/cli.d.ts +1 -1
  77. package/dist/lib/crabbox/cli.js +6 -6
  78. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  79. package/dist/lib/crabbox/runtimes.js +5 -5
  80. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  81. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  82. package/dist/lib/daemon/auth-sync-service.js +32 -1
  83. package/dist/lib/daemon/daemon.d.ts +6 -16
  84. package/dist/lib/daemon/daemon.js +52 -97
  85. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  86. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  87. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  88. package/dist/lib/daemon/harness-update-service.js +1 -1
  89. package/dist/lib/daemon/runner.d.ts +15 -3
  90. package/dist/lib/daemon/runner.js +49 -24
  91. package/dist/lib/daemon-health.d.ts +1 -2
  92. package/dist/lib/daemon-health.js +7 -6
  93. package/dist/lib/daemon-services.d.ts +1 -1
  94. package/dist/lib/daemon-services.js +1 -11
  95. package/dist/lib/daemon-webhooks.d.ts +8 -7
  96. package/dist/lib/daemon-webhooks.js +10 -9
  97. package/dist/lib/device-config.js +1 -1
  98. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  99. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  100. package/dist/lib/devices/harness-inventory.js +126 -4
  101. package/dist/lib/doctor-diff.js +2 -1
  102. package/dist/lib/exec-account-home.d.ts +38 -0
  103. package/dist/lib/exec-account-home.js +164 -0
  104. package/dist/lib/exec.d.ts +30 -0
  105. package/dist/lib/exec.js +101 -31
  106. package/dist/lib/fleet/apply.d.ts +2 -2
  107. package/dist/lib/fleet/apply.js +4 -4
  108. package/dist/lib/fleet/auth-sync.js +4 -3
  109. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  110. package/dist/lib/harness/adapter.d.ts +7 -7
  111. package/dist/lib/harness/adapter.js +10 -6
  112. package/dist/lib/harness/adapters/grok.js +8 -3
  113. package/dist/lib/harness/adapters/muse.js +1 -1
  114. package/dist/lib/harness/adapters/opencode.js +12 -3
  115. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  116. package/dist/lib/harness-auth-capabilities.js +52 -0
  117. package/dist/lib/helper-versions.d.ts +6 -4
  118. package/dist/lib/helper-versions.js +5 -4
  119. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  120. package/dist/lib/hosts/credential-transport.js +37 -0
  121. package/dist/lib/hosts/dispatch.d.ts +7 -0
  122. package/dist/lib/hosts/dispatch.js +13 -6
  123. package/dist/lib/identity/client.d.ts +3 -3
  124. package/dist/lib/identity/client.js +3 -3
  125. package/dist/lib/installations/index.d.ts +1 -1
  126. package/dist/lib/installations/index.js +1 -1
  127. package/dist/lib/installations/migrate.js +9 -0
  128. package/dist/lib/installations/resolve.js +1 -1
  129. package/dist/lib/installations/shims.d.ts +21 -4
  130. package/dist/lib/installations/shims.js +86 -5
  131. package/dist/lib/installations/store.d.ts +43 -0
  132. package/dist/lib/installations/store.js +75 -7
  133. package/dist/lib/installations/versions.js +1 -1
  134. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  135. package/dist/lib/menubar/install-menubar.js +3 -3
  136. package/dist/lib/native-accounts.d.ts +35 -0
  137. package/dist/lib/native-accounts.js +41 -0
  138. package/dist/lib/net-close.d.ts +11 -0
  139. package/dist/lib/net-close.js +25 -0
  140. package/dist/lib/openclaw-keychain.js +27 -1
  141. package/dist/lib/profiles.js +22 -9
  142. package/dist/lib/project-resources.js +1 -1
  143. package/dist/lib/reserved-stores.d.ts +53 -0
  144. package/dist/lib/reserved-stores.js +120 -0
  145. package/dist/lib/secrets-client.d.ts +199 -0
  146. package/dist/lib/secrets-client.js +718 -0
  147. package/dist/lib/secrets-policy.d.ts +235 -0
  148. package/dist/lib/secrets-policy.js +505 -0
  149. package/dist/lib/secrets-types.d.ts +188 -0
  150. package/dist/lib/secrets-types.js +24 -0
  151. package/dist/lib/self-heal/checks/shims.js +5 -3
  152. package/dist/lib/service-manifest.js +3 -4
  153. package/dist/lib/session/db.d.ts +18 -0
  154. package/dist/lib/session/db.js +47 -14
  155. package/dist/lib/session/sync/config.js +2 -2
  156. package/dist/lib/sha256-asset.d.ts +11 -16
  157. package/dist/lib/sha256-asset.js +11 -16
  158. package/dist/lib/share/config.js +14 -5
  159. package/dist/lib/signin-badge.d.ts +26 -0
  160. package/dist/lib/signin-badge.js +42 -0
  161. package/dist/lib/staleness/detectors/workflows.js +3 -105
  162. package/dist/lib/staleness/writers/workflows.js +2 -1
  163. package/dist/lib/state.d.ts +7 -4
  164. package/dist/lib/state.js +18 -7
  165. package/dist/lib/sync-umbrella.js +2 -2
  166. package/dist/lib/teams/agents.js +1 -1
  167. package/dist/lib/types.d.ts +74 -33
  168. package/dist/lib/view-types.d.ts +6 -3
  169. package/dist/lib/workflows-registry.d.ts +71 -0
  170. package/dist/lib/workflows-registry.js +280 -0
  171. package/dist/lib/workflows.d.ts +21 -21
  172. package/dist/lib/workflows.js +12 -327
  173. package/package.json +2 -3
  174. package/scripts/postinstall.js +35 -30
  175. package/dist/commands/secrets-import.d.ts +0 -18
  176. package/dist/commands/secrets-import.js +0 -74
  177. package/dist/commands/secrets-migrate.d.ts +0 -25
  178. package/dist/commands/secrets-migrate.js +0 -334
  179. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  180. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  181. package/dist/commands/secrets-sync.d.ts +0 -11
  182. package/dist/commands/secrets-sync.js +0 -153
  183. package/dist/commands/secrets-vault.d.ts +0 -10
  184. package/dist/commands/secrets-vault.js +0 -130
  185. package/dist/commands/secrets.d.ts +0 -212
  186. package/dist/commands/secrets.js +0 -3030
  187. package/dist/lib/accounts/connect.d.ts +0 -176
  188. package/dist/lib/accounts/connect.js +0 -453
  189. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  190. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  191. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  192. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  193. package/dist/lib/secrets/agent.d.ts +0 -358
  194. package/dist/lib/secrets/agent.js +0 -1291
  195. package/dist/lib/secrets/audit.d.ts +0 -46
  196. package/dist/lib/secrets/audit.js +0 -101
  197. package/dist/lib/secrets/bundles.d.ts +0 -290
  198. package/dist/lib/secrets/bundles.js +0 -1547
  199. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  200. package/dist/lib/secrets/download-keychain.js +0 -70
  201. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  202. package/dist/lib/secrets/drivers/rush.js +0 -90
  203. package/dist/lib/secrets/fallback.d.ts +0 -48
  204. package/dist/lib/secrets/fallback.js +0 -48
  205. package/dist/lib/secrets/filestore.d.ts +0 -222
  206. package/dist/lib/secrets/filestore.js +0 -1099
  207. package/dist/lib/secrets/headless.d.ts +0 -43
  208. package/dist/lib/secrets/headless.js +0 -56
  209. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  210. package/dist/lib/secrets/icloud-import.js +0 -206
  211. package/dist/lib/secrets/index.d.ts +0 -451
  212. package/dist/lib/secrets/index.js +0 -1568
  213. package/dist/lib/secrets/install-helper.d.ts +0 -72
  214. package/dist/lib/secrets/install-helper.js +0 -245
  215. package/dist/lib/secrets/lease.d.ts +0 -25
  216. package/dist/lib/secrets/lease.js +0 -44
  217. package/dist/lib/secrets/linux.d.ts +0 -77
  218. package/dist/lib/secrets/linux.js +0 -393
  219. package/dist/lib/secrets/list-filter.d.ts +0 -109
  220. package/dist/lib/secrets/list-filter.js +0 -261
  221. package/dist/lib/secrets/mcp.d.ts +0 -93
  222. package/dist/lib/secrets/mcp.js +0 -211
  223. package/dist/lib/secrets/profiles.d.ts +0 -10
  224. package/dist/lib/secrets/profiles.js +0 -13
  225. package/dist/lib/secrets/push.d.ts +0 -133
  226. package/dist/lib/secrets/push.js +0 -273
  227. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  228. package/dist/lib/secrets/rc-hygiene.js +0 -143
  229. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  230. package/dist/lib/secrets/read-backoff.js +0 -64
  231. package/dist/lib/secrets/reaper.d.ts +0 -90
  232. package/dist/lib/secrets/reaper.js +0 -243
  233. package/dist/lib/secrets/remote.d.ts +0 -152
  234. package/dist/lib/secrets/remote.js +0 -344
  235. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  236. package/dist/lib/secrets/reserved-sync.js +0 -147
  237. package/dist/lib/secrets/scope.d.ts +0 -26
  238. package/dist/lib/secrets/scope.js +0 -29
  239. package/dist/lib/secrets/session-store.d.ts +0 -107
  240. package/dist/lib/secrets/session-store.js +0 -342
  241. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  242. package/dist/lib/secrets/sync-backend.js +0 -13
  243. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  244. package/dist/lib/secrets/sync-commands.js +0 -21
  245. package/dist/lib/secrets/sync.d.ts +0 -48
  246. package/dist/lib/secrets/sync.js +0 -237
  247. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  248. package/dist/lib/secrets/unlock-hints.js +0 -36
  249. package/dist/lib/secrets/usage-db.d.ts +0 -46
  250. package/dist/lib/secrets/usage-db.js +0 -96
  251. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  252. package/dist/lib/secrets/vault-age-helper.js +0 -34
  253. package/dist/lib/secrets/vault.d.ts +0 -49
  254. package/dist/lib/secrets/vault.js +0 -399
  255. package/dist/lib/secrets/windows.d.ts +0 -81
  256. package/dist/lib/secrets/windows.js +0 -556
  257. package/scripts/install-helper.js +0 -97
  258. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  259. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -15,10 +15,14 @@
15
15
  */
16
16
  import chalk from 'chalk';
17
17
  import { ALL_AGENT_IDS, accountDisplayLabel, getAccountInfo, } from '../agents.js';
18
- import { deriveUsageStatusFromSnapshot, getUsageInfoByIdentity, getUsageLookupKey, usageErrorForDisplay, } from '../accounting/usage.js';
18
+ import { deriveUsageStatusFromSnapshot, classifyUsageErrorKind, getUsageInfoByIdentity, getUsageLookupKey, usageErrorForDisplay, } from '../accounting/usage.js';
19
19
  import { getVersionHomePath, listInstalledVersions } from '../installations/store.js';
20
20
  import { readMeta } from '../state.js';
21
21
  import { findNativeAccountByIdentity } from '../account-registry.js';
22
+ import { authCacheKey, readAuthHealthCache } from '../auth-health.js';
23
+ import { machineId } from '../machine-id.js';
24
+ import { fixFor } from '../signin-badge.js';
25
+ import { harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
22
26
  /**
23
27
  * Roll one usage snapshot into a {@link QuotaSummary}. Mirrors the blocking-window
24
28
  * selection of {@link deriveUsageStatusFromSnapshot} (the model-specific
@@ -79,6 +83,51 @@ export function summarizeQuota(snapshot, unavailableReason = null, accountStatus
79
83
  unavailableReason: null,
80
84
  };
81
85
  }
86
+ /**
87
+ * Scope/permission failures on a worker mean the usage endpoint cannot be read.
88
+ * They are not evidence of exhausted credits, even when stale account metadata
89
+ * previously carried that coarse status.
90
+ */
91
+ export function summarizeObservedQuota(snapshot, error, accountStatus = null) {
92
+ const displayError = usageErrorForDisplay(error);
93
+ const unverified = classifyUsageErrorKind(error) === 'headless-scope'
94
+ || /scope|permission/i.test(displayError ?? '');
95
+ return {
96
+ quota: unverified
97
+ ? summarizeQuota(null, displayError, null)
98
+ : summarizeQuota(snapshot, displayError, accountStatus),
99
+ unverified,
100
+ };
101
+ }
102
+ /**
103
+ * A scope/permission failure on the usage endpoint is not a credit claim.
104
+ * Strip utilization and, only when auth still looks healthy, surface
105
+ * `unverified` instead of `live`/`rate_limited`. A real auth failure
106
+ * (expired/revoked/missing) stays the auth failure.
107
+ */
108
+ export function applyUsageHonesty(verdict, usage) {
109
+ if (!usage)
110
+ return { verdict, usage };
111
+ const reason = usage.unavailableReason;
112
+ const scopeUnknown = !!reason && (classifyUsageErrorKind(reason) === 'headless-scope'
113
+ || /scope|permission|headless/i.test(reason));
114
+ if (scopeUnknown) {
115
+ const stripped = {
116
+ ...usage,
117
+ status: null,
118
+ verdict: 'unavailable',
119
+ usedPercent: null,
120
+ };
121
+ if (verdict === 'live' || verdict === 'rate_limited') {
122
+ return { verdict: 'unverified', usage: stripped };
123
+ }
124
+ return { verdict, usage: stripped };
125
+ }
126
+ if (usage.status === 'rate_limited' && (verdict === 'live' || verdict === 'unverified')) {
127
+ return { verdict: 'rate_limited', usage };
128
+ }
129
+ return { verdict, usage };
130
+ }
82
131
  /**
83
132
  * "Ready" = signed in AND not rate-limited. A missing quota snapshot does NOT
84
133
  * block readiness: the account is signed in and usable, we just have no live
@@ -119,6 +168,8 @@ export async function collectLocalHarnessInventory(opts) {
119
168
  ? (await getUsageInfoByIdentity(usageInputs, opts?.refresh ? { forceRefresh: true } : undefined)).usageByKey
120
169
  : new Map();
121
170
  const meta = readMeta();
171
+ const authCache = readAuthHealthCache();
172
+ const host = machineId();
122
173
  return pending.map(({ agent, version, info }) => {
123
174
  const key = getUsageLookupKey(info);
124
175
  const usage = key ? usageByKey.get(key) : undefined;
@@ -126,13 +177,34 @@ export async function collectLocalHarnessInventory(opts) {
126
177
  // Normalize the internal 'stale' not-collected sentinel out before it reaches
127
178
  // the JSON `quota.unavailableReason` — same leak PHNX-3348 fixed for
128
179
  // `agents view --json`, here for `agents devices harnesses/accounts --json`.
129
- const quota = summarizeQuota(snapshot, usageErrorForDisplay(usage?.error), info?.usageStatus ?? null);
180
+ const observed = summarizeObservedQuota(snapshot, usage?.error, info?.usageStatus ?? null);
181
+ const quota = observed.quota;
130
182
  const signedIn = !!info?.signedIn;
131
- const { ready, reason } = computeReady(signedIn, quota);
132
183
  const display = info ? accountDisplayLabel(info) || null : null;
133
184
  const saved = findNativeAccountByIdentity(meta, agent, info);
134
185
  const account = saved ? `${saved.name} · ${display || saved.identityLabel || saved.identityKey}` : display;
135
- return { agent, version, account, signedIn, quota, ready, reason };
186
+ const cachedVerdict = authCache[authCacheKey(host, agent, version)]?.verdict;
187
+ const fromAuth = cachedVerdict === 'unconfigured' || !signedIn
188
+ ? 'missing'
189
+ : cachedVerdict === 'error' || !cachedVerdict
190
+ ? 'unverified'
191
+ : cachedVerdict;
192
+ const honest = applyUsageHonesty(fromAuth, quota);
193
+ const verdict = honest.verdict;
194
+ const honestQuota = honest.usage ?? quota;
195
+ const { ready, reason } = computeReady(signedIn, honestQuota);
196
+ const provisioning = harnessWorkerIsPerDevice(agent) ? 'per-device' : 'portable';
197
+ return {
198
+ agent,
199
+ version,
200
+ account,
201
+ signedIn,
202
+ quota: honestQuota,
203
+ ready,
204
+ reason,
205
+ verdict,
206
+ fix: fixFor({ agent, verdict, name: saved?.name, version, provisioning }),
207
+ };
136
208
  });
137
209
  }
138
210
  /**
@@ -280,3 +352,53 @@ export function renderAccountsMatrix(results) {
280
352
  lines.push(chalk.gray(' one row per account · agents = harnesses signed into it · ready = signed in and not rate-limited'));
281
353
  return lines;
282
354
  }
355
+ function fleetVerdictLabel(verdict) {
356
+ if (verdict === 'rate_limited')
357
+ return 'LIMITED';
358
+ return verdict.toUpperCase();
359
+ }
360
+ /**
361
+ * Account × device matrix used by `agents accounts list --fleet`.
362
+ * Rows are logical accounts, not installations. Columns come from the
363
+ * daemon-state observations already joined onto each account row.
364
+ */
365
+ export function renderAccountFleetMatrix(rows, opts = {}) {
366
+ const devices = [...new Set(rows.flatMap((row) => row.devices.map((device) => device.device)))].sort();
367
+ const accountLabels = rows.map((row) => `${row.harness}#${row.name ?? 'unnamed'} ${row.identityLabel}`);
368
+ const accountW = colWidth(accountLabels, 16);
369
+ const deviceWidths = new Map(devices.map((device) => [
370
+ device,
371
+ colWidth([
372
+ device,
373
+ ...rows.map((row) => fleetVerdictLabel(row.devices.find((item) => item.device === device)?.verdict ?? 'missing')),
374
+ ], 7),
375
+ ]));
376
+ const lines = [chalk.bold('Fleet accounts')];
377
+ if (rows.length === 0) {
378
+ lines.push(chalk.gray(' no accounts'));
379
+ }
380
+ else {
381
+ lines.push(` ${'ACCOUNT'.padEnd(accountW)} `
382
+ + devices.map((device) => device.padEnd(deviceWidths.get(device))).join(' '));
383
+ for (const [index, row] of rows.entries()) {
384
+ const label = accountLabels[index].padEnd(accountW);
385
+ const cells = devices.map((device) => {
386
+ const verdict = row.devices.find((item) => item.device === device)?.verdict ?? 'missing';
387
+ const plain = fleetVerdictLabel(verdict).padEnd(deviceWidths.get(device));
388
+ if (verdict === 'live')
389
+ return chalk.green(plain);
390
+ if (verdict === 'revoked' || verdict === 'expired' || verdict === 'missing')
391
+ return chalk.red(plain);
392
+ if (verdict === 'rate_limited')
393
+ return chalk.yellow(plain);
394
+ return chalk.gray(plain);
395
+ });
396
+ lines.push(` ${chalk.cyan(label)} ${cells.join(' ')}`);
397
+ }
398
+ }
399
+ const uncovered = (opts.uncoveredDevices ?? []).filter((device) => !devices.includes(device));
400
+ if (uncovered.length > 0) {
401
+ lines.push(chalk.gray(` ${uncovered.join(', ')}: daemon-state carries no account verdicts (older release)`));
402
+ }
403
+ return lines;
404
+ }
@@ -45,7 +45,8 @@ import { dirsContentMatch, filesContentMatch } from './resource-content-diff.js'
45
45
  import { subagentContentMatches } from './subagents-registry.js';
46
46
  import { getMcpServersByName, mcpServerMatches } from './mcp.js';
47
47
  import { permissionsGroupMatches, PERMISSIONS_REPRESENTABLE } from './permissions.js';
48
- import { workflowContentMatches, resolveWorkflowRef } from './workflows.js';
48
+ import { resolveWorkflowRef } from './workflows.js';
49
+ import { workflowContentMatches } from './workflows-registry.js';
49
50
  import { listMemoryFacts, memoryTargetDir } from './memory.js';
50
51
  import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, compareVersions, } from './installations/versions.js';
51
52
  import { discoverPlugins, marketplaceSpecForName } from './plugins/plugins.js';
@@ -0,0 +1,38 @@
1
+ import { isSymlinkAdoptedHarness } from './installations/shims.js';
2
+ import type { AgentId, DeviceAccountSlot, Meta } from './types.js';
3
+ export { isAccountSlotDir } from './agents.js';
4
+ export { isSymlinkAdoptedHarness };
5
+ export type SpawnHomeSource = 'slot' | 'legacy-home' | 'provisioned';
6
+ export interface NativeSpawnHome {
7
+ execHome: string;
8
+ source: SpawnHomeSource;
9
+ slot?: DeviceAccountSlot;
10
+ /** Installation label when `source` is `legacy-home`. */
11
+ label?: string;
12
+ }
13
+ export declare function missingSlotHint(agent: AgentId, name: string): string;
14
+ export declare function symlinkAdoptedAccountError(agent: AgentId, name: string, defaultName: string | undefined): string;
15
+ export declare function adoptedSymlinkMismatchError(agent: AgentId, name: string, execHome: string): string;
16
+ /** The adopted `~/.<config>` symlink's resolved target, or null when missing / not a symlink. */
17
+ export declare function adoptedConfigTarget(agent: AgentId): string | null;
18
+ export declare function adoptedConfigPointsAtHome(agent: AgentId, home: string): boolean;
19
+ /**
20
+ * For a symlink-adopted harness, point `~/.<config>` at this account's slot
21
+ * or throw. Does not write the harness default — the caller records that only
22
+ * after this succeeds.
23
+ */
24
+ export declare function ensureAdoptedDefaultRepoint(agent: AgentId, account: {
25
+ id: string;
26
+ name: string;
27
+ agent: AgentId;
28
+ }, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): void;
29
+ /**
30
+ * Resolve the spawn HOME for a native account on THIS device.
31
+ * Existing slot, then a provisionable worker slot, then a leftover `homes`
32
+ * label, then identity match across installed homes, then fail loud.
33
+ */
34
+ export declare function resolveNativeSpawnHome(agent: AgentId, account: {
35
+ id: string;
36
+ name: string;
37
+ agent: AgentId;
38
+ }, meta?: Pick<Meta, 'accounts' | 'deviceAccounts'>): Promise<NativeSpawnHome>;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Spawn-time native-account HOME resolution (PHNX-3940 T5).
3
+ *
4
+ * Order: existing slot → provisionable worker slot → recorded leftover `homes`
5
+ * label → identity match across installed version homes → fail loud with the
6
+ * T4 hint. Never a wrong home that looks like success. Identity matching is
7
+ * the pre-T5 fallback (`resolveAccountVersion` over each installed home's own
8
+ * credential file) so a native account whose backfill never wrote a `homes`
9
+ * label still spawns from the home that actually holds its login.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as os from 'node:os';
13
+ import * as path from 'node:path';
14
+ import { listNativeAccounts, nativeAccountHome } from './account-registry.js';
15
+ import { readSlots } from './accounts/slots.js';
16
+ import { workerProvisioningHint } from './accounts/add.js';
17
+ import { AGENTS, credentialPresence, getAccountInfo } from './agents.js';
18
+ import { AUTH_BUNDLE, claudeAccountTokenKey, provisionWorkerSlot, readReservedCredential } from './claude-account-token.js';
19
+ import { isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
20
+ import { harnessWorkerKinds } from './harness-auth-capabilities.js';
21
+ import { getAgentConfigPath, isSymlinkAdoptedHarness, repointAdoptedConfigToHome } from './installations/shims.js';
22
+ import { getVersionHomePath, listInstalledVersions } from './installations/versions.js';
23
+ export { isAccountSlotDir } from './agents.js';
24
+ export { isSymlinkAdoptedHarness };
25
+ export function missingSlotHint(agent, name) {
26
+ const role = selfConfiguredDeviceRole();
27
+ if (!isHeadedDeviceRole(role)) {
28
+ return `${agent}#${name} has no slot on this device. `
29
+ + `Add the account on your personal device with \`agents accounts add ${agent} ${name}\`; `
30
+ + `workers are provisioned from the durable credential automatically `
31
+ + `(${agent}: ${workerProvisioningHint(agent)}).`;
32
+ }
33
+ return `${agent}#${name} has no slot on this device. Sign in with: agents accounts login ${agent}#${name}`;
34
+ }
35
+ export function symlinkAdoptedAccountError(agent, name, defaultName) {
36
+ if (!defaultName) {
37
+ return `${agent} holds one active account per device (adopted ~/.<config> symlink). `
38
+ + `Set the default first: agents accounts default ${agent} ${name}`;
39
+ }
40
+ return `${agent} holds one active account per device (adopted ~/.<config> symlink). `
41
+ + `'${name}' is not the default ('${defaultName}'). `
42
+ + `Switch with: agents accounts default ${agent} ${name}`;
43
+ }
44
+ export function adoptedSymlinkMismatchError(agent, name, execHome) {
45
+ return `${agent} default is '${name}' but the adopted ~/.<config> does not point at that account's home (${execHome}). `
46
+ + `Switch with: agents accounts default ${agent} ${name}`;
47
+ }
48
+ /** The adopted `~/.<config>` symlink's resolved target, or null when missing / not a symlink. */
49
+ export function adoptedConfigTarget(agent) {
50
+ const configPath = getAgentConfigPath(agent);
51
+ try {
52
+ const stat = fs.lstatSync(configPath);
53
+ if (!stat.isSymbolicLink())
54
+ return null;
55
+ return path.resolve(path.dirname(configPath), fs.readlinkSync(configPath));
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ export function adoptedConfigPointsAtHome(agent, home) {
62
+ if (!isSymlinkAdoptedHarness(agent))
63
+ return true;
64
+ const current = adoptedConfigTarget(agent);
65
+ if (!current)
66
+ return false;
67
+ const configDirName = path.relative(os.homedir(), AGENTS[agent].configDir);
68
+ return current === path.resolve(home, configDirName);
69
+ }
70
+ /**
71
+ * For a symlink-adopted harness, point `~/.<config>` at this account's slot
72
+ * or throw. Does not write the harness default — the caller records that only
73
+ * after this succeeds.
74
+ */
75
+ export function ensureAdoptedDefaultRepoint(agent, account, meta) {
76
+ if (!isSymlinkAdoptedHarness(agent))
77
+ return;
78
+ const slot = readSlots(meta)[account.id];
79
+ if (!slot || !fs.existsSync(slot.slotDir)) {
80
+ throw new Error(`${agent}#${account.name} has no slot on this device, so the adopted ~/.<config> cannot be pointed at it. `
81
+ + missingSlotHint(agent, account.name));
82
+ }
83
+ const result = repointAdoptedConfigToHome(agent, slot.slotDir);
84
+ if (!result.success) {
85
+ throw new Error(result.error ?? `Failed to point ${agent} at account '${account.name}'.`);
86
+ }
87
+ }
88
+ function nativeRow(accountId, meta) {
89
+ return listNativeAccounts(meta).find((row) => row.id === accountId);
90
+ }
91
+ function isProvisionableWorker(account) {
92
+ if (isHeadedDeviceRole(selfConfiguredDeviceRole()))
93
+ return false;
94
+ const durable = harnessWorkerKinds(account.agent).some((kind) => kind === 'setup-token' || kind.startsWith('api-key'));
95
+ if (!durable)
96
+ return false;
97
+ if (account.workerCredential) {
98
+ return readReservedCredential(account.workerCredential.bundle, account.workerCredential.key) != null;
99
+ }
100
+ // Pre-T1 Claude rows key the legacy `auth` bundle by email.
101
+ if (account.agent === 'claude' && account.identityLabel) {
102
+ return readReservedCredential(AUTH_BUNDLE, claudeAccountTokenKey(account.identityLabel)) != null;
103
+ }
104
+ return false;
105
+ }
106
+ async function matchLegacyIdentityHome(agent, account, meta) {
107
+ const row = nativeRow(account.id, meta);
108
+ const needles = [row?.identityKey, row?.identityLabel]
109
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
110
+ .map((value) => value.trim().toLowerCase());
111
+ if (needles.length === 0)
112
+ return null;
113
+ const needleSet = new Set(needles);
114
+ const preferred = nativeAccountHome(account.id, meta);
115
+ const versions = listInstalledVersions(agent);
116
+ const ordered = preferred && versions.includes(preferred)
117
+ ? [preferred, ...versions.filter((version) => version !== preferred)]
118
+ : versions;
119
+ for (const label of ordered) {
120
+ const home = getVersionHomePath(agent, label);
121
+ if (!fs.existsSync(home))
122
+ continue;
123
+ const info = await getAccountInfo(agent, home);
124
+ const presence = credentialPresence(agent, home);
125
+ if (!info.signedIn)
126
+ continue;
127
+ if (presence.knownLocation && !presence.perVersion)
128
+ continue;
129
+ const keys = [info.accountKey, info.email]
130
+ .filter((value) => typeof value === 'string' && value.length > 0)
131
+ .map((value) => value.toLowerCase());
132
+ if (keys.some((key) => needleSet.has(key)))
133
+ return { home, label };
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * Resolve the spawn HOME for a native account on THIS device.
139
+ * Existing slot, then a provisionable worker slot, then a leftover `homes`
140
+ * label, then identity match across installed homes, then fail loud.
141
+ */
142
+ export async function resolveNativeSpawnHome(agent, account, meta = { accounts: undefined, deviceAccounts: undefined }) {
143
+ const slot = readSlots(meta)[account.id];
144
+ if (slot && fs.existsSync(slot.slotDir)) {
145
+ return { execHome: slot.slotDir, source: 'slot', slot };
146
+ }
147
+ const row = nativeRow(account.id, meta);
148
+ if (row && isProvisionableWorker(row)) {
149
+ const provisioned = provisionWorkerSlot(row);
150
+ return { execHome: provisioned.slotDir, source: 'provisioned', slot: provisioned };
151
+ }
152
+ const label = nativeAccountHome(account.id, meta);
153
+ if (label) {
154
+ const legacyHome = getVersionHomePath(agent, label);
155
+ if (fs.existsSync(legacyHome)) {
156
+ return { execHome: legacyHome, source: 'legacy-home', label };
157
+ }
158
+ }
159
+ const matched = await matchLegacyIdentityHome(agent, account, meta);
160
+ if (matched) {
161
+ return { execHome: matched.home, source: 'legacy-home', label: matched.label };
162
+ }
163
+ throw new Error(missingSlotHint(agent, account.name));
164
+ }
@@ -133,6 +133,12 @@ export interface ExecOptions {
133
133
  version?: string;
134
134
  /** Version home whose native auth/config is overlaid onto this run's binary. */
135
135
  configVersion?: string;
136
+ /**
137
+ * HOME-shaped slot dir whose native auth/config is overlaid onto this run's
138
+ * binary (PHNX-3940 T5). Wins over {@link configVersion}. The binary still
139
+ * comes from {@link version} (the managed install unless `@<label>` pins one).
140
+ */
141
+ execHome?: string;
136
142
  /** Omit to launch the CLI interactively -- no prompt, no --print, stdio fully inherited. */
137
143
  prompt?: string;
138
144
  /** Force interactive mode even when a prompt is provided. Wins over `headless`. */
@@ -656,6 +662,30 @@ export type ClaudeRefusalAction = {
656
662
  action: 'none';
657
663
  };
658
664
  export declare function classifyClaudeRunRefusal(output: string, exitCode: number): ClaudeRefusalAction;
665
+ /**
666
+ * Parse Codex's usage-limit refusal reset. Codex prints
667
+ * `ERROR: You've hit your usage limit. … try again at Sep 12th, 2026 8:32 AM.`
668
+ * (or `…try again at 8:32 AM.` for the 5-hour window). The reset is what makes
669
+ * this a session-style limit that auto-clears on its clock rather than a
670
+ * clock-less billing exhaustion. Ordinal suffixes (`12th`) are stripped so
671
+ * `Date.parse` accepts the date; a time-only reset resolves against today/tomorrow.
672
+ * Returns null when there is no future reset to parse (caller then leaves the
673
+ * marker untouched rather than persisting a sticky one it cannot expire).
674
+ */
675
+ export declare function parseCodexUsageLimitReset(text: string, nowMs?: number): Date | null;
676
+ /**
677
+ * Classify what a Codex run's output + exit code means for the account's
678
+ * persisted refusal marker — the Codex sibling of {@link classifyClaudeRunRefusal}
679
+ * (the `ClaudeRefusalAction` shape is harness-generic; the usage cache it drives
680
+ * is identity-keyed and shared across harnesses). Codex's usage limit is
681
+ * time-windowed and carries a reset ("try again at <date>"), so it is noted as a
682
+ * clock-bearing session limit that auto-clears — NOT a clock-less out_of_credits,
683
+ * which would stay sticky until a successful run the excluded account can never
684
+ * get. A genuine credit/quota exhaustion IS clock-less. A clean run clears any
685
+ * stale marker; a limit with no parseable reset is left untouched (the run still
686
+ * cascades via detectRateLimit) rather than persisting an unexpirable marker.
687
+ */
688
+ export declare function classifyCodexRunRefusal(output: string, exitCode: number, nowMs?: number): ClaudeRefusalAction;
659
689
  /**
660
690
  * Patterns that indicate an authentication failure — the agent is logged out,
661
691
  * its token was revoked, or the session expired. These are the user-visible
package/dist/lib/exec.js CHANGED
@@ -15,7 +15,7 @@ import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled,
15
15
  import { resolveModel, buildReasoningFlags } from './models.js';
16
16
  import { isTierToken, resolveTier } from './model-tiers.js';
17
17
  import { emit, createTimer, redactPrompt, redactArgs } from './feed/events.js';
18
- import { sanitizeProcessEnv } from './secrets/bundles.js';
18
+ import { sanitizeProcessEnv } from './secrets-client.js';
19
19
  import { resolveActor, actorEnv } from './actor.js';
20
20
  import { expandLocalHome } from './project-root.js';
21
21
  import { getShimsDir, getHistoryDir, getRuntimeStateDir } from './state.js';
@@ -360,8 +360,9 @@ export function buildExecEnv(options) {
360
360
  if (configAdapter.applyExecConfigEnv) {
361
361
  // Resolve version/versionHome here (this module already imports
362
362
  // installations/versions); the adapters must not import it themselves or they
363
- // close a versions -> shims -> harness -> adapter import cycle.
364
- const { version, versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion ?? options.version);
363
+ // close a versions -> shims -> harness -> adapter import cycle. execHome is
364
+ // the account slot (PHNX-3940 T5) and wins over a legacy configVersion label.
365
+ const { version, versionHome } = resolveExecConfigHome(options);
365
366
  configAdapter.applyExecConfigEnv(result, {
366
367
  agent: options.agent,
367
368
  version,
@@ -373,16 +374,6 @@ export function buildExecEnv(options) {
373
374
  }
374
375
  else {
375
376
  stripForeignConfigDir(result);
376
- // Grok's adapter owns the same GROK_HOME export in generated shims, but a
377
- // labeled account deliberately runs one version's binary with another
378
- // version's config. Apply that overlay here because the shim's binary-slot
379
- // export cannot express a distinct account slot.
380
- if (options.agent === 'grok' && options.configVersion) {
381
- const { versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion);
382
- if (!versionHome)
383
- throw new Error(`Cannot resolve Grok account config home for ${options.configVersion}.`);
384
- result.GROK_HOME = path.join(versionHome, '.grok');
385
- }
386
377
  }
387
378
  // Point the agent at its own mailbox so the PreToolUse `mailbox-inject` hook
388
379
  // knows which box to drain and inject mid-run. Keyed by the session id — the
@@ -458,8 +449,16 @@ export function ensureVendorHomeDir(agent, versionHome) {
458
449
  fs.mkdirSync(vendorHome, { recursive: true });
459
450
  return vendorHome;
460
451
  }
452
+ function resolveExecConfigHome(options) {
453
+ if (options.execHome) {
454
+ const resolved = options.version
455
+ ?? resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.version).version;
456
+ return { version: resolved ?? null, versionHome: options.execHome };
457
+ }
458
+ return resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion ?? options.version);
459
+ }
461
460
  function ensureVendorHomeForSpawn(options) {
462
- const { versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion ?? options.version);
461
+ const { versionHome } = resolveExecConfigHome(options);
463
462
  if (versionHome)
464
463
  ensureVendorHomeDir(options.agent, versionHome);
465
464
  }
@@ -2158,6 +2157,73 @@ export function classifyClaudeRunRefusal(output, exitCode) {
2158
2157
  return { action: 'clear' };
2159
2158
  return { action: 'none' };
2160
2159
  }
2160
+ /**
2161
+ * Parse Codex's usage-limit refusal reset. Codex prints
2162
+ * `ERROR: You've hit your usage limit. … try again at Sep 12th, 2026 8:32 AM.`
2163
+ * (or `…try again at 8:32 AM.` for the 5-hour window). The reset is what makes
2164
+ * this a session-style limit that auto-clears on its clock rather than a
2165
+ * clock-less billing exhaustion. Ordinal suffixes (`12th`) are stripped so
2166
+ * `Date.parse` accepts the date; a time-only reset resolves against today/tomorrow.
2167
+ * Returns null when there is no future reset to parse (caller then leaves the
2168
+ * marker untouched rather than persisting a sticky one it cannot expire).
2169
+ */
2170
+ export function parseCodexUsageLimitReset(text, nowMs = Date.now()) {
2171
+ // Gate on the CLI's own refusal phrasing, NOT a bare "usage limit" substring:
2172
+ // this runs against a 16KB stdout tail of `codex exec`, which streams the whole
2173
+ // agent transcript, so a session that merely *discusses* usage-limit code would
2174
+ // otherwise false-positive and wrongly mark a HEALTHY account excluded. Mirrors
2175
+ // the narrow Claude convention (`hit your session limit`).
2176
+ if (!/hit your usage limit/i.test(text))
2177
+ return null;
2178
+ const match = /try again (?:at|on)\s+([^.\n]+)/i.exec(text);
2179
+ if (!match)
2180
+ return null;
2181
+ const segment = match[1].trim().replace(/(\d{1,2})(st|nd|rd|th)\b/gi, '$1');
2182
+ const absolute = Date.parse(segment);
2183
+ if (!Number.isNaN(absolute)) {
2184
+ // A full date parsed — honor it only while still in the future; a past reset
2185
+ // means the window already recovered, so there is nothing to note. Do NOT
2186
+ // fall through to the clock path, which would understate the real date as
2187
+ // today/tomorrow.
2188
+ return absolute > nowMs ? new Date(absolute) : null;
2189
+ }
2190
+ // No date parsed — the 5-hour "try again at 8:32 AM" (time-only) form. Resolve
2191
+ // it against today, rolling to tomorrow when the clock has already passed.
2192
+ const clock = /(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i.exec(segment);
2193
+ if (!clock)
2194
+ return null;
2195
+ let hour = Number(clock[1]) % 12;
2196
+ if (clock[3].toLowerCase() === 'pm')
2197
+ hour += 12;
2198
+ const minute = clock[2] ? Number(clock[2]) : 0;
2199
+ const result = new Date(nowMs);
2200
+ result.setHours(hour, minute, 0, 0);
2201
+ if (result.getTime() <= nowMs)
2202
+ result.setDate(result.getDate() + 1);
2203
+ return result;
2204
+ }
2205
+ /**
2206
+ * Classify what a Codex run's output + exit code means for the account's
2207
+ * persisted refusal marker — the Codex sibling of {@link classifyClaudeRunRefusal}
2208
+ * (the `ClaudeRefusalAction` shape is harness-generic; the usage cache it drives
2209
+ * is identity-keyed and shared across harnesses). Codex's usage limit is
2210
+ * time-windowed and carries a reset ("try again at <date>"), so it is noted as a
2211
+ * clock-bearing session limit that auto-clears — NOT a clock-less out_of_credits,
2212
+ * which would stay sticky until a successful run the excluded account can never
2213
+ * get. A genuine credit/quota exhaustion IS clock-less. A clean run clears any
2214
+ * stale marker; a limit with no parseable reset is left untouched (the run still
2215
+ * cascades via detectRateLimit) rather than persisting an unexpirable marker.
2216
+ */
2217
+ export function classifyCodexRunRefusal(output, exitCode, nowMs = Date.now()) {
2218
+ const reset = parseCodexUsageLimitReset(output, nowMs);
2219
+ if (reset)
2220
+ return { action: 'note_session', resetsAt: reset };
2221
+ if (detectOutOfCredits(output))
2222
+ return { action: 'note_out_of_credits' };
2223
+ if (exitCode === 0)
2224
+ return { action: 'clear' };
2225
+ return { action: 'none' };
2226
+ }
2161
2227
  /**
2162
2228
  * Patterns that indicate an authentication failure — the agent is logged out,
2163
2229
  * its token was revoked, or the session expired. These are the user-visible
@@ -2380,24 +2446,28 @@ export async function runWithFallback(options) {
2380
2446
  }
2381
2447
  const output = `${result.stderr}\n${result.stdout}`;
2382
2448
  // Persist a per-account refusal marker so rotation stops re-picking a
2383
- // known-dead account: a session-limit recovers on its clock, a billing
2449
+ // known-dead account: a session/usage limit recovers on its clock, a billing
2384
2450
  // exhaustion (tokens/credits) recovers only on a later successful run, and a
2385
- // clean run clears any stale marker. Decision extracted + unit-tested in
2386
- // classifyClaudeRunRefusal.
2387
- const sessionLimitReset = agent === 'claude' ? parseClaudeSessionLimitReset(output) : null;
2388
- if (agent === 'claude' && version) {
2389
- const refusal = classifyClaudeRunRefusal(output, result.exitCode ?? 1);
2390
- if (refusal.action !== 'none') {
2391
- const account = await getAccountInfo(agent, getVersionHomePath(agent, version));
2392
- const usageKey = getUsageLookupKey(account);
2393
- if (usageKey) {
2394
- if (refusal.action === 'note_session')
2395
- noteClaudeSessionLimit(usageKey, refusal.resetsAt);
2396
- else if (refusal.action === 'note_out_of_credits')
2397
- noteClaudeOutOfCredits(usageKey);
2398
- else if (refusal.action === 'clear')
2399
- clearClaudeAccountRefusal(usageKey);
2400
- }
2451
+ // clean run clears any stale marker. Claude and Codex both surface these as
2452
+ // run-ending text, and the usage cache is identity-keyed (shared across
2453
+ // harnesses), so one path notes either. Decisions extracted + unit-tested in
2454
+ // classify{Claude,Codex}RunRefusal.
2455
+ const refusal = agent === 'claude'
2456
+ ? classifyClaudeRunRefusal(output, result.exitCode ?? 1)
2457
+ : agent === 'codex'
2458
+ ? classifyCodexRunRefusal(output, result.exitCode ?? 1)
2459
+ : null;
2460
+ const sessionLimitReset = refusal?.action === 'note_session' ? refusal.resetsAt : null;
2461
+ if (refusal && version && refusal.action !== 'none') {
2462
+ const account = await getAccountInfo(agent, getVersionHomePath(agent, version));
2463
+ const usageKey = getUsageLookupKey(account);
2464
+ if (usageKey) {
2465
+ if (refusal.action === 'note_session')
2466
+ noteClaudeSessionLimit(usageKey, refusal.resetsAt);
2467
+ else if (refusal.action === 'note_out_of_credits')
2468
+ noteClaudeOutOfCredits(usageKey);
2469
+ else if (refusal.action === 'clear')
2470
+ clearClaudeAccountRefusal(usageKey);
2401
2471
  }
2402
2472
  }
2403
2473
  if (result.exitCode === 0 && !sessionLimitReset)
@@ -8,7 +8,7 @@
8
8
  * (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
9
9
  */
10
10
  import type { DeviceProfile } from '../devices/registry.js';
11
- import { type RemoteBackend } from '../secrets/push.js';
11
+ import type { RemoteBackend } from '../secrets-types.js';
12
12
  import type { DeviceDesired, DeviceProbe, DeviceDiff, FleetAction, FleetPlan, AuthFilePayload } from './types.js';
13
13
  /** Strip a version suffix from an agent spec: `claude@latest` -> `claude`. */
14
14
  export declare function agentIdOf(spec: string): string;
@@ -149,7 +149,7 @@ export interface ExecContext {
149
149
  dryRun?: boolean;
150
150
  }
151
151
  /** Execute one device's planned actions in order. Real SSH — no mocks. */
152
- export declare function reconcileDevice(row: DeviceDiff, device: DeviceProfile, ctx: ExecContext): DeviceApplyResult;
152
+ export declare function reconcileDevice(row: DeviceDiff, device: DeviceProfile, ctx: ExecContext): Promise<DeviceApplyResult>;
153
153
  /** Run a pool of async tasks with a concurrency cap, preserving input order. */
154
154
  export declare function pool<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
155
155
  /** Reconcile every device row in parallel (capped). */
@@ -8,8 +8,8 @@
8
8
  * (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
9
9
  */
10
10
  import * as os from 'os';
11
- import { pushBundleToHost } from '../secrets/push.js';
12
- import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle, isReservedBundleName } from '../secrets/bundles.js';
11
+ import { pushBundleToHost } from '../secrets-client.js';
12
+ import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle, isReservedBundleName } from '../secrets-policy.js';
13
13
  import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
14
14
  import { readyProbe, bootstrapAgentsCli } from '../hosts/ready.js';
15
15
  import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
@@ -371,7 +371,7 @@ export function parseRemoteBundles(stdout) {
371
371
  }
372
372
  }
373
373
  /** Execute one device's planned actions in order. Real SSH — no mocks. */
374
- export function reconcileDevice(row, device, ctx) {
374
+ export async function reconcileDevice(row, device, ctx) {
375
375
  if (!row.probe.reachable) {
376
376
  return { device: row.device, ok: false, steps: [], note: row.probe.note ?? 'unreachable' };
377
377
  }
@@ -438,7 +438,7 @@ export function reconcileDevice(row, device, ctx) {
438
438
  }
439
439
  const backend = isReservedBundleName(bundle) || device.platform === 'linux' ? 'file' : 'keychain';
440
440
  try {
441
- const out = pushBundleToHost(bundle, target, {
441
+ const out = await pushBundleToHost(bundle, target, {
442
442
  remoteBackend: backend,
443
443
  operation: `fleet apply ${row.device}`,
444
444
  // No passphrase, ever, from this path. On the file backend the remote