@phnx-labs/agents-cli 1.22.83 → 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 +96 -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/sessions-trace.d.ts +11 -0
- package/dist/commands/sessions-trace.js +71 -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-ticks.js +23 -2
- 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/feed/activity-stream.d.ts +60 -0
- package/dist/lib/feed/activity-stream.js +271 -0
- package/dist/lib/feed/activity.d.ts +7 -0
- package/dist/lib/feed/activity.js +10 -4
- package/dist/lib/feed/watch.d.ts +9 -3
- package/dist/lib/feed/watch.js +72 -36
- 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/fleet-shared-state.d.ts +6 -0
- 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/active.d.ts +38 -4
- package/dist/lib/session/active.js +36 -4
- package/dist/lib/session/bash-command.js +10 -0
- package/dist/lib/session/db.d.ts +65 -2
- package/dist/lib/session/db.js +178 -18
- package/dist/lib/session/mirror.d.ts +5 -0
- package/dist/lib/session/mirror.js +166 -0
- package/dist/lib/session/parse.d.ts +49 -0
- package/dist/lib/session/parse.js +324 -30
- package/dist/lib/session/prompt.d.ts +93 -2
- package/dist/lib/session/prompt.js +266 -15
- package/dist/lib/session/remote/peer-stream.d.ts +47 -0
- package/dist/lib/session/remote/peer-stream.js +142 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +41 -42
- package/dist/lib/session/session-cache.d.ts +9 -0
- package/dist/lib/session/session-cache.js +40 -2
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/timeline-pass.d.ts +129 -0
- package/dist/lib/session/timeline-pass.js +323 -0
- package/dist/lib/session/timeline.d.ts +182 -0
- package/dist/lib/session/timeline.js +636 -0
- package/dist/lib/session/types.d.ts +155 -1
- 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/summarizer/pass.d.ts +2 -0
- package/dist/lib/summarizer/pass.js +14 -1
- package/dist/lib/summarizer/summarize.d.ts +7 -0
- package/dist/lib/summarizer/summarize.js +3 -0
- 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 -170
- package/dist/lib/accounts/connect.js +0 -383
- 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
|
@@ -24,10 +24,11 @@ import chalk from 'chalk';
|
|
|
24
24
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
25
25
|
import { getUserAgentsDir, readMeta, updateMeta } from './state.js';
|
|
26
26
|
import { isAgentId } from './types.js';
|
|
27
|
-
import {
|
|
28
|
-
import { bundleExists, deleteBundle, listBundles, readAndResolveBundleEnv, readBundle, renameBundle, writeBundleWithItems } from './secrets/bundles.js';
|
|
27
|
+
import { bundleExistsSync, deleteBundleSync, deleteKeychainTokenSync, getKeychainTokenSync, hasKeychainTokenSync, listBundlesSync, readAndResolveBundleEnvSync, readBundleSync, renameBundleSync, writeBundleWithItemsSync, } from './secrets-client.js';
|
|
29
28
|
import { getAccountProvider } from './account-provider-registry.js';
|
|
30
29
|
import { accountSecretItem, buildAccountBundle, parseAccountBundle, secretVarFor } from './account-schema.js';
|
|
30
|
+
export { recordSlot, readSlots } from './accounts/slots.js';
|
|
31
|
+
export { registeredNativeAccountForEmail, parseNativeIdentityKey } from './native-accounts.js';
|
|
31
32
|
const NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
32
33
|
const NATIVE_LABEL = /^[a-zA-Z0-9][a-zA-Z0-9@._+-]*$/;
|
|
33
34
|
const AUTH_KINDS = ['api-key', 'setup-token', 'bearer-token'];
|
|
@@ -56,7 +57,7 @@ function toCredentialAccount(record) {
|
|
|
56
57
|
/** Every account bundle currently on this device, keyed by stable id. */
|
|
57
58
|
function readAccountBundles() {
|
|
58
59
|
const out = [];
|
|
59
|
-
for (const bundle of
|
|
60
|
+
for (const bundle of listBundlesSync()) {
|
|
60
61
|
const record = parseAccountBundle(bundle);
|
|
61
62
|
if (record)
|
|
62
63
|
out.push(toCredentialAccount(record));
|
|
@@ -100,8 +101,8 @@ function migrateLegacyRegistryFile(base) {
|
|
|
100
101
|
const provider = String(item.provider ?? '');
|
|
101
102
|
const legacySecretRef = String(item.secretRef ?? `agents-cli.accounts.${id}.credential`);
|
|
102
103
|
retiredSecretItems.push(legacySecretRef);
|
|
103
|
-
if (
|
|
104
|
-
const existing = parseAccountBundle(
|
|
104
|
+
if (bundleExistsSync(name)) {
|
|
105
|
+
const existing = parseAccountBundle(readBundleSync(name));
|
|
105
106
|
if (!existing || existing.id !== id) {
|
|
106
107
|
throw new Error(`Cannot migrate account '${name}': a different secrets bundle already uses that name.`);
|
|
107
108
|
}
|
|
@@ -109,17 +110,17 @@ function migrateLegacyRegistryFile(base) {
|
|
|
109
110
|
}
|
|
110
111
|
const baseUrl = item.baseUrl ? String(item.baseUrl) : undefined;
|
|
111
112
|
const record = { id, name, provider, auth, baseUrl };
|
|
112
|
-
const secret =
|
|
113
|
+
const secret = hasKeychainTokenSync(legacySecretRef) ? getKeychainTokenSync(legacySecretRef) : '';
|
|
113
114
|
const { bundle, items } = buildAccountBundle(record, secret || 'x');
|
|
114
115
|
if (!secret)
|
|
115
116
|
items.clear(); // no device-local secret: write metadata only
|
|
116
|
-
|
|
117
|
+
writeBundleWithItemsSync(bundle, items);
|
|
117
118
|
}
|
|
118
119
|
// Success: the file's accounts all exist as bundles now. Archive it and drop
|
|
119
120
|
// the superseded per-account keychain items.
|
|
120
121
|
archiveLegacyFile(file, 'accounts.migrated.yaml');
|
|
121
122
|
for (const legacyItem of retiredSecretItems)
|
|
122
|
-
|
|
123
|
+
deleteKeychainTokenSync(legacyItem);
|
|
123
124
|
}
|
|
124
125
|
function archiveLegacyFile(file, archiveName) {
|
|
125
126
|
const archived = path.join(path.dirname(file), archiveName);
|
|
@@ -275,7 +276,7 @@ function assertUniqueUnifiedName(name, meta, doc, exceptIds, agent) {
|
|
|
275
276
|
}
|
|
276
277
|
/**
|
|
277
278
|
* Validate a native account NAME (charset + per-harness uniqueness) WITHOUT a
|
|
278
|
-
* known identity — the pre-flight `agents accounts
|
|
279
|
+
* known identity — the pre-flight `agents accounts add` runs before it
|
|
279
280
|
* installs a home and drives a login, so a bad/colliding name fails before any
|
|
280
281
|
* side effect instead of orphaning a freshly-minted home (PHNX-3940).
|
|
281
282
|
*/
|
|
@@ -335,71 +336,24 @@ export function labelNativeAccount(agent, identityKey, identityLabel, label, sco
|
|
|
335
336
|
if (rowScope === 'device') {
|
|
336
337
|
const native = { ...current.deviceAccounts?.native };
|
|
337
338
|
for (const row of matches)
|
|
338
|
-
native[row.id] = {
|
|
339
|
+
native[row.id] = { ...native[row.id], name: resolvedLabel, identityLabel, scope: rowScope };
|
|
339
340
|
return { ...current, deviceAccounts: { ...current.deviceAccounts, native } };
|
|
340
341
|
}
|
|
341
342
|
const native = { ...current.accounts?.native };
|
|
342
343
|
for (const row of matches)
|
|
343
|
-
native[row.id] = {
|
|
344
|
+
native[row.id] = { ...native[row.id], name: resolvedLabel, identityLabel, scope: rowScope };
|
|
344
345
|
return { ...current, accounts: { ...current.accounts, native } };
|
|
345
346
|
});
|
|
346
347
|
return { ...matches[0], name: resolvedLabel, identityLabel, scope: rowScope };
|
|
347
348
|
}
|
|
348
349
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* fleet-synced central identity row. Idempotent. A reconnect reads it back via
|
|
353
|
-
* {@link nativeAccountHome} to reuse the exact home even when the credential has
|
|
354
|
-
* expired and local signed-in discovery can no longer find it.
|
|
350
|
+
* This box's recorded home LABEL for an account, or null.
|
|
351
|
+
* Reads the leftover device-scoped `homes` map so legacy `acct-*` installation
|
|
352
|
+
* labels still resolve (T5/T7); spawn-time HOME is {@link readSlots}[id].slotDir.
|
|
355
353
|
*/
|
|
356
|
-
export function setNativeAccountHome(accountId, installationLabel) {
|
|
357
|
-
updateMeta(current => ({
|
|
358
|
-
...current,
|
|
359
|
-
deviceAccounts: {
|
|
360
|
-
...current.deviceAccounts,
|
|
361
|
-
homes: { ...current.deviceAccounts?.homes, [accountId]: installationLabel },
|
|
362
|
-
},
|
|
363
|
-
}));
|
|
364
|
-
}
|
|
365
|
-
/** This box's recorded connect home for an account, or null. */
|
|
366
354
|
export function nativeAccountHome(accountId, meta) {
|
|
367
355
|
return meta.deviceAccounts?.homes?.[accountId] ?? null;
|
|
368
356
|
}
|
|
369
|
-
/**
|
|
370
|
-
* Every installation label THIS box has recorded as SOME account's connect home
|
|
371
|
-
* (PHNX-3940). These are identity-bearing and MUST NEVER be re-minted for a new
|
|
372
|
-
* account — the safe-allocation invariant that stops a new connect from
|
|
373
|
-
* overwriting another account's login.
|
|
374
|
-
*/
|
|
375
|
-
export function ownedConnectHomeLabels(meta) {
|
|
376
|
-
return new Set(Object.values(meta.deviceAccounts?.homes ?? {}));
|
|
377
|
-
}
|
|
378
|
-
function pendingConnectKey(agent, name) {
|
|
379
|
-
return `${agent}:${name.toLowerCase()}`;
|
|
380
|
-
}
|
|
381
|
-
/** This box's in-flight connect slot for `(agent, name)`, or null. */
|
|
382
|
-
export function pendingConnectSlot(agent, name, meta) {
|
|
383
|
-
return meta.deviceAccounts?.pendingConnects?.[pendingConnectKey(agent, name)] ?? null;
|
|
384
|
-
}
|
|
385
|
-
/** Record an in-flight connect slot for `(agent, name)` (device-scoped). */
|
|
386
|
-
export function setPendingConnectSlot(agent, name, slot) {
|
|
387
|
-
updateMeta(current => ({
|
|
388
|
-
...current,
|
|
389
|
-
deviceAccounts: {
|
|
390
|
-
...current.deviceAccounts,
|
|
391
|
-
pendingConnects: { ...current.deviceAccounts?.pendingConnects, [pendingConnectKey(agent, name)]: slot },
|
|
392
|
-
},
|
|
393
|
-
}));
|
|
394
|
-
}
|
|
395
|
-
/** Clear the in-flight connect slot for `(agent, name)` once the account lands. */
|
|
396
|
-
export function clearPendingConnectSlot(agent, name) {
|
|
397
|
-
updateMeta(current => {
|
|
398
|
-
const pendingConnects = { ...current.deviceAccounts?.pendingConnects };
|
|
399
|
-
delete pendingConnects[pendingConnectKey(agent, name)];
|
|
400
|
-
return { ...current, deviceAccounts: { ...current.deviceAccounts, pendingConnects } };
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
357
|
/**
|
|
404
358
|
* Set the per-harness default account by NAME only when none is configured
|
|
405
359
|
* (PHNX-3940). Never overrides an existing choice — a first connect selecting a
|
|
@@ -528,11 +482,11 @@ export function addAccount(name, provider, auth, secret, base = getUserAgentsDir
|
|
|
528
482
|
assertUniqueUnifiedName(name, readMeta(), readAccountRegistry(base));
|
|
529
483
|
const adapter = getAccountProvider(provider);
|
|
530
484
|
adapter.validate(auth, secret);
|
|
531
|
-
if (
|
|
485
|
+
if (bundleExistsSync(name))
|
|
532
486
|
throw new Error(`Secrets bundle '${name}' already exists. Choose a different account name.`);
|
|
533
487
|
const record = { id: crypto.randomUUID(), name, provider: adapter.provider, auth, baseUrl: opts.baseUrl };
|
|
534
488
|
const { bundle, items } = buildAccountBundle(record, secret);
|
|
535
|
-
|
|
489
|
+
writeBundleWithItemsSync(bundle, items);
|
|
536
490
|
return toCredentialAccount(record);
|
|
537
491
|
}
|
|
538
492
|
export function setAccountSecret(name, secret, base = getUserAgentsDir()) {
|
|
@@ -542,8 +496,8 @@ export function setAccountSecret(name, secret, base = getUserAgentsDir()) {
|
|
|
542
496
|
getAccountProvider(account.provider).validate(account.auth, secret);
|
|
543
497
|
const record = { id: account.id, name: account.name, provider: account.provider, auth: account.auth, baseUrl: account.baseUrl };
|
|
544
498
|
const { bundle, items } = buildAccountBundle(record, secret);
|
|
545
|
-
bundle.created_at =
|
|
546
|
-
|
|
499
|
+
bundle.created_at = readBundleSync(account.name).created_at; // rotate the secret, keep the bundle's birth time
|
|
500
|
+
writeBundleWithItemsSync(bundle, items);
|
|
547
501
|
}
|
|
548
502
|
/**
|
|
549
503
|
* `oldSelector` is a bare name, a row id, or `<harness>#<name>`. A native row
|
|
@@ -606,7 +560,7 @@ export function renameAccount(oldSelector, newName, base = getUserAgentsDir()) {
|
|
|
606
560
|
}
|
|
607
561
|
return { ...current, accounts: { ...current.accounts, defaults } };
|
|
608
562
|
});
|
|
609
|
-
|
|
563
|
+
renameBundleSync(account.name, newName); // moves metadata + secret, preserves ACCOUNT_ID
|
|
610
564
|
renameProfileConsumers(account.name, newName, base);
|
|
611
565
|
}
|
|
612
566
|
/** `selector` is a bare name, a row id, or `<harness>#<name>` (see {@link renameAccount}). */
|
|
@@ -622,20 +576,23 @@ export function removeAccount(selector, base = getUserAgentsDir()) {
|
|
|
622
576
|
// Sweep every row for the identity (PHNX-3206) from its owning store (PHNX-3315).
|
|
623
577
|
const rowScope = rows[0].scope;
|
|
624
578
|
updateMeta(current => {
|
|
625
|
-
// Drop this box's connect-home
|
|
579
|
+
// Drop this box's connect-home and slot records for the removed account (PHNX-3940).
|
|
626
580
|
const homes = { ...current.deviceAccounts?.homes };
|
|
627
|
-
|
|
581
|
+
const slots = { ...current.deviceAccounts?.slots };
|
|
582
|
+
for (const id of ids) {
|
|
628
583
|
delete homes[id];
|
|
584
|
+
delete slots[id];
|
|
585
|
+
}
|
|
629
586
|
if (rowScope === 'device') {
|
|
630
587
|
const accounts = { ...current.deviceAccounts?.native };
|
|
631
588
|
for (const id of ids)
|
|
632
589
|
delete accounts[id];
|
|
633
|
-
return { ...current, deviceAccounts: { ...current.deviceAccounts, native: accounts, homes } };
|
|
590
|
+
return { ...current, deviceAccounts: { ...current.deviceAccounts, native: accounts, homes, slots } };
|
|
634
591
|
}
|
|
635
592
|
const accounts = { ...current.accounts?.native };
|
|
636
593
|
for (const id of ids)
|
|
637
594
|
delete accounts[id];
|
|
638
|
-
return { ...current, accounts: { ...current.accounts, native: accounts }, deviceAccounts: { ...current.deviceAccounts, homes } };
|
|
595
|
+
return { ...current, accounts: { ...current.accounts, native: accounts }, deviceAccounts: { ...current.deviceAccounts, homes, slots } };
|
|
639
596
|
});
|
|
640
597
|
return;
|
|
641
598
|
}
|
|
@@ -654,17 +611,17 @@ export function removeAccount(selector, base = getUserAgentsDir()) {
|
|
|
654
611
|
const consumers = [...new Set([...profileConsumers(account.name, base), ...profileConsumers(account.id, base)])].sort();
|
|
655
612
|
if (consumers.length)
|
|
656
613
|
throw new Error(`Account '${account.name}' is used by harness${consumers.length === 1 ? '' : 'es'}: ${consumers.join(', ')}. Reassign them before removing it.`);
|
|
657
|
-
|
|
658
|
-
|
|
614
|
+
deleteKeychainTokenSync(account.secretRef);
|
|
615
|
+
deleteBundleSync(account.name);
|
|
659
616
|
}
|
|
660
617
|
export function inspectAccount(name, base = getUserAgentsDir()) {
|
|
661
618
|
const account = findAccount(name, readAccountRegistry(base));
|
|
662
619
|
if (!account)
|
|
663
620
|
throw new Error(`Unknown account '${name}'.`);
|
|
664
|
-
const bundle =
|
|
621
|
+
const bundle = readBundleSync(account.name);
|
|
665
622
|
if (bundle.policy !== 'never')
|
|
666
623
|
throw new Error(`Account bundle '${account.name}' must use secrets policy 'never'.`);
|
|
667
|
-
return { ...account, secretPresent:
|
|
624
|
+
return { ...account, secretPresent: hasKeychainTokenSync(account.secretRef), policy: bundle.policy };
|
|
668
625
|
}
|
|
669
626
|
export function resolveCredentialAccount(name, host, expectedProvider, base = getUserAgentsDir()) {
|
|
670
627
|
const account = findAccount(name, readAccountRegistry(base));
|
|
@@ -677,15 +634,15 @@ export function resolveCredentialAccount(name, host, expectedProvider, base = ge
|
|
|
677
634
|
throw new Error(`Provider '${account.provider}' cannot use a setup-token with the ${host} harness.`);
|
|
678
635
|
}
|
|
679
636
|
const envVar = account.auth === 'setup-token' ? 'CLAUDE_CODE_OAUTH_TOKEN' : adapter.envFor(host, account.auth);
|
|
680
|
-
if (!
|
|
637
|
+
if (!hasKeychainTokenSync(account.secretRef))
|
|
681
638
|
throw new Error(`Credential for account '${account.name}' is missing on this device. Add it with 'agents accounts set-key ${account.name}'.`);
|
|
682
639
|
const secretVar = secretVarFor(account.auth);
|
|
683
640
|
// Account bundles are policy `never`, so their value items carry no biometry
|
|
684
641
|
// ACL. Resolve through the bundle path that verifies that policy and attests
|
|
685
|
-
// `silentNoAcl` to the headless keychain guard. Calling
|
|
642
|
+
// `silentNoAcl` to the headless keychain guard. Calling getKeychainTokenSync()
|
|
686
643
|
// directly makes a headless --account launch reject the prompt-free item as
|
|
687
644
|
// if it required Touch ID before the helper ever reads it (PHNX-2939).
|
|
688
|
-
const secret =
|
|
645
|
+
const secret = readAndResolveBundleEnvSync(account.name, {
|
|
689
646
|
keys: [secretVar],
|
|
690
647
|
keyMode: 'storage',
|
|
691
648
|
agentOnly: true,
|
|
@@ -742,8 +699,8 @@ export function resolveSpawnAccount(explicit, agent, version, meta, opts = {}) {
|
|
|
742
699
|
return null;
|
|
743
700
|
}
|
|
744
701
|
const remedy = selection.source === 'binding'
|
|
745
|
-
? `
|
|
746
|
-
: `Add an account named '${selection.id}'
|
|
702
|
+
? `The binding for ${target} points at an account that no longer exists.`
|
|
703
|
+
: `Add an account named '${selection.id}' with: agents accounts add ${agent} ${selection.id}`;
|
|
747
704
|
throw new Error(`Unknown account '${selection.id}' for ${agent} harness. ${remedy}`);
|
|
748
705
|
}
|
|
749
706
|
if (unified.kind === 'native') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SecretsBundle } from './secrets
|
|
1
|
+
import type { SecretsBundle } from './secrets-types.js';
|
|
2
2
|
import type { AccountAuthKind } from './account-provider-registry.js';
|
|
3
3
|
/** Canonical bundle variable names for a provider-account bundle. */
|
|
4
4
|
export declare const ACCOUNT_VARS: {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* record back out, with no I/O. The registry ([[account-registry]]) owns
|
|
21
21
|
* reads and writes; the catalog ([[account-catalog]]) owns discovery.
|
|
22
22
|
*/
|
|
23
|
-
import { parseBundleValue, secretsKeychainItem } from './secrets
|
|
23
|
+
import { parseBundleValue, secretsKeychainItem } from './secrets-client.js';
|
|
24
24
|
/** Canonical bundle variable names for a provider-account bundle. */
|
|
25
25
|
export const ACCOUNT_VARS = {
|
|
26
26
|
id: 'ACCOUNT_ID',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readAccountRegistry } from '../account-registry.js';
|
|
2
|
-
import {
|
|
2
|
+
import { hasKeychainTokenSync, isSecretsTransportError } from '../secrets-client.js';
|
|
3
3
|
import { getGlobalDefault, listInstalledVersions } from '../installations/versions.js';
|
|
4
4
|
import { collectRunCandidates } from './rotate.js';
|
|
5
5
|
import { registryPoolCandidates } from './account-pool.js';
|
|
@@ -11,9 +11,21 @@ import { registryPoolCandidates } from './account-pool.js';
|
|
|
11
11
|
* rotating to a healthy one.
|
|
12
12
|
*/
|
|
13
13
|
function localRegistryRecords() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// Read-only launch input: an unreachable standalone `secrets` (missing binary,
|
|
15
|
+
// timeout, non-JSON) must not abort the run — it just means no provider
|
|
16
|
+
// accounts are foldable here, so the run proceeds on the native login rather
|
|
17
|
+
// than hanging or crashing (PHNX-3989, honoring the ≤3s sync bound). A genuine
|
|
18
|
+
// non-transport error still throws. DIST-1 holds: no embedded-engine fallback.
|
|
19
|
+
try {
|
|
20
|
+
return Object.values(readAccountRegistry().accounts)
|
|
21
|
+
.filter((a) => hasKeychainTokenSync(a.secretRef))
|
|
22
|
+
.map((a) => ({ name: a.name, provider: a.provider, auth: a.auth, secretPresent: true }));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (isSecretsTransportError(err))
|
|
26
|
+
return [];
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
17
29
|
}
|
|
18
30
|
/**
|
|
19
31
|
* PURE: fold provider-account candidates into the native version-home list.
|
|
@@ -62,6 +62,16 @@ export interface RotateCandidate {
|
|
|
62
62
|
* authenticates through the existing provider-account injection.
|
|
63
63
|
*/
|
|
64
64
|
providerAccount?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Native account name when this candidate is a slot (PHNX-3940 T5). The
|
|
67
|
+
* picker and `--account` / `#name` selector use this; `version` is the
|
|
68
|
+
* binary (managed install) and is no longer the account identity.
|
|
69
|
+
*/
|
|
70
|
+
nativeAccount?: string;
|
|
71
|
+
/** Slot dir when this candidate is a slot — the spawn HOME. */
|
|
72
|
+
slotDir?: string;
|
|
73
|
+
/** True when this row came from `deviceAccounts.slots`, not a version home. */
|
|
74
|
+
fromSlot?: boolean;
|
|
65
75
|
}
|
|
66
76
|
export interface RotateResult {
|
|
67
77
|
/** The version picked for this run. */
|
|
@@ -402,16 +412,19 @@ export declare function collectHarnessCandidates(agentIds?: AgentId[]): Promise<
|
|
|
402
412
|
* (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
|
|
403
413
|
* Returns null when nothing matches, so the caller can fall back and warn.
|
|
404
414
|
*/
|
|
415
|
+
export declare function matchAccountCandidate(candidates: RotateCandidate[], account: string, preferredLabel?: string | null): RotateCandidate | null;
|
|
405
416
|
export declare function matchAccountVersion(candidates: RotateCandidate[], account: string, preferredLabel?: string | null): string | null;
|
|
406
417
|
/**
|
|
407
|
-
* Resolve a routine's `account:` pin (login email
|
|
408
|
-
*
|
|
409
|
-
* {@link collectRunCandidates} + {@link
|
|
410
|
-
* no signed-in
|
|
411
|
-
* durable cure for the shared-single-use-refresh-token
|
|
412
|
-
* (RUSH-1957): the pinned run never rotates and never lands
|
|
413
|
-
* routine's credential.
|
|
418
|
+
* Resolve a routine's `account:` pin (login email, account key, or native
|
|
419
|
+
* account name) to the candidate currently holding that account. Thin I/O
|
|
420
|
+
* wrapper over {@link collectRunCandidates} + {@link matchAccountCandidate};
|
|
421
|
+
* returns null when no signed-in candidate matches. Pinning a routine to a
|
|
422
|
+
* distinct account is the durable cure for the shared-single-use-refresh-token
|
|
423
|
+
* revocation storm (RUSH-1957): the pinned run never rotates and never lands
|
|
424
|
+
* on another routine's credential. Post-T5 the candidate's `nativeAccount`
|
|
425
|
+
* (not just `version`) is what disambiguates two slots on one managed install.
|
|
414
426
|
*/
|
|
427
|
+
export declare function resolveAccountCandidate(agent: AgentId, account: string, preferredLabel?: string | null): Promise<RotateCandidate | null>;
|
|
415
428
|
export declare function resolveAccountVersion(agent: AgentId, account: string, preferredLabel?: string | null): Promise<string | null>;
|
|
416
429
|
/**
|
|
417
430
|
* Pick a healthy version for `agent` using weighted random by remaining
|
|
@@ -10,6 +10,9 @@ import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
|
|
|
10
10
|
import { accountDisplayLabel, getAccountInfo, credentialPresence, ALL_AGENT_IDS, } from '../agents.js';
|
|
11
11
|
import { readMeta, writeMeta, getHelpersDir } from '../state.js';
|
|
12
12
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from '../installations/versions.js';
|
|
13
|
+
import { resolveManagedInstallation } from '../installations/store.js';
|
|
14
|
+
import { listNativeAccounts } from '../account-registry.js';
|
|
15
|
+
import { readSlots } from '../accounts/slots.js';
|
|
13
16
|
import { getProjectRunConfigs } from '../run-config.js';
|
|
14
17
|
import { emit } from '../feed/events.js';
|
|
15
18
|
import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, } from './usage.js';
|
|
@@ -74,7 +77,14 @@ export function setGlobalRunStrategy(agent, strategy) {
|
|
|
74
77
|
* warning can never disagree: an account is eligible iff its readiness is
|
|
75
78
|
* `ready` — signed in, not server-revoked, and not out of usage.
|
|
76
79
|
*/
|
|
80
|
+
/** Slot verdicts balanced/available will launch (PHNX-3940 T5). */
|
|
81
|
+
const LAUNCHABLE_SLOT_VERDICTS = new Set(['live', 'unverified']);
|
|
82
|
+
function isLaunchableSlotVerdict(verdict) {
|
|
83
|
+
return verdict !== null && LAUNCHABLE_SLOT_VERDICTS.has(verdict);
|
|
84
|
+
}
|
|
77
85
|
function isRotationEligible(candidate) {
|
|
86
|
+
if (candidate.fromSlot && !isLaunchableSlotVerdict(candidate.authVerdict))
|
|
87
|
+
return false;
|
|
78
88
|
return readinessFromCandidate(candidate).ready;
|
|
79
89
|
}
|
|
80
90
|
/**
|
|
@@ -684,9 +694,65 @@ export async function collectRunCandidates(agent) {
|
|
|
684
694
|
// LOCAL host — routing decides which local version to launch.
|
|
685
695
|
const authCache = readAuthHealthCache();
|
|
686
696
|
const localHost = machineId();
|
|
687
|
-
const
|
|
697
|
+
const meta = readMeta();
|
|
698
|
+
const binaryLabel = resolveManagedInstallation(agent)?.label ?? versions[0];
|
|
699
|
+
const slotRows = [];
|
|
700
|
+
const slotIdentities = new Set();
|
|
701
|
+
const slotDirs = new Set();
|
|
702
|
+
if (binaryLabel) {
|
|
703
|
+
const slots = readSlots(meta);
|
|
704
|
+
const natives = listNativeAccounts(meta).filter((row) => row.agent === agent);
|
|
705
|
+
const probed = await Promise.all(natives.map(async (account) => {
|
|
706
|
+
const slot = slots[account.id];
|
|
707
|
+
if (!slot || !fs.existsSync(slot.slotDir))
|
|
708
|
+
return null;
|
|
709
|
+
const home = slot.slotDir;
|
|
710
|
+
const info = await getAccountInfo(agent, home);
|
|
711
|
+
const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
|
|
712
|
+
const slotOk = isLaunchableSlotVerdict(slot.verdict);
|
|
713
|
+
return {
|
|
714
|
+
agent,
|
|
715
|
+
version: binaryLabel,
|
|
716
|
+
home,
|
|
717
|
+
info,
|
|
718
|
+
accountKey: launchable ? info.accountKey : null,
|
|
719
|
+
accountLabel: account.name || (launchable ? accountDisplayLabel(info) : ''),
|
|
720
|
+
email: launchable ? info.email : null,
|
|
721
|
+
usageStatus: launchable ? info.usageStatus : null,
|
|
722
|
+
plan: launchable ? info.plan : null,
|
|
723
|
+
signedIn: launchable && slotOk,
|
|
724
|
+
authVerdict: slot.verdict,
|
|
725
|
+
authCheckedAt: (() => {
|
|
726
|
+
const ts = slot.checkedAt ? Date.parse(slot.checkedAt) : NaN;
|
|
727
|
+
return Number.isFinite(ts) ? ts : null;
|
|
728
|
+
})(),
|
|
729
|
+
lastActive: info.lastActive,
|
|
730
|
+
nativeAccount: account.name,
|
|
731
|
+
slotDir: home,
|
|
732
|
+
fromSlot: true,
|
|
733
|
+
};
|
|
734
|
+
}));
|
|
735
|
+
for (const row of probed) {
|
|
736
|
+
if (!row)
|
|
737
|
+
continue;
|
|
738
|
+
slotRows.push(row);
|
|
739
|
+
slotDirs.add(path.resolve(row.home));
|
|
740
|
+
if (row.info.accountKey)
|
|
741
|
+
slotIdentities.add(row.info.accountKey);
|
|
742
|
+
if (row.email)
|
|
743
|
+
slotIdentities.add(row.email.toLowerCase());
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Legacy per-account installations (`acct-*` homes) until T7 migrates them.
|
|
747
|
+
const versionRows = await Promise.all(versions.map(async (version) => {
|
|
688
748
|
const home = getVersionHomePath(agent, version);
|
|
749
|
+
if (slotDirs.has(path.resolve(home)))
|
|
750
|
+
return null;
|
|
689
751
|
const info = await getAccountInfo(agent, home);
|
|
752
|
+
if (info.accountKey && slotIdentities.has(info.accountKey))
|
|
753
|
+
return null;
|
|
754
|
+
if (info.email && slotIdentities.has(info.email.toLowerCase()))
|
|
755
|
+
return null;
|
|
690
756
|
// We used to additionally call isClaudeAuthValid(home), which reads
|
|
691
757
|
// "Claude Code-credentials-<hash>" from the system keychain. That item is
|
|
692
758
|
// written by Claude Code itself with its own process in the ACL, so our
|
|
@@ -717,6 +783,7 @@ export async function collectRunCandidates(agent) {
|
|
|
717
783
|
lastActive: info.lastActive,
|
|
718
784
|
};
|
|
719
785
|
}));
|
|
786
|
+
const rows = [...slotRows, ...versionRows.filter((row) => row !== null)];
|
|
720
787
|
// These candidates feed a routing decision on the `agents run` hot path, so
|
|
721
788
|
// this read is CACHE-ONLY (`readOnly`): it never blocks on a live provider
|
|
722
789
|
// fetch. A snapshot older than USAGE_DECISION_MAX_AGE_MS is not trusted for
|
|
@@ -775,26 +842,35 @@ export async function collectHarnessCandidates(agentIds = ALL_AGENT_IDS) {
|
|
|
775
842
|
* (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
|
|
776
843
|
* Returns null when nothing matches, so the caller can fall back and warn.
|
|
777
844
|
*/
|
|
778
|
-
export function
|
|
845
|
+
export function matchAccountCandidate(candidates, account, preferredLabel) {
|
|
779
846
|
const needle = account.trim().toLowerCase();
|
|
780
847
|
if (!needle)
|
|
781
848
|
return null;
|
|
782
849
|
const matching = candidates.filter((c) => c.signedIn &&
|
|
783
|
-
(c.email?.toLowerCase() === needle
|
|
784
|
-
|
|
850
|
+
(c.email?.toLowerCase() === needle
|
|
851
|
+
|| c.accountKey?.toLowerCase() === needle
|
|
852
|
+
|| c.nativeAccount?.toLowerCase() === needle));
|
|
853
|
+
return matching.find(candidate => candidate.version === preferredLabel) ?? matching[0] ?? null;
|
|
854
|
+
}
|
|
855
|
+
export function matchAccountVersion(candidates, account, preferredLabel) {
|
|
856
|
+
return matchAccountCandidate(candidates, account, preferredLabel)?.version ?? null;
|
|
785
857
|
}
|
|
786
858
|
/**
|
|
787
|
-
* Resolve a routine's `account:` pin (login email
|
|
788
|
-
*
|
|
789
|
-
* {@link collectRunCandidates} + {@link
|
|
790
|
-
* no signed-in
|
|
791
|
-
* durable cure for the shared-single-use-refresh-token
|
|
792
|
-
* (RUSH-1957): the pinned run never rotates and never lands
|
|
793
|
-
* routine's credential.
|
|
859
|
+
* Resolve a routine's `account:` pin (login email, account key, or native
|
|
860
|
+
* account name) to the candidate currently holding that account. Thin I/O
|
|
861
|
+
* wrapper over {@link collectRunCandidates} + {@link matchAccountCandidate};
|
|
862
|
+
* returns null when no signed-in candidate matches. Pinning a routine to a
|
|
863
|
+
* distinct account is the durable cure for the shared-single-use-refresh-token
|
|
864
|
+
* revocation storm (RUSH-1957): the pinned run never rotates and never lands
|
|
865
|
+
* on another routine's credential. Post-T5 the candidate's `nativeAccount`
|
|
866
|
+
* (not just `version`) is what disambiguates two slots on one managed install.
|
|
794
867
|
*/
|
|
795
|
-
export async function
|
|
868
|
+
export async function resolveAccountCandidate(agent, account, preferredLabel) {
|
|
796
869
|
const candidates = await collectRunCandidates(agent);
|
|
797
|
-
return
|
|
870
|
+
return matchAccountCandidate(candidates, account, preferredLabel);
|
|
871
|
+
}
|
|
872
|
+
export async function resolveAccountVersion(agent, account, preferredLabel) {
|
|
873
|
+
return (await resolveAccountCandidate(agent, account, preferredLabel))?.version ?? null;
|
|
798
874
|
}
|
|
799
875
|
/**
|
|
800
876
|
* Pick a healthy version for `agent` using weighted random by remaining
|
|
@@ -19,7 +19,7 @@ import { promisify } from 'util';
|
|
|
19
19
|
import chalk from 'chalk';
|
|
20
20
|
import { decodeJwtPayload, decryptDroidAuthPayload } from '../agents.js';
|
|
21
21
|
import { walkForFiles } from '../fs-walk.js';
|
|
22
|
-
import {
|
|
22
|
+
import { deleteKeychainToken, deleteKeychainTokenSync, getKeychainTokenSync, setKeychainToken, } from '../secrets-client.js';
|
|
23
23
|
import { resolveClaudeSetupToken } from '../claude-account-token.js';
|
|
24
24
|
import { formatBackoffRemaining, noteUsageRateLimited, usageRateLimitedUntil, } from '../usage-backoff.js';
|
|
25
25
|
import { getCacheDir } from '../state.js';
|
|
@@ -1547,7 +1547,7 @@ function claudeOauthCacheItem(service) {
|
|
|
1547
1547
|
*/
|
|
1548
1548
|
function deleteCachedClaudeOauth(service) {
|
|
1549
1549
|
try {
|
|
1550
|
-
|
|
1550
|
+
deleteKeychainTokenSync(claudeOauthCacheItem(service));
|
|
1551
1551
|
}
|
|
1552
1552
|
catch {
|
|
1553
1553
|
/* best-effort — cache is an optimization */
|
|
@@ -1627,13 +1627,11 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1627
1627
|
// dispatch does not (SING-1b / RUSH-2359). The OS keychain/keyring step is
|
|
1628
1628
|
// macOS/Linux-only; Windows and any
|
|
1629
1629
|
// fileOnly caller skip to the .credentials.json read below (the Claude CLI
|
|
1630
|
-
// stores its OAuth token in that file too).
|
|
1631
|
-
|
|
1632
|
-
if (!opts?.fileOnly
|
|
1633
|
-
&& (process.platform === 'darwin' || process.platform === 'linux' || isKeychainBackendOverridden())) {
|
|
1630
|
+
// stores its OAuth token in that file too).
|
|
1631
|
+
if (!opts?.fileOnly && (process.platform === 'darwin' || process.platform === 'linux')) {
|
|
1634
1632
|
const service = getClaudeKeychainService(home);
|
|
1635
1633
|
try {
|
|
1636
|
-
const fromKeychain = parseClaudeOauthPayload(
|
|
1634
|
+
const fromKeychain = parseClaudeOauthPayload(getKeychainTokenSync(service));
|
|
1637
1635
|
if (fromKeychain)
|
|
1638
1636
|
return fromKeychain;
|
|
1639
1637
|
}
|
|
@@ -1658,12 +1656,9 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1658
1656
|
* Exported for regression tests; not part of the public command surface.
|
|
1659
1657
|
*/
|
|
1660
1658
|
export async function saveClaudeOauth(home, credentials) {
|
|
1661
|
-
// Windows not yet supported
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1664
|
-
// credential is written OR a stale no-ACL cache item an earlier version wrote
|
|
1665
|
-
// is evicted (deleteCachedClaudeOauth) — leaving that stale item behind.
|
|
1666
|
-
if (process.platform !== 'darwin' && process.platform !== 'linux' && !isKeychainBackendOverridden()) {
|
|
1659
|
+
// Windows not yet supported: Claude Code keeps its credential in the file
|
|
1660
|
+
// there, so the rotated credential is written by the harness, not here.
|
|
1661
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
1667
1662
|
return false;
|
|
1668
1663
|
}
|
|
1669
1664
|
try {
|
|
@@ -1671,7 +1666,7 @@ export async function saveClaudeOauth(home, credentials) {
|
|
|
1671
1666
|
// Read existing payload to preserve other fields
|
|
1672
1667
|
let existingPayload = {};
|
|
1673
1668
|
try {
|
|
1674
|
-
const stdout =
|
|
1669
|
+
const stdout = getKeychainTokenSync(service);
|
|
1675
1670
|
existingPayload = JSON.parse(stdout.trim());
|
|
1676
1671
|
}
|
|
1677
1672
|
catch {
|
|
@@ -1691,12 +1686,12 @@ export async function saveClaudeOauth(home, credentials) {
|
|
|
1691
1686
|
const payloadJson = JSON.stringify(newPayload);
|
|
1692
1687
|
// Delete existing entry first, then add updated entry
|
|
1693
1688
|
try {
|
|
1694
|
-
deleteKeychainToken(service);
|
|
1689
|
+
await deleteKeychainToken(service);
|
|
1695
1690
|
}
|
|
1696
1691
|
catch {
|
|
1697
1692
|
// Entry might not exist, ignore
|
|
1698
1693
|
}
|
|
1699
|
-
setKeychainToken(service, payloadJson);
|
|
1694
|
+
await setKeychainToken(service, payloadJson);
|
|
1700
1695
|
// A new credential rotation means any cached access token is stale.
|
|
1701
1696
|
deleteCachedClaudeOauth(service);
|
|
1702
1697
|
return true;
|