@phnx-labs/agents-cli 1.22.52 → 1.22.54

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 (180) hide show
  1. package/CHANGELOG.md +336 -0
  2. package/README.md +42 -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 +220 -174
  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 +26 -17
  16. package/dist/commands/fleet-capture.js +7 -0
  17. package/dist/commands/focus.d.ts +1 -0
  18. package/dist/commands/focus.js +4 -2
  19. package/dist/commands/go.d.ts +5 -4
  20. package/dist/commands/go.js +8 -7
  21. package/dist/commands/insights.js +9 -0
  22. package/dist/commands/monitors.js +85 -30
  23. package/dist/commands/output.js +8 -2
  24. package/dist/commands/repo.js +18 -0
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions-inject.js +8 -3
  27. package/dist/commands/sessions-picker.js +2 -1
  28. package/dist/commands/sessions.d.ts +20 -12
  29. package/dist/commands/sessions.js +94 -40
  30. package/dist/commands/setup-accounts.d.ts +8 -0
  31. package/dist/commands/setup-accounts.js +47 -0
  32. package/dist/commands/setup.d.ts +1 -1
  33. package/dist/commands/setup.js +11 -2
  34. package/dist/commands/share.d.ts +52 -3
  35. package/dist/commands/share.js +262 -18
  36. package/dist/commands/ssh.d.ts +7 -0
  37. package/dist/commands/ssh.js +53 -14
  38. package/dist/commands/status.js +14 -0
  39. package/dist/commands/sync.js +44 -0
  40. package/dist/commands/view.d.ts +3 -1
  41. package/dist/commands/view.js +5 -4
  42. package/dist/lib/account-registry.d.ts +15 -5
  43. package/dist/lib/account-registry.js +165 -53
  44. package/dist/lib/accounting/rotate.d.ts +20 -6
  45. package/dist/lib/accounting/rotate.js +38 -7
  46. package/dist/lib/accounting/usage.d.ts +37 -1
  47. package/dist/lib/accounting/usage.js +71 -6
  48. package/dist/lib/agent-spec/agents.d.ts +5 -2
  49. package/dist/lib/agent-spec/agents.js +25 -7
  50. package/dist/lib/analytics/mix-commands.js +12 -6
  51. package/dist/lib/answer-router.js +2 -1
  52. package/dist/lib/auth-mint.d.ts +150 -0
  53. package/dist/lib/auth-mint.js +434 -0
  54. package/dist/lib/browser/profiles.d.ts +18 -0
  55. package/dist/lib/browser/profiles.js +26 -1
  56. package/dist/lib/browser/registry.d.ts +44 -14
  57. package/dist/lib/browser/registry.js +141 -45
  58. package/dist/lib/browser/remote-control.d.ts +9 -7
  59. package/dist/lib/browser/remote-control.js +9 -7
  60. package/dist/lib/claude-account-token.d.ts +10 -0
  61. package/dist/lib/claude-account-token.js +14 -4
  62. package/dist/lib/config-drift.d.ts +37 -0
  63. package/dist/lib/config-drift.js +72 -0
  64. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  65. package/dist/lib/daemon/auth-sync-service.js +34 -0
  66. package/dist/lib/daemon/daemon.js +30 -4
  67. package/dist/lib/daemon/runner.js +10 -2
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/device-config.d.ts +3 -3
  71. package/dist/lib/device-config.js +8 -7
  72. package/dist/lib/devices/config-migration.js +147 -1
  73. package/dist/lib/devices/connect.d.ts +26 -0
  74. package/dist/lib/devices/connect.js +48 -1
  75. package/dist/lib/devices/device-docs.d.ts +35 -0
  76. package/dist/lib/devices/device-docs.js +163 -0
  77. package/dist/lib/devices/discovery-policy.d.ts +14 -2
  78. package/dist/lib/devices/discovery-policy.js +31 -21
  79. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  80. package/dist/lib/devices/doctor-findings.js +19 -1
  81. package/dist/lib/devices/registry.d.ts +11 -5
  82. package/dist/lib/devices/registry.js +46 -18
  83. package/dist/lib/exec.d.ts +88 -30
  84. package/dist/lib/exec.js +138 -34
  85. package/dist/lib/feed/feed.d.ts +10 -2
  86. package/dist/lib/feed/feed.js +35 -2
  87. package/dist/lib/feed-broadcast.js +1 -1
  88. package/dist/lib/fleet/apply.d.ts +11 -0
  89. package/dist/lib/fleet/apply.js +23 -3
  90. package/dist/lib/fleet/auth-sync.js +5 -3
  91. package/dist/lib/help.d.ts +9 -0
  92. package/dist/lib/help.js +29 -1
  93. package/dist/lib/hosts/dispatch.d.ts +4 -3
  94. package/dist/lib/hosts/dispatch.js +12 -8
  95. package/dist/lib/hosts/passthrough.d.ts +1 -10
  96. package/dist/lib/hosts/passthrough.js +1 -13
  97. package/dist/lib/hosts/providers/local.d.ts +9 -3
  98. package/dist/lib/hosts/providers/local.js +23 -12
  99. package/dist/lib/hosts/reconnect.d.ts +7 -4
  100. package/dist/lib/hosts/reconnect.js +29 -25
  101. package/dist/lib/hosts/registry.js +4 -1
  102. package/dist/lib/hosts/remote-os.js +3 -1
  103. package/dist/lib/installations/versions.js +9 -1
  104. package/dist/lib/linux-userns.d.ts +58 -0
  105. package/dist/lib/linux-userns.js +116 -0
  106. package/dist/lib/memory.d.ts +26 -0
  107. package/dist/lib/memory.js +80 -1
  108. package/dist/lib/monitors/config.d.ts +11 -0
  109. package/dist/lib/monitors/config.js +8 -0
  110. package/dist/lib/monitors/engine.js +8 -1
  111. package/dist/lib/monitors/state.d.ts +37 -1
  112. package/dist/lib/monitors/state.js +79 -4
  113. package/dist/lib/permissions-registry.d.ts +2 -0
  114. package/dist/lib/permissions-registry.js +116 -14
  115. package/dist/lib/permissions.d.ts +5 -3
  116. package/dist/lib/permissions.js +25 -27
  117. package/dist/lib/profiles.d.ts +8 -7
  118. package/dist/lib/profiles.js +12 -0
  119. package/dist/lib/project-key.d.ts +9 -0
  120. package/dist/lib/project-key.js +11 -0
  121. package/dist/lib/secrets/bundles.d.ts +35 -0
  122. package/dist/lib/secrets/bundles.js +78 -1
  123. package/dist/lib/secrets/push.d.ts +3 -8
  124. package/dist/lib/secrets/push.js +18 -14
  125. package/dist/lib/secrets/remote.d.ts +9 -18
  126. package/dist/lib/secrets/remote.js +11 -26
  127. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  128. package/dist/lib/secrets/reserved-sync.js +129 -0
  129. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  130. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  131. package/dist/lib/self-heal/registry.js +4 -0
  132. package/dist/lib/self-heal/types.d.ts +1 -1
  133. package/dist/lib/session/active.d.ts +10 -1
  134. package/dist/lib/session/active.js +8 -5
  135. package/dist/lib/session/actor-sidecar.d.ts +7 -0
  136. package/dist/lib/session/actor-sidecar.js +2 -0
  137. package/dist/lib/session/db.d.ts +39 -4
  138. package/dist/lib/session/db.js +168 -31
  139. package/dist/lib/session/discover.d.ts +32 -4
  140. package/dist/lib/session/discover.js +126 -37
  141. package/dist/lib/session/insights.d.ts +14 -0
  142. package/dist/lib/session/insights.js +25 -2
  143. package/dist/lib/session/linear.js +1 -1
  144. package/dist/lib/session/live-metadata.js +1 -0
  145. package/dist/lib/session/pid-registry.d.ts +7 -0
  146. package/dist/lib/session/prompt.d.ts +15 -0
  147. package/dist/lib/session/prompt.js +21 -0
  148. package/dist/lib/session/shell-programs.d.ts +17 -0
  149. package/dist/lib/session/shell-programs.js +21 -0
  150. package/dist/lib/session/state.js +2 -1
  151. package/dist/lib/session/stream-render.js +2 -1
  152. package/dist/lib/session/tool-calls.js +2 -5
  153. package/dist/lib/session/trajectory-html.js +2 -1
  154. package/dist/lib/session/trajectory.js +3 -12
  155. package/dist/lib/session/types.d.ts +25 -0
  156. package/dist/lib/session/types.js +10 -0
  157. package/dist/lib/share/publish.d.ts +53 -5
  158. package/dist/lib/share/publish.js +99 -17
  159. package/dist/lib/share/worker-template.js +594 -64
  160. package/dist/lib/startup/root-command.js +2 -1
  161. package/dist/lib/state.d.ts +24 -0
  162. package/dist/lib/state.js +318 -54
  163. package/dist/lib/sync-status.d.ts +4 -0
  164. package/dist/lib/sync-status.js +3 -0
  165. package/dist/lib/terminal/resolve.d.ts +7 -0
  166. package/dist/lib/terminal/resolve.js +41 -2
  167. package/dist/lib/traces/classify.js +24 -19
  168. package/dist/lib/traces/insights.d.ts +67 -0
  169. package/dist/lib/traces/insights.js +178 -0
  170. package/dist/lib/traces/phenotype.d.ts +67 -0
  171. package/dist/lib/traces/phenotype.js +437 -0
  172. package/dist/lib/traces/segments.d.ts +133 -0
  173. package/dist/lib/traces/segments.js +301 -0
  174. package/dist/lib/traces/sync.d.ts +33 -0
  175. package/dist/lib/traces/sync.js +11 -2
  176. package/dist/lib/types.d.ts +47 -1
  177. package/dist/lib/usage-refresh.js +2 -1
  178. package/dist/lib/view-types.d.ts +2 -0
  179. package/dist/lib/watchdog/runner.js +18 -4
  180. package/package.json +2 -1
@@ -20,9 +20,21 @@ import { discoverNativeAccounts } from '../lib/account-catalog.js';
20
20
  import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
21
21
  import { getAccountProvider, listAccountProviders, providerAuthenticatesHarness } from '../lib/account-provider-registry.js';
22
22
  import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
23
+ import { registerMintCommand } from './auth-mint.js';
23
24
  function cleanCommandError(command, err) {
24
25
  command.error(err instanceof Error ? err.message : String(err), { exitCode: 1, code: 'accounts.error' });
25
26
  }
27
+ /** Turn a thrown user-facing Error into commander's clean CLI error instead of Node's stack dump. */
28
+ async function runAccountsAction(command, fn) {
29
+ try {
30
+ await fn();
31
+ }
32
+ catch (err) {
33
+ if (isPromptCancelled(err))
34
+ process.exit(130);
35
+ cleanCommandError(command, err);
36
+ }
37
+ }
26
38
  function parseInstallation(raw) {
27
39
  const at = raw.lastIndexOf('@');
28
40
  if (at < 1 || at === raw.length - 1)
@@ -106,6 +118,17 @@ function secretFromBundle(raw) {
106
118
  const { bundle, key } = parseBundleKey(raw);
107
119
  return readAndResolveBundleEnv(bundle, { keys: [key], keyMode: 'storage', agentOnly: true, caller: 'accounts import' }).env[key];
108
120
  }
121
+ /** Interactive secret entry for add/set-key. Ctrl+C must not become accounts.error. */
122
+ async function promptAccountSecret(message) {
123
+ try {
124
+ return await password({ message });
125
+ }
126
+ catch (err) {
127
+ if (isPromptCancelled(err))
128
+ return null;
129
+ throw err;
130
+ }
131
+ }
109
132
  function publicAccount(account) {
110
133
  return { kind: 'provider', id: account.id, name: account.name, provider: account.provider, auth: account.auth, baseUrl: account.baseUrl, policy: account.policy, secretPresent: account.secretPresent };
111
134
  }
@@ -380,243 +403,264 @@ export function registerAccountsCommand(program) {
380
403
  const accounts = program.command('accounts').description('Browse native logins and manage provider account bundles')
381
404
  .option('--json', 'Machine-readable account metadata')
382
405
  .option('--fleet', 'Show harness-native signed-in identities across reachable devices')
383
- .action((o) => printAccounts(!!o.json, !!o.fleet));
384
- accounts.command('list').description('List credential accounts').option('--json', 'Machine-readable account metadata').action((o, command) => printAccounts(!!(o.json || command.optsWithGlobals().json)));
406
+ .action(async (o, command) => {
407
+ await runAccountsAction(command, () => printAccounts(!!o.json, !!o.fleet));
408
+ });
409
+ accounts.command('list').description('List credential accounts').option('--json', 'Machine-readable account metadata').action(async (o, command) => {
410
+ await runAccountsAction(command, () => printAccounts(!!(o.json || command.optsWithGlobals().json)));
411
+ });
412
+ registerMintCommand(accounts);
385
413
  accounts.command('add <name>')
386
414
  .description('Add a durable API key, setup token, or bearer token')
387
415
  .requiredOption('--provider <provider>', `Credential provider (${listAccountProviders().join(', ')})`)
388
416
  .requiredOption('--auth <type>', 'Credential type: api-key | setup-token | bearer-token')
389
417
  .option('--base-url <url>', 'Optional endpoint override stored with the account')
390
418
  .option('--from-secrets <bundle:key>', 'Import from an existing agents secrets entry')
391
- .action(async (name, o) => {
392
- const auth = parseAuth(o.auth);
393
- const provider = getAccountProvider(o.provider);
394
- if (!provider.authKinds.includes(auth))
395
- throw new Error(`Provider '${provider.provider}' does not support ${auth}. Supported: ${provider.authKinds.join(', ')}.`);
396
- const secret = o.fromSecrets ? secretFromBundle(o.fromSecrets) : await password({ message: `Enter ${provider.provider} ${auth} for '${name}':` });
397
- const account = addAccount(name, provider.provider, auth, secret, undefined, { baseUrl: o.baseUrl });
398
- console.log(chalk.green(`Added ${account.provider} ${account.auth} account '${account.name}'.`));
399
- console.log(chalk.gray(`Secret bundle '${account.name}' is the account and uses policy never, so agent launches never request Touch ID.`));
419
+ .action(async (name, o, command) => {
420
+ await runAccountsAction(command, async () => {
421
+ const auth = parseAuth(o.auth);
422
+ const provider = getAccountProvider(o.provider);
423
+ if (!provider.authKinds.includes(auth))
424
+ throw new Error(`Provider '${provider.provider}' does not support ${auth}. Supported: ${provider.authKinds.join(', ')}.`);
425
+ const secret = o.fromSecrets
426
+ ? secretFromBundle(o.fromSecrets)
427
+ : await promptAccountSecret(`Enter ${provider.provider} ${auth} for '${name}':`);
428
+ if (secret === null) {
429
+ process.exit(130);
430
+ return;
431
+ }
432
+ const account = addAccount(name, provider.provider, auth, secret, undefined, { baseUrl: o.baseUrl });
433
+ console.log(chalk.green(`Added ${account.provider} ${account.auth} account '${account.name}'.`));
434
+ console.log(chalk.gray(`Secret bundle '${account.name}' is the account and uses policy never, so agent launches never request Touch ID.`));
435
+ });
400
436
  });
401
437
  accounts.command('set-key <name>')
402
438
  .description('Rotate an account credential without changing its identity')
403
439
  .option('--from-secrets <bundle:key>', 'Import from an existing agents secrets entry')
404
- .action(async (name, o) => {
405
- const account = findAccount(name);
406
- if (!account)
407
- throw new Error(`Unknown provider account '${name}'.`);
408
- const secret = o.fromSecrets ? secretFromBundle(o.fromSecrets) : await password({ message: `Enter new ${account.provider} ${account.auth} for '${name}':` });
409
- setAccountSecret(name, secret);
410
- console.log(chalk.green(`Updated credential for account '${name}'.`));
440
+ .action(async (name, o, command) => {
441
+ await runAccountsAction(command, async () => {
442
+ const account = findAccount(name);
443
+ if (!account)
444
+ throw new Error(`Unknown provider account '${name}'.`);
445
+ const secret = o.fromSecrets
446
+ ? secretFromBundle(o.fromSecrets)
447
+ : await promptAccountSecret(`Enter new ${account.provider} ${account.auth} for '${name}':`);
448
+ if (secret === null) {
449
+ process.exit(130);
450
+ return;
451
+ }
452
+ setAccountSecret(name, secret);
453
+ console.log(chalk.green(`Updated credential for account '${name}'.`));
454
+ });
411
455
  });
412
456
  accounts.command('view <name>').alias('inspect').description('Show safe account metadata, custody, and attachments').option('--json', 'Machine-readable output').action(async (name, o, command) => {
413
- const meta = readMeta();
414
- const unified = findUnifiedAccount(name, meta);
415
- if (!unified)
416
- throw new Error(`Unknown account '${name}'.`);
417
- const account = unified.kind === 'provider'
418
- ? { ...publicAccount(inspectAccount(unified.name)), custody: 'agents secrets (policy never)', attached: accountBindings(unified.id, meta) }
419
- : { ...unified, custody: `${unified.agent} (not stored by agents-cli)`, attached: accountBindings(unified.id, meta) };
420
- if (o.json || command.optsWithGlobals().json)
421
- return console.log(JSON.stringify(account, null, 2));
422
- console.log(chalk.bold(account.name));
423
- console.log(` kind: ${account.kind}`);
424
- console.log(` id: ${account.id}`);
425
- console.log(` custody: ${account.custody}`);
426
- if (account.kind === 'provider') {
427
- console.log(` provider: ${account.provider}`);
428
- console.log(` auth: ${account.auth}`);
429
- console.log(` credential: ${account.secretPresent ? 'present on this device' : 'missing on this device'}`);
430
- }
431
- else {
432
- console.log(` identity: ${account.identityLabel ?? account.identityKey}`);
433
- console.log(` scope: ${account.scope}`);
434
- }
435
- console.log(` attached: ${account.attached.length ? account.attached.join(', ') : 'none'}`);
457
+ await runAccountsAction(command, async () => {
458
+ const meta = readMeta();
459
+ const unified = findUnifiedAccount(name, meta);
460
+ if (!unified)
461
+ throw new Error(`Unknown account '${name}'.`);
462
+ const account = unified.kind === 'provider'
463
+ ? { ...publicAccount(inspectAccount(unified.name)), custody: 'agents secrets (policy never)', attached: accountBindings(unified.id, meta) }
464
+ : { ...unified, custody: `${unified.agent} (not stored by agents-cli)`, attached: accountBindings(unified.id, meta) };
465
+ if (o.json || command.optsWithGlobals().json)
466
+ return console.log(JSON.stringify(account, null, 2));
467
+ console.log(chalk.bold(account.name));
468
+ console.log(` kind: ${account.kind}`);
469
+ console.log(` id: ${account.id}`);
470
+ console.log(` custody: ${account.custody}`);
471
+ if (account.kind === 'provider') {
472
+ console.log(` provider: ${account.provider}`);
473
+ console.log(` auth: ${account.auth}`);
474
+ console.log(` credential: ${account.secretPresent ? 'present on this device' : 'missing on this device'}`);
475
+ }
476
+ else {
477
+ console.log(` identity: ${account.identityLabel ?? account.identityKey}`);
478
+ console.log(` scope: ${account.scope}`);
479
+ }
480
+ console.log(` attached: ${account.attached.length ? account.attached.join(', ') : 'none'}`);
481
+ });
436
482
  });
437
483
  accounts.command('name <source> <name>')
438
484
  .description('Name a signed-in native installation without copying its OAuth credentials')
439
485
  .action(async (source, name, _o, command) => {
440
- try {
486
+ await runAccountsAction(command, async () => {
441
487
  const identity = await nativeIdentityFromSource(source);
442
488
  const account = addNativeAccount(name, identity.agent, identity.identityKey, identity.identityLabel, identity.scope);
443
489
  console.log(chalk.green(`Named ${source} as ${account.name}.`));
444
490
  if (account.scope === 'device')
445
491
  console.log(chalk.gray(`${identity.agent} authentication is device-scoped; attach '${account.name}' to '${identity.agent}', not an individual version.`));
446
- }
447
- catch (err) {
448
- cleanCommandError(command, err);
449
- }
492
+ });
450
493
  });
451
494
  const labelCmd = accounts.command('label <source> [label]')
452
495
  .description('Label a native login by harness or <harness>@<version>; the label binds to the account identity, not the version')
453
496
  .option('--account <email-or-id>', 'Native identity to label when the harness has multiple logins')
454
497
  .action(async (source, label, o, command) => {
455
- try {
456
- await runAccountsLabel(source, label, o);
457
- }
458
- catch (err) {
459
- cleanCommandError(command, err);
460
- }
498
+ await runAccountsAction(command, () => runAccountsLabel(source, label, o));
461
499
  });
462
500
  setHelpSections(labelCmd, {
463
501
  examples: `agents accounts label codex work
464
502
  agents accounts label codex@0.146.0 personal
465
503
  agents accounts label codex work --account you@example.com
466
504
  agents run codex#work`,
467
- notes: 'The label binds to the signed-in account identity, not the version — codex#work keeps selecting that account after it moves to a newer install. One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
505
+ notes: 'The label binds to the signed-in account identity, not the version — codex#work keeps selecting that account after it moves to a newer install. Labels live on the central account rows in agents.yaml, which `agents repo push/pull` already syncs fleet-wide, keyed by (agent, identityKey). One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
468
506
  });
469
507
  accounts.command('attach <account> <target>')
470
508
  .description('Attach a named account to a native installation or custom harness')
471
- .action(async (name, target) => {
472
- const meta = readMeta();
473
- const account = findUnifiedAccount(name, meta);
474
- if (!account)
475
- throw new Error(`Unknown account '${name}'.`);
476
- if (account.kind === 'native')
477
- assertNativeAccountNameable(account.agent);
478
- // Validate the target exists before mutating any binding.
479
- const t = classifyAttachTarget(target);
480
- const targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
481
- if (account.kind === 'native') {
482
- // A provider-backed profile injects provider env at spawn, which would run
483
- // under a different credential than the native identity claims — refuse it.
484
- if (t.kind === 'profile' && t.profile.provider) {
485
- throw new Error(`Custom harness '${target}' is provider-backed (${t.profile.provider}); it cannot host the native login account '${account.name}'. Attach a matching provider account instead.`);
486
- }
487
- if (targetAgent !== account.agent)
488
- throw new Error(`Account '${account.name}' is a ${account.agent} login; '${target}' runs ${targetAgent}.`);
489
- if (account.scope === 'device') {
490
- if (t.kind !== 'device-agent')
491
- throw new Error(`${account.agent} authentication is device-scoped. Attach it with 'agents accounts attach ${account.name} ${account.agent}'.`);
509
+ .action(async (name, target, _o, command) => {
510
+ await runAccountsAction(command, async () => {
511
+ const meta = readMeta();
512
+ const account = findUnifiedAccount(name, meta);
513
+ if (!account)
514
+ throw new Error(`Unknown account '${name}'.`);
515
+ if (account.kind === 'native')
516
+ assertNativeAccountNameable(account.agent);
517
+ // Validate the target exists before mutating any binding.
518
+ const t = classifyAttachTarget(target);
519
+ const targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
520
+ if (account.kind === 'native') {
521
+ // A provider-backed profile injects provider env at spawn, which would run
522
+ // under a different credential than the native identity claims — refuse it.
523
+ if (t.kind === 'profile' && t.profile.provider) {
524
+ throw new Error(`Custom harness '${target}' is provider-backed (${t.profile.provider}); it cannot host the native login account '${account.name}'. Attach a matching provider account instead.`);
525
+ }
526
+ if (targetAgent !== account.agent)
527
+ throw new Error(`Account '${account.name}' is a ${account.agent} login; '${target}' runs ${targetAgent}.`);
528
+ if (account.scope === 'device') {
529
+ if (t.kind !== 'device-agent')
530
+ throw new Error(`${account.agent} authentication is device-scoped. Attach it with 'agents accounts attach ${account.name} ${account.agent}'.`);
531
+ }
532
+ else {
533
+ if (t.kind !== 'installation')
534
+ throw new Error(`${account.agent} authentication is per-version. Attach '${account.name}' to a specific ${account.agent}@<version>.`);
535
+ const identity = await nativeIdentityFromSource(target);
536
+ if (identity.identityKey !== account.identityKey)
537
+ throw new Error(`'${target}' is signed in to a different identity than account '${account.name}'.`);
538
+ }
492
539
  }
493
540
  else {
494
- if (t.kind !== 'installation')
495
- throw new Error(`${account.agent} authentication is per-version. Attach '${account.name}' to a specific ${account.agent}@<version>.`);
496
- const identity = await nativeIdentityFromSource(target);
497
- if (identity.identityKey !== account.identityKey)
498
- throw new Error(`'${target}' is signed in to a different identity than account '${account.name}'.`);
541
+ // Provider account: it must be able to authenticate the target's harness.
542
+ getAccountProvider(account.provider).envFor(targetAgent, account.auth);
499
543
  }
500
- }
501
- else {
502
- // Provider account: it must be able to authenticate the target's harness.
503
- getAccountProvider(account.provider).envFor(targetAgent, account.auth);
504
- }
505
- bindAccount(name, target);
506
- writeClaudeInteractiveOauthToken(t, targetAgent);
507
- console.log(chalk.green(`Attached ${account.name} to ${target}.`));
544
+ bindAccount(name, target);
545
+ writeClaudeInteractiveOauthToken(t, targetAgent);
546
+ console.log(chalk.green(`Attached ${account.name} to ${target}.`));
547
+ });
508
548
  });
509
549
  accounts.command('detach <account> <target>')
510
550
  .description('Remove one account attachment')
511
- .action((name, target) => {
512
- unbindAccount(name, target);
513
- // With the binding gone, no setup-token resolves for this version home, so this
514
- // clears any .oauth_token the attach left behind (else interactive runs would keep
515
- // authenticating as the just-detached account).
516
- try {
517
- const t = classifyAttachTarget(target);
518
- writeClaudeInteractiveOauthToken(t, t.kind === 'profile' ? t.profile.host.agent : t.agent);
519
- }
520
- catch { /* an unresolvable target has no version home to clean */ }
521
- console.log(chalk.green(`Detached ${name} from ${target}.`));
551
+ .action(async (name, target, _o, command) => {
552
+ await runAccountsAction(command, () => {
553
+ unbindAccount(name, target);
554
+ // With the binding gone, no setup-token resolves for this version home, so this
555
+ // clears any .oauth_token the attach left behind (else interactive runs would keep
556
+ // authenticating as the just-detached account).
557
+ try {
558
+ const t = classifyAttachTarget(target);
559
+ writeClaudeInteractiveOauthToken(t, t.kind === 'profile' ? t.profile.host.agent : t.agent);
560
+ }
561
+ catch { /* an unresolvable target has no version home to clean */ }
562
+ console.log(chalk.green(`Detached ${name} from ${target}.`));
563
+ });
564
+ });
565
+ accounts.command('rename <old> <new>').description('Rename an account without changing its stable id').action(async (oldName, newName, _o, command) => {
566
+ await runAccountsAction(command, () => { renameAccount(oldName, newName); });
567
+ });
568
+ accounts.command('remove <name>').description('Remove an account and its device-local credential').action(async (name, _o, command) => {
569
+ await runAccountsAction(command, () => { removeAccount(name); });
522
570
  });
523
- accounts.command('rename <old> <new>').description('Rename an account without changing its stable id').action((oldName, newName) => renameAccount(oldName, newName));
524
- accounts.command('remove <name>').description('Remove an account and its device-local credential').action((name) => removeAccount(name));
525
571
  accounts.command('set-default <agent> <name>')
526
572
  .description('Use this account for a harness when --account is omitted')
527
- .action((agentRaw, name, _o, command) => {
528
- try {
573
+ .action(async (agentRaw, name, _o, command) => {
574
+ await runAccountsAction(command, () => {
529
575
  const { agent, account } = setDefaultAccount(agentRaw, name);
530
576
  console.log(chalk.green(`${agent} now uses account '${account.name}' unless --account overrides it.`));
531
- }
532
- catch (err) {
533
- cleanCommandError(command, err);
534
- }
577
+ });
535
578
  });
536
579
  const switchCmd = accounts.command('switch <harness> [account]')
537
580
  .description('Pick the default account for a harness')
538
581
  .option('--json', 'Machine-readable account list or the resulting default')
539
582
  .action(async (harness, account, o, command) => {
540
- try {
541
- await runAccountsSwitch(harness, account, { json: !!(o.json || command.optsWithGlobals().json) });
542
- }
543
- catch (err) {
544
- cleanCommandError(command, err);
545
- }
583
+ await runAccountsAction(command, () => runAccountsSwitch(harness, account, { json: !!(o.json || command.optsWithGlobals().json) }));
546
584
  });
547
585
  accounts.command('clear-default <agent>')
548
586
  .description('Return a harness to native login or balanced account selection')
549
- .action((agentRaw) => {
550
- parseHarness(agentRaw);
551
- updateMeta(meta => {
552
- const defaults = { ...meta.accounts?.defaults };
553
- delete defaults[agentRaw];
554
- return { ...meta, accounts: { ...meta.accounts, defaults } };
587
+ .action(async (agentRaw, _o, command) => {
588
+ await runAccountsAction(command, () => {
589
+ parseHarness(agentRaw);
590
+ updateMeta(meta => {
591
+ const defaults = { ...meta.accounts?.defaults };
592
+ delete defaults[agentRaw];
593
+ return { ...meta, accounts: { ...meta.accounts, defaults } };
594
+ });
595
+ console.log(chalk.green(`Cleared the default account for ${agentRaw}.`));
555
596
  });
556
- console.log(chalk.green(`Cleared the default account for ${agentRaw}.`));
557
597
  });
558
598
  accounts.command('logout <target>')
559
599
  .description('Sign out a harness-native OAuth login. API-key / setup-token / bearer accounts use `accounts remove` instead.')
560
- .action(async (target) => {
561
- const provider = findAccount(target);
562
- if (provider) {
563
- throw new Error(`Account '${provider.name}' uses ${provider.auth}, not OAuth. Remove it with: agents accounts remove ${provider.name}`);
564
- }
565
- const agentRaw = target.split('@')[0];
566
- if (!ALL_AGENT_IDS.includes(agentRaw)) {
567
- throw new Error(`Unknown target '${target}'. Pass a native harness (claude, codex, …) or a provider account name. Provider API-key accounts use \`agents accounts remove\`.`);
568
- }
569
- const agent = agentRaw;
570
- const { spawnSync } = await import('child_process');
571
- const { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } = await import('../lib/installations/versions.js');
572
- const installed = listInstalledVersions(agent);
573
- const version = getGlobalDefault(agent) ?? installed[installed.length - 1];
574
- if (!version)
575
- throw new Error(`No installed version of ${agent}. Install one with: agents add ${agent}`);
576
- const bin = getBinaryPath(agent, version);
577
- const home = getVersionHomePath(agent, version);
578
- const result = spawnSync(bin, ['logout'], {
579
- env: { ...process.env, HOME: home },
580
- stdio: 'inherit',
600
+ .action(async (target, _o, command) => {
601
+ await runAccountsAction(command, async () => {
602
+ const provider = findAccount(target);
603
+ if (provider) {
604
+ throw new Error(`Account '${provider.name}' uses ${provider.auth}, not OAuth. Remove it with: agents accounts remove ${provider.name}`);
605
+ }
606
+ const agentRaw = target.split('@')[0];
607
+ if (!ALL_AGENT_IDS.includes(agentRaw)) {
608
+ throw new Error(`Unknown target '${target}'. Pass a native harness (claude, codex, …) or a provider account name. Provider API-key accounts use \`agents accounts remove\`.`);
609
+ }
610
+ const agent = agentRaw;
611
+ const { spawnSync } = await import('child_process');
612
+ const { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } = await import('../lib/installations/versions.js');
613
+ const installed = listInstalledVersions(agent);
614
+ const version = getGlobalDefault(agent) ?? installed[installed.length - 1];
615
+ if (!version)
616
+ throw new Error(`No installed version of ${agent}. Install one with: agents add ${agent}`);
617
+ const bin = getBinaryPath(agent, version);
618
+ const home = getVersionHomePath(agent, version);
619
+ const result = spawnSync(bin, ['logout'], {
620
+ env: { ...process.env, HOME: home },
621
+ stdio: 'inherit',
622
+ });
623
+ if (result.error)
624
+ throw result.error;
625
+ if ((result.status ?? 1) !== 0) {
626
+ throw new Error(`${agent} logout exited ${result.status ?? 'null'}. If this harness has no logout verb, sign out from its own UI.`);
627
+ }
628
+ console.log(chalk.green(`Signed out native ${agent} login (${version}).`));
581
629
  });
582
- if (result.error)
583
- throw result.error;
584
- if ((result.status ?? 1) !== 0) {
585
- throw new Error(`${agent} logout exited ${result.status ?? 'null'}. If this harness has no logout verb, sign out from its own UI.`);
586
- }
587
- console.log(chalk.green(`Signed out native ${agent} login (${version}).`));
588
630
  });
589
631
  accounts.command('sync <name> [device]')
590
632
  .description('Copy one provider account bundle to a worker device')
591
633
  .option('--device <device>', 'Deprecated destination form; use the positional device')
592
634
  .option('--force', 'Replace matching keys on the destination')
593
- .action(async (name, deviceArg, o) => {
594
- const device = deviceArg ?? o.device;
595
- if (!device)
596
- throw new Error('Missing destination device. Usage: agents accounts sync <account> <device>.');
597
- const account = findAccount(name);
598
- if (!account)
599
- throw new Error(`Unknown provider account '${name}'.`);
600
- const sshTarget = await resolveHostSshTarget(device);
601
- assertCredentialTransportHostPinned(sshTarget);
602
- const remoteBackend = resolveRemoteOsSync(device) === 'win32' ? 'keychain' : 'file';
603
- const literalValues = {
604
- ACCOUNT_ID: account.id,
605
- PROVIDER: account.provider,
606
- AUTH_TYPE: account.auth,
607
- ...(account.baseUrl ? { BASE_URL: account.baseUrl } : {}),
608
- };
609
- const result = pushBundleToHost(account.name, device, {
610
- remoteBackend,
611
- force: o.force,
612
- operation: 'accounts sync',
613
- policyNever: true,
614
- agentOnly: false,
615
- literalValues,
635
+ .action(async (name, deviceArg, o, command) => {
636
+ await runAccountsAction(command, async () => {
637
+ const device = deviceArg ?? o.device;
638
+ if (!device)
639
+ throw new Error('Missing destination device. Usage: agents accounts sync <account> <device>.');
640
+ const account = findAccount(name);
641
+ if (!account)
642
+ throw new Error(`Unknown provider account '${name}'.`);
643
+ const sshTarget = await resolveHostSshTarget(device);
644
+ assertCredentialTransportHostPinned(sshTarget);
645
+ const remoteBackend = resolveRemoteOsSync(device) === 'win32' ? 'keychain' : 'file';
646
+ const literalValues = {
647
+ ACCOUNT_ID: account.id,
648
+ PROVIDER: account.provider,
649
+ AUTH_TYPE: account.auth,
650
+ ...(account.baseUrl ? { BASE_URL: account.baseUrl } : {}),
651
+ };
652
+ const result = pushBundleToHost(account.name, device, {
653
+ remoteBackend,
654
+ force: o.force,
655
+ operation: 'accounts sync',
656
+ policyNever: true,
657
+ agentOnly: false,
658
+ literalValues,
659
+ });
660
+ if (!result.ok)
661
+ throw new Error(`${result.message}\nRetry: agents accounts sync ${account.name} ${device}${o.force ? ' --force' : ''}`);
662
+ console.log(chalk.green(`${account.name} synced to ${device} (${result.keyCount} keys, ${remoteBackend} backend, policy never).`));
616
663
  });
617
- if (!result.ok)
618
- throw new Error(`${result.message}\nRetry: agents accounts sync ${account.name} ${device}${o.force ? ' --force' : ''}`);
619
- console.log(chalk.green(`${account.name} synced to ${device} (${result.keyCount} keys, ${remoteBackend} backend, policy never).`));
620
664
  });
621
665
  setHelpSections(switchCmd, {
622
666
  examples: `agents accounts switch claude
@@ -626,7 +670,9 @@ agents run claude`,
626
670
  notes: 'Switch writes the existing per-harness default (same binding as set-default). Rotation already honors it. Pass an account name to skip the picker. Native name/attach is only for harnesses agents-cli can isolate (claude, codex, grok); provider add is unrestricted.',
627
671
  });
628
672
  setHelpSections(accounts, {
629
- examples: `agents accounts add work --provider anthropic --auth setup-token
673
+ examples: `agents accounts mint claude
674
+ agents accounts mint claude --account work --email ada@example.com --fleet
675
+ agents accounts add work --provider anthropic --auth setup-token
630
676
  agents accounts add openrouter-work --provider openrouter --auth api-key --from-secrets openrouter.ai:OPENROUTER_API_KEY
631
677
  agents accounts set-key work
632
678
  agents accounts name claude@2.1.220 work
@@ -639,6 +685,6 @@ agents accounts set-default claude work
639
685
  agents accounts sync openrouter-work yosemite-s0
640
686
  agents run claude --account work
641
687
  agents accounts logout claude`,
642
- notes: 'Native account records contain metadata only; harness-owned OAuth credentials are never copied. Provider accounts are explicit portable bundles with policy never. `accounts switch` is the fast picker over the same default `set-default` writes. Harness-native OAuth sign-out is `agents accounts logout <harness>` (API-key accounts use `accounts remove`). Synced vault unlock is `agents secrets vault unlock`.',
688
+ notes: 'Native account records contain metadata only; harness-owned OAuth credentials are never copied. Provider accounts are explicit portable bundles with policy never. `accounts mint claude` drives `claude setup-token` and seeds both the named account and the reserved file-based auth bundle (same command as `agents auth mint`). `accounts switch` is the fast picker over the same default `set-default` writes. Harness-native OAuth sign-out is `agents accounts logout <harness>` (API-key accounts use `accounts remove`). Synced vault unlock is `agents secrets vault unlock`.',
643
689
  });
644
690
  }
@@ -18,7 +18,8 @@ import { isHostPinned, managedKnownHostsPath } from '../lib/devices/known-hosts.
18
18
  import { ensureDevicesRegistered } from '../lib/devices/sync.js';
19
19
  import { readFleetFile, resolveDesired } from '../lib/fleet/manifest.js';
20
20
  import { snapshotAuth } from '../lib/fleet/auth-sync.js';
21
- import { agentIdOf, diffFleet, probeDevice, runFleetApply, pool, sourceHome, expandAllSpecs, rosterNeedsVersions, } from '../lib/fleet/apply.js';
21
+ import { AUTH_BUNDLE_NAME } from '../lib/secrets/bundles.js';
22
+ import { agentIdOf, diffFleet, probeDevice, runFleetApply, pool, sourceHome, expandAllSpecs, rosterNeedsVersions, fleetSecretsBundles, } from '../lib/fleet/apply.js';
22
23
  import { listInstalledVersions } from '../lib/installations/versions.js';
23
24
  /** Version of the running agents-cli — the fleet target version. */
24
25
  function localCliVersion() {
@@ -210,7 +211,9 @@ async function runApply(opts) {
210
211
  // One extra `secrets list --json` per device, and only when it can change the
211
212
  // plan: the manifest declares bundles AND provisioning is on. Same cost
212
213
  // discipline as `withVersions` — a fleet that uses no bundles never pays it.
213
- const withSecrets = opts.provisionSecrets === true && (manifest.secrets?.bundles?.length ?? 0) > 0;
214
+ const secretsBundles = fleetSecretsBundles(manifest.secrets?.bundles);
215
+ const withSecrets = (opts.provisionSecrets === true && secretsBundles.length > 0)
216
+ || secretsBundles.includes(AUTH_BUNDLE_NAME);
214
217
  console.log(chalk.gray(`Probing ${desired.length} device(s)…`));
215
218
  const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device), { withVersions, withSecrets }));
216
219
  const probes = new Map(probeList.map((p) => [p.device, p]));
@@ -218,7 +221,7 @@ async function runApply(opts) {
218
221
  let plan = diffFleet(desired, probes, {
219
222
  targetCliVersion,
220
223
  sourceAuth,
221
- secretsBundles: manifest.secrets?.bundles,
224
+ secretsBundles,
222
225
  provisionSecrets: opts.provisionSecrets === true,
223
226
  forceSecrets: opts.force === true,
224
227
  // Portable secret values only ever go to a host whose key is already pinned.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared `mint` subcommand registered on both `agents auth` (the ticket
3
+ * surface) and `agents accounts` (the credential noun). Implementation lives
4
+ * in lib/auth-mint.ts so neither command file owns the seed path.
5
+ */
6
+ import type { Command } from 'commander';
7
+ import { type MintDriveHooks } from '../lib/auth-mint.js';
8
+ export declare function registerMintCommand(parent: Command, testHooks?: MintDriveHooks): Command;