@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
@@ -1,176 +0,0 @@
1
- import type { AgentId, Meta } from '../types.js';
2
- import { type NativeAccount } from '../account-registry.js';
3
- /** Which harnesses `connect` supports, and how their native login is launched. */
4
- export interface LoginInvocation {
5
- /** argv passed to the installed binary to start the native login. */
6
- args: string[];
7
- /** Flag that pre-fills the login email (appended as `[emailFlag, email]`), when supported. */
8
- emailFlag?: string;
9
- /** One-line hint shown before the login flow takes over. */
10
- hint?: string;
11
- }
12
- /** Whether `connect` can drive this harness (isolation + a real native login). */
13
- export declare function connectSupported(agent: AgentId): boolean;
14
- /**
15
- * Named reason connect refuses a harness, or null when supported. Distinguishes
16
- * "cannot isolate/name this login" (capability) from "no native login command
17
- * wired yet" so the message is honest about which limit was hit.
18
- */
19
- export declare function connectRefusal(agent: AgentId): string | null;
20
- export declare function assertConnectSupported(agent: AgentId): void;
21
- /**
22
- * Named reason connect refuses on a non-headed device, or null when this box
23
- * may run an interactive login. Workers never mint a native OAuth login
24
- * (credential-management.md invariant 7 + Provisioning model).
25
- */
26
- export declare function connectWorkerRefusal(agent: AgentId, name?: string): string | null;
27
- export declare function loginInvocation(agent: AgentId): LoginInvocation;
28
- /**
29
- * Mint a fresh, opaque installation slot. `acct-<hex>` — alnum + hyphen only, so
30
- * it satisfies VERSION_RE, and is visibly an account slot, not a release. It is
31
- * RANDOM, never derived from the account name or identity: a name-derived slot
32
- * recomputes an already-occupied home after a rename and lets a new connect
33
- * overwrite another account's login (PHNX-3940 security fix). Retry-idempotency
34
- * is instead provided by the device-scoped pending-connect map, and collision
35
- * safety by allocating around occupied slots — see `allocateConnectSlot`.
36
- */
37
- export declare function mintConnectLabel(): string;
38
- export type ConnectMode = 'new' | 'reconnect';
39
- export interface ConnectPlan {
40
- mode: ConnectMode;
41
- agent: AgentId;
42
- /** The installation label whose isolated home hosts this account's login. */
43
- label: string;
44
- /** The human account name to register (new) or already registered (reconnect). */
45
- name?: string;
46
- /** For reconnect: the existing account being re-authenticated. */
47
- existing?: NativeAccount;
48
- /**
49
- * For reconnect: the label was ADOPTED (freshly minted) because the existing
50
- * account carried no recorded home and none was discoverable — so its identity
51
- * must still be re-verified against the login, but there is no prior home to
52
- * reuse. `false` when an existing home is being reused.
53
- */
54
- adoptedHome?: boolean;
55
- }
56
- /**
57
- * Resolve the home to reuse for reconnecting an existing account: THIS box's
58
- * recorded connect home first (device-scoped, survives an expired credential),
59
- * else a currently-signed-in local home carrying its identity. Returns null when
60
- * neither is known — a legacy account with no recorded or live home, for which
61
- * connect adopts a fresh home.
62
- */
63
- export declare function resolveExistingHomeLabel(existing: NativeAccount, deviceHome: string | null, signedInHomes: Array<{
64
- agent: AgentId;
65
- identityKey: string;
66
- label: string;
67
- }>): string | null;
68
- /**
69
- * Decide the release-independent connect plan (pure). `existing` is the native
70
- * account the name resolves to for THIS harness (or null for a new connect);
71
- * `existingHomeLabel` is its reusable home from {@link resolveExistingHomeLabel};
72
- * `freshSlot` is a safely-allocated opaque slot (see `allocateConnectSlot`) used
73
- * for a new connect or an adopted reconnect home — NEVER a name-derived label.
74
- */
75
- export declare function planConnect(input: {
76
- agent: AgentId;
77
- name?: string;
78
- existing: NativeAccount | null;
79
- existingHomeLabel: string | null;
80
- freshSlot: string;
81
- }): ConnectPlan;
82
- /**
83
- * Safely allocate the opaque slot for a NEW connect or an ADOPTED reconnect home
84
- * (PHNX-3940 security fix). Never reuses an identity-bearing slot:
85
- *
86
- * - `occupied` is every slot already owned by an account's home OR currently
87
- * signed in — an allocated slot is guaranteed disjoint from it, so a new
88
- * connect can never land on another account's home (the rename-collision flaw).
89
- * - A named connect first reuses its device-scoped PENDING slot (a prior
90
- * failed/cancelled attempt) IF that slot is not occupied, so a retry lands in
91
- * the same fresh home instead of orphaning a new one.
92
- * - Otherwise it mints random slots until one is neither occupied nor installed.
93
- */
94
- export declare function allocateConnectSlot(input: {
95
- agent: AgentId;
96
- name?: string;
97
- existing: NativeAccount | null;
98
- occupied: ReadonlySet<string>;
99
- installedLabels: ReadonlySet<string>;
100
- pending: string | null;
101
- mint?: () => string;
102
- }): string;
103
- /**
104
- * Fail-closed identity check after the login completes (pure).
105
- *
106
- * - Not signed in (no live credential) → the login did not complete; a metadata
107
- * identity key alone is NOT proof, so `signedIn` is required.
108
- * - Reconnect whose completed identity differs from the account's → REFUSE
109
- * registering the binding. The native credential in that home DID change (the
110
- * user signed in), so the message says the account BINDING is left unchanged —
111
- * it does not claim nothing happened.
112
- * A new connect accepts whatever identity signed in (that is the account being
113
- * created); the caller registers it.
114
- */
115
- export declare function verifyConnectedIdentity(plan: ConnectPlan, observed: Pick<ObservedIdentity, 'identityKey' | 'signedIn'>): void;
116
- /** Resolve the existing native account a connect `name` refers to for a harness. */
117
- export declare function findConnectAccount(agent: AgentId, name: string | undefined, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): NativeAccount | null;
118
- /** The observed identity of a completed login. */
119
- export interface ObservedIdentity {
120
- identityKey: string | null;
121
- email: string | null;
122
- releaseVersion: string | null;
123
- /** Live credential presence — proof of sign-in, not just a metadata identity claim. */
124
- signedIn: boolean;
125
- }
126
- /**
127
- * Side-effecting operations connect needs, injected so the planning/verification
128
- * path is unit-testable and the parent can own the real E2E login+verify.
129
- */
130
- export interface ConnectRunners {
131
- installedLabels(agent: AgentId): string[];
132
- /** Install the CURRENT release into `label`'s isolated home (opaque label). */
133
- install(agent: AgentId, label: string, onProgress?: (m: string) => void): Promise<{
134
- success: boolean;
135
- error?: string;
136
- }>;
137
- /** Launch the harness's native login under `label`'s home; resolves on exit. */
138
- launchLogin(agent: AgentId, label: string, invocation: LoginInvocation, email?: string, signal?: AbortSignal): Promise<{
139
- code: number | null;
140
- }>;
141
- observeIdentity(agent: AgentId, label: string): Promise<ObservedIdentity>;
142
- signedInHomes(): Promise<Array<{
143
- agent: AgentId;
144
- identityKey: string;
145
- label: string;
146
- }>>;
147
- }
148
- export interface ConnectResult {
149
- mode: ConnectMode;
150
- agent: AgentId;
151
- label: string;
152
- name?: string;
153
- identityKey: string;
154
- email: string | null;
155
- releaseVersion: string | null;
156
- /** True when this connect became the harness's default (only set if none was configured). */
157
- becameDefault: boolean;
158
- /** For an UNNAMED connect: the command to name the login (no name was forced). */
159
- nameHint?: string;
160
- }
161
- /**
162
- * Drive one `agents accounts connect`. Pure decisions (plan, fail-closed verify)
163
- * come from the helpers above; the install + login + identity read are injected.
164
- *
165
- * The default runners are loaded via dynamic import so this module — reachable
166
- * from `account-registry` consumers — never statically pulls in the install
167
- * engine or exec path and closes an import cycle (the intended pattern here).
168
- *
169
- * `stateDir` is only for tests: override the runtime-state dir used by the
170
- * per-harness auth-operation mutex (defaults to the real runtime state dir).
171
- */
172
- export declare function runConnect(agent: AgentId, name: string | undefined, opts: {
173
- meta: Pick<Meta, 'accounts' | 'deviceAccounts'>;
174
- onProgress?: (m: string) => void;
175
- stateDir?: string;
176
- }, runners?: ConnectRunners): Promise<ConnectResult>;
@@ -1,453 +0,0 @@
1
- import * as crypto from 'node:crypto';
2
- import { nativeAccountCapability, nativeAccountNamingRefusal } from '../account-capabilities.js';
3
- import { listNativeAccounts } from '../account-registry.js';
4
- import { providerAuthenticatesHarness } from '../account-provider-registry.js';
5
- import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
6
- import { machineId } from '../machine-id.js';
7
- import { readMeta } from '../state.js';
8
- import { acquireAuthOperationLock } from './auth-operation-lock.js';
9
- /**
10
- * `agents accounts connect <harness> [name]` — the stable-account front door
11
- * (PHNX-3940).
12
- *
13
- * Headed devices only. A worker (or unmarked box) is refused before any slot,
14
- * install, or browser login — workers never run an interactive OAuth flow
15
- * (credential-management.md invariant 7). Add the account on a personal/desktop
16
- * box; workers are provisioned from the durable credential.
17
- *
18
- * An account is stable INDEPENDENT of releases: connecting a NEW account mints a
19
- * fresh opaque installation label, installs the current release into that
20
- * label's isolated home (even when the same release is already installed under
21
- * another label), and launches the harness's own native login there — no
22
- * credential is copied or refreshed, the OAuth login stays owned by the harness.
23
- * Reconnecting a NAMED EXISTING account reuses that account's existing home and
24
- * fails CLOSED if the completed login is a different identity, so a reconnect can
25
- * never overwrite some other account's home.
26
- *
27
- * This module owns the release-independent decision (plan), the opaque label,
28
- * and the fail-closed identity check. The actual install + login spawn is
29
- * injected (`ConnectRunners`) so the pure decision path is testable without a
30
- * network install or a TTY, and so the parent can own the real E2E login+verify.
31
- */
32
- /**
33
- * Ambient provider-credential env vars stripped before a native login spawn, so
34
- * the OAuth flow authenticates as the human's identity rather than being
35
- * short-circuited by an injected API key / setup-token that would impersonate a
36
- * different account into the fresh home.
37
- */
38
- const PROVIDER_AUTH_ENV_KEYS = [
39
- 'ANTHROPIC_API_KEY',
40
- 'CLAUDE_CODE_OAUTH_TOKEN',
41
- 'ANTHROPIC_AUTH_TOKEN',
42
- 'OPENAI_API_KEY',
43
- 'CODEX_API_KEY',
44
- ];
45
- /**
46
- * Native-login invocation per harness. Only harnesses with a REAL, finite login
47
- * COMMAND are listed — connect fails clearly for anything else rather than faking
48
- * a flow that never signs the user in. Verified against the installed CLIs
49
- * (PHNX-3940): `claude auth login --help` → "Sign in to your Anthropic account"
50
- * with `--email`; `codex login` drives the OAuth flow. A harness is added here
51
- * only once its isolated login command is verified.
52
- */
53
- const LOGIN_INVOCATIONS = {
54
- claude: { args: ['auth', 'login'], emailFlag: '--email', hint: 'Complete the Claude sign-in in your browser.' },
55
- codex: { args: ['login'] },
56
- };
57
- /** Whether `connect` can drive this harness (isolation + a real native login). */
58
- export function connectSupported(agent) {
59
- const cap = nativeAccountCapability(agent);
60
- return cap.scope === 'version' && !!LOGIN_INVOCATIONS[agent];
61
- }
62
- /**
63
- * Named reason connect refuses a harness, or null when supported. Distinguishes
64
- * "cannot isolate/name this login" (capability) from "no native login command
65
- * wired yet" so the message is honest about which limit was hit.
66
- */
67
- export function connectRefusal(agent) {
68
- const capabilityRefusal = nativeAccountNamingRefusal(agent);
69
- if (capabilityRefusal)
70
- return capabilityRefusal;
71
- const cap = nativeAccountCapability(agent);
72
- if (cap.scope !== 'version') {
73
- return `${agent} authentication is ${cap.scope}-scoped; connect creates per-account isolated homes, which needs a version-scoped login.`;
74
- }
75
- if (!LOGIN_INVOCATIONS[agent]) {
76
- const supported = Object.keys(LOGIN_INVOCATIONS).sort().join(', ');
77
- return `connect does not yet drive ${agent}'s native login. Supported: ${supported}.`;
78
- }
79
- return null;
80
- }
81
- export function assertConnectSupported(agent) {
82
- const reason = connectRefusal(agent);
83
- if (reason)
84
- throw new Error(reason);
85
- }
86
- /**
87
- * Native API-key provider for a harness that `connect` can drive AND that
88
- * workers provision from a portable credential (credential-management.md
89
- * invariant 7). The id is the registry adapter that authenticates this harness
90
- * with `api-key` — not a parallel invention. Claude is a setup-token, not an
91
- * API key, and is handled separately. Only LOGIN_INVOCATIONS harnesses belong
92
- * here; a mapping for a harness connect cannot drive is dead code.
93
- */
94
- const NATIVE_API_KEY_PROVIDER = {
95
- codex: 'openai',
96
- };
97
- function nativeApiKeyProvider(agent) {
98
- const id = NATIVE_API_KEY_PROVIDER[agent];
99
- if (!id)
100
- return null;
101
- if (!providerAuthenticatesHarness(id, 'api-key', agent)) {
102
- throw new Error(`Internal: provider '${id}' does not authenticate ${agent} with an api-key.`);
103
- }
104
- return id;
105
- }
106
- function workerCredentialHint(agent, name, device) {
107
- if (agent === 'claude')
108
- return 'the setup-token minted by agents accounts mint claude';
109
- const provider = nativeApiKeyProvider(agent);
110
- if (provider) {
111
- const account = name ?? '<name>';
112
- return `a provider API key — agents accounts add ${account} --provider ${provider} --auth api-key then agents accounts sync ${account} ${device}`;
113
- }
114
- // LOGIN_INVOCATIONS today is {claude, codex}; this arm is the loud failure
115
- // if a future harness is added there without a portable-credential mapping.
116
- // Names no command — a guessed `fleet login <harness>` is not a real invocation.
117
- return `no portable credential for ${agent}`;
118
- }
119
- /**
120
- * Named reason connect refuses on a non-headed device, or null when this box
121
- * may run an interactive login. Workers never mint a native OAuth login
122
- * (credential-management.md invariant 7 + Provisioning model).
123
- */
124
- export function connectWorkerRefusal(agent, name) {
125
- const role = selfConfiguredDeviceRole();
126
- if (isHeadedDeviceRole(role))
127
- return null;
128
- const device = machineId();
129
- const roleLabel = role ?? 'unmarked';
130
- const selector = name ? `${agent}#${name}` : agent;
131
- const connectCmd = name
132
- ? `agents accounts connect ${agent} ${name}`
133
- : `agents accounts connect ${agent} <name>`;
134
- return `${selector}: this device is a worker (role ${roleLabel}) and never runs an interactive login. `
135
- + `Add the account on your personal device with \`${connectCmd}\`; `
136
- + `workers are provisioned from the durable credential automatically `
137
- + `(${agent}: ${workerCredentialHint(agent, name, device)}). `
138
- + `To mark this box as your interactive seat: agents devices role ${device} personal.`;
139
- }
140
- function assertConnectAllowedOnThisDevice(agent, name) {
141
- const reason = connectWorkerRefusal(agent, name);
142
- if (reason)
143
- throw new Error(reason);
144
- }
145
- export function loginInvocation(agent) {
146
- const invocation = LOGIN_INVOCATIONS[agent];
147
- if (!invocation)
148
- throw new Error(`No native login command is wired for ${agent}.`);
149
- return invocation;
150
- }
151
- /**
152
- * Mint a fresh, opaque installation slot. `acct-<hex>` — alnum + hyphen only, so
153
- * it satisfies VERSION_RE, and is visibly an account slot, not a release. It is
154
- * RANDOM, never derived from the account name or identity: a name-derived slot
155
- * recomputes an already-occupied home after a rename and lets a new connect
156
- * overwrite another account's login (PHNX-3940 security fix). Retry-idempotency
157
- * is instead provided by the device-scoped pending-connect map, and collision
158
- * safety by allocating around occupied slots — see `allocateConnectSlot`.
159
- */
160
- export function mintConnectLabel() {
161
- return `acct-${crypto.randomBytes(6).toString('hex')}`;
162
- }
163
- /**
164
- * Resolve the home to reuse for reconnecting an existing account: THIS box's
165
- * recorded connect home first (device-scoped, survives an expired credential),
166
- * else a currently-signed-in local home carrying its identity. Returns null when
167
- * neither is known — a legacy account with no recorded or live home, for which
168
- * connect adopts a fresh home.
169
- */
170
- export function resolveExistingHomeLabel(existing, deviceHome, signedInHomes) {
171
- if (deviceHome)
172
- return deviceHome;
173
- const match = signedInHomes.find(h => h.agent === existing.agent && h.identityKey === existing.identityKey);
174
- return match?.label ?? null;
175
- }
176
- /**
177
- * Decide the release-independent connect plan (pure). `existing` is the native
178
- * account the name resolves to for THIS harness (or null for a new connect);
179
- * `existingHomeLabel` is its reusable home from {@link resolveExistingHomeLabel};
180
- * `freshSlot` is a safely-allocated opaque slot (see `allocateConnectSlot`) used
181
- * for a new connect or an adopted reconnect home — NEVER a name-derived label.
182
- */
183
- export function planConnect(input) {
184
- if (input.existing) {
185
- const reuse = input.existingHomeLabel;
186
- return {
187
- mode: 'reconnect',
188
- agent: input.agent,
189
- label: reuse ?? input.freshSlot,
190
- name: input.existing.name,
191
- existing: input.existing,
192
- adoptedHome: !reuse,
193
- };
194
- }
195
- return { mode: 'new', agent: input.agent, label: input.freshSlot, name: input.name };
196
- }
197
- /**
198
- * Safely allocate the opaque slot for a NEW connect or an ADOPTED reconnect home
199
- * (PHNX-3940 security fix). Never reuses an identity-bearing slot:
200
- *
201
- * - `occupied` is every slot already owned by an account's home OR currently
202
- * signed in — an allocated slot is guaranteed disjoint from it, so a new
203
- * connect can never land on another account's home (the rename-collision flaw).
204
- * - A named connect first reuses its device-scoped PENDING slot (a prior
205
- * failed/cancelled attempt) IF that slot is not occupied, so a retry lands in
206
- * the same fresh home instead of orphaning a new one.
207
- * - Otherwise it mints random slots until one is neither occupied nor installed.
208
- */
209
- export function allocateConnectSlot(input) {
210
- const mint = input.mint ?? mintConnectLabel;
211
- // Retry reuse is only for a NEW named connect (pending is keyed by name); a
212
- // pending slot that has since become occupied is abandoned, never overwritten.
213
- if (!input.existing && input.name && input.pending && !input.occupied.has(input.pending)) {
214
- return input.pending;
215
- }
216
- let slot = mint();
217
- while (input.occupied.has(slot) || input.installedLabels.has(slot))
218
- slot = mint();
219
- return slot;
220
- }
221
- /**
222
- * Fail-closed identity check after the login completes (pure).
223
- *
224
- * - Not signed in (no live credential) → the login did not complete; a metadata
225
- * identity key alone is NOT proof, so `signedIn` is required.
226
- * - Reconnect whose completed identity differs from the account's → REFUSE
227
- * registering the binding. The native credential in that home DID change (the
228
- * user signed in), so the message says the account BINDING is left unchanged —
229
- * it does not claim nothing happened.
230
- * A new connect accepts whatever identity signed in (that is the account being
231
- * created); the caller registers it.
232
- */
233
- export function verifyConnectedIdentity(plan, observed) {
234
- if (!observed.signedIn || !observed.identityKey) {
235
- throw new Error(`No ${plan.agent} login completed in ${plan.agent}@${plan.label} (no live credential). Nothing was connected.`);
236
- }
237
- if (plan.mode === 'reconnect' && plan.existing && observed.identityKey !== plan.existing.identityKey) {
238
- throw new Error(`The ${plan.agent} login that just completed in ${plan.agent}@${plan.label} is a different identity `
239
- + `(${observed.identityKey}) than account '${plan.existing.name}' (${plan.existing.identityKey}). `
240
- + `Account '${plan.existing.name}' still points at its original identity (binding unchanged); `
241
- + `connect a NEW account for this login instead.`);
242
- }
243
- }
244
- /** Resolve the existing native account a connect `name` refers to for a harness. */
245
- export function findConnectAccount(agent, name, meta) {
246
- if (!name)
247
- return null;
248
- const needle = name.toLowerCase();
249
- return listNativeAccounts(meta).find(a => a.agent === agent && (a.id === name || a.name.toLowerCase() === needle || a.identityLabel?.toLowerCase() === needle)) ?? null;
250
- }
251
- /**
252
- * Drive one `agents accounts connect`. Pure decisions (plan, fail-closed verify)
253
- * come from the helpers above; the install + login + identity read are injected.
254
- *
255
- * The default runners are loaded via dynamic import so this module — reachable
256
- * from `account-registry` consumers — never statically pulls in the install
257
- * engine or exec path and closes an import cycle (the intended pattern here).
258
- *
259
- * `stateDir` is only for tests: override the runtime-state dir used by the
260
- * per-harness auth-operation mutex (defaults to the real runtime state dir).
261
- */
262
- export async function runConnect(agent, name, opts, runners) {
263
- assertConnectSupported(agent);
264
- // Owner rule (invariant 7): a worker NEVER runs an interactive login. Refuse
265
- // before the lock, slot, install, or browser — no side effects on a worker.
266
- assertConnectAllowedOnThisDevice(agent, name);
267
- // Acquire the per-harness auth-operation mutex BEFORE any meta read, slot
268
- // allocation, or name validation — two parallel connects for the same harness
269
- // would otherwise both see "name available" and "slot free", then race to
270
- // register the same slot or the same name, overwriting one account's home.
271
- const lock = acquireAuthOperationLock(agent, opts.stateDir);
272
- try {
273
- return await _runConnectLocked(agent, name, opts, lock, runners);
274
- }
275
- finally {
276
- lock.release();
277
- }
278
- }
279
- async function _runConnectLocked(agent, name, opts, lock, runners) {
280
- const run = runners ?? await defaultConnectRunners();
281
- const registry = await import('../account-registry.js');
282
- lock.assertHeld();
283
- // Re-read meta AFTER acquiring the lock so we see any writes a concurrent
284
- // connect committed before we were serialized past it. `opts.meta` was a
285
- // snapshot taken before the lock was available and must not be used here.
286
- const meta = readMeta();
287
- const existing = findConnectAccount(agent, name, meta);
288
- // Validate the requested NAME and its collisions BEFORE any install or login,
289
- // so a bad name never mints an orphan home and drives a login that can't be
290
- // recorded. Only a genuinely NEW named connect needs this (a reconnect names an
291
- // account that already passed the check).
292
- if (!existing && name)
293
- registry.assertNativeAccountNameAvailable(name, agent);
294
- const signedIn = await run.signedInHomes();
295
- lock.assertHeld();
296
- const existingHomeLabel = existing
297
- ? resolveExistingHomeLabel(existing, registry.nativeAccountHome(existing.id, meta), signedIn)
298
- : null;
299
- // SECURITY (PHNX-3940): allocate a fresh slot that is DISJOINT from every
300
- // occupied home — a slot already owned by an account OR currently signed in.
301
- // This is what stops a NEW connect (e.g. `connect work` after `work` was
302
- // renamed `personal`) from landing on and overwriting another account's login.
303
- const occupied = new Set([
304
- ...registry.ownedConnectHomeLabels(meta),
305
- ...signedIn.filter(h => h.agent === agent).map(h => h.label),
306
- ]);
307
- const freshSlot = allocateConnectSlot({
308
- agent,
309
- name,
310
- existing,
311
- occupied,
312
- installedLabels: new Set(run.installedLabels(agent)),
313
- pending: (!existing && name) ? registry.pendingConnectSlot(agent, name, meta) : null,
314
- });
315
- // Record the in-flight slot for a named connect so a failed retry reuses it.
316
- if (!existing && name && freshSlot !== registry.pendingConnectSlot(agent, name, meta)) {
317
- registry.setPendingConnectSlot(agent, name, freshSlot);
318
- }
319
- const plan = planConnect({ agent, name, existing, existingHomeLabel, freshSlot });
320
- // Pre-launch guard (both modes): if the chosen home is already installed AND
321
- // signed in, refuse to launch a login that would OVERWRITE its identity —
322
- // unless it is the SAME identity we are (re)connecting. For a NEW connect any
323
- // signed-in identity is a stranger; for a reconnect only a DIFFERENT one is.
324
- if (run.installedLabels(agent).includes(plan.label)) {
325
- const current = await run.observeIdentity(agent, plan.label);
326
- lock.assertHeld();
327
- const intended = existing?.identityKey;
328
- if (current.signedIn && current.identityKey && current.identityKey !== intended) {
329
- throw new Error(`${agent}@${plan.label} is currently signed in as ${current.identityKey}`
330
- + `${existing ? ` (not account '${existing.name}')` : ''}. `
331
- + `Refusing to launch a login that would overwrite it.`);
332
- }
333
- }
334
- // Install the current release into the account's home. A reconnect that reuses
335
- // an already-installed home skips the install (reuse, don't re-mint); a new
336
- // account (or an adopted reconnect home) installs the current release under its
337
- // opaque label even when the same release is already installed elsewhere. A
338
- // retried named connect reuses the same pending slot rather than orphaning.
339
- const alreadyInstalled = run.installedLabels(agent).includes(plan.label);
340
- lock.assertHeld();
341
- if (!(alreadyInstalled && (plan.mode === 'reconnect' || !!name))) {
342
- opts.onProgress?.(`Installing ${agent} into ${plan.label}…`);
343
- const install = await run.install(agent, plan.label, opts.onProgress);
344
- lock.assertHeld();
345
- if (!install.success)
346
- throw new Error(`Could not install ${agent} for the account home: ${install.error ?? 'unknown error'}`);
347
- }
348
- const invocation = loginInvocation(agent);
349
- if (invocation.hint)
350
- opts.onProgress?.(invocation.hint);
351
- const loginEmail = existing?.identityLabel;
352
- lock.assertHeld();
353
- const login = await run.launchLogin(agent, plan.label, invocation, loginEmail, lock.signal);
354
- lock.assertHeld();
355
- // A cancelled or failed login exits non-zero — do NOT read stale metadata and
356
- // report success. A NAMED connect can retry the same home (its slot is pending);
357
- // an UNNAMED connect has no pending slot, so a re-run allocates a new home.
358
- if (login.code !== 0) {
359
- const retry = name ? 're-run to retry the same home' : 're-run to try again (a new home is allocated)';
360
- throw new Error(`${agent} login did not complete (exit ${login.code ?? 'null'}) in ${agent}@${plan.label}. Nothing was recorded; ${retry}.`);
361
- }
362
- const observed = await run.observeIdentity(agent, plan.label);
363
- lock.assertHeld();
364
- verifyConnectedIdentity(plan, observed);
365
- const identityKey = observed.identityKey;
366
- // Persist THIS box's account⇄home binding (device-scoped) so a later reconnect
367
- // reuses this exact home even after the credential expires, and clear the
368
- // in-flight pending slot now that it is a real account home.
369
- let becameDefault = false;
370
- let nameHint;
371
- if (plan.mode === 'new') {
372
- if (name) {
373
- const account = registry.addNativeAccount(name, agent, identityKey, observed.email ?? undefined, 'version');
374
- registry.setNativeAccountHome(account.id, plan.label);
375
- registry.clearPendingConnectSlot(agent, name);
376
- // Select this account as the harness default ONLY if none is configured —
377
- // never override an existing choice, and never force a duplicate name.
378
- becameDefault = registry.setDefaultAccountIfAbsent(agent, account.name);
379
- }
380
- else {
381
- // No name was given: do not invent/force one. Point the user at the command
382
- // that names this login so it becomes selectable and default-eligible.
383
- nameHint = `agents accounts label ${agent} <name>`;
384
- }
385
- }
386
- else if (existing) {
387
- registry.setNativeAccountHome(existing.id, plan.label);
388
- }
389
- return {
390
- mode: plan.mode,
391
- agent,
392
- label: plan.label,
393
- name: plan.name ?? name,
394
- identityKey,
395
- email: observed.email,
396
- releaseVersion: observed.releaseVersion,
397
- becameDefault,
398
- nameHint,
399
- };
400
- }
401
- /** Real runners: the install engine, an inherited-stdio login spawn, and identity read. */
402
- async function defaultConnectRunners() {
403
- const [{ installVersion, getVersionHomePath, listInstalledVersions }, store, agentsMod, execMod] = await Promise.all([
404
- import('../installations/versions.js'),
405
- import('../installations/store.js'),
406
- import('../agents.js'),
407
- import('../exec.js'),
408
- ]);
409
- const { readInstallation } = store;
410
- const { getAccountInfo } = agentsMod;
411
- const { buildExecEnv } = execMod;
412
- const { nativeAccountCapability, nativeIdentityKey } = await import('../account-capabilities.js');
413
- const { runNativeAccountCommand } = await import('../installations/native-command.js');
414
- return {
415
- installedLabels: (agent) => listInstalledVersions(agent),
416
- install: async (agent, label, onProgress) => {
417
- const result = await installVersion(agent, 'latest', onProgress, { installationLabel: label });
418
- return { success: result.success, error: result.error };
419
- },
420
- launchLogin: async (agent, label, invocation, email, signal) => {
421
- // Pin the harness's own config-dir env (CLAUDE_CONFIG_DIR / CODEX_HOME) to
422
- // this label's isolated home so the native login lands there — no
423
- // credential is copied; the login writes into the home it authenticates.
424
- const env = buildExecEnv({ agent, version: label, configVersion: label, interactive: true, mode: 'auto', effort: 'auto', cwd: process.cwd() });
425
- // Strip any ambient provider API-key / setup-token env so the native login
426
- // authenticates as the human's OAuth identity, not an injected credential
427
- // that would impersonate a different account into this home.
428
- for (const key of PROVIDER_AUTH_ENV_KEYS)
429
- delete env[key];
430
- const args = email && invocation.emailFlag ? [...invocation.args, invocation.emailFlag, email] : invocation.args;
431
- return runNativeAccountCommand(agent, label, args, env, signal);
432
- },
433
- observeIdentity: async (agent, label) => {
434
- const home = getVersionHomePath(agent, label);
435
- const info = await getAccountInfo(agent, home);
436
- const { isLaunchableSignedIn } = await import('../account-catalog.js');
437
- return {
438
- identityKey: nativeIdentityKey(info, nativeAccountCapability(agent)),
439
- email: info.email,
440
- releaseVersion: readInstallation(agent, label)?.releaseVersion ?? null,
441
- // Strict: a live credential in THIS home, not a bare metadata identity.
442
- signedIn: isLaunchableSignedIn(agent, home, info),
443
- };
444
- },
445
- signedInHomes: async () => {
446
- const { collectNativeHomeRows } = await import('../account-catalog.js');
447
- const rows = await collectNativeHomeRows();
448
- return rows
449
- .filter(r => r.signedIn && (r.accountKey ?? r.email))
450
- .map(r => ({ agent: r.agent, identityKey: (r.accountKey ?? r.email.toLowerCase()), label: r.label }));
451
- },
452
- };
453
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * Keychain orphan reaper tick as a `PeriodicService` (RUSH-3193 P3).
3
- *
4
- * Kills stuck keychain helper and `agents` processes whose keychain call
5
- * never returned (RUSH-2232). Single-executor: only the daemon runs this, so
6
- * no cross-device race.
7
- */
8
- import { BasePeriodicService, type DaemonContext } from './service.js';
9
- import type { DaemonServiceId } from '../daemon-services.js';
10
- export declare class KeychainReapService extends BasePeriodicService {
11
- readonly id: DaemonServiceId;
12
- readonly intervalMs: number;
13
- readonly deadlineMs = 30000;
14
- protected onStart(_ctx: DaemonContext): Promise<void>;
15
- protected onStop(): Promise<void>;
16
- protected onTick(ctx: DaemonContext): Promise<void>;
17
- }