@phnx-labs/agents-cli 1.22.84 → 1.22.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +80 -53
  3. package/dist/bootstrap.d.ts +7 -6
  4. package/dist/bootstrap.js +20 -27
  5. package/dist/cli/command-registry.js +5 -1
  6. package/dist/commands/accounts.d.ts +6 -13
  7. package/dist/commands/accounts.js +330 -143
  8. package/dist/commands/apply.js +1 -1
  9. package/dist/commands/auth-mint.d.ts +7 -3
  10. package/dist/commands/auth-mint.js +21 -9
  11. package/dist/commands/auth.js +2 -3
  12. package/dist/commands/browser.js +10 -10
  13. package/dist/commands/daemon.d.ts +7 -3
  14. package/dist/commands/daemon.js +40 -25
  15. package/dist/commands/doctor.js +28 -2
  16. package/dist/commands/exec.js +87 -39
  17. package/dist/commands/fleet-capture.js +21 -2
  18. package/dist/commands/harness-wizard.js +2 -2
  19. package/dist/commands/lease.js +7 -12
  20. package/dist/commands/profiles.d.ts +2 -2
  21. package/dist/commands/profiles.js +20 -13
  22. package/dist/commands/repo.js +3 -3
  23. package/dist/commands/run-account-picker.d.ts +3 -3
  24. package/dist/commands/run-account-picker.js +29 -30
  25. package/dist/commands/secrets-passthrough.d.ts +20 -0
  26. package/dist/commands/secrets-passthrough.js +43 -0
  27. package/dist/commands/setup-accounts.d.ts +3 -2
  28. package/dist/commands/setup-accounts.js +4 -4
  29. package/dist/commands/setup-secrets.d.ts +16 -21
  30. package/dist/commands/setup-secrets.js +43 -224
  31. package/dist/commands/ssh.js +2 -2
  32. package/dist/commands/sync.js +1 -1
  33. package/dist/commands/update.d.ts +10 -0
  34. package/dist/commands/update.js +43 -40
  35. package/dist/commands/versions.d.ts +11 -11
  36. package/dist/commands/versions.js +131 -83
  37. package/dist/commands/view.d.ts +1 -9
  38. package/dist/commands/view.js +41 -55
  39. package/dist/commands/webhook.js +5 -5
  40. package/dist/commands/workflows.js +2 -1
  41. package/dist/index.d.ts +12 -12
  42. package/dist/index.js +13 -42
  43. package/dist/lib/account-capabilities.d.ts +1 -1
  44. package/dist/lib/account-capabilities.js +1 -1
  45. package/dist/lib/account-catalog.d.ts +121 -6
  46. package/dist/lib/account-catalog.js +421 -7
  47. package/dist/lib/account-registry.d.ts +8 -30
  48. package/dist/lib/account-registry.js +36 -79
  49. package/dist/lib/account-schema.d.ts +1 -1
  50. package/dist/lib/account-schema.js +1 -1
  51. package/dist/lib/accounting/account-pool-collect.js +16 -4
  52. package/dist/lib/accounting/rotate.d.ts +20 -7
  53. package/dist/lib/accounting/rotate.js +89 -13
  54. package/dist/lib/accounting/usage.js +11 -16
  55. package/dist/lib/accounts/add.d.ts +138 -0
  56. package/dist/lib/accounts/add.js +651 -0
  57. package/dist/lib/accounts/migrate.d.ts +117 -0
  58. package/dist/lib/accounts/migrate.js +536 -0
  59. package/dist/lib/accounts/slots.d.ts +13 -0
  60. package/dist/lib/accounts/slots.js +100 -0
  61. package/dist/lib/agent-spec/agents.d.ts +16 -0
  62. package/dist/lib/agent-spec/agents.js +32 -4
  63. package/dist/lib/app-bundle-install.js +5 -4
  64. package/dist/lib/auth-health.d.ts +3 -0
  65. package/dist/lib/auth-health.js +11 -0
  66. package/dist/lib/auth-mint.d.ts +52 -16
  67. package/dist/lib/auth-mint.js +117 -39
  68. package/dist/lib/browser/chrome.d.ts +1 -1
  69. package/dist/lib/browser/chrome.js +5 -5
  70. package/dist/lib/byok-usage.js +3 -3
  71. package/dist/lib/claude-account-token.d.ts +47 -3
  72. package/dist/lib/claude-account-token.js +172 -50
  73. package/dist/lib/cloud/antigravity.js +4 -4
  74. package/dist/lib/cloud/cursor.js +4 -4
  75. package/dist/lib/crabbox/cli.d.ts +1 -1
  76. package/dist/lib/crabbox/cli.js +6 -6
  77. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  78. package/dist/lib/crabbox/runtimes.js +5 -5
  79. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  80. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  81. package/dist/lib/daemon/auth-sync-service.js +28 -1
  82. package/dist/lib/daemon/daemon.d.ts +6 -16
  83. package/dist/lib/daemon/daemon.js +52 -97
  84. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  85. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  86. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  87. package/dist/lib/daemon/harness-update-service.js +1 -1
  88. package/dist/lib/daemon/runner.d.ts +15 -3
  89. package/dist/lib/daemon/runner.js +49 -24
  90. package/dist/lib/daemon-health.d.ts +1 -2
  91. package/dist/lib/daemon-health.js +7 -6
  92. package/dist/lib/daemon-services.d.ts +1 -1
  93. package/dist/lib/daemon-services.js +1 -11
  94. package/dist/lib/daemon-webhooks.d.ts +8 -7
  95. package/dist/lib/daemon-webhooks.js +10 -9
  96. package/dist/lib/device-config.js +1 -1
  97. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  98. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  99. package/dist/lib/devices/harness-inventory.js +126 -4
  100. package/dist/lib/doctor-diff.js +2 -1
  101. package/dist/lib/exec-account-home.d.ts +38 -0
  102. package/dist/lib/exec-account-home.js +164 -0
  103. package/dist/lib/exec.d.ts +30 -0
  104. package/dist/lib/exec.js +101 -31
  105. package/dist/lib/fleet/apply.d.ts +2 -2
  106. package/dist/lib/fleet/apply.js +4 -4
  107. package/dist/lib/fleet/auth-sync.js +4 -3
  108. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  109. package/dist/lib/harness/adapter.d.ts +7 -7
  110. package/dist/lib/harness/adapter.js +10 -6
  111. package/dist/lib/harness/adapters/grok.js +8 -3
  112. package/dist/lib/harness/adapters/muse.js +1 -1
  113. package/dist/lib/harness/adapters/opencode.js +12 -3
  114. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  115. package/dist/lib/harness-auth-capabilities.js +52 -0
  116. package/dist/lib/helper-versions.d.ts +6 -4
  117. package/dist/lib/helper-versions.js +5 -4
  118. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  119. package/dist/lib/hosts/credential-transport.js +37 -0
  120. package/dist/lib/hosts/dispatch.d.ts +7 -0
  121. package/dist/lib/hosts/dispatch.js +13 -6
  122. package/dist/lib/identity/client.d.ts +3 -3
  123. package/dist/lib/identity/client.js +3 -3
  124. package/dist/lib/installations/index.d.ts +1 -1
  125. package/dist/lib/installations/index.js +1 -1
  126. package/dist/lib/installations/migrate.js +9 -0
  127. package/dist/lib/installations/resolve.js +1 -1
  128. package/dist/lib/installations/shims.d.ts +15 -0
  129. package/dist/lib/installations/shims.js +69 -0
  130. package/dist/lib/installations/store.d.ts +43 -0
  131. package/dist/lib/installations/store.js +75 -7
  132. package/dist/lib/installations/versions.js +1 -1
  133. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  134. package/dist/lib/menubar/install-menubar.js +3 -3
  135. package/dist/lib/native-accounts.d.ts +35 -0
  136. package/dist/lib/native-accounts.js +41 -0
  137. package/dist/lib/net-close.d.ts +11 -0
  138. package/dist/lib/net-close.js +25 -0
  139. package/dist/lib/openclaw-keychain.js +27 -1
  140. package/dist/lib/profiles.js +22 -9
  141. package/dist/lib/project-resources.js +1 -1
  142. package/dist/lib/reserved-stores.d.ts +53 -0
  143. package/dist/lib/reserved-stores.js +120 -0
  144. package/dist/lib/secrets-client.d.ts +191 -0
  145. package/dist/lib/secrets-client.js +710 -0
  146. package/dist/lib/secrets-policy.d.ts +222 -0
  147. package/dist/lib/secrets-policy.js +483 -0
  148. package/dist/lib/secrets-types.d.ts +188 -0
  149. package/dist/lib/secrets-types.js +24 -0
  150. package/dist/lib/service-manifest.js +3 -4
  151. package/dist/lib/session/db.d.ts +18 -0
  152. package/dist/lib/session/db.js +47 -14
  153. package/dist/lib/session/sync/config.js +2 -2
  154. package/dist/lib/sha256-asset.d.ts +11 -16
  155. package/dist/lib/sha256-asset.js +11 -16
  156. package/dist/lib/share/config.js +14 -5
  157. package/dist/lib/signin-badge.d.ts +26 -0
  158. package/dist/lib/signin-badge.js +42 -0
  159. package/dist/lib/staleness/detectors/workflows.js +3 -105
  160. package/dist/lib/staleness/writers/workflows.js +2 -1
  161. package/dist/lib/state.d.ts +7 -4
  162. package/dist/lib/state.js +18 -7
  163. package/dist/lib/sync-umbrella.js +2 -2
  164. package/dist/lib/teams/agents.js +1 -1
  165. package/dist/lib/types.d.ts +74 -33
  166. package/dist/lib/view-types.d.ts +6 -3
  167. package/dist/lib/workflows-registry.d.ts +71 -0
  168. package/dist/lib/workflows-registry.js +280 -0
  169. package/dist/lib/workflows.d.ts +21 -21
  170. package/dist/lib/workflows.js +12 -327
  171. package/package.json +2 -3
  172. package/scripts/postinstall.js +4 -29
  173. package/dist/commands/secrets-import.d.ts +0 -18
  174. package/dist/commands/secrets-import.js +0 -74
  175. package/dist/commands/secrets-migrate.d.ts +0 -25
  176. package/dist/commands/secrets-migrate.js +0 -334
  177. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  178. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  179. package/dist/commands/secrets-sync.d.ts +0 -11
  180. package/dist/commands/secrets-sync.js +0 -153
  181. package/dist/commands/secrets-vault.d.ts +0 -10
  182. package/dist/commands/secrets-vault.js +0 -130
  183. package/dist/commands/secrets.d.ts +0 -212
  184. package/dist/commands/secrets.js +0 -3030
  185. package/dist/lib/accounts/connect.d.ts +0 -176
  186. package/dist/lib/accounts/connect.js +0 -453
  187. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  188. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  189. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  190. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  191. package/dist/lib/secrets/agent.d.ts +0 -358
  192. package/dist/lib/secrets/agent.js +0 -1291
  193. package/dist/lib/secrets/audit.d.ts +0 -46
  194. package/dist/lib/secrets/audit.js +0 -101
  195. package/dist/lib/secrets/bundles.d.ts +0 -290
  196. package/dist/lib/secrets/bundles.js +0 -1547
  197. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  198. package/dist/lib/secrets/download-keychain.js +0 -70
  199. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  200. package/dist/lib/secrets/drivers/rush.js +0 -90
  201. package/dist/lib/secrets/fallback.d.ts +0 -48
  202. package/dist/lib/secrets/fallback.js +0 -48
  203. package/dist/lib/secrets/filestore.d.ts +0 -222
  204. package/dist/lib/secrets/filestore.js +0 -1099
  205. package/dist/lib/secrets/headless.d.ts +0 -43
  206. package/dist/lib/secrets/headless.js +0 -56
  207. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  208. package/dist/lib/secrets/icloud-import.js +0 -206
  209. package/dist/lib/secrets/index.d.ts +0 -451
  210. package/dist/lib/secrets/index.js +0 -1568
  211. package/dist/lib/secrets/install-helper.d.ts +0 -72
  212. package/dist/lib/secrets/install-helper.js +0 -245
  213. package/dist/lib/secrets/lease.d.ts +0 -25
  214. package/dist/lib/secrets/lease.js +0 -44
  215. package/dist/lib/secrets/linux.d.ts +0 -77
  216. package/dist/lib/secrets/linux.js +0 -393
  217. package/dist/lib/secrets/list-filter.d.ts +0 -109
  218. package/dist/lib/secrets/list-filter.js +0 -261
  219. package/dist/lib/secrets/mcp.d.ts +0 -93
  220. package/dist/lib/secrets/mcp.js +0 -211
  221. package/dist/lib/secrets/profiles.d.ts +0 -10
  222. package/dist/lib/secrets/profiles.js +0 -13
  223. package/dist/lib/secrets/push.d.ts +0 -133
  224. package/dist/lib/secrets/push.js +0 -273
  225. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  226. package/dist/lib/secrets/rc-hygiene.js +0 -143
  227. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  228. package/dist/lib/secrets/read-backoff.js +0 -64
  229. package/dist/lib/secrets/reaper.d.ts +0 -90
  230. package/dist/lib/secrets/reaper.js +0 -243
  231. package/dist/lib/secrets/remote.d.ts +0 -152
  232. package/dist/lib/secrets/remote.js +0 -344
  233. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  234. package/dist/lib/secrets/reserved-sync.js +0 -147
  235. package/dist/lib/secrets/scope.d.ts +0 -26
  236. package/dist/lib/secrets/scope.js +0 -29
  237. package/dist/lib/secrets/session-store.d.ts +0 -107
  238. package/dist/lib/secrets/session-store.js +0 -342
  239. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  240. package/dist/lib/secrets/sync-backend.js +0 -13
  241. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  242. package/dist/lib/secrets/sync-commands.js +0 -21
  243. package/dist/lib/secrets/sync.d.ts +0 -48
  244. package/dist/lib/secrets/sync.js +0 -237
  245. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  246. package/dist/lib/secrets/unlock-hints.js +0 -36
  247. package/dist/lib/secrets/usage-db.d.ts +0 -46
  248. package/dist/lib/secrets/usage-db.js +0 -96
  249. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  250. package/dist/lib/secrets/vault-age-helper.js +0 -34
  251. package/dist/lib/secrets/vault.d.ts +0 -49
  252. package/dist/lib/secrets/vault.js +0 -399
  253. package/dist/lib/secrets/windows.d.ts +0 -81
  254. package/dist/lib/secrets/windows.js +0 -556
  255. package/scripts/install-helper.js +0 -97
  256. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  257. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -138,9 +138,10 @@ export const FLEET_LOGIN_FLOWS = {
138
138
  // claude: interactive `/login` inside the TUI is a loopback browser flow, and
139
139
  // the token is keychain-bound on macOS. That native login stays per-box and
140
140
  // is never copied. The shareable `claude setup-token` mint is a different
141
- // credential class — wired as `agents accounts mint claude` / `agents auth mint`
142
- // (`lib/auth-mint.ts`), not as a fleet-login device-code flow (a setup-token
143
- // is account-scoped, not per-machine).
141
+ // credential class — minted by `agents accounts add claude [name]` /
142
+ // `agents accounts login claude#<name>` (`lib/auth-mint.ts`), not as a
143
+ // fleet-login device-code flow (a setup-token is account-scoped, not
144
+ // per-machine).
144
145
  claude: {
145
146
  loginCommand: 'claude',
146
147
  flowType: 'loopback',
@@ -267,15 +267,12 @@ async function performFleetSharedRepoSync(root, device, timeoutMs) {
267
267
  }
268
268
  const ownedFile = fleetSharedStatePath(device, root);
269
269
  const relativeOwnedFile = path.relative(root, ownedFile).split(path.sep).join('/');
270
- // The central `agents.yaml` is fleet-shared state too — `agents accounts label`
271
- // writes version-scoped native account rows there (lib/state.ts writeMetaUnlocked),
272
- // as a plain file write with no commit. Publishing only the per-device file and
273
- // then `rebase --autostash`-ing over a dirty central file silently destroyed
274
- // those labels: every box lost its account labels on its next daemon publish
275
- // (PHNX-3887). Commit it alongside the device doc so the rebase carries the
276
- // rows instead of stashing them, which is also what `accounts label --help`
277
- // already promises ("labels live on the central account rows in agents.yaml,
278
- // which repo push/pull already syncs fleet-wide").
270
+ // The central `agents.yaml` is fleet-shared state too — native account rows
271
+ // land there (lib/state.ts writeMetaUnlocked) as a plain file write with no
272
+ // commit. Publishing only the per-device file and then `rebase --autostash`-ing
273
+ // over a dirty central file silently destroyed those names: every box lost its
274
+ // account names on its next daemon publish (PHNX-3887). Commit it alongside the
275
+ // device doc so the rebase carries the rows instead of stashing them.
279
276
  const centralFile = path.join(root, 'agents.yaml');
280
277
  const publishPaths = [relativeOwnedFile];
281
278
  if (fs.existsSync(centralFile))
@@ -150,16 +150,16 @@ export interface HarnessAdapter {
150
150
  routineModeArgs?(cmd: string[], ctx: RoutineLaunchCtx): void;
151
151
  }
152
152
  /**
153
- * The four config-dir env keys a harness pins to its version home. Every
154
- * per-harness branch in the old buildExecEnv deleted the ones it does NOT set,
155
- * so its own config pointer never leaks into a foreign harness's invocation.
156
- * (XDG_CONFIG_HOME / XDG_DATA_HOME — Muse/Cursor — are deliberately absent: the
157
- * old code sets them but never strips them, and this preserves that exactly.)
153
+ * Config-dir env keys a harness pins to its slot / version home. Every
154
+ * per-harness branch in buildExecEnv deletes the ones it does NOT set, so a
155
+ * slot never inherits another account's dir (PHNX-3940 T5). GROK_HOME,
156
+ * OPENCODE_CONFIG_DIR, and the XDG pair were previously omitted, which let a
157
+ * parent agent's pin leak into a sibling slot.
158
158
  */
159
- export declare const CONFIG_DIR_ENV_KEYS: readonly ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME", "KIMI_CODE_HOME"];
159
+ export declare const CONFIG_DIR_ENV_KEYS: readonly ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME", "KIMI_CODE_HOME", "GROK_HOME", "OPENCODE_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME"];
160
160
  /**
161
161
  * Strip every config-dir env key except the one(s) this harness sets. `keep=[]`
162
- * (the default / no-config-dir harness) deletes all four — the old `else` arm.
162
+ * (the default / no-config-dir harness) deletes all of them — the old `else` arm.
163
163
  */
164
164
  export declare function stripForeignConfigDir(result: NodeJS.ProcessEnv, keep?: readonly string[]): void;
165
165
  export declare function registerHarnessAdapter(adapter: HarnessAdapter): void;
@@ -1,19 +1,23 @@
1
1
  /**
2
- * The four config-dir env keys a harness pins to its version home. Every
3
- * per-harness branch in the old buildExecEnv deleted the ones it does NOT set,
4
- * so its own config pointer never leaks into a foreign harness's invocation.
5
- * (XDG_CONFIG_HOME / XDG_DATA_HOME — Muse/Cursor — are deliberately absent: the
6
- * old code sets them but never strips them, and this preserves that exactly.)
2
+ * Config-dir env keys a harness pins to its slot / version home. Every
3
+ * per-harness branch in buildExecEnv deletes the ones it does NOT set, so a
4
+ * slot never inherits another account's dir (PHNX-3940 T5). GROK_HOME,
5
+ * OPENCODE_CONFIG_DIR, and the XDG pair were previously omitted, which let a
6
+ * parent agent's pin leak into a sibling slot.
7
7
  */
8
8
  export const CONFIG_DIR_ENV_KEYS = [
9
9
  'CLAUDE_CONFIG_DIR',
10
10
  'CODEX_HOME',
11
11
  'COPILOT_HOME',
12
12
  'KIMI_CODE_HOME',
13
+ 'GROK_HOME',
14
+ 'OPENCODE_CONFIG_DIR',
15
+ 'XDG_CONFIG_HOME',
16
+ 'XDG_DATA_HOME',
13
17
  ];
14
18
  /**
15
19
  * Strip every config-dir env key except the one(s) this harness sets. `keep=[]`
16
- * (the default / no-config-dir harness) deletes all four — the old `else` arm.
20
+ * (the default / no-config-dir harness) deletes all of them — the old `else` arm.
17
21
  */
18
22
  export function stripForeignConfigDir(result, keep = []) {
19
23
  for (const key of CONFIG_DIR_ENV_KEYS) {
@@ -1,8 +1,13 @@
1
- // Grok has no exec-time config-dir env pin (buildExecEnv's `else` strips the
2
- // foreign keys — the registry fallback preserves that). It DOES relocate its
3
- // whole config tree via GROK_HOME in the generated shim.
1
+ import * as path from 'path';
2
+ import { stripForeignConfigDir } from '../adapter.js';
4
3
  export const grokAdapter = {
5
4
  id: 'grok',
5
+ applyExecConfigEnv(result, ctx) {
6
+ if (ctx.versionHome) {
7
+ result.GROK_HOME = path.join(ctx.versionHome, '.grok');
8
+ }
9
+ stripForeignConfigDir(result, ['GROK_HOME']);
10
+ },
6
11
  shimConfigEnvBash() {
7
12
  return `
8
13
  # Grok Build uses GROK_HOME to isolate its entire configuration tree
@@ -13,7 +13,7 @@ export const museAdapter = {
13
13
  result.XDG_CONFIG_HOME = path.join(ctx.versionHome, '.config');
14
14
  result.XDG_DATA_HOME = path.join(ctx.versionHome, '.local', 'share');
15
15
  }
16
- stripForeignConfigDir(result);
16
+ stripForeignConfigDir(result, ['XDG_CONFIG_HOME', 'XDG_DATA_HOME']);
17
17
  },
18
18
  shimConfigEnvBash() {
19
19
  return `
@@ -1,8 +1,17 @@
1
- // OpenCode has no exec-time config-dir env pin (buildExecEnv's `else` strips the
2
- // foreign keys — the registry fallback preserves that). It reads config-directory
3
- // resources from OPENCODE_CONFIG_DIR, pinned in the generated shim.
1
+ import * as path from 'path';
2
+ import { stripForeignConfigDir } from '../adapter.js';
3
+ // OpenCode reads plugins/agents/commands from OPENCODE_CONFIG_DIR and auth from
4
+ // $XDG_DATA_HOME/opencode (HARNESS_AUTH.slotEnv). Pin both at the slot / version
5
+ // home so two accounts in one install never share a credential (PHNX-3940 T5).
4
6
  export const opencodeAdapter = {
5
7
  id: 'opencode',
8
+ applyExecConfigEnv(result, ctx) {
9
+ if (ctx.versionHome) {
10
+ result.OPENCODE_CONFIG_DIR = path.join(ctx.versionHome, '.config', 'opencode');
11
+ result.XDG_DATA_HOME = path.join(ctx.versionHome, '.local', 'share');
12
+ }
13
+ stripForeignConfigDir(result, ['OPENCODE_CONFIG_DIR', 'XDG_DATA_HOME']);
14
+ },
6
15
  shimConfigEnvBash() {
7
16
  return `
8
17
  # OpenCode reads plugins, agents, commands, and other config-directory
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Per-harness auth capability table (PHNX-3940).
3
+ *
4
+ * One row per `ALL_AGENT_IDS` entry. Governs `accounts add` / connect login
5
+ * argv, identity strength, the durable worker credential (if any), and the
6
+ * config-dir env that pins a slot. Completeness is pinned by the test against
7
+ * `ALL_AGENT_IDS` — a new harness without a row is a type error and a failing
8
+ * test, never a silent skip.
9
+ *
10
+ * Values are taken from evidence-harness-auth.md (2026-09-06) and the adapters
11
+ * under `lib/harness/adapters/`. `LOGIN_INVOCATIONS` is the connect-era subset
12
+ * (claude, codex) derived from this table so existing connect callers keep
13
+ * working until T4 folds connect into `accounts add`.
14
+ */
15
+ import type { AgentId } from './types.js';
16
+ export type HarnessIdentityKind = 'strong' | 'email' | 'opaque';
17
+ /**
18
+ * Durable worker credential, or `none` when the harness must log in per box.
19
+ * Codex is both: an API key (portable, bills the API) OR a per-device
20
+ * ChatGPT-plan device-auth login (the plan seat; never stored in the reserved
21
+ * store because it is a rotating session).
22
+ */
23
+ export type HarnessWorkerKind = 'setup-token' | 'none' | `api-key:${string}` | `per-device${'' | `:${string}`}`;
24
+ export type HarnessWorker = HarnessWorkerKind | HarnessWorkerKind[];
25
+ export interface HarnessAuthCapability {
26
+ /** argv after the harness binary to start native login, or null when there is no finite login command. */
27
+ login: string[] | null;
28
+ /** argv to probe login status, or null when the CLI has no status command. */
29
+ status: string[] | null;
30
+ identity: HarnessIdentityKind;
31
+ worker: HarnessWorker;
32
+ /** Config-dir env that pins a slot, or null when isolation is HOME-swap only. */
33
+ slotEnv: string | null;
34
+ }
35
+ export declare const HARNESS_AUTH: Record<AgentId, HarnessAuthCapability>;
36
+ export declare function harnessAuth(agent: AgentId): HarnessAuthCapability;
37
+ /** Worker kinds as a flat list (codex's dual path is two entries). */
38
+ export declare function harnessWorkerKinds(agent: AgentId): HarnessWorkerKind[];
39
+ /**
40
+ * True when this harness has no portable worker credential and must log in
41
+ * per box. `worker: 'none'` (kimi, antigravity, …) and a sole `per-device…`
42
+ * kind both count; a dual path like Codex (API key OR device-auth) does not.
43
+ */
44
+ export declare function harnessWorkerIsPerDevice(agent: AgentId): boolean;
45
+ /**
46
+ * Native-login invocation per harness. Only harnesses with a REAL, finite login
47
+ * COMMAND that connect currently drives — connect fails clearly for anything
48
+ * else rather than faking a flow that never signs the user in. Verified against
49
+ * the installed CLIs (PHNX-3940): `claude auth login --help` → "Sign in to your
50
+ * Anthropic account" with `--email`; `codex login` drives the OAuth flow.
51
+ * Args are the `HARNESS_AUTH.login` values for the same ids.
52
+ */
53
+ export interface LoginInvocation {
54
+ /** argv passed to the installed binary to start the native login. */
55
+ args: string[];
56
+ /** Flag that pre-fills the login email (appended as `[emailFlag, email]`), when supported. */
57
+ emailFlag?: string;
58
+ /** One-line hint shown before the login flow takes over. */
59
+ hint?: string;
60
+ }
61
+ export declare const LOGIN_INVOCATIONS: Partial<Record<AgentId, LoginInvocation>>;
@@ -0,0 +1,52 @@
1
+ export const HARNESS_AUTH = {
2
+ claude: { login: ['auth', 'login'], status: ['auth', 'status'], identity: 'strong', worker: 'setup-token', slotEnv: 'CLAUDE_CONFIG_DIR' },
3
+ codex: { login: ['login'], status: ['login', 'status'], identity: 'strong', worker: ['api-key:OPENAI_API_KEY', 'per-device:device-auth'], slotEnv: 'CODEX_HOME' },
4
+ grok: { login: ['login'], status: null, identity: 'strong', worker: 'api-key:XAI_API_KEY', slotEnv: 'GROK_HOME' },
5
+ // auth.json has no email claim; identity is the sorted provider-id join
6
+ // (`resolveOpenCodeAccountId`). NATIVE_ACCOUNT_CAPABILITIES.opencode.inspection
7
+ // is already 'opaque'.
8
+ opencode: { login: ['auth', 'login'], status: ['auth', 'list'], identity: 'opaque', worker: 'api-key:provider', slotEnv: 'XDG_DATA_HOME' },
9
+ cursor: { login: ['login'], status: ['status'], identity: 'strong', worker: 'api-key:CURSOR_API_KEY', slotEnv: null },
10
+ // No finite login argv — launch bare `kimi`, then `/login` in the TUI
11
+ // (`loginHint('kimi') === 'kimi'`, fleet/auth-sync `loginCommand: 'kimi'`).
12
+ kimi: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: 'KIMI_CODE_HOME' },
13
+ antigravity: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
14
+ droid: { login: null, status: null, identity: 'opaque', worker: 'api-key:FACTORY_API_KEY', slotEnv: null },
15
+ gemini: { login: null, status: null, identity: 'email', worker: 'none', slotEnv: null },
16
+ copilot: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: 'COPILOT_HOME' },
17
+ openclaw: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
18
+ amp: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
19
+ goose: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
20
+ hermes: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
21
+ // Auth lives under $XDG_CONFIG_HOME/muse (adapter pins XDG_CONFIG_HOME +
22
+ // XDG_DATA_HOME). slotEnv names the config pin; T5 must not HOME-swap.
23
+ muse: { login: null, status: null, identity: 'email', worker: 'none', slotEnv: 'XDG_CONFIG_HOME' },
24
+ warp: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
25
+ };
26
+ export function harnessAuth(agent) {
27
+ const cap = HARNESS_AUTH[agent];
28
+ if (!cap)
29
+ throw new Error(`No harness-auth capability for '${agent}'.`);
30
+ return cap;
31
+ }
32
+ /** Worker kinds as a flat list (codex's dual path is two entries). */
33
+ export function harnessWorkerKinds(agent) {
34
+ const worker = harnessAuth(agent).worker;
35
+ return Array.isArray(worker) ? worker : [worker];
36
+ }
37
+ /**
38
+ * True when this harness has no portable worker credential and must log in
39
+ * per box. `worker: 'none'` (kimi, antigravity, …) and a sole `per-device…`
40
+ * kind both count; a dual path like Codex (API key OR device-auth) does not.
41
+ */
42
+ export function harnessWorkerIsPerDevice(agent) {
43
+ return harnessWorkerKinds(agent).every((kind) => kind === 'none' || kind.startsWith('per-device'));
44
+ }
45
+ export const LOGIN_INVOCATIONS = {
46
+ claude: {
47
+ args: HARNESS_AUTH.claude.login,
48
+ emailFlag: '--email',
49
+ hint: 'Complete the Claude sign-in in your browser.',
50
+ },
51
+ codex: { args: HARNESS_AUTH.codex.login },
52
+ };
@@ -27,7 +27,7 @@
27
27
  * never be derived from `getCliVersion()`.
28
28
  */
29
29
  /** The helpers that have their own release train. */
30
- export type HelperName = 'menubar' | 'keychain' | 'computer-mac' | 'computer-win';
30
+ export type HelperName = 'menubar' | 'computer-mac' | 'computer-win';
31
31
  /** One helper's release identity. */
32
32
  export interface HelperRelease {
33
33
  /** Tag prefix — the release is `<tagPrefix>/v<version>`. */
@@ -41,9 +41,11 @@ export interface HelperRelease {
41
41
  /**
42
42
  * Floors, by helper.
43
43
  *
44
- * `menubar` and `keychain` start at 1.0.0 — the first builds published under
45
- * their own tags rather than the CLI's. They are not "version 1 of the helper";
46
- * they are version 1 of its independent release train.
44
+ * `menubar` starts at 1.0.0 — the first build published under its own tag
45
+ * rather than the CLI's. It is not "version 1 of the helper"; it is version 1
46
+ * of its independent release train. The keychain helper moved with the
47
+ * standalone `secrets` engine (PHNX-3989) — it downloads and verifies its own
48
+ * helper release now, off this table entirely.
47
49
  */
48
50
  export declare const HELPER_RELEASES: Readonly<Record<HelperName, HelperRelease>>;
49
51
  /** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
@@ -29,13 +29,14 @@
29
29
  /**
30
30
  * Floors, by helper.
31
31
  *
32
- * `menubar` and `keychain` start at 1.0.0 — the first builds published under
33
- * their own tags rather than the CLI's. They are not "version 1 of the helper";
34
- * they are version 1 of its independent release train.
32
+ * `menubar` starts at 1.0.0 — the first build published under its own tag
33
+ * rather than the CLI's. It is not "version 1 of the helper"; it is version 1
34
+ * of its independent release train. The keychain helper moved with the
35
+ * standalone `secrets` engine (PHNX-3989) — it downloads and verifies its own
36
+ * helper release now, off this table entirely.
35
37
  */
36
38
  export const HELPER_RELEASES = {
37
39
  menubar: { tagPrefix: 'menubar', floor: '1.0.0' },
38
- keychain: { tagPrefix: 'keychain', floor: '1.0.0' },
39
40
  'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
40
41
  // The Windows helper is a bare .exe, not an .app bundle, so it does not share
41
42
  // helper-download.ts's zip/codesign/notarize machinery -- but it has the same
@@ -0,0 +1,10 @@
1
+ /** The host part of an ssh target (`user@host` -> `host`) for known_hosts matching. */
2
+ export declare function hostKeyLookupName(target: string): string;
3
+ /** Refuse durable credential transfer until the destination SSH key is pinned. */
4
+ export declare function assertCredentialTransportHostPinned(target: string, pinned?: boolean): void;
5
+ /**
6
+ * Resolve a `--device` value to an ssh target string for a credential-carrying
7
+ * remote operation. Delegates to the same resolver `run --device` uses; on a
8
+ * miss, treats the value as a raw ssh target and validates it.
9
+ */
10
+ export declare function resolveHostSshTarget(nameOrAlias: string): Promise<string>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Credential transport policy for a fleet host (agents-owned, PHNX-3989).
3
+ *
4
+ * Whether a durable credential may leave this device for a given host is
5
+ * agents fleet policy, not a secrets-engine concern: it hangs off the managed
6
+ * `known_hosts` pin and the hosts/devices registry. The standalone `secrets`
7
+ * CLI only carries the bundle once agents-cli has decided the destination is
8
+ * trustworthy, so these guards live here and are passed INTO the transport,
9
+ * never re-implemented on the engine side.
10
+ */
11
+ import { assertValidSshTarget } from '../ssh-exec.js';
12
+ import { resolveHost } from './registry.js';
13
+ import { sshTargetFor } from './types.js';
14
+ import { isHostPinned } from '../devices/known-hosts.js';
15
+ /** The host part of an ssh target (`user@host` -> `host`) for known_hosts matching. */
16
+ export function hostKeyLookupName(target) {
17
+ return target.split('@').pop() ?? target;
18
+ }
19
+ /** Refuse durable credential transfer until the destination SSH key is pinned. */
20
+ export function assertCredentialTransportHostPinned(target, pinned = isHostPinned(hostKeyLookupName(target))) {
21
+ if (pinned)
22
+ return;
23
+ throw new Error(`Refusing to transfer provider credentials to '${target}' before its SSH host key is pinned. ` +
24
+ `Connect once with 'agents ssh ${target}' and verify the host, then retry.`);
25
+ }
26
+ /**
27
+ * Resolve a `--device` value to an ssh target string for a credential-carrying
28
+ * remote operation. Delegates to the same resolver `run --device` uses; on a
29
+ * miss, treats the value as a raw ssh target and validates it.
30
+ */
31
+ export async function resolveHostSshTarget(nameOrAlias) {
32
+ const host = await resolveHost(nameOrAlias);
33
+ if (host)
34
+ return sshTargetFor(host);
35
+ assertValidSshTarget(nameOrAlias);
36
+ return nameOrAlias;
37
+ }
@@ -170,6 +170,13 @@ export interface DispatchOptions {
170
170
  * (remote-cmd.ts) — keep the two in lockstep; run-forwarding.test.ts asserts
171
171
  * the table side.
172
172
  */
173
+ /** Compose `agent[@version][#account]` so the peer resolves ITS slot (PHNX-3940 T5). */
174
+ export declare function runAgentSpecArg(opts: {
175
+ agent: string;
176
+ version?: string;
177
+ account?: string;
178
+ accountPicker?: boolean;
179
+ }): string;
173
180
  export declare function buildRunForwardedArgs(opts: DispatchOptions): string[];
174
181
  export interface InteractiveDispatchOptions {
175
182
  agent: string;
@@ -359,8 +359,19 @@ async function launchDetached(host, target, opts) {
359
359
  * (remote-cmd.ts) — keep the two in lockstep; run-forwarding.test.ts asserts
360
360
  * the table side.
361
361
  */
362
+ /** Compose `agent[@version][#account]` so the peer resolves ITS slot (PHNX-3940 T5). */
363
+ export function runAgentSpecArg(opts) {
364
+ if (opts.accountPicker)
365
+ return `${opts.agent}@`;
366
+ let spec = opts.agent;
367
+ if (opts.version)
368
+ spec += `@${opts.version}`;
369
+ if (opts.account)
370
+ spec += `#${opts.account}`;
371
+ return spec;
372
+ }
362
373
  export function buildRunForwardedArgs(opts) {
363
- const agentArg = opts.version ? `${opts.agent}@${opts.version}` : opts.agent;
374
+ const agentArg = runAgentSpecArg(opts);
364
375
  const args = ['run', agentArg, opts.prompt, '--quiet'];
365
376
  if (opts.mode)
366
377
  args.push('--mode', opts.mode);
@@ -427,11 +438,7 @@ export function buildInteractiveRunForwardedArgs(opts) {
427
438
  if (opts.version && opts.accountPicker) {
428
439
  throw new Error('Interactive host dispatch cannot combine an account picker with a version pin');
429
440
  }
430
- const agentArg = opts.accountPicker
431
- ? `${opts.agent}@`
432
- : opts.version
433
- ? `${opts.agent}@${opts.version}`
434
- : opts.agent;
441
+ const agentArg = runAgentSpecArg(opts);
435
442
  const args = ['run', agentArg];
436
443
  if (opts.prompt && opts.forceInteractive)
437
444
  args.push(opts.prompt);
@@ -10,9 +10,9 @@
10
10
  * here and nothing else.
11
11
  *
12
12
  * The shape mirrors the seams this repo already proved elsewhere —
13
- * `SyncBackend` (`lib/secrets/sync-backend.ts`) and `CloudProvider`
14
- * (`lib/cloud/types.ts`) — so a second identity backend, if one is ever
15
- * needed, is a swap here rather than a sweep across commands.
13
+ * the bounded secrets process client (`lib/secrets-client.ts`) and
14
+ * `CloudProvider` (`lib/cloud/types.ts`) — so a second identity backend, if
15
+ * one is ever needed, is a swap here rather than a sweep across commands.
16
16
  */
17
17
  /**
18
18
  * Where the account backend lives. Config, never a literal at a call site.
@@ -10,9 +10,9 @@
10
10
  * here and nothing else.
11
11
  *
12
12
  * The shape mirrors the seams this repo already proved elsewhere —
13
- * `SyncBackend` (`lib/secrets/sync-backend.ts`) and `CloudProvider`
14
- * (`lib/cloud/types.ts`) — so a second identity backend, if one is ever
15
- * needed, is a swap here rather than a sweep across commands.
13
+ * the bounded secrets process client (`lib/secrets-client.ts`) and
14
+ * `CloudProvider` (`lib/cloud/types.ts`) — so a second identity backend, if
15
+ * one is ever needed, is a swap here rather than a sweep across commands.
16
16
  */
17
17
  import * as fs from 'fs';
18
18
  import * as path from 'path';
@@ -8,7 +8,7 @@
8
8
  * per-installation isolation track consumes exactly this).
9
9
  */
10
10
  export { type Installation, type InstallationRelease, type UpdateOutcome, type UpdateStrategyId, type UpdatePolicy, } from './types.js';
11
- export { createInstallation, listInstallations, recordRelease, } from './store.js';
11
+ export { createInstallation, listInstallations, recordRelease, MANAGED_INSTALLATION_LABEL, resolveManagedInstallation, ensureHarnessInstallation, type EnsureHarnessInstallationResult, } from './store.js';
12
12
  export { describeInstallation, resolveInstallation, type ResolveInstallationOptions, } from './resolve.js';
13
13
  export { selectUpdateStrategy, supportsPinnedUpdate, type UpdateStrategy, } from './strategies.js';
14
14
  export { updateInstallation, type UpdateInstallationOptions } from './update.js';
@@ -7,7 +7,7 @@
7
7
  * it, so the internal split can change without breaking callers (the Cursor
8
8
  * per-installation isolation track consumes exactly this).
9
9
  */
10
- export { createInstallation, listInstallations, recordRelease, } from './store.js';
10
+ export { createInstallation, listInstallations, recordRelease, MANAGED_INSTALLATION_LABEL, resolveManagedInstallation, ensureHarnessInstallation, } from './store.js';
11
11
  export { describeInstallation, resolveInstallation, } from './resolve.js';
12
12
  export { selectUpdateStrategy, supportsPinnedUpdate, } from './strategies.js';
13
13
  export { updateInstallation } from './update.js';
@@ -2366,4 +2366,13 @@ export async function runMigration() {
2366
2366
  catch {
2367
2367
  // best-effort — a migration must never fail because tmux wasn't reachable
2368
2368
  }
2369
+ // PHNX-3940 T7: report leftover per-account installations. Dry-run only —
2370
+ // moving homes requires an explicit `agents accounts migrate --apply`.
2371
+ try {
2372
+ const { reportAccountSlotMigrationOnUpgrade } = await import('../accounts/migrate.js');
2373
+ await reportAccountSlotMigrationOnUpgrade();
2374
+ }
2375
+ catch {
2376
+ // best-effort — a corrupt install must not block the rest of upgrade
2377
+ }
2369
2378
  }
@@ -21,7 +21,7 @@ export class InstallationNotFoundError extends Error {
21
21
  // Nothing installed is a different problem from "your selector missed", and
22
22
  // the remedy differs — say which one it is rather than printing an empty list.
23
23
  super(available.length === 0
24
- ? `No ${AGENTS[agent].name} installations are managed by agents-cli. Install one with: agents add ${agent}@latest`
24
+ ? `No ${AGENTS[agent].name} installations are managed by agents-cli. Install one with: agents add ${agent}`
25
25
  : `No ${AGENTS[agent].name} installation matches '${selector}'. Installed: ${available.map((i) => describeInstallation(i)).join(', ')}`);
26
26
  this.agent = agent;
27
27
  this.selector = selector;
@@ -201,6 +201,21 @@ export declare const CONFIG_ENV_ISOLATED_AGENTS: readonly AgentId[];
201
201
  * separate from the user's real `~/.<agent>`. See {@link CONFIG_ENV_ISOLATED_AGENTS}.
202
202
  */
203
203
  export declare function supportsIsolatedInstall(agent: AgentId): boolean;
204
+ /**
205
+ * Harnesses that isolate by symlink-adopting `~/.<config>` rather than a
206
+ * config-dir env (PHNX-3940 T5). One active slot per device; `accounts default`
207
+ * repoints the adopted symlink under the auth-op lock.
208
+ */
209
+ export declare function isSymlinkAdoptedHarness(agent: AgentId): boolean;
210
+ /**
211
+ * Repoint this harness's adopted `~/.<config>` symlink at `home`'s config dir.
212
+ * No-op for env-isolated harnesses. Fails loud when the adopted path is a real
213
+ * directory rather than a symlink — adopting that is `agents use`, not default.
214
+ */
215
+ export declare function repointAdoptedConfigToHome(agent: AgentId, home: string): {
216
+ success: boolean;
217
+ error?: string;
218
+ };
204
219
  /**
205
220
  * Generate a versioned alias script that directly execs a specific version.
206
221
  * e.g., claude@2.0.65 -> directly runs that version's binary
@@ -10,6 +10,7 @@ import { IS_WINDOWS, prependToWindowsUserPath } from '../platform/index.js';
10
10
  import { getShimsDir, getVersionsDir, getBackupsDir, getHistoryDir, ensureAgentsDir } from '../state.js';
11
11
  export { getShimsDir };
12
12
  import { AGENTS, agentConfigDirName, readAuthAccountIdentity } from '../agents.js';
13
+ import { acquireAuthOperationLock } from '../accounts/auth-operation-lock.js';
13
14
  import { codexHomeShimBash } from '../codex-home.js';
14
15
  import { resolveHarnessAdapter } from '../harness/index.js';
15
16
  import { randomUUID } from 'node:crypto';
@@ -1130,6 +1131,74 @@ export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'cursor
1130
1131
  export function supportsIsolatedInstall(agent) {
1131
1132
  return CONFIG_ENV_ISOLATED_AGENTS.includes(agent);
1132
1133
  }
1134
+ /**
1135
+ * Harnesses that isolate by symlink-adopting `~/.<config>` rather than a
1136
+ * config-dir env (PHNX-3940 T5). One active slot per device; `accounts default`
1137
+ * repoints the adopted symlink under the auth-op lock.
1138
+ */
1139
+ export function isSymlinkAdoptedHarness(agent) {
1140
+ return !CONFIG_ENV_ISOLATED_AGENTS.includes(agent);
1141
+ }
1142
+ /**
1143
+ * Repoint this harness's adopted `~/.<config>` symlink at `home`'s config dir.
1144
+ * No-op for env-isolated harnesses. Fails loud when the adopted path is a real
1145
+ * directory rather than a symlink — adopting that is `agents use`, not default.
1146
+ */
1147
+ export function repointAdoptedConfigToHome(agent, home) {
1148
+ if (!isSymlinkAdoptedHarness(agent))
1149
+ return { success: true };
1150
+ const lock = acquireAuthOperationLock(agent);
1151
+ try {
1152
+ lock.assertHeld();
1153
+ const configPath = getAgentConfigPath(agent);
1154
+ const configDirName = path.relative(os.homedir(), AGENTS[agent].configDir);
1155
+ const target = path.join(home, configDirName);
1156
+ fs.mkdirSync(target, { recursive: true });
1157
+ try {
1158
+ const stat = fs.lstatSync(configPath);
1159
+ if (stat.isSymbolicLink()) {
1160
+ const current = path.resolve(path.dirname(configPath), fs.readlinkSync(configPath));
1161
+ if (current === path.resolve(target))
1162
+ return { success: true };
1163
+ }
1164
+ else {
1165
+ return {
1166
+ success: false,
1167
+ error: `${configPath} is not a symlink; refusing to replace a real config directory. `
1168
+ + `Run \`agents use ${agent}\` once to adopt it, then \`agents accounts default ${agent}\`.`,
1169
+ };
1170
+ }
1171
+ }
1172
+ catch (err) {
1173
+ if (err.code !== 'ENOENT')
1174
+ throw err;
1175
+ }
1176
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
1177
+ // Atomic retarget: create temp symlink, rename over existing (same pattern
1178
+ // as switchHomeFileSymlinks). A window of "path missing" would let a
1179
+ // harness process observe neither the old nor the new target.
1180
+ const tmpPath = `${configPath}.agents-tmp-${process.pid}`;
1181
+ try {
1182
+ fs.unlinkSync(tmpPath);
1183
+ }
1184
+ catch { /* leftover from a killed prior swap */ }
1185
+ try {
1186
+ fs.symlinkSync(target, tmpPath, process.platform === 'win32' ? 'junction' : undefined);
1187
+ fs.renameSync(tmpPath, configPath);
1188
+ }
1189
+ catch (err) {
1190
+ try {
1191
+ fs.unlinkSync(tmpPath);
1192
+ }
1193
+ catch { /* rename may have already consumed it */ }
1194
+ throw err;
1195
+ }
1196
+ return { success: true };
1197
+ }
1198
+ finally {
1199
+ lock.release();
1200
+ }
1201
+ }
1133
1202
  /**
1134
1203
  * Generate a versioned alias script that directly execs a specific version.
1135
1204
  * e.g., claude@2.0.65 -> directly runs that version's binary