@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.
- package/CHANGELOG.md +57 -0
- package/README.md +80 -53
- package/dist/bootstrap.d.ts +7 -6
- package/dist/bootstrap.js +20 -27
- package/dist/cli/command-registry.js +5 -1
- package/dist/commands/accounts.d.ts +6 -13
- package/dist/commands/accounts.js +330 -143
- package/dist/commands/apply.js +1 -1
- package/dist/commands/auth-mint.d.ts +7 -3
- package/dist/commands/auth-mint.js +21 -9
- package/dist/commands/auth.js +2 -3
- package/dist/commands/browser.js +10 -10
- package/dist/commands/daemon.d.ts +7 -3
- package/dist/commands/daemon.js +40 -25
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/exec.js +87 -39
- package/dist/commands/fleet-capture.js +21 -2
- package/dist/commands/harness-wizard.js +2 -2
- package/dist/commands/lease.js +7 -12
- package/dist/commands/profiles.d.ts +2 -2
- package/dist/commands/profiles.js +20 -13
- package/dist/commands/repo.js +3 -3
- package/dist/commands/run-account-picker.d.ts +3 -3
- package/dist/commands/run-account-picker.js +29 -30
- package/dist/commands/secrets-passthrough.d.ts +20 -0
- package/dist/commands/secrets-passthrough.js +43 -0
- package/dist/commands/setup-accounts.d.ts +3 -2
- package/dist/commands/setup-accounts.js +4 -4
- package/dist/commands/setup-secrets.d.ts +16 -21
- package/dist/commands/setup-secrets.js +43 -224
- package/dist/commands/ssh.js +2 -2
- package/dist/commands/sync.js +1 -1
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.js +43 -40
- package/dist/commands/versions.d.ts +11 -11
- package/dist/commands/versions.js +131 -83
- package/dist/commands/view.d.ts +1 -9
- package/dist/commands/view.js +41 -55
- package/dist/commands/webhook.js +5 -5
- package/dist/commands/workflows.js +2 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +13 -42
- package/dist/lib/account-capabilities.d.ts +1 -1
- package/dist/lib/account-capabilities.js +1 -1
- package/dist/lib/account-catalog.d.ts +121 -6
- package/dist/lib/account-catalog.js +421 -7
- package/dist/lib/account-registry.d.ts +8 -30
- package/dist/lib/account-registry.js +36 -79
- package/dist/lib/account-schema.d.ts +1 -1
- package/dist/lib/account-schema.js +1 -1
- package/dist/lib/accounting/account-pool-collect.js +16 -4
- package/dist/lib/accounting/rotate.d.ts +20 -7
- package/dist/lib/accounting/rotate.js +89 -13
- package/dist/lib/accounting/usage.js +11 -16
- package/dist/lib/accounts/add.d.ts +138 -0
- package/dist/lib/accounts/add.js +651 -0
- package/dist/lib/accounts/migrate.d.ts +117 -0
- package/dist/lib/accounts/migrate.js +536 -0
- package/dist/lib/accounts/slots.d.ts +13 -0
- package/dist/lib/accounts/slots.js +100 -0
- package/dist/lib/agent-spec/agents.d.ts +16 -0
- package/dist/lib/agent-spec/agents.js +32 -4
- package/dist/lib/app-bundle-install.js +5 -4
- package/dist/lib/auth-health.d.ts +3 -0
- package/dist/lib/auth-health.js +11 -0
- package/dist/lib/auth-mint.d.ts +52 -16
- package/dist/lib/auth-mint.js +117 -39
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +5 -5
- package/dist/lib/byok-usage.js +3 -3
- package/dist/lib/claude-account-token.d.ts +47 -3
- package/dist/lib/claude-account-token.js +172 -50
- package/dist/lib/cloud/antigravity.js +4 -4
- package/dist/lib/cloud/cursor.js +4 -4
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +6 -6
- package/dist/lib/crabbox/runtimes.d.ts +3 -3
- package/dist/lib/crabbox/runtimes.js +5 -5
- package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
- package/dist/lib/daemon/account-state-daemon-service.js +199 -3
- package/dist/lib/daemon/auth-sync-service.js +28 -1
- package/dist/lib/daemon/daemon.d.ts +6 -16
- package/dist/lib/daemon/daemon.js +52 -97
- package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
- package/dist/lib/daemon/daemon.test-fixture.js +22 -40
- package/dist/lib/daemon/harness-update-service.d.ts +1 -1
- package/dist/lib/daemon/harness-update-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +15 -3
- package/dist/lib/daemon/runner.js +49 -24
- package/dist/lib/daemon-health.d.ts +1 -2
- package/dist/lib/daemon-health.js +7 -6
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +1 -11
- package/dist/lib/daemon-webhooks.d.ts +8 -7
- package/dist/lib/daemon-webhooks.js +10 -9
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/harness-inventory.d.ts +39 -0
- package/dist/lib/devices/harness-inventory.js +126 -4
- package/dist/lib/doctor-diff.js +2 -1
- package/dist/lib/exec-account-home.d.ts +38 -0
- package/dist/lib/exec-account-home.js +164 -0
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +101 -31
- package/dist/lib/fleet/apply.d.ts +2 -2
- package/dist/lib/fleet/apply.js +4 -4
- package/dist/lib/fleet/auth-sync.js +4 -3
- package/dist/lib/fleet-shared-repo-sync.js +6 -9
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapter.js +10 -6
- package/dist/lib/harness/adapters/grok.js +8 -3
- package/dist/lib/harness/adapters/muse.js +1 -1
- package/dist/lib/harness/adapters/opencode.js +12 -3
- package/dist/lib/harness-auth-capabilities.d.ts +61 -0
- package/dist/lib/harness-auth-capabilities.js +52 -0
- package/dist/lib/helper-versions.d.ts +6 -4
- package/dist/lib/helper-versions.js +5 -4
- package/dist/lib/hosts/credential-transport.d.ts +10 -0
- package/dist/lib/hosts/credential-transport.js +37 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +13 -6
- package/dist/lib/identity/client.d.ts +3 -3
- package/dist/lib/identity/client.js +3 -3
- package/dist/lib/installations/index.d.ts +1 -1
- package/dist/lib/installations/index.js +1 -1
- package/dist/lib/installations/migrate.js +9 -0
- package/dist/lib/installations/resolve.js +1 -1
- package/dist/lib/installations/shims.d.ts +15 -0
- package/dist/lib/installations/shims.js +69 -0
- package/dist/lib/installations/store.d.ts +43 -0
- package/dist/lib/installations/store.js +75 -7
- package/dist/lib/installations/versions.js +1 -1
- package/dist/lib/menubar/install-menubar.d.ts +3 -3
- package/dist/lib/menubar/install-menubar.js +3 -3
- package/dist/lib/native-accounts.d.ts +35 -0
- package/dist/lib/native-accounts.js +41 -0
- package/dist/lib/net-close.d.ts +11 -0
- package/dist/lib/net-close.js +25 -0
- package/dist/lib/openclaw-keychain.js +27 -1
- package/dist/lib/profiles.js +22 -9
- package/dist/lib/project-resources.js +1 -1
- package/dist/lib/reserved-stores.d.ts +53 -0
- package/dist/lib/reserved-stores.js +120 -0
- package/dist/lib/secrets-client.d.ts +191 -0
- package/dist/lib/secrets-client.js +710 -0
- package/dist/lib/secrets-policy.d.ts +222 -0
- package/dist/lib/secrets-policy.js +483 -0
- package/dist/lib/secrets-types.d.ts +188 -0
- package/dist/lib/secrets-types.js +24 -0
- package/dist/lib/service-manifest.js +3 -4
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +47 -14
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/sha256-asset.d.ts +11 -16
- package/dist/lib/sha256-asset.js +11 -16
- package/dist/lib/share/config.js +14 -5
- package/dist/lib/signin-badge.d.ts +26 -0
- package/dist/lib/signin-badge.js +42 -0
- package/dist/lib/staleness/detectors/workflows.js +3 -105
- package/dist/lib/staleness/writers/workflows.js +2 -1
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +18 -7
- package/dist/lib/sync-umbrella.js +2 -2
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +74 -33
- package/dist/lib/view-types.d.ts +6 -3
- package/dist/lib/workflows-registry.d.ts +71 -0
- package/dist/lib/workflows-registry.js +280 -0
- package/dist/lib/workflows.d.ts +21 -21
- package/dist/lib/workflows.js +12 -327
- package/package.json +2 -3
- package/scripts/postinstall.js +4 -29
- package/dist/commands/secrets-import.d.ts +0 -18
- package/dist/commands/secrets-import.js +0 -74
- package/dist/commands/secrets-migrate.d.ts +0 -25
- package/dist/commands/secrets-migrate.js +0 -334
- package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
- package/dist/commands/secrets-rotate-passphrase.js +0 -96
- package/dist/commands/secrets-sync.d.ts +0 -11
- package/dist/commands/secrets-sync.js +0 -153
- package/dist/commands/secrets-vault.d.ts +0 -10
- package/dist/commands/secrets-vault.js +0 -130
- package/dist/commands/secrets.d.ts +0 -212
- package/dist/commands/secrets.js +0 -3030
- package/dist/lib/accounts/connect.d.ts +0 -176
- package/dist/lib/accounts/connect.js +0 -453
- package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
- package/dist/lib/daemon/keychain-reap-service.js +0 -32
- package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
- package/dist/lib/daemon/secrets-broker-service.js +0 -51
- package/dist/lib/secrets/agent.d.ts +0 -358
- package/dist/lib/secrets/agent.js +0 -1291
- package/dist/lib/secrets/audit.d.ts +0 -46
- package/dist/lib/secrets/audit.js +0 -101
- package/dist/lib/secrets/bundles.d.ts +0 -290
- package/dist/lib/secrets/bundles.js +0 -1547
- package/dist/lib/secrets/download-keychain.d.ts +0 -47
- package/dist/lib/secrets/download-keychain.js +0 -70
- package/dist/lib/secrets/drivers/rush.d.ts +0 -14
- package/dist/lib/secrets/drivers/rush.js +0 -90
- package/dist/lib/secrets/fallback.d.ts +0 -48
- package/dist/lib/secrets/fallback.js +0 -48
- package/dist/lib/secrets/filestore.d.ts +0 -222
- package/dist/lib/secrets/filestore.js +0 -1099
- package/dist/lib/secrets/headless.d.ts +0 -43
- package/dist/lib/secrets/headless.js +0 -56
- package/dist/lib/secrets/icloud-import.d.ts +0 -79
- package/dist/lib/secrets/icloud-import.js +0 -206
- package/dist/lib/secrets/index.d.ts +0 -451
- package/dist/lib/secrets/index.js +0 -1568
- package/dist/lib/secrets/install-helper.d.ts +0 -72
- package/dist/lib/secrets/install-helper.js +0 -245
- package/dist/lib/secrets/lease.d.ts +0 -25
- package/dist/lib/secrets/lease.js +0 -44
- package/dist/lib/secrets/linux.d.ts +0 -77
- package/dist/lib/secrets/linux.js +0 -393
- package/dist/lib/secrets/list-filter.d.ts +0 -109
- package/dist/lib/secrets/list-filter.js +0 -261
- package/dist/lib/secrets/mcp.d.ts +0 -93
- package/dist/lib/secrets/mcp.js +0 -211
- package/dist/lib/secrets/profiles.d.ts +0 -10
- package/dist/lib/secrets/profiles.js +0 -13
- package/dist/lib/secrets/push.d.ts +0 -133
- package/dist/lib/secrets/push.js +0 -273
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
- package/dist/lib/secrets/rc-hygiene.js +0 -143
- package/dist/lib/secrets/read-backoff.d.ts +0 -27
- package/dist/lib/secrets/read-backoff.js +0 -64
- package/dist/lib/secrets/reaper.d.ts +0 -90
- package/dist/lib/secrets/reaper.js +0 -243
- package/dist/lib/secrets/remote.d.ts +0 -152
- package/dist/lib/secrets/remote.js +0 -344
- package/dist/lib/secrets/reserved-sync.d.ts +0 -66
- package/dist/lib/secrets/reserved-sync.js +0 -147
- package/dist/lib/secrets/scope.d.ts +0 -26
- package/dist/lib/secrets/scope.js +0 -29
- package/dist/lib/secrets/session-store.d.ts +0 -107
- package/dist/lib/secrets/session-store.js +0 -342
- package/dist/lib/secrets/sync-backend.d.ts +0 -48
- package/dist/lib/secrets/sync-backend.js +0 -13
- package/dist/lib/secrets/sync-commands.d.ts +0 -21
- package/dist/lib/secrets/sync-commands.js +0 -21
- package/dist/lib/secrets/sync.d.ts +0 -48
- package/dist/lib/secrets/sync.js +0 -237
- package/dist/lib/secrets/unlock-hints.d.ts +0 -27
- package/dist/lib/secrets/unlock-hints.js +0 -36
- package/dist/lib/secrets/usage-db.d.ts +0 -46
- package/dist/lib/secrets/usage-db.js +0 -96
- package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
- package/dist/lib/secrets/vault-age-helper.js +0 -34
- package/dist/lib/secrets/vault.d.ts +0 -49
- package/dist/lib/secrets/vault.js +0 -399
- package/dist/lib/secrets/windows.d.ts +0 -81
- package/dist/lib/secrets/windows.js +0 -556
- package/scripts/install-helper.js +0 -97
- /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
- /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { AGENT_IDS } from '../types.js';
|
|
5
|
+
import { nativeAccountCapability, nativeAccountNamingRefusal, nativeIdentityKey } from '../account-capabilities.js';
|
|
6
|
+
import { getAccountProvider } from '../account-provider-registry.js';
|
|
7
|
+
import { listNativeAccounts } from '../account-registry.js';
|
|
8
|
+
import { harnessAuth, harnessWorkerKinds, LOGIN_INVOCATIONS } from '../harness-auth-capabilities.js';
|
|
9
|
+
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
|
|
10
|
+
import { machineId } from '../machine-id.js';
|
|
11
|
+
import { readMeta, updateMeta } from '../state.js';
|
|
12
|
+
import { ambientClaudeToken, loginHint } from '../signin-badge.js';
|
|
13
|
+
import { acquireAuthOperationLock } from './auth-operation-lock.js';
|
|
14
|
+
import { ensureSlot, readSlots, recordSlot, slotDir } from './slots.js';
|
|
15
|
+
export { LOGIN_INVOCATIONS };
|
|
16
|
+
/**
|
|
17
|
+
* `agents accounts add <harness> [name]` / `agents accounts login <harness>#<name>`
|
|
18
|
+
* — the account onboarding front door (PHNX-3940, track T4; successor of the
|
|
19
|
+
* `connect` module, kept as the `runConnect` alias for one release).
|
|
20
|
+
*
|
|
21
|
+
* An account is a credential SLOT, not an installation: the harness has ONE
|
|
22
|
+
* managed installation (`ensureHarnessInstallation`, label `main`); every
|
|
23
|
+
* account gets a HOME-shaped slot under `~/.agents/.history/accounts/<harness>/<id>/`
|
|
24
|
+
* with no binary in it. The native login runs with HOME = slot (plus the
|
|
25
|
+
* harness's `slotEnv` pin), so the OAuth credential is minted directly into the
|
|
26
|
+
* slot and never copied.
|
|
27
|
+
*
|
|
28
|
+
* Headed devices only for harnesses with a portable worker credential
|
|
29
|
+
* (credential-management.md invariant 7): a worker is refused before any slot,
|
|
30
|
+
* install, or browser login. Per-device harnesses (worker `none`, e.g. kimi)
|
|
31
|
+
* may `accounts login` on any box — they are logged in per box by design.
|
|
32
|
+
*
|
|
33
|
+
* The worker credential is minted/collected per account id into the reserved
|
|
34
|
+
* `__<harness>__` store (claude: `setup-token` drive; api-key harnesses:
|
|
35
|
+
* `--api-key` or an interactive prompt — never derived from OAuth). The account
|
|
36
|
+
* row carries only the `{bundle, key}` pointer, never the secret.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Ambient provider-credential env vars stripped before a native login spawn, so
|
|
40
|
+
* the OAuth flow authenticates as the human's identity rather than being
|
|
41
|
+
* short-circuited by an injected API key / setup-token that would impersonate a
|
|
42
|
+
* different account into the fresh slot. The target harness's own
|
|
43
|
+
* `api-key:<ENV>` worker env (from HARNESS_AUTH) is stripped on top of this.
|
|
44
|
+
*/
|
|
45
|
+
const PROVIDER_AUTH_ENV_KEYS = [
|
|
46
|
+
'ANTHROPIC_API_KEY',
|
|
47
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
48
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
49
|
+
'OPENAI_API_KEY',
|
|
50
|
+
'CODEX_API_KEY',
|
|
51
|
+
'XAI_API_KEY',
|
|
52
|
+
'CURSOR_API_KEY',
|
|
53
|
+
'FACTORY_API_KEY',
|
|
54
|
+
];
|
|
55
|
+
/** The env var an api-key worker credential injects as, from HARNESS_AUTH. */
|
|
56
|
+
export function workerApiKeyEnv(agent) {
|
|
57
|
+
for (const kind of harnessWorkerKinds(agent)) {
|
|
58
|
+
if (!kind.startsWith('api-key:'))
|
|
59
|
+
continue;
|
|
60
|
+
const env = kind.slice('api-key:'.length);
|
|
61
|
+
if (env !== 'provider')
|
|
62
|
+
return env;
|
|
63
|
+
// opencode keys are provider-specific; the provider registry's own adapter
|
|
64
|
+
// names the harness's default env (opencode → OPENCODE_API_KEY).
|
|
65
|
+
try {
|
|
66
|
+
return getAccountProvider(agent).envFor(agent, 'api-key');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function hasPortableWorkerKind(agent) {
|
|
75
|
+
return harnessWorkerKinds(agent).some((k) => k === 'setup-token' || k.startsWith('api-key:'));
|
|
76
|
+
}
|
|
77
|
+
/** Whether `accounts add` can drive this harness (nameable, version-scoped, a real login command). */
|
|
78
|
+
export function addSupported(agent) {
|
|
79
|
+
const cap = nativeAccountCapability(agent);
|
|
80
|
+
return cap.scope === 'version' && harnessAuth(agent).login !== null;
|
|
81
|
+
}
|
|
82
|
+
/** One-line description of how a harness's workers get their credential. */
|
|
83
|
+
export function workerProvisioningHint(agent) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
for (const kind of harnessWorkerKinds(agent)) {
|
|
86
|
+
if (kind === 'setup-token')
|
|
87
|
+
parts.push('a setup-token minted during add');
|
|
88
|
+
else if (kind.startsWith('api-key:')) {
|
|
89
|
+
const env = kind.slice('api-key:'.length);
|
|
90
|
+
parts.push(env === 'provider' ? 'a provider API key collected by add (--api-key)' : `the ${env} collected by add (--api-key)`);
|
|
91
|
+
}
|
|
92
|
+
else if (kind.startsWith('per-device')) {
|
|
93
|
+
parts.push(`per-device login on each worker (--per-device / agents fleet login ${agent})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (parts.length === 0)
|
|
97
|
+
return `${agent} has no portable credential — it logs in per box (agents fleet login ${agent})`;
|
|
98
|
+
return parts.join(', or ');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Named reason `accounts add` refuses a harness, or null when supported.
|
|
102
|
+
* Distinguishes "cannot isolate/name this login" (capability) from "no finite
|
|
103
|
+
* native login command" so the message is honest about which limit was hit.
|
|
104
|
+
*/
|
|
105
|
+
export function addRefusal(agent) {
|
|
106
|
+
const capabilityRefusal = nativeAccountNamingRefusal(agent);
|
|
107
|
+
if (capabilityRefusal)
|
|
108
|
+
return capabilityRefusal;
|
|
109
|
+
const cap = nativeAccountCapability(agent);
|
|
110
|
+
if (cap.scope !== 'version') {
|
|
111
|
+
return `${agent} authentication is ${cap.scope}-scoped; accounts add creates per-account slots, which needs a version-scoped login.`;
|
|
112
|
+
}
|
|
113
|
+
if (!harnessAuth(agent).login) {
|
|
114
|
+
const supported = supportedAddHarnesses().join(', ');
|
|
115
|
+
return `${agent} has no finite login command — ${workerProvisioningHint(agent)}. accounts add drives: ${supported}.`;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
export function supportedAddHarnesses() {
|
|
120
|
+
return AGENT_IDS.filter(addSupported).sort();
|
|
121
|
+
}
|
|
122
|
+
export function assertAddSupported(agent) {
|
|
123
|
+
const reason = addRefusal(agent);
|
|
124
|
+
if (reason)
|
|
125
|
+
throw new Error(reason);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Named reason add/login refuses on a non-headed device, or null when this box
|
|
129
|
+
* may run an interactive login. Workers never mint a native OAuth login
|
|
130
|
+
* (credential-management.md invariant 7 + Provisioning model).
|
|
131
|
+
*/
|
|
132
|
+
export function addWorkerRefusal(agent, name) {
|
|
133
|
+
const role = selfConfiguredDeviceRole();
|
|
134
|
+
if (isHeadedDeviceRole(role))
|
|
135
|
+
return null;
|
|
136
|
+
const device = machineId();
|
|
137
|
+
const roleLabel = role ?? 'unmarked';
|
|
138
|
+
const selector = name ? `${agent}#${name}` : agent;
|
|
139
|
+
const addCmd = name ? `agents accounts add ${agent} ${name}` : `agents accounts add ${agent} <name>`;
|
|
140
|
+
return `${selector}: this device is a worker (role ${roleLabel}) and never runs an interactive login. `
|
|
141
|
+
+ `Add the account on your personal device with \`${addCmd}\`; `
|
|
142
|
+
+ `workers are provisioned from the durable credential automatically `
|
|
143
|
+
+ `(${agent}: ${workerProvisioningHint(agent)}). `
|
|
144
|
+
+ `To mark this box as your interactive seat: agents devices role ${device} personal.`;
|
|
145
|
+
}
|
|
146
|
+
function assertAddAllowedOnThisDevice(agent, name) {
|
|
147
|
+
const reason = addWorkerRefusal(agent, name);
|
|
148
|
+
if (reason)
|
|
149
|
+
throw new Error(reason);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Refuse to mint a Claude setup-token while an ambient CLAUDE_CODE_OAUTH_TOKEN
|
|
153
|
+
* is set in this shell — the ambient token is ONE account, so minting under it
|
|
154
|
+
* would collapse every slot to that account (signin-badge.ts `ambientClaudeToken`).
|
|
155
|
+
*/
|
|
156
|
+
export function ambientTokenRefusal(agent, env = process.env) {
|
|
157
|
+
if (!ambientClaudeToken(agent, env))
|
|
158
|
+
return null;
|
|
159
|
+
return 'An ambient CLAUDE_CODE_OAUTH_TOKEN is set in this shell; minting under it would collapse every slot to that one account. '
|
|
160
|
+
+ 'Unset it and re-run, or pass --no-worker-token to skip minting.';
|
|
161
|
+
}
|
|
162
|
+
export function loginInvocation(agent) {
|
|
163
|
+
const invocation = LOGIN_INVOCATIONS[agent];
|
|
164
|
+
if (invocation)
|
|
165
|
+
return invocation;
|
|
166
|
+
const login = harnessAuth(agent).login;
|
|
167
|
+
if (!login)
|
|
168
|
+
throw new Error(`No native login command is wired for ${agent}.`);
|
|
169
|
+
return { args: login };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Fail-closed identity check after the login completes (pure).
|
|
173
|
+
*
|
|
174
|
+
* - Not signed in (no live credential) → the login did not complete; a metadata
|
|
175
|
+
* identity key alone is NOT proof, so `signedIn` is required.
|
|
176
|
+
* - A re-auth (`existing`) whose completed identity differs from the account's
|
|
177
|
+
* → REFUSE: the account keeps pointing at its original identity.
|
|
178
|
+
* A new add accepts whatever identity signed in (that is the account being
|
|
179
|
+
* created); the caller registers it.
|
|
180
|
+
*/
|
|
181
|
+
export function verifyConnectedIdentity(ctx, observed) {
|
|
182
|
+
if (!observed.signedIn || !observed.identityKey) {
|
|
183
|
+
throw new Error(`No ${ctx.agent} login completed in the slot (no live credential). Nothing was registered.`);
|
|
184
|
+
}
|
|
185
|
+
if (ctx.existing && observed.identityKey !== ctx.existing.identityKey) {
|
|
186
|
+
throw new Error(`The ${ctx.agent} login that just completed is a different identity `
|
|
187
|
+
+ `(${observed.identityKey}) than account '${ctx.existing.name}' (${ctx.existing.identityKey}). `
|
|
188
|
+
+ `Account '${ctx.existing.name}' still points at its original identity; nothing was changed. `);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Resolve the existing native account a name refers to for a harness. */
|
|
192
|
+
export function findAddAccount(agent, name, meta) {
|
|
193
|
+
if (!name)
|
|
194
|
+
return null;
|
|
195
|
+
const needle = name.toLowerCase();
|
|
196
|
+
return listNativeAccounts(meta).find(a => a.agent === agent && (a.id === name || a.name.toLowerCase() === needle || a.identityLabel?.toLowerCase() === needle)) ?? null;
|
|
197
|
+
}
|
|
198
|
+
/** Write account-model-v2 fields onto the just-registered row (central or device-scoped store). */
|
|
199
|
+
function patchNativeAccountRow(accountId, patch) {
|
|
200
|
+
updateMeta((current) => {
|
|
201
|
+
if (current.accounts?.native?.[accountId]) {
|
|
202
|
+
const native = { ...current.accounts.native, [accountId]: { ...current.accounts.native[accountId], ...patch } };
|
|
203
|
+
return { ...current, accounts: { ...current.accounts, native } };
|
|
204
|
+
}
|
|
205
|
+
if (current.deviceAccounts?.native?.[accountId]) {
|
|
206
|
+
const native = { ...current.deviceAccounts.native, [accountId]: { ...current.deviceAccounts.native[accountId], ...patch } };
|
|
207
|
+
return { ...current, deviceAccounts: { ...current.deviceAccounts, native } };
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`Account row '${accountId}' vanished before v2 fields could be recorded.`);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Derive the account name for an unnamed add from the login's email local-part. */
|
|
213
|
+
function deriveAccountName(email) {
|
|
214
|
+
if (!email)
|
|
215
|
+
return null;
|
|
216
|
+
const local = email.split('@')[0].trim().toLowerCase().replace(/[^a-z0-9._-]/g, '-');
|
|
217
|
+
return local || null;
|
|
218
|
+
}
|
|
219
|
+
function removeSlotDir(dir) {
|
|
220
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Mint or collect the worker credential for a freshly-registered account and
|
|
224
|
+
* record the `{bundle, key}` pointer on its row. Claude drives `setup-token` in
|
|
225
|
+
* the slot; api-key harnesses take `--api-key` or an interactive prompt (never
|
|
226
|
+
* derived from OAuth). Returns the outcome + provisioning for the result.
|
|
227
|
+
*/
|
|
228
|
+
async function mintWorkerCredential(agent, account, home, opts, runners, warnings) {
|
|
229
|
+
const kinds = harnessWorkerKinds(agent);
|
|
230
|
+
if (opts.noWorkerToken) {
|
|
231
|
+
warnings.push(`No worker credential minted for ${agent}#${account.name}: workers can't run this account until one exists `
|
|
232
|
+
+ `(agents accounts login ${agent}#${account.name} re-mints). The account works on this device now.`);
|
|
233
|
+
return { outcome: 'skipped', provisioning: 'per-device' };
|
|
234
|
+
}
|
|
235
|
+
if (opts.perDevice) {
|
|
236
|
+
if (!kinds.some((k) => k.startsWith('per-device'))) {
|
|
237
|
+
throw new Error(`--per-device is only valid for a harness with a per-device worker path (codex); ${agent} is provisioned from ${workerProvisioningHint(agent)}.`);
|
|
238
|
+
}
|
|
239
|
+
return { outcome: 'per-device', provisioning: 'per-device' };
|
|
240
|
+
}
|
|
241
|
+
const { seedReservedStoreKey, seedReservedAuthToken, workerCredentialStoreKey, } = await import('../auth-mint.js');
|
|
242
|
+
if (kinds.includes('setup-token')) {
|
|
243
|
+
// Claude: a second browser grant, driven in the slot so the token is minted
|
|
244
|
+
// under the account's own identity.
|
|
245
|
+
const refusal = ambientTokenRefusal(agent, opts.env ?? process.env);
|
|
246
|
+
if (refusal)
|
|
247
|
+
throw new Error(refusal);
|
|
248
|
+
if (!runners.mintSetupToken)
|
|
249
|
+
throw new Error(`No setup-token mint driver available for ${agent}.`);
|
|
250
|
+
const token = await runners.mintSetupToken(agent, { home });
|
|
251
|
+
const key = workerCredentialStoreKey(agent, account.id);
|
|
252
|
+
const ref = seedReservedStoreKey(agent, 'setup-token', key, token);
|
|
253
|
+
// Legacy `auth` bundle key for this release, so the pre-v2 usage/probe and
|
|
254
|
+
// worker-inject readers keep resolving the account.
|
|
255
|
+
if (account.identityLabel)
|
|
256
|
+
seedReservedAuthToken(account.identityLabel, token);
|
|
257
|
+
return { outcome: 'minted', ref, provisioning: 'portable' };
|
|
258
|
+
}
|
|
259
|
+
const env = workerApiKeyEnv(agent);
|
|
260
|
+
if (env && kinds.some((k) => k.startsWith('api-key:'))) {
|
|
261
|
+
let value = opts.apiKey?.trim();
|
|
262
|
+
if (!value) {
|
|
263
|
+
if (!runners.promptApiKey) {
|
|
264
|
+
throw new Error(`${agent}'s worker credential is an ${env} API key. Pass --api-key <key> (or --no-worker-token to skip, --per-device for a codex ChatGPT-plan account).`);
|
|
265
|
+
}
|
|
266
|
+
const entered = await runners.promptApiKey(agent, env);
|
|
267
|
+
if (entered === null) {
|
|
268
|
+
warnings.push(`No ${env} collected for ${agent}#${account.name}: workers can't run this account until one is stored `
|
|
269
|
+
+ `(agents accounts login ${agent}#${account.name} --api-key <key>). The account works on this device now.`);
|
|
270
|
+
return { outcome: 'skipped', provisioning: 'per-device' };
|
|
271
|
+
}
|
|
272
|
+
value = entered.trim();
|
|
273
|
+
}
|
|
274
|
+
if (!value)
|
|
275
|
+
throw new Error(`${env} cannot be empty.`);
|
|
276
|
+
const key = workerCredentialStoreKey(agent, account.id);
|
|
277
|
+
const ref = seedReservedStoreKey(agent, 'api-key', key, value);
|
|
278
|
+
return { outcome: 'stored', ref, provisioning: 'portable' };
|
|
279
|
+
}
|
|
280
|
+
// worker: none (kimi/antigravity shape) — nothing portable exists.
|
|
281
|
+
return { outcome: 'per-device', provisioning: 'per-device' };
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Drive one `agents accounts add <harness> [name]`: headed gate → one managed
|
|
285
|
+
* installation → fresh slot → native login in the slot → register the row →
|
|
286
|
+
* mint/collect the worker credential → request a daemon reconcile.
|
|
287
|
+
*/
|
|
288
|
+
export async function runAdd(agent, name, opts, runners) {
|
|
289
|
+
assertAddSupported(agent);
|
|
290
|
+
// Owner rule (invariant 7): a worker NEVER runs an interactive login. Refuse
|
|
291
|
+
// before the lock, slot, install, or browser — no side effects on a worker.
|
|
292
|
+
assertAddAllowedOnThisDevice(agent, name);
|
|
293
|
+
const kinds = harnessWorkerKinds(agent);
|
|
294
|
+
if (opts.perDevice && !kinds.some((k) => k.startsWith('per-device'))) {
|
|
295
|
+
throw new Error(`--per-device is only valid for a harness with a per-device worker path (codex); ${agent} is provisioned from ${workerProvisioningHint(agent)}.`);
|
|
296
|
+
}
|
|
297
|
+
// The ambient-token refusal is checked up front (not at the mint step) so the
|
|
298
|
+
// user doesn't complete a browser login only to be refused at the end.
|
|
299
|
+
if (!opts.noWorkerToken && !opts.perDevice && kinds.includes('setup-token')) {
|
|
300
|
+
const refusal = ambientTokenRefusal(agent, opts.env ?? process.env);
|
|
301
|
+
if (refusal)
|
|
302
|
+
throw new Error(refusal);
|
|
303
|
+
}
|
|
304
|
+
const lock = acquireAuthOperationLock(agent, opts.stateDir);
|
|
305
|
+
try {
|
|
306
|
+
return await _runAddLocked(agent, name, opts, lock, runners);
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
lock.release();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function _runAddLocked(agent, name, opts, lock, runners) {
|
|
313
|
+
const run = runners ?? await defaultAddRunners();
|
|
314
|
+
const registry = await import('../account-registry.js');
|
|
315
|
+
lock.assertHeld();
|
|
316
|
+
// Re-read meta AFTER acquiring the lock so a concurrent add committed before
|
|
317
|
+
// we were serialized past it is visible (`opts.meta` is a pre-lock snapshot).
|
|
318
|
+
const meta = readMeta();
|
|
319
|
+
// Idempotency: a name that already resolves to an account of this harness is
|
|
320
|
+
// a re-auth, not an add.
|
|
321
|
+
const existing = findAddAccount(agent, name, meta);
|
|
322
|
+
if (existing) {
|
|
323
|
+
throw new Error(`${agent}#${existing.name} is already added. Re-auth with: agents accounts login ${agent}#${existing.name}`);
|
|
324
|
+
}
|
|
325
|
+
// Validate the requested NAME before any install, slot, or login — a bad or
|
|
326
|
+
// colliding name never mints an orphan slot and drives a login that can't be
|
|
327
|
+
// recorded.
|
|
328
|
+
if (name)
|
|
329
|
+
registry.assertNativeAccountNameAvailable(name, agent);
|
|
330
|
+
opts.onProgress?.(`Ensuring the ${agent} installation…`);
|
|
331
|
+
await run.ensureInstallation(agent, opts.onProgress);
|
|
332
|
+
lock.assertHeld();
|
|
333
|
+
// The slot is keyed by the account id, which only exists after registration —
|
|
334
|
+
// mint a pending id, then rename the dir onto the real id once the row lands.
|
|
335
|
+
const pendingId = crypto.randomUUID();
|
|
336
|
+
const slot = ensureSlot(agent, pendingId);
|
|
337
|
+
const fail = (err) => {
|
|
338
|
+
removeSlotDir(slot.slotDir);
|
|
339
|
+
throw err;
|
|
340
|
+
};
|
|
341
|
+
const invocation = loginInvocation(agent);
|
|
342
|
+
if (invocation.hint)
|
|
343
|
+
opts.onProgress?.(invocation.hint);
|
|
344
|
+
const login = await run.launchLogin(agent, { home: slot.slotDir, args: invocation.args, signal: lock.signal });
|
|
345
|
+
lock.assertHeld();
|
|
346
|
+
if (login.code !== 0) {
|
|
347
|
+
fail(new Error(`${agent} login did not complete (exit ${login.code ?? 'null'}). The slot was removed; re-run to try again.`));
|
|
348
|
+
}
|
|
349
|
+
const observed = await run.observeIdentity(agent, slot.slotDir);
|
|
350
|
+
lock.assertHeld();
|
|
351
|
+
try {
|
|
352
|
+
verifyConnectedIdentity({ agent, home: slot.slotDir }, observed);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
fail(err);
|
|
356
|
+
}
|
|
357
|
+
const identityKey = observed.identityKey;
|
|
358
|
+
// The completed login may be an identity already registered under another
|
|
359
|
+
// name — adding it again would split one identity across two slots.
|
|
360
|
+
const duplicate = listNativeAccounts(readMeta()).find(a => a.agent === agent && a.identityKey === identityKey);
|
|
361
|
+
if (duplicate) {
|
|
362
|
+
fail(new Error(`This ${agent} login is already added as '${duplicate.name}'. Re-auth with: agents accounts login ${agent}#${duplicate.name}`));
|
|
363
|
+
}
|
|
364
|
+
const resolvedName = name ?? deriveAccountName(observed.email);
|
|
365
|
+
if (!resolvedName) {
|
|
366
|
+
fail(new Error(`Signed in, but ${agent} exposed no email to derive an account name from. `
|
|
367
|
+
+ `Re-run with an explicit name: agents accounts add ${agent} <name>`));
|
|
368
|
+
}
|
|
369
|
+
if (!name)
|
|
370
|
+
registry.assertNativeAccountNameAvailable(resolvedName, agent);
|
|
371
|
+
const account = registry.addNativeAccount(resolvedName, agent, identityKey, observed.email ?? undefined, 'version');
|
|
372
|
+
const finalDir = slotDir(agent, account.id);
|
|
373
|
+
fs.renameSync(slot.slotDir, finalDir);
|
|
374
|
+
const record = {
|
|
375
|
+
accountId: account.id,
|
|
376
|
+
slotDir: finalDir,
|
|
377
|
+
authMode: hasPortableWorkerKind(agent) ? 'native' : 'per-device',
|
|
378
|
+
verdict: 'live',
|
|
379
|
+
checkedAt: new Date().toISOString(),
|
|
380
|
+
};
|
|
381
|
+
recordSlot(account.id, record);
|
|
382
|
+
// Select this account as the harness default ONLY if none is configured.
|
|
383
|
+
const becameDefault = registry.setDefaultAccountIfAbsent(agent, account.name);
|
|
384
|
+
const warnings = [];
|
|
385
|
+
const minted = await mintWorkerCredential(agent, account, finalDir, opts, run, warnings);
|
|
386
|
+
patchNativeAccountRow(account.id, {
|
|
387
|
+
provisioning: minted.provisioning,
|
|
388
|
+
createdOn: machineId(),
|
|
389
|
+
...(minted.ref
|
|
390
|
+
? { workerCredential: { ...minted.ref, kind: harnessWorkerKinds(agent).includes('setup-token') ? 'setup-token' : 'api-key', mintedAt: new Date().toISOString() } }
|
|
391
|
+
: {}),
|
|
392
|
+
});
|
|
393
|
+
await run.requestReconcile?.();
|
|
394
|
+
return {
|
|
395
|
+
mode: 'new',
|
|
396
|
+
agent,
|
|
397
|
+
accountId: account.id,
|
|
398
|
+
name: account.name,
|
|
399
|
+
identityKey,
|
|
400
|
+
email: observed.email,
|
|
401
|
+
slotDir: finalDir,
|
|
402
|
+
releaseVersion: observed.releaseVersion,
|
|
403
|
+
becameDefault,
|
|
404
|
+
provisioning: minted.provisioning,
|
|
405
|
+
workerCredential: minted.outcome,
|
|
406
|
+
workerCredentialRef: minted.ref,
|
|
407
|
+
warnings,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Drive one `agents accounts login <harness>#<name>`: re-auth into the SAME
|
|
412
|
+
* slot (re-running add's steps 4–8), re-minting the worker credential and
|
|
413
|
+
* re-syncing. On a per-device harness (worker `none`) any box may run it —
|
|
414
|
+
* that IS how such a box logs in.
|
|
415
|
+
*/
|
|
416
|
+
export async function runLogin(agent, name, opts, runners) {
|
|
417
|
+
// A harness with a portable worker credential authenticates workers from that
|
|
418
|
+
// credential, so the interactive re-login stays headed-only. A per-device
|
|
419
|
+
// harness (kimi) is logged in per box BY DESIGN — any role may run this.
|
|
420
|
+
if (hasPortableWorkerKind(agent))
|
|
421
|
+
assertAddAllowedOnThisDevice(agent, name);
|
|
422
|
+
if (!opts.noWorkerToken && harnessWorkerKinds(agent).includes('setup-token')) {
|
|
423
|
+
const refusal = ambientTokenRefusal(agent, opts.env ?? process.env);
|
|
424
|
+
if (refusal)
|
|
425
|
+
throw new Error(refusal);
|
|
426
|
+
}
|
|
427
|
+
const namingRefusal = nativeAccountNamingRefusal(agent);
|
|
428
|
+
if (namingRefusal)
|
|
429
|
+
throw new Error(namingRefusal);
|
|
430
|
+
const lock = acquireAuthOperationLock(agent, opts.stateDir);
|
|
431
|
+
try {
|
|
432
|
+
return await _runLoginLocked(agent, name, opts, lock, runners);
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
lock.release();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function _runLoginLocked(agent, name, opts, lock, runners) {
|
|
439
|
+
const run = runners ?? await defaultAddRunners();
|
|
440
|
+
lock.assertHeld();
|
|
441
|
+
const meta = readMeta();
|
|
442
|
+
const account = findAddAccount(agent, name, meta);
|
|
443
|
+
if (!account) {
|
|
444
|
+
throw new Error(`No ${agent} account '${name}'. Add it on your personal device: agents accounts add ${agent} ${name}`);
|
|
445
|
+
}
|
|
446
|
+
await run.ensureInstallation(agent, opts.onProgress);
|
|
447
|
+
lock.assertHeld();
|
|
448
|
+
// The SAME slot: the recorded one, adopted (legacy account with none yet) or
|
|
449
|
+
// re-materialized if the dir is gone from disk.
|
|
450
|
+
const recorded = readSlots(meta)[account.id];
|
|
451
|
+
const home = recorded?.slotDir ?? slotDir(agent, account.id);
|
|
452
|
+
if (!recorded || !fs.existsSync(home)) {
|
|
453
|
+
const slot = ensureSlot(agent, account.id);
|
|
454
|
+
recordSlot(account.id, {
|
|
455
|
+
...slot,
|
|
456
|
+
authMode: hasPortableWorkerKind(agent) ? 'native' : 'per-device',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
// Pre-launch guard: refuse to launch a login into a slot signed in as a
|
|
460
|
+
// DIFFERENT identity — that would overwrite some other login's home.
|
|
461
|
+
const current = fs.existsSync(home) ? await run.observeIdentity(agent, home) : null;
|
|
462
|
+
lock.assertHeld();
|
|
463
|
+
if (current?.signedIn && current.identityKey && current.identityKey !== account.identityKey) {
|
|
464
|
+
throw new Error(`The ${agent} slot for '${account.name}' is currently signed in as ${current.identityKey} `
|
|
465
|
+
+ `(not ${account.identityKey}). Refusing to launch a login that would overwrite it.`);
|
|
466
|
+
}
|
|
467
|
+
const cap = harnessAuth(agent);
|
|
468
|
+
const args = cap.login ?? [];
|
|
469
|
+
if (args.length === 0) {
|
|
470
|
+
// No finite login command (kimi): launch the bare harness in the slot; the
|
|
471
|
+
// human completes the in-TUI login (`kimi`, then /login).
|
|
472
|
+
opts.onProgress?.(`Launching ${loginHint(agent)} — complete the login there.`);
|
|
473
|
+
}
|
|
474
|
+
const login = await run.launchLogin(agent, { home, args, email: account.identityLabel, signal: lock.signal });
|
|
475
|
+
lock.assertHeld();
|
|
476
|
+
if (login.code !== 0) {
|
|
477
|
+
throw new Error(`${agent} login did not complete (exit ${login.code ?? 'null'}). The account is unchanged; re-run to retry.`);
|
|
478
|
+
}
|
|
479
|
+
const observed = await run.observeIdentity(agent, home);
|
|
480
|
+
lock.assertHeld();
|
|
481
|
+
verifyConnectedIdentity({ agent, home, existing: account }, observed);
|
|
482
|
+
const warnings = [];
|
|
483
|
+
let outcome = 'per-device';
|
|
484
|
+
let ref;
|
|
485
|
+
let provisioning = account.provisioning ?? (hasPortableWorkerKind(agent) ? 'portable' : 'per-device');
|
|
486
|
+
if (hasPortableWorkerKind(agent)) {
|
|
487
|
+
if (opts.apiKey && workerApiKeyEnv(agent)) {
|
|
488
|
+
// Explicit rotation of the stored API key.
|
|
489
|
+
const minted = await mintWorkerCredential(agent, account, home, opts, run, warnings);
|
|
490
|
+
outcome = minted.outcome;
|
|
491
|
+
ref = minted.ref;
|
|
492
|
+
provisioning = minted.provisioning;
|
|
493
|
+
}
|
|
494
|
+
else if (!account.workerCredential || harnessWorkerKinds(agent).includes('setup-token')) {
|
|
495
|
+
// Claude re-mints (the setup-token drive can always mint fresh from the
|
|
496
|
+
// live login); an api-key harness with a stored key keeps it unless
|
|
497
|
+
// --api-key rotates it.
|
|
498
|
+
const minted = await mintWorkerCredential(agent, account, home, opts, run, warnings);
|
|
499
|
+
outcome = minted.outcome;
|
|
500
|
+
ref = minted.ref;
|
|
501
|
+
provisioning = minted.provisioning;
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
outcome = 'kept';
|
|
505
|
+
ref = account.workerCredential ? { bundle: account.workerCredential.bundle, key: account.workerCredential.key } : undefined;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (ref) {
|
|
509
|
+
patchNativeAccountRow(account.id, {
|
|
510
|
+
provisioning,
|
|
511
|
+
workerCredential: {
|
|
512
|
+
...ref,
|
|
513
|
+
kind: harnessWorkerKinds(agent).includes('setup-token') ? 'setup-token' : 'api-key',
|
|
514
|
+
mintedAt: new Date().toISOString(),
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
patchNativeAccountRow(account.id, { provisioning });
|
|
520
|
+
}
|
|
521
|
+
recordSlot(account.id, {
|
|
522
|
+
accountId: account.id,
|
|
523
|
+
slotDir: home,
|
|
524
|
+
authMode: hasPortableWorkerKind(agent) ? 'native' : 'per-device',
|
|
525
|
+
verdict: 'live',
|
|
526
|
+
checkedAt: new Date().toISOString(),
|
|
527
|
+
});
|
|
528
|
+
await run.requestReconcile?.();
|
|
529
|
+
return {
|
|
530
|
+
mode: 'reconnect',
|
|
531
|
+
agent,
|
|
532
|
+
accountId: account.id,
|
|
533
|
+
name: account.name,
|
|
534
|
+
identityKey: account.identityKey,
|
|
535
|
+
email: observed.email,
|
|
536
|
+
slotDir: home,
|
|
537
|
+
releaseVersion: observed.releaseVersion,
|
|
538
|
+
becameDefault: false,
|
|
539
|
+
provisioning,
|
|
540
|
+
workerCredential: outcome,
|
|
541
|
+
workerCredentialRef: ref,
|
|
542
|
+
warnings,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/** Back-compat alias for the retired `connect` module surface (one release). */
|
|
546
|
+
export const runConnect = runAdd;
|
|
547
|
+
export const connectSupported = addSupported;
|
|
548
|
+
export const connectRefusal = addRefusal;
|
|
549
|
+
export const connectWorkerRefusal = addWorkerRefusal;
|
|
550
|
+
/** Real runners: the install engine, an inherited-stdio login spawn, identity read, and the mint drive. */
|
|
551
|
+
async function defaultAddRunners() {
|
|
552
|
+
const [store, agentsMod, execMod] = await Promise.all([
|
|
553
|
+
import('../installations/store.js'),
|
|
554
|
+
import('../agents.js'),
|
|
555
|
+
import('../exec.js'),
|
|
556
|
+
]);
|
|
557
|
+
const { ensureHarnessInstallation, getBinaryPath, readInstallation } = store;
|
|
558
|
+
const { agentConfigDirName, getAccountInfo } = agentsMod;
|
|
559
|
+
const { buildExecEnv } = execMod;
|
|
560
|
+
const { runNativeAccountCommand } = await import('../installations/native-command.js');
|
|
561
|
+
// A slot is HOME-shaped; the harness's config dir lives directly inside it.
|
|
562
|
+
const slotConfigDir = (agent, home) => path.join(home, agentConfigDirName(agent));
|
|
563
|
+
const loginEnv = (agent, installLabel, home) => {
|
|
564
|
+
const env = buildExecEnv({ agent, version: installLabel, configVersion: installLabel, interactive: true, mode: 'auto', effort: 'auto', cwd: process.cwd() });
|
|
565
|
+
// HOME = slot; the harness's slotEnv (CLAUDE_CONFIG_DIR / CODEX_HOME / …)
|
|
566
|
+
// pins its config dir inside the slot so the login lands there.
|
|
567
|
+
env.HOME = home;
|
|
568
|
+
const pin = harnessAuth(agent).slotEnv;
|
|
569
|
+
if (pin)
|
|
570
|
+
env[pin] = slotConfigDir(agent, home);
|
|
571
|
+
// Strip any ambient provider API-key / setup-token env so the native login
|
|
572
|
+
// authenticates as the human's OAuth identity, not an injected credential
|
|
573
|
+
// that would impersonate a different account into this slot.
|
|
574
|
+
for (const key of PROVIDER_AUTH_ENV_KEYS)
|
|
575
|
+
delete env[key];
|
|
576
|
+
const workerEnv = workerApiKeyEnv(agent);
|
|
577
|
+
if (workerEnv)
|
|
578
|
+
delete env[workerEnv];
|
|
579
|
+
return env;
|
|
580
|
+
};
|
|
581
|
+
return {
|
|
582
|
+
ensureInstallation: async (agent, onProgress) => {
|
|
583
|
+
const result = await ensureHarnessInstallation(agent, { onProgress });
|
|
584
|
+
return { label: result.installation.label };
|
|
585
|
+
},
|
|
586
|
+
launchLogin: async (agent, { home, args, email, signal }) => {
|
|
587
|
+
const install = await ensureHarnessInstallation(agent, {});
|
|
588
|
+
const invocation = LOGIN_INVOCATIONS[agent];
|
|
589
|
+
const finalArgs = email && invocation?.emailFlag ? [...args, invocation.emailFlag, email] : args;
|
|
590
|
+
return runNativeAccountCommand(agent, install.installation.label, finalArgs, loginEnv(agent, install.installation.label, home), signal);
|
|
591
|
+
},
|
|
592
|
+
observeIdentity: async (agent, home) => {
|
|
593
|
+
const info = await getAccountInfo(agent, home);
|
|
594
|
+
const { isLaunchableSignedIn } = await import('../account-catalog.js');
|
|
595
|
+
return {
|
|
596
|
+
identityKey: nativeIdentityKey(info, nativeAccountCapability(agent)),
|
|
597
|
+
email: info.email,
|
|
598
|
+
releaseVersion: readInstallation(agent, (await ensureHarnessInstallation(agent, {})).installation.label)?.releaseVersion ?? null,
|
|
599
|
+
// Strict: a live credential in THIS slot, not a bare metadata identity.
|
|
600
|
+
signedIn: isLaunchableSignedIn(agent, home, info),
|
|
601
|
+
};
|
|
602
|
+
},
|
|
603
|
+
mintSetupToken: async (agent, { home }) => {
|
|
604
|
+
const { MINT_FLOWS, buildMintCommand, driveSetupTokenMint } = await import('../auth-mint.js');
|
|
605
|
+
const flow = MINT_FLOWS[agent];
|
|
606
|
+
if (!flow || flow.auth !== 'setup-token')
|
|
607
|
+
throw new Error(`No setup-token mint flow for ${agent}.`);
|
|
608
|
+
const install = await ensureHarnessInstallation(agent, {});
|
|
609
|
+
const command = buildMintCommand(flow, getBinaryPath(agent, install.installation.label), home, {
|
|
610
|
+
...(harnessAuth(agent).slotEnv ? { [harnessAuth(agent).slotEnv]: slotConfigDir(agent, home) } : {}),
|
|
611
|
+
});
|
|
612
|
+
const driven = await driveSetupTokenMint(command, flow, {
|
|
613
|
+
readCode: async () => {
|
|
614
|
+
const { input } = await import('@inquirer/prompts');
|
|
615
|
+
return input({ message: 'Paste the authorization code from the browser' });
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
return driven.token;
|
|
619
|
+
},
|
|
620
|
+
promptApiKey: async (_agent, env) => {
|
|
621
|
+
const { password } = await import('@inquirer/prompts');
|
|
622
|
+
try {
|
|
623
|
+
return await password({ message: `Enter the ${env} API key (stored as this account's worker credential):` });
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
const { isPromptCancelled } = await import('../../commands/utils.js');
|
|
627
|
+
if (isPromptCancelled(err))
|
|
628
|
+
return null;
|
|
629
|
+
throw err;
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
requestReconcile: async () => {
|
|
633
|
+
// The daemon owns the tick; this queues a one-shot auth-sync restart so
|
|
634
|
+
// the reconcile runs now instead of at the next 15-minute cadence. A
|
|
635
|
+
// stopped daemon is fine — its next boot reconciles.
|
|
636
|
+
try {
|
|
637
|
+
const [{ isDaemonRunning, signalDaemonReload }, { queueDaemonServiceRestart }] = await Promise.all([
|
|
638
|
+
import('../daemon/daemon.js'),
|
|
639
|
+
import('../daemon-services.js'),
|
|
640
|
+
]);
|
|
641
|
+
if (!isDaemonRunning())
|
|
642
|
+
return;
|
|
643
|
+
queueDaemonServiceRestart('auth-sync');
|
|
644
|
+
signalDaemonReload();
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// Best-effort request: the reconcile also happens on the regular cadence.
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
}
|