@phnx-labs/agents-cli 1.22.78 → 1.22.79

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 (90) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +28 -1
  3. package/dist/bootstrap.js +40 -12
  4. package/dist/commands/accounts.d.ts +22 -0
  5. package/dist/commands/accounts.js +158 -35
  6. package/dist/commands/config.js +37 -0
  7. package/dist/commands/exec.js +21 -10
  8. package/dist/commands/update.js +169 -18
  9. package/dist/commands/versions.d.ts +11 -0
  10. package/dist/commands/versions.js +30 -4
  11. package/dist/commands/view.d.ts +4 -0
  12. package/dist/commands/view.js +114 -19
  13. package/dist/index.js +35 -2
  14. package/dist/lib/account-catalog.d.ts +97 -1
  15. package/dist/lib/account-catalog.js +134 -6
  16. package/dist/lib/account-registry.d.ts +43 -0
  17. package/dist/lib/account-registry.js +97 -2
  18. package/dist/lib/accounting/rotate.d.ts +2 -2
  19. package/dist/lib/accounting/rotate.js +5 -5
  20. package/dist/lib/accounts/auth-operation-lock.d.ts +9 -0
  21. package/dist/lib/accounts/auth-operation-lock.js +55 -0
  22. package/dist/lib/accounts/connect.d.ts +170 -0
  23. package/dist/lib/accounts/connect.js +383 -0
  24. package/dist/lib/capabilities.js +2 -0
  25. package/dist/lib/commands.js +2 -0
  26. package/dist/lib/config-keys.d.ts +11 -2
  27. package/dist/lib/config-keys.js +21 -1
  28. package/dist/lib/daemon/daemon.js +5 -0
  29. package/dist/lib/daemon/harness-update-service.d.ts +110 -0
  30. package/dist/lib/daemon/harness-update-service.js +216 -0
  31. package/dist/lib/daemon-services.d.ts +1 -1
  32. package/dist/lib/daemon-services.js +5 -0
  33. package/dist/lib/device-config.d.ts +0 -1
  34. package/dist/lib/device-config.js +50 -5
  35. package/dist/lib/exec.js +26 -2
  36. package/dist/lib/fs-atomic.d.ts +2 -0
  37. package/dist/lib/fs-atomic.js +2 -0
  38. package/dist/lib/hooks/install.js +7 -2
  39. package/dist/lib/installations/active-check.d.ts +48 -0
  40. package/dist/lib/installations/active-check.js +84 -0
  41. package/dist/lib/installations/index.d.ts +5 -1
  42. package/dist/lib/installations/index.js +4 -0
  43. package/dist/lib/installations/installation-lock.d.ts +6 -0
  44. package/dist/lib/installations/installation-lock.js +29 -0
  45. package/dist/lib/installations/launch-gate.d.ts +69 -0
  46. package/dist/lib/installations/launch-gate.js +133 -0
  47. package/dist/lib/installations/native-command.d.ts +5 -0
  48. package/dist/lib/installations/native-command.js +52 -0
  49. package/dist/lib/installations/shims.d.ts +8 -2
  50. package/dist/lib/installations/shims.js +105 -2
  51. package/dist/lib/installations/store.d.ts +5 -1
  52. package/dist/lib/installations/store.js +24 -3
  53. package/dist/lib/installations/strategies.js +55 -35
  54. package/dist/lib/installations/types.d.ts +17 -0
  55. package/dist/lib/installations/update-cancellation.d.ts +82 -0
  56. package/dist/lib/installations/update-cancellation.js +122 -0
  57. package/dist/lib/installations/update-policy.d.ts +69 -0
  58. package/dist/lib/installations/update-policy.js +114 -0
  59. package/dist/lib/installations/update-runtime.d.ts +118 -0
  60. package/dist/lib/installations/update-runtime.js +321 -0
  61. package/dist/lib/installations/update.d.ts +25 -0
  62. package/dist/lib/installations/update.js +141 -2
  63. package/dist/lib/installations/versions.d.ts +1 -0
  64. package/dist/lib/installations/versions.js +166 -131
  65. package/dist/lib/platform/process.d.ts +3 -1
  66. package/dist/lib/platform/process.js +2 -2
  67. package/dist/lib/staleness/detectors/commands.d.ts +1 -2
  68. package/dist/lib/staleness/detectors/hooks.d.ts +1 -2
  69. package/dist/lib/staleness/detectors/mcp.d.ts +1 -2
  70. package/dist/lib/staleness/detectors/permissions.d.ts +1 -2
  71. package/dist/lib/staleness/detectors/plugins.d.ts +1 -7
  72. package/dist/lib/staleness/detectors/rules.d.ts +1 -2
  73. package/dist/lib/staleness/detectors/skills.d.ts +1 -2
  74. package/dist/lib/staleness/detectors/subagents.d.ts +1 -7
  75. package/dist/lib/staleness/detectors/workflows.d.ts +1 -2
  76. package/dist/lib/staleness/writers/commands.d.ts +1 -2
  77. package/dist/lib/staleness/writers/hooks.d.ts +1 -2
  78. package/dist/lib/staleness/writers/mcp.d.ts +1 -2
  79. package/dist/lib/staleness/writers/permissions.d.ts +1 -12
  80. package/dist/lib/staleness/writers/plugins.d.ts +1 -6
  81. package/dist/lib/staleness/writers/rules.d.ts +1 -2
  82. package/dist/lib/staleness/writers/skills.d.ts +1 -2
  83. package/dist/lib/staleness/writers/subagents.d.ts +1 -2
  84. package/dist/lib/staleness/writers/workflows.d.ts +1 -8
  85. package/dist/lib/state.d.ts +3 -1
  86. package/dist/lib/state.js +38 -13
  87. package/dist/lib/types.d.ts +26 -1
  88. package/dist/lib/types.js +5 -0
  89. package/dist/lib/view-types.d.ts +6 -0
  90. package/package.json +1 -1
@@ -221,6 +221,16 @@ function assertUniqueUnifiedName(name, meta, doc, exceptIds, agent) {
221
221
  if (provider && !exceptIds?.has(provider.id))
222
222
  throw new Error(`Account '${name}' already exists.`);
223
223
  }
224
+ /**
225
+ * Validate a native account NAME (charset + per-harness uniqueness) WITHOUT a
226
+ * known identity — the pre-flight `agents accounts connect` runs before it
227
+ * installs a home and drives a login, so a bad/colliding name fails before any
228
+ * side effect instead of orphaning a freshly-minted home (PHNX-3940).
229
+ */
230
+ export function assertNativeAccountNameAvailable(name, agent) {
231
+ assertNativeLabel(name);
232
+ assertUniqueUnifiedName(name, readMeta(), undefined, undefined, agent);
233
+ }
224
234
  export function addNativeAccount(name, agent, identityKey, identityLabel, scope) {
225
235
  assertNativeLabel(name);
226
236
  const meta = readMeta();
@@ -283,6 +293,87 @@ export function labelNativeAccount(agent, identityKey, identityLabel, label, sco
283
293
  });
284
294
  return { ...matches[0], name: resolvedLabel, identityLabel, scope: rowScope };
285
295
  }
296
+ /**
297
+ * Record THIS box's connect home for an account (PHNX-3940). Device-scoped: the
298
+ * home a box minted for an account is not assumed to exist on any other box, so
299
+ * it lives in the device doc keyed by the stable account id, never on the
300
+ * fleet-synced central identity row. Idempotent. A reconnect reads it back via
301
+ * {@link nativeAccountHome} to reuse the exact home even when the credential has
302
+ * expired and local signed-in discovery can no longer find it.
303
+ */
304
+ export function setNativeAccountHome(accountId, installationLabel) {
305
+ updateMeta(current => ({
306
+ ...current,
307
+ deviceAccounts: {
308
+ ...current.deviceAccounts,
309
+ homes: { ...current.deviceAccounts?.homes, [accountId]: installationLabel },
310
+ },
311
+ }));
312
+ }
313
+ /** This box's recorded connect home for an account, or null. */
314
+ export function nativeAccountHome(accountId, meta) {
315
+ return meta.deviceAccounts?.homes?.[accountId] ?? null;
316
+ }
317
+ /**
318
+ * Every installation label THIS box has recorded as SOME account's connect home
319
+ * (PHNX-3940). These are identity-bearing and MUST NEVER be re-minted for a new
320
+ * account — the safe-allocation invariant that stops a new connect from
321
+ * overwriting another account's login.
322
+ */
323
+ export function ownedConnectHomeLabels(meta) {
324
+ return new Set(Object.values(meta.deviceAccounts?.homes ?? {}));
325
+ }
326
+ function pendingConnectKey(agent, name) {
327
+ return `${agent}:${name.toLowerCase()}`;
328
+ }
329
+ /** This box's in-flight connect slot for `(agent, name)`, or null. */
330
+ export function pendingConnectSlot(agent, name, meta) {
331
+ return meta.deviceAccounts?.pendingConnects?.[pendingConnectKey(agent, name)] ?? null;
332
+ }
333
+ /** Record an in-flight connect slot for `(agent, name)` (device-scoped). */
334
+ export function setPendingConnectSlot(agent, name, slot) {
335
+ updateMeta(current => ({
336
+ ...current,
337
+ deviceAccounts: {
338
+ ...current.deviceAccounts,
339
+ pendingConnects: { ...current.deviceAccounts?.pendingConnects, [pendingConnectKey(agent, name)]: slot },
340
+ },
341
+ }));
342
+ }
343
+ /** Clear the in-flight connect slot for `(agent, name)` once the account lands. */
344
+ export function clearPendingConnectSlot(agent, name) {
345
+ updateMeta(current => {
346
+ const pendingConnects = { ...current.deviceAccounts?.pendingConnects };
347
+ delete pendingConnects[pendingConnectKey(agent, name)];
348
+ return { ...current, deviceAccounts: { ...current.deviceAccounts, pendingConnects } };
349
+ });
350
+ }
351
+ /**
352
+ * Set the per-harness default account by NAME only when none is configured
353
+ * (PHNX-3940). Never overrides an existing choice — a first connect selecting a
354
+ * default is a convenience, not a takeover. Returns whether it set the default.
355
+ *
356
+ * The check-then-write is performed INSIDE the `updateMeta` callback so two
357
+ * concurrent callers (concurrent Promises or back-to-back calls on an async
358
+ * boundary) cannot both observe "no default" and then both set themselves. Only
359
+ * the first write wins; the second callback sees the first's write and returns
360
+ * the current state unchanged.
361
+ */
362
+ export function setDefaultAccountIfAbsent(agent, name) {
363
+ let set = false;
364
+ updateMeta(current => {
365
+ if (current.accounts?.defaults?.[agent])
366
+ return current; // already set — no-op
367
+ if (current.agents?.[agent] || current.isolatedAgents?.[agent])
368
+ return current; // preserve a legacy home default
369
+ set = true;
370
+ return {
371
+ ...current,
372
+ accounts: { ...current.accounts, defaults: { ...current.accounts?.defaults, [agent]: name } },
373
+ };
374
+ });
375
+ return set;
376
+ }
286
377
  export function bindAccount(nameOrId, target, preferAgent) {
287
378
  const meta = readMeta();
288
379
  // Scope to the harness being bound to: a bare identity selector matches every
@@ -463,16 +554,20 @@ export function removeAccount(name, base = getUserAgentsDir()) {
463
554
  // Sweep every row for the identity (PHNX-3206) from its owning store (PHNX-3315).
464
555
  const rowScope = rows[0].scope;
465
556
  updateMeta(current => {
557
+ // Drop this box's connect-home record for the removed account (PHNX-3940).
558
+ const homes = { ...current.deviceAccounts?.homes };
559
+ for (const id of ids)
560
+ delete homes[id];
466
561
  if (rowScope === 'device') {
467
562
  const accounts = { ...current.deviceAccounts?.native };
468
563
  for (const id of ids)
469
564
  delete accounts[id];
470
- return { ...current, deviceAccounts: { ...current.deviceAccounts, native: accounts } };
565
+ return { ...current, deviceAccounts: { ...current.deviceAccounts, native: accounts, homes } };
471
566
  }
472
567
  const accounts = { ...current.accounts?.native };
473
568
  for (const id of ids)
474
569
  delete accounts[id];
475
- return { ...current, accounts: { ...current.accounts, native: accounts } };
570
+ return { ...current, accounts: { ...current.accounts, native: accounts }, deviceAccounts: { ...current.deviceAccounts, homes } };
476
571
  });
477
572
  return;
478
573
  }
@@ -402,7 +402,7 @@ export declare function collectHarnessCandidates(agentIds?: AgentId[]): Promise<
402
402
  * (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
403
403
  * Returns null when nothing matches, so the caller can fall back and warn.
404
404
  */
405
- export declare function matchAccountVersion(candidates: RotateCandidate[], account: string): string | null;
405
+ export declare function matchAccountVersion(candidates: RotateCandidate[], account: string, preferredLabel?: string | null): string | null;
406
406
  /**
407
407
  * Resolve a routine's `account:` pin (login email or account key) to the
408
408
  * installed version currently holding that account. Thin I/O wrapper over
@@ -412,7 +412,7 @@ export declare function matchAccountVersion(candidates: RotateCandidate[], accou
412
412
  * (RUSH-1957): the pinned run never rotates and never lands on another
413
413
  * routine's credential.
414
414
  */
415
- export declare function resolveAccountVersion(agent: AgentId, account: string): Promise<string | null>;
415
+ export declare function resolveAccountVersion(agent: AgentId, account: string, preferredLabel?: string | null): Promise<string | null>;
416
416
  /**
417
417
  * Pick a healthy version for `agent` using weighted random by remaining
418
418
  * capacity. See `pickBalancedCandidate` for algorithm details.
@@ -775,13 +775,13 @@ export async function collectHarnessCandidates(agentIds = ALL_AGENT_IDS) {
775
775
  * (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
776
776
  * Returns null when nothing matches, so the caller can fall back and warn.
777
777
  */
778
- export function matchAccountVersion(candidates, account) {
778
+ export function matchAccountVersion(candidates, account, preferredLabel) {
779
779
  const needle = account.trim().toLowerCase();
780
780
  if (!needle)
781
781
  return null;
782
- const match = candidates.find((c) => c.signedIn &&
782
+ const matching = candidates.filter((c) => c.signedIn &&
783
783
  (c.email?.toLowerCase() === needle || c.accountKey?.toLowerCase() === needle));
784
- return match?.version ?? null;
784
+ return matching.find(candidate => candidate.version === preferredLabel)?.version ?? matching[0]?.version ?? null;
785
785
  }
786
786
  /**
787
787
  * Resolve a routine's `account:` pin (login email or account key) to the
@@ -792,9 +792,9 @@ export function matchAccountVersion(candidates, account) {
792
792
  * (RUSH-1957): the pinned run never rotates and never lands on another
793
793
  * routine's credential.
794
794
  */
795
- export async function resolveAccountVersion(agent, account) {
795
+ export async function resolveAccountVersion(agent, account, preferredLabel) {
796
796
  const candidates = await collectRunCandidates(agent);
797
- return matchAccountVersion(candidates, account);
797
+ return matchAccountVersion(candidates, account, preferredLabel);
798
798
  }
799
799
  /**
800
800
  * Pick a healthy version for `agent` using weighted random by remaining
@@ -0,0 +1,9 @@
1
+ import { type AgentId } from '../types.js';
2
+ export declare function authLockFilePath(agent: AgentId, stateDir?: string): string;
3
+ export interface AuthOperationLock {
4
+ readonly signal: AbortSignal;
5
+ assertHeld(): void;
6
+ release(): void;
7
+ }
8
+ /** Uses the same lock primitive as configuration writes; no fail-open fallback. */
9
+ export declare function acquireAuthOperationLock(agent: AgentId, stateDir?: string): AuthOperationLock;
@@ -0,0 +1,55 @@
1
+ /** Cross-process exclusion for native connect/logout, including browser waits. */
2
+ import * as path from 'node:path';
3
+ import lockfile from 'proper-lockfile';
4
+ import { ensureLockTarget } from '../fs-atomic.js';
5
+ import { getRuntimeStateDir } from '../state.js';
6
+ import { isAgentId } from '../types.js';
7
+ export function authLockFilePath(agent, stateDir) {
8
+ if (!isAgentId(agent))
9
+ throw new Error('Unknown authentication harness.');
10
+ return path.join(stateDir ?? getRuntimeStateDir(), `auth-op-lock-${agent}.json`);
11
+ }
12
+ /** Uses the same lock primitive as configuration writes; no fail-open fallback. */
13
+ export function acquireAuthOperationLock(agent, stateDir) {
14
+ const target = authLockFilePath(agent, stateDir);
15
+ ensureLockTarget(target, '{}', 0o700);
16
+ let compromised = null;
17
+ const controller = new AbortController();
18
+ let unlock;
19
+ try {
20
+ unlock = lockfile.lockSync(target, {
21
+ stale: 10 * 60_000,
22
+ update: 1_000,
23
+ // The primitive refreshes the lock while a browser flow awaits input.
24
+ // Acquisition is immediate: never silently queue native logins.
25
+ onCompromised: (error) => {
26
+ compromised = new Error(`Authentication lock was lost: ${error.message}`);
27
+ controller.abort(compromised);
28
+ },
29
+ });
30
+ }
31
+ catch (error) {
32
+ throw new Error(`Cannot safely start ${agent} authentication: another sign-in or sign-out may be in progress. ${error.message}`);
33
+ }
34
+ let released = false;
35
+ return {
36
+ signal: controller.signal,
37
+ assertHeld() {
38
+ controller.signal.throwIfAborted();
39
+ if (released)
40
+ throw new Error('Authentication lock was already released.');
41
+ },
42
+ release() {
43
+ if (released)
44
+ return;
45
+ released = true;
46
+ try {
47
+ unlock();
48
+ }
49
+ finally {
50
+ if (compromised)
51
+ throw new Error(`Authentication lock was lost: ${compromised.message}`);
52
+ }
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,170 @@
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
+ export declare function loginInvocation(agent: AgentId): LoginInvocation;
22
+ /**
23
+ * Mint a fresh, opaque installation slot. `acct-<hex>` — alnum + hyphen only, so
24
+ * it satisfies VERSION_RE, and is visibly an account slot, not a release. It is
25
+ * RANDOM, never derived from the account name or identity: a name-derived slot
26
+ * recomputes an already-occupied home after a rename and lets a new connect
27
+ * overwrite another account's login (PHNX-3940 security fix). Retry-idempotency
28
+ * is instead provided by the device-scoped pending-connect map, and collision
29
+ * safety by allocating around occupied slots — see `allocateConnectSlot`.
30
+ */
31
+ export declare function mintConnectLabel(): string;
32
+ export type ConnectMode = 'new' | 'reconnect';
33
+ export interface ConnectPlan {
34
+ mode: ConnectMode;
35
+ agent: AgentId;
36
+ /** The installation label whose isolated home hosts this account's login. */
37
+ label: string;
38
+ /** The human account name to register (new) or already registered (reconnect). */
39
+ name?: string;
40
+ /** For reconnect: the existing account being re-authenticated. */
41
+ existing?: NativeAccount;
42
+ /**
43
+ * For reconnect: the label was ADOPTED (freshly minted) because the existing
44
+ * account carried no recorded home and none was discoverable — so its identity
45
+ * must still be re-verified against the login, but there is no prior home to
46
+ * reuse. `false` when an existing home is being reused.
47
+ */
48
+ adoptedHome?: boolean;
49
+ }
50
+ /**
51
+ * Resolve the home to reuse for reconnecting an existing account: THIS box's
52
+ * recorded connect home first (device-scoped, survives an expired credential),
53
+ * else a currently-signed-in local home carrying its identity. Returns null when
54
+ * neither is known — a legacy account with no recorded or live home, for which
55
+ * connect adopts a fresh home.
56
+ */
57
+ export declare function resolveExistingHomeLabel(existing: NativeAccount, deviceHome: string | null, signedInHomes: Array<{
58
+ agent: AgentId;
59
+ identityKey: string;
60
+ label: string;
61
+ }>): string | null;
62
+ /**
63
+ * Decide the release-independent connect plan (pure). `existing` is the native
64
+ * account the name resolves to for THIS harness (or null for a new connect);
65
+ * `existingHomeLabel` is its reusable home from {@link resolveExistingHomeLabel};
66
+ * `freshSlot` is a safely-allocated opaque slot (see `allocateConnectSlot`) used
67
+ * for a new connect or an adopted reconnect home — NEVER a name-derived label.
68
+ */
69
+ export declare function planConnect(input: {
70
+ agent: AgentId;
71
+ name?: string;
72
+ existing: NativeAccount | null;
73
+ existingHomeLabel: string | null;
74
+ freshSlot: string;
75
+ }): ConnectPlan;
76
+ /**
77
+ * Safely allocate the opaque slot for a NEW connect or an ADOPTED reconnect home
78
+ * (PHNX-3940 security fix). Never reuses an identity-bearing slot:
79
+ *
80
+ * - `occupied` is every slot already owned by an account's home OR currently
81
+ * signed in — an allocated slot is guaranteed disjoint from it, so a new
82
+ * connect can never land on another account's home (the rename-collision flaw).
83
+ * - A named connect first reuses its device-scoped PENDING slot (a prior
84
+ * failed/cancelled attempt) IF that slot is not occupied, so a retry lands in
85
+ * the same fresh home instead of orphaning a new one.
86
+ * - Otherwise it mints random slots until one is neither occupied nor installed.
87
+ */
88
+ export declare function allocateConnectSlot(input: {
89
+ agent: AgentId;
90
+ name?: string;
91
+ existing: NativeAccount | null;
92
+ occupied: ReadonlySet<string>;
93
+ installedLabels: ReadonlySet<string>;
94
+ pending: string | null;
95
+ mint?: () => string;
96
+ }): string;
97
+ /**
98
+ * Fail-closed identity check after the login completes (pure).
99
+ *
100
+ * - Not signed in (no live credential) → the login did not complete; a metadata
101
+ * identity key alone is NOT proof, so `signedIn` is required.
102
+ * - Reconnect whose completed identity differs from the account's → REFUSE
103
+ * registering the binding. The native credential in that home DID change (the
104
+ * user signed in), so the message says the account BINDING is left unchanged —
105
+ * it does not claim nothing happened.
106
+ * A new connect accepts whatever identity signed in (that is the account being
107
+ * created); the caller registers it.
108
+ */
109
+ export declare function verifyConnectedIdentity(plan: ConnectPlan, observed: Pick<ObservedIdentity, 'identityKey' | 'signedIn'>): void;
110
+ /** Resolve the existing native account a connect `name` refers to for a harness. */
111
+ export declare function findConnectAccount(agent: AgentId, name: string | undefined, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): NativeAccount | null;
112
+ /** The observed identity of a completed login. */
113
+ export interface ObservedIdentity {
114
+ identityKey: string | null;
115
+ email: string | null;
116
+ releaseVersion: string | null;
117
+ /** Live credential presence — proof of sign-in, not just a metadata identity claim. */
118
+ signedIn: boolean;
119
+ }
120
+ /**
121
+ * Side-effecting operations connect needs, injected so the planning/verification
122
+ * path is unit-testable and the parent can own the real E2E login+verify.
123
+ */
124
+ export interface ConnectRunners {
125
+ installedLabels(agent: AgentId): string[];
126
+ /** Install the CURRENT release into `label`'s isolated home (opaque label). */
127
+ install(agent: AgentId, label: string, onProgress?: (m: string) => void): Promise<{
128
+ success: boolean;
129
+ error?: string;
130
+ }>;
131
+ /** Launch the harness's native login under `label`'s home; resolves on exit. */
132
+ launchLogin(agent: AgentId, label: string, invocation: LoginInvocation, email?: string, signal?: AbortSignal): Promise<{
133
+ code: number | null;
134
+ }>;
135
+ observeIdentity(agent: AgentId, label: string): Promise<ObservedIdentity>;
136
+ signedInHomes(): Promise<Array<{
137
+ agent: AgentId;
138
+ identityKey: string;
139
+ label: string;
140
+ }>>;
141
+ }
142
+ export interface ConnectResult {
143
+ mode: ConnectMode;
144
+ agent: AgentId;
145
+ label: string;
146
+ name?: string;
147
+ identityKey: string;
148
+ email: string | null;
149
+ releaseVersion: string | null;
150
+ /** True when this connect became the harness's default (only set if none was configured). */
151
+ becameDefault: boolean;
152
+ /** For an UNNAMED connect: the command to name the login (no name was forced). */
153
+ nameHint?: string;
154
+ }
155
+ /**
156
+ * Drive one `agents accounts connect`. Pure decisions (plan, fail-closed verify)
157
+ * come from the helpers above; the install + login + identity read are injected.
158
+ *
159
+ * The default runners are loaded via dynamic import so this module — reachable
160
+ * from `account-registry` consumers — never statically pulls in the install
161
+ * engine or exec path and closes an import cycle (the intended pattern here).
162
+ *
163
+ * `stateDir` is only for tests: override the runtime-state dir used by the
164
+ * per-harness auth-operation mutex (defaults to the real runtime state dir).
165
+ */
166
+ export declare function runConnect(agent: AgentId, name: string | undefined, opts: {
167
+ meta: Pick<Meta, 'accounts' | 'deviceAccounts'>;
168
+ onProgress?: (m: string) => void;
169
+ stateDir?: string;
170
+ }, runners?: ConnectRunners): Promise<ConnectResult>;