@phnx-labs/agents-cli 1.22.53 → 1.22.55

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 (166) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +59 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +219 -173
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +10 -8
  16. package/dist/commands/focus.d.ts +1 -0
  17. package/dist/commands/focus.js +2 -2
  18. package/dist/commands/go.d.ts +5 -4
  19. package/dist/commands/go.js +7 -7
  20. package/dist/commands/insights.js +9 -0
  21. package/dist/commands/monitors.js +85 -30
  22. package/dist/commands/output.js +8 -2
  23. package/dist/commands/repo.js +18 -0
  24. package/dist/commands/routines.js +31 -2
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions.d.ts +20 -12
  27. package/dist/commands/sessions.js +64 -20
  28. package/dist/commands/setup-accounts.d.ts +8 -0
  29. package/dist/commands/setup-accounts.js +47 -0
  30. package/dist/commands/setup.d.ts +1 -1
  31. package/dist/commands/setup.js +11 -2
  32. package/dist/commands/share.d.ts +79 -3
  33. package/dist/commands/share.js +347 -18
  34. package/dist/commands/ssh.d.ts +7 -0
  35. package/dist/commands/ssh.js +18 -2
  36. package/dist/commands/status.js +14 -0
  37. package/dist/commands/view.d.ts +11 -1
  38. package/dist/commands/view.js +35 -7
  39. package/dist/lib/account-registry.js +15 -3
  40. package/dist/lib/accounting/rotate.d.ts +20 -6
  41. package/dist/lib/accounting/rotate.js +38 -7
  42. package/dist/lib/accounting/usage.d.ts +68 -1
  43. package/dist/lib/accounting/usage.js +116 -10
  44. package/dist/lib/agent-spec/agents.d.ts +5 -2
  45. package/dist/lib/agent-spec/agents.js +25 -7
  46. package/dist/lib/analytics/mix-commands.js +12 -6
  47. package/dist/lib/auth-mint.d.ts +150 -0
  48. package/dist/lib/auth-mint.js +434 -0
  49. package/dist/lib/browser/cdp.d.ts +1 -1
  50. package/dist/lib/browser/cdp.js +1 -1
  51. package/dist/lib/browser/ffmpeg.d.ts +12 -0
  52. package/dist/lib/browser/ffmpeg.js +184 -0
  53. package/dist/lib/browser/remote-control.d.ts +9 -7
  54. package/dist/lib/browser/remote-control.js +9 -7
  55. package/dist/lib/browser/service.js +119 -25
  56. package/dist/lib/claude-account-token.d.ts +10 -0
  57. package/dist/lib/claude-account-token.js +14 -4
  58. package/dist/lib/config-drift.d.ts +37 -0
  59. package/dist/lib/config-drift.js +72 -0
  60. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  61. package/dist/lib/daemon/auth-sync-service.js +34 -0
  62. package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
  63. package/dist/lib/daemon/browser-task-reap-service.js +26 -0
  64. package/dist/lib/daemon/daemon.js +87 -176
  65. package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
  66. package/dist/lib/daemon/heartbeat-service.js +26 -0
  67. package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
  68. package/dist/lib/daemon/monitor-engine-service.js +15 -7
  69. package/dist/lib/daemon/runner.d.ts +15 -0
  70. package/dist/lib/daemon/runner.js +23 -0
  71. package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
  72. package/dist/lib/daemon/secrets-broker-service.js +17 -32
  73. package/dist/lib/daemon/service.d.ts +2 -2
  74. package/dist/lib/daemon/service.js +1 -1
  75. package/dist/lib/daemon/session-state-service.d.ts +21 -0
  76. package/dist/lib/daemon/session-state-service.js +34 -0
  77. package/dist/lib/daemon/supervisor.d.ts +17 -7
  78. package/dist/lib/daemon/supervisor.js +87 -14
  79. package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
  80. package/dist/lib/daemon/tmux-reap-service.js +28 -0
  81. package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
  82. package/dist/lib/daemon/webhook-receiver-service.js +17 -0
  83. package/dist/lib/daemon-services.d.ts +1 -1
  84. package/dist/lib/daemon-services.js +25 -0
  85. package/dist/lib/device-config.d.ts +3 -3
  86. package/dist/lib/device-config.js +5 -5
  87. package/dist/lib/devices/connect.d.ts +26 -0
  88. package/dist/lib/devices/connect.js +48 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  90. package/dist/lib/devices/doctor-findings.js +19 -1
  91. package/dist/lib/devices/harness-inventory.js +5 -2
  92. package/dist/lib/exec.d.ts +28 -0
  93. package/dist/lib/exec.js +73 -7
  94. package/dist/lib/feed/feed.d.ts +1 -1
  95. package/dist/lib/feed/feed.js +23 -1
  96. package/dist/lib/feed-broadcast.js +1 -1
  97. package/dist/lib/fleet/apply.d.ts +11 -0
  98. package/dist/lib/fleet/apply.js +23 -3
  99. package/dist/lib/fleet/auth-sync.js +5 -3
  100. package/dist/lib/help.d.ts +9 -0
  101. package/dist/lib/help.js +29 -1
  102. package/dist/lib/hosts/passthrough.d.ts +1 -10
  103. package/dist/lib/hosts/passthrough.js +1 -13
  104. package/dist/lib/installations/versions.js +9 -1
  105. package/dist/lib/linux-userns.d.ts +58 -0
  106. package/dist/lib/linux-userns.js +116 -0
  107. package/dist/lib/memory.d.ts +26 -0
  108. package/dist/lib/memory.js +80 -1
  109. package/dist/lib/monitors/config.d.ts +11 -0
  110. package/dist/lib/monitors/config.js +8 -0
  111. package/dist/lib/monitors/engine.d.ts +5 -1
  112. package/dist/lib/monitors/engine.js +13 -4
  113. package/dist/lib/monitors/state.d.ts +37 -1
  114. package/dist/lib/monitors/state.js +79 -4
  115. package/dist/lib/permissions-registry.d.ts +2 -0
  116. package/dist/lib/permissions-registry.js +116 -14
  117. package/dist/lib/permissions.d.ts +5 -3
  118. package/dist/lib/permissions.js +25 -27
  119. package/dist/lib/profiles.d.ts +8 -7
  120. package/dist/lib/profiles.js +12 -0
  121. package/dist/lib/project-key.d.ts +9 -0
  122. package/dist/lib/project-key.js +11 -0
  123. package/dist/lib/scheduling/routines.d.ts +47 -0
  124. package/dist/lib/scheduling/routines.js +70 -1
  125. package/dist/lib/secrets/bundles.d.ts +35 -0
  126. package/dist/lib/secrets/bundles.js +78 -1
  127. package/dist/lib/secrets/push.d.ts +3 -8
  128. package/dist/lib/secrets/push.js +18 -14
  129. package/dist/lib/secrets/remote.d.ts +9 -18
  130. package/dist/lib/secrets/remote.js +11 -26
  131. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  132. package/dist/lib/secrets/reserved-sync.js +129 -0
  133. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  134. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  135. package/dist/lib/self-heal/registry.js +4 -0
  136. package/dist/lib/self-heal/types.d.ts +1 -1
  137. package/dist/lib/session/active.js +1 -4
  138. package/dist/lib/session/db.d.ts +41 -5
  139. package/dist/lib/session/db.js +132 -30
  140. package/dist/lib/session/discover.d.ts +32 -4
  141. package/dist/lib/session/discover.js +119 -25
  142. package/dist/lib/session/insights.d.ts +14 -0
  143. package/dist/lib/session/insights.js +25 -2
  144. package/dist/lib/session/linear.js +1 -1
  145. package/dist/lib/session/shell-programs.d.ts +17 -0
  146. package/dist/lib/session/shell-programs.js +21 -0
  147. package/dist/lib/session/state.js +2 -1
  148. package/dist/lib/session/stream-render.js +2 -1
  149. package/dist/lib/session/tool-calls.js +2 -5
  150. package/dist/lib/session/trajectory-html.js +2 -1
  151. package/dist/lib/session/trajectory.js +3 -12
  152. package/dist/lib/session/types.d.ts +8 -0
  153. package/dist/lib/share/capture.js +11 -2
  154. package/dist/lib/share/publish.d.ts +56 -5
  155. package/dist/lib/share/publish.js +126 -18
  156. package/dist/lib/share/worker-template.d.ts +3 -12
  157. package/dist/lib/share/worker-template.js +860 -59
  158. package/dist/lib/startup/root-command.js +2 -1
  159. package/dist/lib/state.d.ts +16 -0
  160. package/dist/lib/state.js +178 -46
  161. package/dist/lib/sync-status.d.ts +4 -0
  162. package/dist/lib/sync-status.js +3 -0
  163. package/dist/lib/traces/classify.js +24 -19
  164. package/dist/lib/usage-refresh.js +2 -1
  165. package/dist/lib/view-types.d.ts +7 -0
  166. package/package.json +9 -2
@@ -34,7 +34,7 @@ import { clearPendingSentinel } from '../lib/devices/pending.js';
34
34
  import { getDeviceDiscoveryStatus, setDeviceDiscoveryStatus } from '../lib/devices/discovery-policy.js';
35
35
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
36
36
  import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
37
- import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
37
+ import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, isAgentsBrowserDrive, markFleetRemote, writeAskpassShim, } from '../lib/devices/connect.js';
38
38
  import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
39
39
  import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
40
40
  import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
@@ -351,9 +351,21 @@ function trySshLeasedBox(name, cmd) {
351
351
  console.error(chalk.red(`Leased box '${name}' is not reachable yet (status: ${boxStatus(box)}).`));
352
352
  process.exit(1);
353
353
  }
354
- const res = spawnSync(sshArgv[0], [...sshArgv.slice(1), ...cmd], { stdio: 'inherit' });
354
+ // Crabbox ssh does not go through buildSshInvocation; stamp the same
355
+ // consent marker so a browser drive on a leased box is gated too.
356
+ const remoteCmd = leasedBoxRemoteCmd(cmd);
357
+ const res = spawnSync(sshArgv[0], [...sshArgv.slice(1), ...remoteCmd], { stdio: 'inherit' });
355
358
  process.exit(res.status ?? 1);
356
359
  }
360
+ /**
361
+ * Remote argv for `agents ssh <slug>` into a leased crabbox. Crabbox ssh does
362
+ * not go through {@link buildSshInvocation}, so this stamps the same
363
+ * AGENTS_FLEET_REMOTE consent marker a registered-device ssh would (PHNX-3065).
364
+ * Exported so the branch is unit-testable without a live crabbox.
365
+ */
366
+ export function leasedBoxRemoteCmd(cmd) {
367
+ return isAgentsBrowserDrive(cmd) ? markFleetRemote(cmd, { shell: 'posix' }) : cmd;
368
+ }
357
369
  /** Resolve a device or exit with a clear error. */
358
370
  async function mustGetDevice(name) {
359
371
  const d = await getDevice(name);
@@ -2412,6 +2424,10 @@ An interactive login with no command mirrors the home-relative directory you
2412
2424
  launched from — 'agents ssh yosemite-s0' from ~/src/app lands in ~/src/app on
2413
2425
  the target when it exists, else the remote home. Same portable-cwd rule as
2414
2426
  'agents run --device'. Passing a command keeps the remote home.
2427
+
2428
+ An 'agents browser …', 'ag browser …', or standalone 'browser …' command is
2429
+ stamped AGENTS_FLEET_REMOTE so the target's browser.remote-control consent
2430
+ gate applies, same as 'agents browser <verb> --device <name>'.
2415
2431
  `)
2416
2432
  .action(async (name, cmd) => {
2417
2433
  // Hidden askpass bridge: ssh execs the shim, which re-invokes us here.
@@ -97,6 +97,20 @@ export function registerStatusCommand(syncCmd) {
97
97
  if (status.totals.orphan > 0) {
98
98
  console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
99
99
  }
100
+ // Config drift is its OWN class, not a per-agent "N missing": a box that has
101
+ // not folded its device-scoped state still carries a stale top-level header or
102
+ // a lingering central fleet/hosts/accounts/browser block. Surface it distinctly
103
+ // so an un-drained box is SEEN here instead of via a mystery pull conflict
104
+ // (PHNX-3315).
105
+ if (status.config.staleHeader || status.config.centralLeaks.length > 0) {
106
+ console.log(` ${'config (device-scoped)'.padEnd(28)} ${chalk.yellow('not drained — folds automatically on normal `agents` use here (idempotent)')}`);
107
+ if (status.config.staleHeader) {
108
+ console.log(chalk.gray(' · top-level agents.yaml header is stale (pre-rename)'));
109
+ }
110
+ for (const leak of status.config.centralLeaks) {
111
+ console.log(chalk.gray(` · central ${leak} should be device-scoped`));
112
+ }
113
+ }
100
114
  // Hand off to the shared interactive/apply flow (summary already printed above).
101
115
  await promptDriftSync({ cwd, yes: opts.yes, status, quiet: true });
102
116
  });
@@ -11,6 +11,7 @@ import { accountDisplayLabel } from '../lib/agents.js';
11
11
  import type { AccountInfo } from '../lib/agents.js';
12
12
  import type { AgentId } from '../lib/types.js';
13
13
  import type { FormatUsageSummaryOpts, UsageInfo } from '../lib/accounting/usage.js';
14
+ import { type ConfiguredDeviceRole } from '../lib/device-config.js';
14
15
  import type { ResourceSection, ViewJsonAgent } from '../lib/view-types.js';
15
16
  export type { ResourceItemJson, ResourceSection, SyncState, VersionResourcesJson, ViewJsonAgent, ViewJsonVersion } from '../lib/view-types.js';
16
17
  import { type ProfileSummary } from '../lib/profiles.js';
@@ -68,6 +69,13 @@ export declare function descriptionForPrefix(desc: string | undefined, prefix: s
68
69
  * host is missing is flagged rather than silently listed as runnable.
69
70
  */
70
71
  export declare function renderHarnessBlocks(harnesses: ProfileSummary[], installedHosts: Set<AgentId>, showPaths: boolean, byokMap?: Map<string, ByokUsageResult>): void;
72
+ /**
73
+ * The USAGE-READ-2 decision, isolated from its runtime inputs so it can be
74
+ * tested directly: a usage read may fall through to the interactive OAuth login
75
+ * ONLY for a foreground human render on a `personal` device. Both conditions are
76
+ * required — role alone is not sufficient.
77
+ */
78
+ export declare function allowInteractiveUsageLogin(role: ConfiguredDeviceRole | undefined, isTTY: boolean): boolean;
71
79
  /** Build the set of resource sections to include in `--json` from the
72
80
  * `--resources` / `--detailed` flags, plus (in --json mode) the per-section
73
81
  * boolean filters (`--skills` etc.) that plain `--json` historically ignored. */
@@ -81,7 +89,9 @@ export declare function parseResourceSections(options: {
81
89
  * agents-cli extension's "resume current session in best available version"
82
90
  * command).
83
91
  */
84
- export declare function collectAgentsJson(filterAgentId?: AgentId, resourceSections?: Set<ResourceSection>): Promise<ViewJsonAgent[]>;
92
+ export declare function collectAgentsJson(filterAgentId?: AgentId, resourceSections?: Set<ResourceSection>, opts?: {
93
+ forceRefresh?: boolean;
94
+ }): Promise<ViewJsonAgent[]>;
85
95
  /**
86
96
  * Identity key for duplicate-install detection. Prefers accountKey — which
87
97
  * encodes account AND org — over the bare email: two installs can share an
@@ -8,7 +8,8 @@ import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanag
8
8
  import { ambientClaudeToken, loginHint } from '../lib/signin-badge.js';
9
9
  import { machineId } from '../lib/machine-id.js';
10
10
  import { authCacheKey, readAuthHealthCache } from '../lib/auth-health.js';
11
- import { agentReportsUsage, classifyUsageErrorKind, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageBenignState, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
11
+ import { agentReportsUsage, classifyUsageErrorKind, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageBenignState, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, usageErrorForDisplay, } from '../lib/accounting/usage.js';
12
+ import { selfConfiguredDeviceRole } from '../lib/device-config.js';
12
13
  import { readManifest } from '../lib/manifest.js';
13
14
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/installations/versions.js';
14
15
  import { ensureVersionedAliasCurrent, removeShim, } from '../lib/installations/shims.js';
@@ -316,6 +317,29 @@ function renderHostClisSection(cwd) {
316
317
  console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
317
318
  }
318
319
  }
320
+ /**
321
+ * The USAGE-READ-2 decision, isolated from its runtime inputs so it can be
322
+ * tested directly: a usage read may fall through to the interactive OAuth login
323
+ * ONLY for a foreground human render on a `personal` device. Both conditions are
324
+ * required — role alone is not sufficient.
325
+ */
326
+ export function allowInteractiveUsageLogin(role, isTTY) {
327
+ return role === 'personal' && isTTY === true;
328
+ }
329
+ /**
330
+ * Whether this `agents view` invocation may fall through to the interactive
331
+ * OAuth login for a usage read (USAGE-READ-2). True only for a foreground human
332
+ * render on a `personal` device: the interactive login is the sole credential
333
+ * carrying the `user:profile` scope the usage endpoint needs, and a human
334
+ * running one command is not the unattended-loop revocation risk RUSH-1822
335
+ * fixed. The `--json` path never reaches these render functions (it returns
336
+ * early via `collectAgentsJson`), and a non-TTY (piped/scripted) run is excluded
337
+ * here too, so a machine reader can never silently acquire the interactive
338
+ * credential — role alone is not sufficient.
339
+ */
340
+ function usageAllowInteractiveLogin() {
341
+ return allowInteractiveUsageLogin(selfConfiguredDeviceRole(), process.stdout.isTTY === true);
342
+ }
319
343
  async function showInstalledVersions(filterAgentId, viewOpts) {
320
344
  const spinnerText = filterAgentId
321
345
  ? `Checking ${agentLabel(filterAgentId)} agents...`
@@ -422,7 +446,10 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
422
446
  cliVersion,
423
447
  info,
424
448
  })),
425
- ], { forceRefresh: viewOpts?.forceRefresh });
449
+ ], {
450
+ forceRefresh: viewOpts?.forceRefresh,
451
+ allowInteractiveLogin: usageAllowInteractiveLogin(),
452
+ });
426
453
  spinner.stop();
427
454
  console.log(chalk.bold('Installed Agent CLIs\n'));
428
455
  const mergeCanonical = (info) => {
@@ -930,7 +957,7 @@ async function showAgentResources(agentId, requestedVersion, filter) {
930
957
  home,
931
958
  cliVersion: version,
932
959
  info: accountInfo,
933
- });
960
+ }, { allowInteractiveLogin: usageAllowInteractiveLogin() });
934
961
  const accountLabel = accountColumnLabel(accountInfo);
935
962
  const emailStr = accountLabel ? chalk.cyan(` ${accountLabel}`) : '';
936
963
  const status = chalk.green(version);
@@ -1209,7 +1236,7 @@ export function parseResourceSections(options, jsonMode) {
1209
1236
  * agents-cli extension's "resume current session in best available version"
1210
1237
  * command).
1211
1238
  */
1212
- export async function collectAgentsJson(filterAgentId, resourceSections) {
1239
+ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
1213
1240
  const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
1214
1241
  const authCache = readAuthHealthCache();
1215
1242
  const host = machineId();
@@ -1230,7 +1257,7 @@ export async function collectAgentsJson(filterAgentId, resourceSections) {
1230
1257
  home,
1231
1258
  cliVersion: version,
1232
1259
  info,
1233
- })));
1260
+ })), { forceRefresh: opts?.forceRefresh === true });
1234
1261
  const mergeCanonical = (info) => {
1235
1262
  const key = getUsageLookupKey(info);
1236
1263
  if (!key)
@@ -1273,6 +1300,7 @@ export async function collectAgentsJson(filterAgentId, resourceSections) {
1273
1300
  plan: info.plan,
1274
1301
  usageStatus: info.usageStatus,
1275
1302
  overageCredits: info.overageCredits,
1303
+ usageError: usageErrorForDisplay(usageInfo?.error),
1276
1304
  windows: snapshot
1277
1305
  ? snapshot.windows.map((w) => ({
1278
1306
  key: w.key,
@@ -1562,7 +1590,7 @@ export async function viewAction(agentArg, options) {
1562
1590
  return;
1563
1591
  }
1564
1592
  if (json) {
1565
- const data = await collectAgentsJson(undefined, resourceSections);
1593
+ const data = await collectAgentsJson(undefined, resourceSections, { forceRefresh });
1566
1594
  console.log(JSON.stringify(data, null, 2));
1567
1595
  return;
1568
1596
  }
@@ -1624,7 +1652,7 @@ export async function viewAction(agentArg, options) {
1624
1652
  if (json) {
1625
1653
  // --json ignores the @version suffix, but --resources/--detailed (or a
1626
1654
  // section flag) now attach each version's resource inventory + sync-state.
1627
- const data = await collectAgentsJson(agentId, resourceSections);
1655
+ const data = await collectAgentsJson(agentId, resourceSections, { forceRefresh });
1628
1656
  console.log(JSON.stringify(data[0] ?? { agent: agentId, versions: [], harnesses: [] }, null, 2));
1629
1657
  return;
1630
1658
  }
@@ -23,9 +23,9 @@ import * as yaml from 'yaml';
23
23
  import { atomicWriteFileSync } from './fs-atomic.js';
24
24
  import { getUserAgentsDir, readMeta, updateMeta } from './state.js';
25
25
  import { deleteKeychainToken, getKeychainToken, hasKeychainToken } from './secrets/index.js';
26
- import { bundleExists, deleteBundle, listBundles, readBundle, renameBundle, writeBundleWithItems } from './secrets/bundles.js';
26
+ import { bundleExists, deleteBundle, listBundles, readAndResolveBundleEnv, readBundle, renameBundle, writeBundleWithItems } from './secrets/bundles.js';
27
27
  import { getAccountProvider } from './account-provider-registry.js';
28
- import { accountSecretItem, buildAccountBundle, parseAccountBundle } from './account-schema.js';
28
+ import { accountSecretItem, buildAccountBundle, parseAccountBundle, secretVarFor } from './account-schema.js';
29
29
  const NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
30
30
  const NATIVE_LABEL = /^[a-zA-Z0-9][a-zA-Z0-9@._+-]*$/;
31
31
  const AUTH_KINDS = ['api-key', 'setup-token', 'bearer-token'];
@@ -455,6 +455,18 @@ export function resolveCredentialAccount(name, host, expectedProvider, base = ge
455
455
  const envVar = account.auth === 'setup-token' ? 'CLAUDE_CODE_OAUTH_TOKEN' : adapter.envFor(host, account.auth);
456
456
  if (!hasKeychainToken(account.secretRef))
457
457
  throw new Error(`Credential for account '${account.name}' is missing on this device. Add it with 'agents accounts set-key ${account.name}'.`);
458
+ const secretVar = secretVarFor(account.auth);
459
+ // Account bundles are policy `never`, so their value items carry no biometry
460
+ // ACL. Resolve through the bundle path that verifies that policy and attests
461
+ // `silentNoAcl` to the headless keychain guard. Calling getKeychainToken()
462
+ // directly makes a headless --account launch reject the prompt-free item as
463
+ // if it required Touch ID before the helper ever reads it (PHNX-2939).
464
+ const secret = readAndResolveBundleEnv(account.name, {
465
+ keys: [secretVar],
466
+ keyMode: 'storage',
467
+ agentOnly: true,
468
+ caller: 'accounts resolve',
469
+ }).env[secretVar];
458
470
  const connectionEnv = { ...adapter.connectionEnvFor(host) };
459
471
  if (account.baseUrl) {
460
472
  const baseUrlEnv = adapter.baseUrlEnvFor(host);
@@ -467,7 +479,7 @@ export function resolveCredentialAccount(name, host, expectedProvider, base = ge
467
479
  name: account.name,
468
480
  provider: account.provider,
469
481
  auth: account.auth,
470
- env: { ...connectionEnv, [envVar]: getKeychainToken(account.secretRef) },
482
+ env: { ...connectionEnv, [envVar]: secret },
471
483
  };
472
484
  }
473
485
  /**
@@ -336,16 +336,30 @@ export declare function resolveAccountVersion(agent: AgentId, account: string):
336
336
  export declare function selectBalancedVersion(agent: AgentId): Promise<RotateResult | null>;
337
337
  /** Select the configured version if available, otherwise another available version. */
338
338
  export declare function selectAvailableVersion(agent: AgentId, preferredVersion?: string | null): Promise<RotateResult | null>;
339
+ /**
340
+ * Resolve the version `agents run` should use when the caller did not pin
341
+ * one with `@version`. The caller supplies the effective strategy.
342
+ *
343
+ * `pinned` still prefers the workspace/global default, but it MUST NOT launch
344
+ * a logged-out (or revoked) default when the same device holds a signed-in
345
+ * version that can run — that was a silent 1-second death on fleet workers
346
+ * whose default home had no credential (PHNX-2685). A rate-limited pin is
347
+ * still honoured: `--strategy pinned` remains the escape hatch to force the
348
+ * default through a throttle. When the default is auth-blocked and nothing
349
+ * else is healthy, `exhausted` is set so the caller fails loud instead of
350
+ * spawning into a credential-less home.
351
+ */
339
352
  export declare function resolveRunVersion(agent: AgentId, strategy: RunStrategy, cwd?: string, collect?: (agent: AgentId) => Promise<RotateCandidate[]>): Promise<{
340
353
  version: string | null;
341
354
  rotation: RotateResult | null;
342
355
  /**
343
- * Set when a non-pinned strategy found ZERO healthy candidates among the
344
- * installed versions: the full excluded set, so callers fail loud with
345
- * per-account reasons instead of launching the exhausted pinned default
346
- * (RUSH-2132). Undefined for pinned, for successful picks, and when no
347
- * version is installed at all (the pre-existing not-installed path — there
348
- * is no account to be "unhealthy").
356
+ * Set when a strategy found ZERO healthy candidates among the installed
357
+ * versions: the full excluded set, so callers fail loud with per-account
358
+ * reasons instead of launching the exhausted pinned default (RUSH-2132).
359
+ * Also set for `pinned` when the default is logged out / revoked and no
360
+ * signed-in alternative exists (PHNX-2685). Undefined for successful picks
361
+ * and when no version is installed at all (the pre-existing not-installed
362
+ * path — there is no account to be "unhealthy").
349
363
  */
350
364
  exhausted?: RotateCandidate[];
351
365
  }>;
@@ -690,12 +690,6 @@ export async function selectBalancedVersion(agent) {
690
690
  export async function selectAvailableVersion(agent, preferredVersion) {
691
691
  return pickAvailableCandidate(await collectRunCandidates(agent), preferredVersion);
692
692
  }
693
- /**
694
- * Resolve the version `agents run` should use when the caller did not pin
695
- * one with `@version`. The caller supplies the effective strategy; if that
696
- * strategy cannot find a usable candidate, fall back to the pinned
697
- * workspace/global version.
698
- */
699
693
  /**
700
694
  * Record a rotation pick so parallel callers see it as recently-used.
701
695
  * Writes a stamp file per agent — lightweight, no locking needed since
@@ -722,12 +716,49 @@ function readRotationStamp(agent) {
722
716
  catch { /* missing or corrupt — treat as no stamp */ }
723
717
  return null;
724
718
  }
719
+ /**
720
+ * Resolve the version `agents run` should use when the caller did not pin
721
+ * one with `@version`. The caller supplies the effective strategy.
722
+ *
723
+ * `pinned` still prefers the workspace/global default, but it MUST NOT launch
724
+ * a logged-out (or revoked) default when the same device holds a signed-in
725
+ * version that can run — that was a silent 1-second death on fleet workers
726
+ * whose default home had no credential (PHNX-2685). A rate-limited pin is
727
+ * still honoured: `--strategy pinned` remains the escape hatch to force the
728
+ * default through a throttle. When the default is auth-blocked and nothing
729
+ * else is healthy, `exhausted` is set so the caller fails loud instead of
730
+ * spawning into a credential-less home.
731
+ */
725
732
  export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), collect = collectRunCandidates) {
726
733
  const fallback = resolveVersion(agent, cwd);
734
+ const candidates = await collect(agent);
727
735
  if (strategy === 'pinned') {
736
+ const pinnedCandidate = fallback
737
+ ? candidates.find((c) => c.version === fallback)
738
+ : undefined;
739
+ // Auth-blocked pin: the home cannot authenticate, so launching it is a
740
+ // guaranteed miss. Prefer a signed-in sibling on this device.
741
+ if (pinnedCandidate && isSignInRecoverable(readinessFromCandidate(pinnedCandidate))) {
742
+ const rotation = pickAvailableCandidate(candidates, fallback);
743
+ if (rotation) {
744
+ emit('rotation.resolved', {
745
+ module: 'rotate',
746
+ agent,
747
+ version: rotation.picked.version,
748
+ strategy,
749
+ healthy: rotation.healthy.length,
750
+ excluded: rotation.excluded.length,
751
+ });
752
+ return { version: rotation.picked.version, rotation };
753
+ }
754
+ return {
755
+ version: fallback,
756
+ rotation: null,
757
+ exhausted: candidates.length > 0 ? candidates : undefined,
758
+ };
759
+ }
728
760
  return { version: fallback, rotation: null };
729
761
  }
730
- const candidates = await collect(agent);
731
762
  const rotation = strategy === 'available'
732
763
  ? pickAvailableCandidate(candidates, fallback)
733
764
  : pickBalancedCandidate(candidates);
@@ -22,6 +22,12 @@ import type { AgentId } from '../types.js';
22
22
  */
23
23
  export declare function usageNoCredentialError(agent: string): string;
24
24
  export declare function usageExpiredCredentialError(agent: string): string;
25
+ /**
26
+ * Kimi-specific expired-credential wording. A normal Kimi launch refreshes its
27
+ * own OAuth access token, so the recovery action for an expired Kimi credential
28
+ * is to run Kimi once — not to re-auth through agents-cli (RUSH-3198).
29
+ */
30
+ export declare function usageExpiredKimiCredentialError(): string;
25
31
  export declare function usageRejectedError(agent: string, status: number): string;
26
32
  /**
27
33
  * Canonical phrase for the Anthropic setup-token scope gap (RUSH-2392).
@@ -39,6 +45,26 @@ export declare const USAGE_HEADLESS_SCOPE_MARKER = "usage unavailable (headless)
39
45
  export declare function usageHeadlessScopeError(agent?: string): string;
40
46
  /** True when an error string is the setup-token scope gap (RUSH-2392). */
41
47
  export declare function isUsageHeadlessScopeError(error: string | null | undefined): boolean;
48
+ /**
49
+ * Canonical phrase for a Claude account the usage reader holds no usable
50
+ * credential for. Distinct from {@link USAGE_HEADLESS_SCOPE_MARKER}, which
51
+ * means a setup-token WAS read and the endpoint refused its scope.
52
+ */
53
+ export declare const USAGE_NO_USAGE_CREDENTIAL_MARKER = "usage unavailable (no usage credential)";
54
+ /**
55
+ * Claude's own no-credential message. The shared
56
+ * {@link usageNoCredentialError} offers "sign in" as the remedy, which holds
57
+ * for Kimi/Droid/Cursor — their CLIs rotate a readable token on the next launch
58
+ * — and is false for Claude: the usage read deliberately never touches the
59
+ * interactive login (RUSH-1822), so an account that IS signed in reads as
60
+ * unreadable here and signing in again changes nothing. Naming only the second
61
+ * remedy would send the operator to `claude setup-token`, whose token then hits
62
+ * the `user:profile` scope gap (RUSH-2392) — the loop reported in #2987 — so
63
+ * this message states both constraints and that the account still runs.
64
+ */
65
+ export declare function usageNoClaudeUsageCredentialError(): string;
66
+ /** True when an error string is the Claude no-usage-credential state (#2987). */
67
+ export declare function isUsageNoUsageCredentialError(error: string | null | undefined): boolean;
42
68
  /**
43
69
  * Detect Anthropic's usage-endpoint scope denial: HTTP 403 whose body names
44
70
  * `user:profile` (or "scope requirement"). A bare 403 without that body stays
@@ -72,6 +98,28 @@ export declare function usageUnreachableError(agent: string, cause?: unknown): s
72
98
  export declare const USAGE_NO_RECENT_USAGE_MARKER = "no usage recorded yet";
73
99
  export declare const USAGE_BENIGN_STATE: unique symbol;
74
100
  export type UsageBenignState = 'no-recent-usage';
101
+ /**
102
+ * Sentinel `UsageInfo.error` for a read-only lookup whose cache held nothing
103
+ * (`getUsageInfoForIdentity`). No request was made and nothing failed — the
104
+ * daemon simply has not collected this account yet. It was an unclassified
105
+ * literal, so `classifyUsageErrorKind` fell through to `'rejected'` and
106
+ * `agents view` printed the generic "usage unavailable" for a cold cache,
107
+ * which reads as a failure the operator should chase (#2987). The string value
108
+ * is unchanged; callers that already compare against `'stale'` keep working.
109
+ */
110
+ export declare const USAGE_NOT_COLLECTED_MARKER = "stale";
111
+ /**
112
+ * Human-facing form of a `UsageInfo.error` for a machine/JSON consumer
113
+ * (`agents view --json`'s `usageError`). Every error string this module
114
+ * constructs is already a full human sentence EXCEPT the internal
115
+ * {@link USAGE_NOT_COLLECTED_MARKER} (`'stale'`) sentinel, which a read-only
116
+ * lookup returns for a never-cached account when `--refresh` was not passed. That
117
+ * value is an internal cache signal, not an error message, and leaking it verbatim
118
+ * contradicts the field's "human-readable" contract (PHNX-3348). Map it to a
119
+ * plain-language, actionable string and pass every genuine error through
120
+ * unchanged. Returns `null` when there is no error.
121
+ */
122
+ export declare function usageErrorForDisplay(error: string | null | undefined): string | null;
75
123
  /**
76
124
  * Shared error-classification + 429 backoff for a networked usage fetch whose
77
125
  * only signal is an HTTP status (or none at all, on a network failure) —
@@ -91,7 +139,7 @@ export declare function classifyUsageFetchFailure(agent: string, agentId: 'antig
91
139
  * distinct causes (RUSH-3040). Matched against the canonical strings this file
92
140
  * constructs — never re-derive these prefixes at a call site.
93
141
  */
94
- export type UsageErrorKind = 'no-credential' | 'expired-credential' | 'rate-limited' | 'rejected' | 'headless-scope' | 'unreachable';
142
+ export type UsageErrorKind = 'no-credential' | 'no-usage-credential' | 'expired-credential' | 'rate-limited' | 'rejected' | 'headless-scope' | 'unreachable' | 'not-collected';
95
143
  /** Classify a `UsageInfo.error` string into its {@link UsageErrorKind}, or null when there is no error. */
96
144
  export declare function classifyUsageErrorKind(error: string | null | undefined): UsageErrorKind | null;
97
145
  /**
@@ -176,6 +224,16 @@ interface UsageOptions {
176
224
  * setup-token, or `<home>/.claude/.credentials.json` only.
177
225
  */
178
226
  fileOnly?: boolean;
227
+ /**
228
+ * When true, a read that finds no file-based setup-token MAY fall through to
229
+ * Claude Code's interactive OAuth login (the only credential carrying
230
+ * `user:profile`, which `/api/oauth/usage` requires). OFF by default and set
231
+ * ONLY by a foreground human `agents view` on a `personal` device (see
232
+ * USAGE-READ-2). Every background caller — daemon usage warm, auth-health
233
+ * probe, watchdog — leaves it unset, preserving the RUSH-1822 guarantee that
234
+ * an unattended loop never transmits the interactive login to Anthropic.
235
+ */
236
+ allowInteractiveLogin?: boolean;
179
237
  }
180
238
  /** Canonical input for a single usage fetch operation. */
181
239
  export interface UsageFetchInput {
@@ -272,6 +330,14 @@ export declare const USAGE_FETCH_CONCURRENCY = 3;
272
330
  export interface UsageLookupOptions {
273
331
  forceRefresh?: boolean;
274
332
  fileOnly?: boolean;
333
+ /**
334
+ * Permit a foreground personal-device usage read to fall through to the
335
+ * interactive login when no setup-token exists (USAGE-READ-2). Set ONLY by
336
+ * `agents view` when `selfConfiguredDeviceRole() === 'personal'` and the
337
+ * output is a human TTY (not `--json`). Threads into `getClaudeUsageInfo` →
338
+ * `loadClaudeOauth`. Unset for every other lookup.
339
+ */
340
+ allowInteractiveLogin?: boolean;
275
341
  }
276
342
  export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[], opts?: UsageLookupOptions): Promise<{
277
343
  canonicalByUsageKey: Map<string, AccountInfo>;
@@ -574,6 +640,7 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
574
640
  export declare function loadClaudeOauth(home?: string, opts?: {
575
641
  accessTokenCache?: boolean;
576
642
  fileOnly?: boolean;
643
+ allowInteractiveLogin?: boolean;
577
644
  }): Promise<ClaudeOauthCredentials | null>;
578
645
  /**
579
646
  * Save Claude OAuth credentials to the system keychain/keyring.