@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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-device account slots (PHNX-3940).
|
|
3
|
+
*
|
|
4
|
+
* An account is a credential slot, not an installation. The binary lives in
|
|
5
|
+
* the one managed harness install; each account gets a HOME-shaped dir under
|
|
6
|
+
* `~/.agents/.history/accounts/<harness>/<accountId>/` with no binary in it.
|
|
7
|
+
* Native OAuth files stay in this dir on the device that minted them and are
|
|
8
|
+
* never copied. Settings and resources are projected through the same writers
|
|
9
|
+
* version homes use (`carryForwardSettings`, `getWriter`) — credentials are
|
|
10
|
+
* excluded by those writers.
|
|
11
|
+
*
|
|
12
|
+
* Slot records live in the device doc (`deviceAccounts.slots`), never central.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { agentConfigDirName } from '../agents.js';
|
|
17
|
+
import { harnessAuth, harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
|
|
18
|
+
import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../installations/store.js';
|
|
19
|
+
import { carryForwardSettings } from '../settings-manifest.js';
|
|
20
|
+
import { getHistoryDir, updateMeta } from '../state.js';
|
|
21
|
+
import { ALL_RESOURCE_KINDS, getDetector, getWriter, kindToCapability } from '../staleness/registry.js';
|
|
22
|
+
import { supports } from '../capabilities.js';
|
|
23
|
+
import { isAgentId } from '../types.js';
|
|
24
|
+
const AUTH_MODES = ['native', 'durable', 'per-device'];
|
|
25
|
+
export function slotDir(harness, accountId) {
|
|
26
|
+
if (!isAgentId(harness))
|
|
27
|
+
throw new Error(`Unknown harness '${harness}' for a slot dir.`);
|
|
28
|
+
if (!accountId || /[\\/]|\.\./.test(accountId)) {
|
|
29
|
+
throw new Error(`Invalid account id '${accountId}' for a slot dir.`);
|
|
30
|
+
}
|
|
31
|
+
return path.join(getHistoryDir(), 'accounts', harness, accountId);
|
|
32
|
+
}
|
|
33
|
+
export function readSlots(meta) {
|
|
34
|
+
return { ...(meta.deviceAccounts?.slots ?? {}) };
|
|
35
|
+
}
|
|
36
|
+
export function recordSlot(accountId, slot) {
|
|
37
|
+
if (slot.accountId !== accountId) {
|
|
38
|
+
throw new Error(`recordSlot accountId mismatch: key '${accountId}' vs slot.accountId '${slot.accountId}'.`);
|
|
39
|
+
}
|
|
40
|
+
if (!AUTH_MODES.includes(slot.authMode)) {
|
|
41
|
+
throw new Error(`recordSlot: unknown authMode '${slot.authMode}'.`);
|
|
42
|
+
}
|
|
43
|
+
updateMeta((current) => ({
|
|
44
|
+
...current,
|
|
45
|
+
deviceAccounts: {
|
|
46
|
+
...current.deviceAccounts,
|
|
47
|
+
slots: { ...current.deviceAccounts?.slots, [accountId]: slot },
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
function defaultAuthMode(harness) {
|
|
52
|
+
return harnessWorkerIsPerDevice(harness) ? 'per-device' : 'native';
|
|
53
|
+
}
|
|
54
|
+
function sourceVersion(harness) {
|
|
55
|
+
return getGlobalDefault(harness) ?? listInstalledVersions(harness)[0] ?? null;
|
|
56
|
+
}
|
|
57
|
+
function projectResources(harness, version, destHome, fromHome) {
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
for (const kind of ALL_RESOURCE_KINDS) {
|
|
60
|
+
if (!supports(harness, kindToCapability(kind), version).ok)
|
|
61
|
+
continue;
|
|
62
|
+
const writer = getWriter(kind, harness);
|
|
63
|
+
if (!writer)
|
|
64
|
+
continue;
|
|
65
|
+
if (kind === 'rules') {
|
|
66
|
+
writer.write({ version, versionHome: destHome, selection: { preset: 'default' }, cwd });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const names = getDetector(kind, harness)?.list({ version, versionHome: fromHome, cwd }) ?? [];
|
|
70
|
+
if (names.length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
writer.write({ version, versionHome: destHome, selection: names, cwd });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Create the HOME-shaped slot dir and project settings/resources from the
|
|
77
|
+
* managed install. Does not copy credentials (carryForwardSettings excludes
|
|
78
|
+
* them; resource writers write skills/hooks/commands, never OAuth files).
|
|
79
|
+
* Does not persist the slot record — call {@link recordSlot} after identity
|
|
80
|
+
* is captured.
|
|
81
|
+
*/
|
|
82
|
+
export function ensureSlot(harness, accountId) {
|
|
83
|
+
harnessAuth(harness);
|
|
84
|
+
const dir = slotDir(harness, accountId);
|
|
85
|
+
fs.mkdirSync(path.join(dir, agentConfigDirName(harness)), { recursive: true });
|
|
86
|
+
const version = sourceVersion(harness);
|
|
87
|
+
if (version) {
|
|
88
|
+
const fromHome = getVersionHomePath(harness, version);
|
|
89
|
+
if (fs.existsSync(fromHome)) {
|
|
90
|
+
carryForwardSettings(harness, fromHome, dir);
|
|
91
|
+
projectResources(harness, version, dir, fromHome);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
accountId,
|
|
96
|
+
slotDir: dir,
|
|
97
|
+
authMode: defaultAuthMode(harness),
|
|
98
|
+
verdict: 'unconfigured',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -236,6 +236,22 @@ export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
|
236
236
|
export declare function accountDisplayLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
237
237
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
238
238
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
239
|
+
/**
|
|
240
|
+
* Extract full account information (identity, plan, usage status, credits) from
|
|
241
|
+
* the agent's local auth/config files. Supports Claude, Codex, and Gemini.
|
|
242
|
+
*/
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a file-auth agent's credential file. Sign-in is account-global, but
|
|
245
|
+
* each installed version gets an isolated home; the credential physically lives
|
|
246
|
+
* only in the home the user logged in under (the one the `~/.<config>` symlink
|
|
247
|
+
* targets). Check the per-version `base` first, then fall back to the active
|
|
248
|
+
* config location under the real HOME so every installed version reflects the
|
|
249
|
+
* true account state (droid/antigravity/kimi all stored login per-version-home
|
|
250
|
+
* and showed non-active versions as "not signed in"). Returns the first
|
|
251
|
+
* existing path, or null.
|
|
252
|
+
*/
|
|
253
|
+
/** True when `dir` is under the per-account slot root (`~/.agents/.history/accounts/`). */
|
|
254
|
+
export declare function isAccountSlotDir(dir: string): boolean;
|
|
239
255
|
/** Where an agent's credential file lives, split into the per-version copy and
|
|
240
256
|
* the active/global copy under the real HOME. */
|
|
241
257
|
export interface CredentialPresence {
|
|
@@ -22,7 +22,8 @@ import { execFileShellSpec } from '../platform/index.js';
|
|
|
22
22
|
import { latestFileMtimeMs } from '../fs-walk.js';
|
|
23
23
|
import { damerauLevenshtein } from '../fuzzy.js';
|
|
24
24
|
import { probeCapture } from '../probe.js';
|
|
25
|
-
import { getCacheDir, getVersionsDir, getShimsDir, getHistoryDir, getCliVersionCachePath } from '../state.js';
|
|
25
|
+
import { getCacheDir, getVersionsDir, getShimsDir, getHistoryDir, getCliVersionCachePath, readMeta } from '../state.js';
|
|
26
|
+
import { registeredNativeAccountForEmail, parseNativeIdentityKey } from '../native-accounts.js';
|
|
26
27
|
import { resolveVersion, getVersionHomePath, getBinaryPath } from '../installations/versions.js';
|
|
27
28
|
import { supports } from '../capabilities.js';
|
|
28
29
|
import { MCP_TARGETS } from '../mcp-registry.js';
|
|
@@ -1384,6 +1385,12 @@ export async function getAccountEmail(agentId, home) {
|
|
|
1384
1385
|
* and showed non-active versions as "not signed in"). Returns the first
|
|
1385
1386
|
* existing path, or null.
|
|
1386
1387
|
*/
|
|
1388
|
+
/** True when `dir` is under the per-account slot root (`~/.agents/.history/accounts/`). */
|
|
1389
|
+
export function isAccountSlotDir(dir) {
|
|
1390
|
+
const root = path.resolve(getHistoryDir(), 'accounts');
|
|
1391
|
+
const resolved = path.resolve(dir);
|
|
1392
|
+
return resolved === root || resolved.startsWith(root + path.sep);
|
|
1393
|
+
}
|
|
1387
1394
|
function resolveAccountCredentialPath(base, ...segments) {
|
|
1388
1395
|
const perVersion = path.join(base, ...segments);
|
|
1389
1396
|
try {
|
|
@@ -1391,6 +1398,10 @@ function resolveAccountCredentialPath(base, ...segments) {
|
|
|
1391
1398
|
return perVersion;
|
|
1392
1399
|
}
|
|
1393
1400
|
catch { /* unreadable */ }
|
|
1401
|
+
// A slot is the account's own HOME (PHNX-3940 T5). Never inherit another
|
|
1402
|
+
// account's adopted ~/.<config> — that is the cross-account leak.
|
|
1403
|
+
if (isAccountSlotDir(base))
|
|
1404
|
+
return null;
|
|
1394
1405
|
const active = path.join(process.env.AGENTS_REAL_HOME || os.homedir(), ...segments);
|
|
1395
1406
|
if (active !== perVersion) {
|
|
1396
1407
|
try {
|
|
@@ -1900,13 +1911,30 @@ export function readClaudeHomeConfig(base) {
|
|
|
1900
1911
|
const oa = config.oauthAccount;
|
|
1901
1912
|
if (!oa)
|
|
1902
1913
|
return null;
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1914
|
+
const email = normalizeIdentityPart(oa.emailAddress);
|
|
1915
|
+
let accountId = normalizeIdentityPart(oa.accountUuid);
|
|
1916
|
+
let organizationId = normalizeIdentityPart(oa.organizationUuid);
|
|
1917
|
+
// Worker slot provisioned from a durable setup-token: only the email was
|
|
1918
|
+
// seeded on disk (seedClaudeWorkerHomeIdentity), so accountUuid/organizationUuid
|
|
1919
|
+
// are absent and both accountKey and usageKey would be null — the row reads
|
|
1920
|
+
// "not connected here" (PHNX-3940, sibling worker-identity diagnosis). Complete
|
|
1921
|
+
// the identity from the fleet-synced registry row for this email, keyed exactly
|
|
1922
|
+
// as a headed login's home carries it. Guarded on email-only so a headed home
|
|
1923
|
+
// (uuids on disk) never pays the readMeta() read, and fails CLOSED to email-only
|
|
1924
|
+
// when zero or several rows match (registeredNativeAccountForEmail never guesses).
|
|
1925
|
+
if (email && !accountId && !organizationId) {
|
|
1926
|
+
const registered = registeredNativeAccountForEmail(readMeta(), 'claude', email);
|
|
1927
|
+
const parts = registered ? parseNativeIdentityKey('claude', registered.identityKey) : null;
|
|
1928
|
+
if (parts) {
|
|
1929
|
+
accountId = normalizeIdentityPart(parts.account);
|
|
1930
|
+
organizationId = normalizeIdentityPart(parts.org);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1905
1933
|
return {
|
|
1906
1934
|
path: activeFile,
|
|
1907
1935
|
config,
|
|
1908
1936
|
identity: {
|
|
1909
|
-
email: oa.emailAddress || null,
|
|
1937
|
+
email: email ?? (oa.emailAddress || null),
|
|
1910
1938
|
accountId,
|
|
1911
1939
|
organizationId,
|
|
1912
1940
|
organizationName: oa.organizationName ?? null,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Atomic, serialized install of a macOS `.app` bundle to a stable user path.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Used by the menu-bar helper (`lib/menubar/install-menubar.ts`), which is
|
|
5
|
+
* (re)installed on the hot path of ordinary `agents` invocations. (A second
|
|
6
|
+
* helper this module used to also serve, the secrets keychain broker, moved
|
|
7
|
+
* out of this repo entirely with the standalone `secrets` engine —
|
|
8
|
+
* PHNX-3989 — and installs itself now.) It previously did a non-atomic
|
|
8
9
|
* `rm -rf dest` + `cp -R src dest` straight onto the live bundle. That copy takes
|
|
9
10
|
* long enough that a concurrent reader (Gatekeeper, or an exec of the bundle) sees
|
|
10
11
|
* a half-written `.app` — a truncated Mach-O / mismatched `_CodeSignature` hash —
|
|
@@ -24,6 +24,8 @@ export interface AuthHealth {
|
|
|
24
24
|
detail?: string;
|
|
25
25
|
/** account label for display (email / id), when known. Never part of the key. */
|
|
26
26
|
account?: string;
|
|
27
|
+
/** Stable registered account id. Display labels are never used as identity. */
|
|
28
|
+
accountId?: string;
|
|
27
29
|
}
|
|
28
30
|
/** Maximum age of an auth verdict used for automatic routing decisions. */
|
|
29
31
|
export declare const AUTH_PROBE_MAX_AGE_MS: number;
|
|
@@ -173,6 +175,7 @@ export interface AuthProbeRow {
|
|
|
173
175
|
agent: AgentId;
|
|
174
176
|
version: string;
|
|
175
177
|
account?: string;
|
|
178
|
+
accountId?: string;
|
|
176
179
|
health: AuthHealth;
|
|
177
180
|
}
|
|
178
181
|
/** One installed (agent, version) home, tagged with its resolved account label. */
|
package/dist/lib/auth-health.js
CHANGED
|
@@ -255,6 +255,7 @@ export function readFleetAuthRows(host) {
|
|
|
255
255
|
agent: agent,
|
|
256
256
|
version: identity.slice(separator + 1),
|
|
257
257
|
account: health.account,
|
|
258
|
+
accountId: health.accountId,
|
|
258
259
|
health,
|
|
259
260
|
});
|
|
260
261
|
}
|
|
@@ -422,6 +423,14 @@ export async function probeLocalFleetAuth(opts) {
|
|
|
422
423
|
inst.info = await getAccountInfo(inst.agent, inst.home).catch(() => null);
|
|
423
424
|
inst.account = authAccountLabel(inst.info);
|
|
424
425
|
}));
|
|
426
|
+
const [{ readMeta }, { findNativeAccountByIdentity }] = await Promise.all([
|
|
427
|
+
import('./state.js'),
|
|
428
|
+
import('./account-registry.js'),
|
|
429
|
+
]);
|
|
430
|
+
const meta = readMeta();
|
|
431
|
+
for (const inst of installs) {
|
|
432
|
+
inst.accountId = findNativeAccountByIdentity(meta, inst.agent, inst.info)?.id;
|
|
433
|
+
}
|
|
425
434
|
// Probe once per (agent, account) — but only for the network-probing agents
|
|
426
435
|
// that can actually 429; best-effort agents stay per-install (see
|
|
427
436
|
// groupFleetAuthInstalls). Groups run in parallel: they target distinct
|
|
@@ -430,12 +439,14 @@ export async function probeLocalFleetAuth(opts) {
|
|
|
430
439
|
const rep = group.probe;
|
|
431
440
|
const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info, forceLive: opts?.forceLive, signal: opts?.signal });
|
|
432
441
|
health.account = authAccountLabel(rep.info);
|
|
442
|
+
health.accountId = rep.accountId;
|
|
433
443
|
if (health.verdict === 'unconfigured')
|
|
434
444
|
return [];
|
|
435
445
|
return group.members.map((inst) => ({
|
|
436
446
|
agent: inst.agent,
|
|
437
447
|
version: inst.version,
|
|
438
448
|
account: health.account,
|
|
449
|
+
accountId: inst.accountId ?? health.accountId,
|
|
439
450
|
// A distinct object per row so a later mutation of one can't bleed across.
|
|
440
451
|
health: { ...health },
|
|
441
452
|
}));
|
package/dist/lib/auth-mint.d.ts
CHANGED
|
@@ -15,25 +15,34 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { AgentId } from './types.js';
|
|
17
17
|
import { type CredentialAccount } from './account-registry.js';
|
|
18
|
+
import { type StorableCredentialKind } from './secrets-policy.js';
|
|
18
19
|
import { type DriveOptions, type PtyDriver } from './fleet/remote-login.js';
|
|
19
20
|
/** Well-formed Claude setup-token as it appears inside a TTY blob. */
|
|
20
21
|
export declare const CLAUDE_SETUP_TOKEN_CAPTURE_RE: RegExp;
|
|
21
22
|
export interface MintFlow {
|
|
22
23
|
harness: AgentId;
|
|
23
24
|
provider: string;
|
|
24
|
-
auth: 'setup-token';
|
|
25
|
-
/** Interactive mint argv after HOME=… <bin>. Null when stdin-seed only. */
|
|
25
|
+
auth: 'setup-token' | 'api-key';
|
|
26
|
+
/** Interactive mint argv after HOME=… <bin>. Null when stdin-seed only or an api-key collection flow. */
|
|
26
27
|
mintArgs: string[] | null;
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
/** api-key flows: the env var the collected key injects as on a worker (e.g. OPENAI_API_KEY). */
|
|
29
|
+
apiKeyEnv?: string;
|
|
30
|
+
/** setup-token flows: authorize-URL and token patterns scraped from the PTY. */
|
|
31
|
+
verificationUrlRegex?: RegExp;
|
|
32
|
+
tokenCapture?: RegExp;
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* Durable-credential flows per harness. Only Claude exposes an interactive
|
|
36
|
+
* setup-token MINT; every other harness's durable credential is a provider API
|
|
37
|
+
* key, COLLECTED via `--api-key` or a prompt by `accounts add` (never derived
|
|
38
|
+
* from OAuth). The api-key entries describe collection only — they have no
|
|
39
|
+
* mint argv, and `mintAndSeed` refuses them with the `accounts add` pointer.
|
|
40
|
+
* Native device-code login (kimi/antigravity) stays on `agents fleet login`.
|
|
41
|
+
* The apiKeyEnv values are pinned to HARNESS_AUTH's `api-key:<ENV>` worker
|
|
42
|
+
* kinds by auth-mint.test.ts — keep them in lockstep.
|
|
35
43
|
*/
|
|
36
44
|
export declare const MINT_FLOWS: Record<string, MintFlow>;
|
|
45
|
+
/** Harnesses with an interactive setup-token mint (api-key flows are collection, not mint). */
|
|
37
46
|
export declare function listMintableHarnesses(): AgentId[];
|
|
38
47
|
export declare function getMintFlow(harnessRaw: string): MintFlow;
|
|
39
48
|
export declare function unmintableMessage(harness: string): string;
|
|
@@ -75,6 +84,32 @@ export declare function resolveMintIdentity(input: ResolveMintIdentityInput): Re
|
|
|
75
84
|
export declare function seedReservedAuthToken(email: string, token: string): {
|
|
76
85
|
key: string;
|
|
77
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* The env var a harness's durable worker credential injects as on a worker
|
|
89
|
+
* (claude's setup-token rides CLAUDE_CODE_OAUTH_TOKEN; api-key harnesses ride
|
|
90
|
+
* their MINT_FLOWS apiKeyEnv). Throws for a harness with no portable worker
|
|
91
|
+
* credential — kimi/antigravity log in per box.
|
|
92
|
+
*/
|
|
93
|
+
export declare function workerCredentialEnv(harness: AgentId): string;
|
|
94
|
+
/**
|
|
95
|
+
* The reserved-store key for one account's worker credential:
|
|
96
|
+
* `<ENV>_<accountId>` — keyed by account id, never by name or email, so a
|
|
97
|
+
* rename or an email change never breaks the key. Hyphens are stripped because
|
|
98
|
+
* the bundle key grammar (`BUNDLE_KEY_PATTERN`) is env-var-shaped.
|
|
99
|
+
*/
|
|
100
|
+
export declare function workerCredentialStoreKey(harness: AgentId, accountId: string): string;
|
|
101
|
+
/**
|
|
102
|
+
* Write (or rotate) one account's worker credential in the reserved
|
|
103
|
+
* `__<harness>__` store (PHNX-3940). Only non-rotating kinds may enter
|
|
104
|
+
* (`assertStorableCredentialKind` refuses a rotating OAuth/session credential at
|
|
105
|
+
* this boundary — RUSH-1958). FILE-backed, policy `never`, same contract as the
|
|
106
|
+
* legacy `auth` bundle: a keychain- or vault-backed store of this name fails
|
|
107
|
+
* loud, since worker provisioning reads the file backend.
|
|
108
|
+
*/
|
|
109
|
+
export declare function seedReservedStoreKey(harness: AgentId, kind: StorableCredentialKind, key: string, value: string): {
|
|
110
|
+
bundle: string;
|
|
111
|
+
key: string;
|
|
112
|
+
};
|
|
78
113
|
/**
|
|
79
114
|
* Create or rotate the named provider account that `agents run --account` and
|
|
80
115
|
* `agents accounts sync` consume. Existing account of a different kind fails
|
|
@@ -104,7 +139,7 @@ export interface DriveSetupTokenMintOpts extends MintDriveHooks {
|
|
|
104
139
|
* Tears the session down on the way out (success, timeout, or throw).
|
|
105
140
|
*/
|
|
106
141
|
export declare function driveSetupTokenMint(command: string, flow: MintFlow, opts?: DriveSetupTokenMintOpts): Promise<DriveMintResult>;
|
|
107
|
-
export declare function buildMintCommand(flow: MintFlow, bin: string, home: string): string;
|
|
142
|
+
export declare function buildMintCommand(flow: MintFlow, bin: string, home: string, env?: Record<string, string>): string;
|
|
108
143
|
export declare function resolveMintInstallation(harness: AgentId): {
|
|
109
144
|
version: string;
|
|
110
145
|
bin: string;
|
|
@@ -146,13 +181,14 @@ export declare function resolveSyncTargets(fleet: boolean, devices: string[]): P
|
|
|
146
181
|
/**
|
|
147
182
|
* True when a Claude setup-token is already seeded on this box (setup status).
|
|
148
183
|
*
|
|
149
|
-
* Read-only status probe (`agents setup`, `agents doctor`) — the
|
|
150
|
-
* auth-bundle check
|
|
151
|
-
*
|
|
152
|
-
* unavailable — PHNX-3385
|
|
153
|
-
*
|
|
154
|
-
* guards, not a diagnostic. An unreachable
|
|
155
|
-
*
|
|
184
|
+
* Read-only status probe (`agents setup`, `agents doctor`) — neither the
|
|
185
|
+
* account-registry read nor the reserved auth-bundle check MUST crash the whole
|
|
186
|
+
* command because the secret store could not be reached (the macOS Keychain
|
|
187
|
+
* helper source unavailable — PHNX-3385 — or the standalone `secrets` CLI
|
|
188
|
+
* missing / unspawnable); the client's calls fail loud by contract, but that
|
|
189
|
+
* contract is for destructive-write guards, not a diagnostic. An unreachable
|
|
190
|
+
* store here just means "cannot confirm," reported honestly rather than
|
|
191
|
+
* propagated.
|
|
156
192
|
*/
|
|
157
193
|
export declare function hasMintedSetupToken(): {
|
|
158
194
|
ready: boolean;
|
package/dist/lib/auth-mint.js
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import { resolveAgentName } from './agents.js';
|
|
2
|
-
import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, } from './claude-account-token.js';
|
|
2
|
+
import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, invalidateClaudeSetupTokenCache, } from './claude-account-token.js';
|
|
3
3
|
import { addAccount, findAccount, readAccountRegistry, setAccountSecret } from './account-registry.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { bundleBackendSync, bundleExists, bundleExistsSync, keychainRef, pushBundleToHost, readBundleSync, rotateBundleSecretSync, secretsKeychainItem, storeSetSync, writeBundleWithItemsSync, } from './secrets-client.js';
|
|
5
|
+
import { assertStorableCredentialKind, reservedStoreName, } from './secrets-policy.js';
|
|
6
6
|
import { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } from './installations/versions.js';
|
|
7
7
|
import { shellQuote } from './ssh-exec.js';
|
|
8
8
|
import { defaultPtyDriver, } from './fleet/remote-login.js';
|
|
9
9
|
import { showUrl } from './open-url.js';
|
|
10
10
|
import { loadDevices } from './devices/registry.js';
|
|
11
11
|
import { isSelfHost } from './devices/self-host.js';
|
|
12
|
-
import {
|
|
13
|
-
import { assertCredentialTransportHostPinned, resolveHostSshTarget } from './secrets/remote.js';
|
|
12
|
+
import { assertCredentialTransportHostPinned, resolveHostSshTarget } from './hosts/credential-transport.js';
|
|
14
13
|
import { resolveRemoteOsSync } from './hosts/remote-os.js';
|
|
15
14
|
/** Well-formed Claude setup-token as it appears inside a TTY blob. */
|
|
16
15
|
export const CLAUDE_SETUP_TOKEN_CAPTURE_RE = /sk-ant-oat01-[A-Za-z0-9_-]+/;
|
|
17
16
|
const ACCOUNT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
18
17
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* Durable-credential flows per harness. Only Claude exposes an interactive
|
|
20
|
+
* setup-token MINT; every other harness's durable credential is a provider API
|
|
21
|
+
* key, COLLECTED via `--api-key` or a prompt by `accounts add` (never derived
|
|
22
|
+
* from OAuth). The api-key entries describe collection only — they have no
|
|
23
|
+
* mint argv, and `mintAndSeed` refuses them with the `accounts add` pointer.
|
|
24
|
+
* Native device-code login (kimi/antigravity) stays on `agents fleet login`.
|
|
25
|
+
* The apiKeyEnv values are pinned to HARNESS_AUTH's `api-key:<ENV>` worker
|
|
26
|
+
* kinds by auth-mint.test.ts — keep them in lockstep.
|
|
24
27
|
*/
|
|
25
28
|
export const MINT_FLOWS = {
|
|
26
29
|
claude: {
|
|
@@ -32,9 +35,15 @@ export const MINT_FLOWS = {
|
|
|
32
35
|
verificationUrlRegex: /(https:\/\/[^\s"'<>]+)/i,
|
|
33
36
|
tokenCapture: CLAUDE_SETUP_TOKEN_CAPTURE_RE,
|
|
34
37
|
},
|
|
38
|
+
codex: { harness: 'codex', provider: 'openai', auth: 'api-key', mintArgs: null, apiKeyEnv: 'OPENAI_API_KEY' },
|
|
39
|
+
grok: { harness: 'grok', provider: 'xai', auth: 'api-key', mintArgs: null, apiKeyEnv: 'XAI_API_KEY' },
|
|
40
|
+
opencode: { harness: 'opencode', provider: 'opencode', auth: 'api-key', mintArgs: null, apiKeyEnv: 'OPENCODE_API_KEY' },
|
|
41
|
+
cursor: { harness: 'cursor', provider: 'cursor', auth: 'api-key', mintArgs: null, apiKeyEnv: 'CURSOR_API_KEY' },
|
|
42
|
+
droid: { harness: 'droid', provider: 'factory', auth: 'api-key', mintArgs: null, apiKeyEnv: 'FACTORY_API_KEY' },
|
|
35
43
|
};
|
|
44
|
+
/** Harnesses with an interactive setup-token mint (api-key flows are collection, not mint). */
|
|
36
45
|
export function listMintableHarnesses() {
|
|
37
|
-
return Object.
|
|
46
|
+
return Object.values(MINT_FLOWS).filter((f) => f.auth === 'setup-token').map((f) => f.harness);
|
|
38
47
|
}
|
|
39
48
|
export function getMintFlow(harnessRaw) {
|
|
40
49
|
const harness = resolveAgentName(harnessRaw);
|
|
@@ -42,8 +51,12 @@ export function getMintFlow(harnessRaw) {
|
|
|
42
51
|
throw new Error(`Unknown harness '${harnessRaw}'. Interactive setup-token mint is implemented for: ${listMintableHarnesses().join(', ')}.`);
|
|
43
52
|
}
|
|
44
53
|
const flow = MINT_FLOWS[harness];
|
|
45
|
-
if (flow)
|
|
54
|
+
if (flow?.auth === 'setup-token')
|
|
46
55
|
return flow;
|
|
56
|
+
if (flow) {
|
|
57
|
+
throw new Error(`'${harness}' has no derivable token — its durable credential is an API key (${flow.apiKeyEnv}). `
|
|
58
|
+
+ `Collect it with: agents accounts add ${harness} <name> --api-key <key>.`);
|
|
59
|
+
}
|
|
47
60
|
throw new Error(unmintableMessage(harness));
|
|
48
61
|
}
|
|
49
62
|
export function unmintableMessage(harness) {
|
|
@@ -76,6 +89,8 @@ export function extractClaudeSetupToken(screen) {
|
|
|
76
89
|
}
|
|
77
90
|
/** First https URL on the screen, trailing punctuation stripped. */
|
|
78
91
|
export function extractMintUrl(screen, flow) {
|
|
92
|
+
if (!flow.verificationUrlRegex)
|
|
93
|
+
return undefined;
|
|
79
94
|
const text = stripAnsi(screen);
|
|
80
95
|
const m = text.match(flow.verificationUrlRegex);
|
|
81
96
|
if (!m)
|
|
@@ -154,14 +169,15 @@ export function resolveMintIdentity(input) {
|
|
|
154
169
|
export function seedReservedAuthToken(email, token) {
|
|
155
170
|
const cleaned = assertValidSetupToken(token);
|
|
156
171
|
const key = claudeAccountTokenKey(email);
|
|
157
|
-
if (
|
|
158
|
-
const backend =
|
|
172
|
+
if (bundleExistsSync(AUTH_BUNDLE)) {
|
|
173
|
+
const backend = bundleBackendSync(AUTH_BUNDLE);
|
|
159
174
|
if (backend !== 'file') {
|
|
160
175
|
throw new Error(`Reserved bundle '${AUTH_BUNDLE}' exists with backend '${backend}', but usage/probe only reads a FILE-backed auth bundle. Recreate it with: agents secrets create ${AUTH_BUNDLE} --backend file --policy never --i-understand --force`);
|
|
161
176
|
}
|
|
162
|
-
const bundle =
|
|
177
|
+
const bundle = readBundleSync(AUTH_BUNDLE);
|
|
163
178
|
if (key in bundle.vars) {
|
|
164
|
-
|
|
179
|
+
rotateBundleSecretSync(bundle, key, { newValue: cleaned, meta: { type: 'token' } });
|
|
180
|
+
invalidateClaudeSetupTokenCache();
|
|
165
181
|
return { key };
|
|
166
182
|
}
|
|
167
183
|
const item = secretsKeychainItem(AUTH_BUNDLE, key);
|
|
@@ -169,7 +185,8 @@ export function seedReservedAuthToken(email, token) {
|
|
|
169
185
|
if (!bundle.meta)
|
|
170
186
|
bundle.meta = {};
|
|
171
187
|
bundle.meta[key] = { type: 'token' };
|
|
172
|
-
|
|
188
|
+
writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
|
|
189
|
+
invalidateClaudeSetupTokenCache();
|
|
173
190
|
return { key };
|
|
174
191
|
}
|
|
175
192
|
const item = secretsKeychainItem(AUTH_BUNDLE, key);
|
|
@@ -181,9 +198,59 @@ export function seedReservedAuthToken(email, token) {
|
|
|
181
198
|
vars: { [key]: keychainRef(key) },
|
|
182
199
|
meta: { [key]: { type: 'token' } },
|
|
183
200
|
};
|
|
184
|
-
|
|
201
|
+
writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
|
|
202
|
+
invalidateClaudeSetupTokenCache();
|
|
185
203
|
return { key };
|
|
186
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* The env var a harness's durable worker credential injects as on a worker
|
|
207
|
+
* (claude's setup-token rides CLAUDE_CODE_OAUTH_TOKEN; api-key harnesses ride
|
|
208
|
+
* their MINT_FLOWS apiKeyEnv). Throws for a harness with no portable worker
|
|
209
|
+
* credential — kimi/antigravity log in per box.
|
|
210
|
+
*/
|
|
211
|
+
export function workerCredentialEnv(harness) {
|
|
212
|
+
if (harness === 'claude')
|
|
213
|
+
return 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
214
|
+
const flow = MINT_FLOWS[harness];
|
|
215
|
+
if (flow?.auth === 'api-key' && flow.apiKeyEnv)
|
|
216
|
+
return flow.apiKeyEnv;
|
|
217
|
+
throw new Error(`No portable worker credential env for '${harness}' — it logs in per box.`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The reserved-store key for one account's worker credential:
|
|
221
|
+
* `<ENV>_<accountId>` — keyed by account id, never by name or email, so a
|
|
222
|
+
* rename or an email change never breaks the key. Hyphens are stripped because
|
|
223
|
+
* the bundle key grammar (`BUNDLE_KEY_PATTERN`) is env-var-shaped.
|
|
224
|
+
*/
|
|
225
|
+
export function workerCredentialStoreKey(harness, accountId) {
|
|
226
|
+
const slug = accountId.replace(/-/g, '');
|
|
227
|
+
if (!/^[A-Za-z0-9_]+$/.test(slug) || !slug) {
|
|
228
|
+
throw new Error(`Invalid account id '${accountId}' for a worker credential key.`);
|
|
229
|
+
}
|
|
230
|
+
return `${workerCredentialEnv(harness)}_${slug}`;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Write (or rotate) one account's worker credential in the reserved
|
|
234
|
+
* `__<harness>__` store (PHNX-3940). Only non-rotating kinds may enter
|
|
235
|
+
* (`assertStorableCredentialKind` refuses a rotating OAuth/session credential at
|
|
236
|
+
* this boundary — RUSH-1958). FILE-backed, policy `never`, same contract as the
|
|
237
|
+
* legacy `auth` bundle: a keychain- or vault-backed store of this name fails
|
|
238
|
+
* loud, since worker provisioning reads the file backend.
|
|
239
|
+
*/
|
|
240
|
+
export function seedReservedStoreKey(harness, kind, key, value) {
|
|
241
|
+
assertStorableCredentialKind(kind, harness);
|
|
242
|
+
const name = reservedStoreName(harness);
|
|
243
|
+
const cleaned = value.trim();
|
|
244
|
+
if (!cleaned)
|
|
245
|
+
throw new Error(`Empty ${kind} for reserved store '${name}' key ${key}.`);
|
|
246
|
+
// A `__<harness>__` reserved store is written and read as raw FILE-backed items,
|
|
247
|
+
// never as a standalone bundle: the standalone's bundle-name validation rejects
|
|
248
|
+
// the `__`-wrapped reserved name, so the worker credential rides the raw item
|
|
249
|
+
// directly, symmetric with how `readReservedCredential` reads it. A first write
|
|
250
|
+
// and a rotation are the same operation — `store.set` overwrites the item value.
|
|
251
|
+
storeSetSync('file', secretsKeychainItem(name, key), cleaned);
|
|
252
|
+
return { bundle: name, key };
|
|
253
|
+
}
|
|
187
254
|
/**
|
|
188
255
|
* Create or rotate the named provider account that `agents run --account` and
|
|
189
256
|
* `agents accounts sync` consume. Existing account of a different kind fails
|
|
@@ -211,6 +278,9 @@ function emitMintProgress(line, json) {
|
|
|
211
278
|
* Tears the session down on the way out (success, timeout, or throw).
|
|
212
279
|
*/
|
|
213
280
|
export async function driveSetupTokenMint(command, flow, opts = {}) {
|
|
281
|
+
if (flow.auth !== 'setup-token' || !flow.tokenCapture) {
|
|
282
|
+
throw new Error(`Harness '${flow.harness}' has no interactive mint command.`);
|
|
283
|
+
}
|
|
214
284
|
const driver = opts.driver ?? defaultPtyDriver();
|
|
215
285
|
const initialDelayMs = opts.drive?.initialDelayMs ?? 1500;
|
|
216
286
|
const pollMs = opts.drive?.pollMs ?? 500;
|
|
@@ -274,12 +344,16 @@ export async function driveSetupTokenMint(command, flow, opts = {}) {
|
|
|
274
344
|
await driver.stop(id).catch(() => { });
|
|
275
345
|
}
|
|
276
346
|
}
|
|
277
|
-
export function buildMintCommand(flow, bin, home) {
|
|
278
|
-
if (!flow.mintArgs) {
|
|
347
|
+
export function buildMintCommand(flow, bin, home, env = {}) {
|
|
348
|
+
if (flow.auth !== 'setup-token' || !flow.mintArgs) {
|
|
279
349
|
throw new Error(`Harness '${flow.harness}' has no interactive mint command.`);
|
|
280
350
|
}
|
|
281
351
|
const args = flow.mintArgs.map(shellQuote).join(' ');
|
|
282
|
-
|
|
352
|
+
const envPrefix = [
|
|
353
|
+
`HOME=${shellQuote(home)}`,
|
|
354
|
+
...Object.entries(env).map(([k, v]) => `${k}=${shellQuote(v)}`),
|
|
355
|
+
].join(' ');
|
|
356
|
+
return `${envPrefix} ${shellQuote(bin)} ${args}`;
|
|
283
357
|
}
|
|
284
358
|
export function resolveMintInstallation(harness) {
|
|
285
359
|
const installed = listInstalledVersions(harness);
|
|
@@ -382,7 +456,7 @@ async function syncMintedBundles(accountName, device) {
|
|
|
382
456
|
const account = findAccount(accountName);
|
|
383
457
|
if (!account)
|
|
384
458
|
throw new Error(`Unknown provider account '${accountName}'.`);
|
|
385
|
-
const accountPush = pushBundleToHost(accountName, device, {
|
|
459
|
+
const accountPush = await pushBundleToHost(accountName, device, {
|
|
386
460
|
remoteBackend,
|
|
387
461
|
force: true,
|
|
388
462
|
operation: 'accounts mint --fleet',
|
|
@@ -397,8 +471,8 @@ async function syncMintedBundles(accountName, device) {
|
|
|
397
471
|
});
|
|
398
472
|
if (!accountPush.ok)
|
|
399
473
|
throw new Error(accountPush.message);
|
|
400
|
-
if (bundleExists(AUTH_BUNDLE)) {
|
|
401
|
-
const authPush = pushBundleToHost(AUTH_BUNDLE, device, {
|
|
474
|
+
if (await bundleExists(AUTH_BUNDLE)) {
|
|
475
|
+
const authPush = await pushBundleToHost(AUTH_BUNDLE, device, {
|
|
402
476
|
remoteBackend: 'file',
|
|
403
477
|
force: true,
|
|
404
478
|
operation: 'accounts mint --fleet',
|
|
@@ -417,32 +491,36 @@ async function syncMintedBundles(accountName, device) {
|
|
|
417
491
|
/**
|
|
418
492
|
* True when a Claude setup-token is already seeded on this box (setup status).
|
|
419
493
|
*
|
|
420
|
-
* Read-only status probe (`agents setup`, `agents doctor`) — the
|
|
421
|
-
* auth-bundle check
|
|
422
|
-
*
|
|
423
|
-
* unavailable — PHNX-3385
|
|
424
|
-
*
|
|
425
|
-
* guards, not a diagnostic. An unreachable
|
|
426
|
-
*
|
|
494
|
+
* Read-only status probe (`agents setup`, `agents doctor`) — neither the
|
|
495
|
+
* account-registry read nor the reserved auth-bundle check MUST crash the whole
|
|
496
|
+
* command because the secret store could not be reached (the macOS Keychain
|
|
497
|
+
* helper source unavailable — PHNX-3385 — or the standalone `secrets` CLI
|
|
498
|
+
* missing / unspawnable); the client's calls fail loud by contract, but that
|
|
499
|
+
* contract is for destructive-write guards, not a diagnostic. An unreachable
|
|
500
|
+
* store here just means "cannot confirm," reported honestly rather than
|
|
501
|
+
* propagated.
|
|
427
502
|
*/
|
|
428
503
|
export function hasMintedSetupToken() {
|
|
429
|
-
const records = Object.values(readAccountRegistry().accounts);
|
|
430
|
-
const setup = records.filter((a) => a.auth === 'setup-token' && a.provider === 'anthropic');
|
|
431
|
-
if (setup.length) {
|
|
432
|
-
return { ready: true, detail: `${setup.length} Claude setup-token account${setup.length === 1 ? '' : 's'}` };
|
|
433
|
-
}
|
|
434
504
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
505
|
+
// The registry is the account bundles behind the standalone `secrets` CLI, so
|
|
506
|
+
// this read is as reachable — or as unreachable — as the auth-bundle read
|
|
507
|
+
// below and sits inside the same guard.
|
|
508
|
+
const records = Object.values(readAccountRegistry().accounts);
|
|
509
|
+
const setup = records.filter((a) => a.auth === 'setup-token' && a.provider === 'anthropic');
|
|
510
|
+
if (setup.length) {
|
|
511
|
+
return { ready: true, detail: `${setup.length} Claude setup-token account${setup.length === 1 ? '' : 's'}` };
|
|
512
|
+
}
|
|
513
|
+
if (bundleExistsSync(AUTH_BUNDLE) && bundleBackendSync(AUTH_BUNDLE) === 'file') {
|
|
514
|
+
const bundle = readBundleSync(AUTH_BUNDLE);
|
|
437
515
|
const keys = Object.keys(bundle.vars).filter((k) => k.startsWith('CLAUDE_CODE_OAUTH_TOKEN_'));
|
|
438
516
|
if (keys.length)
|
|
439
517
|
return { ready: true, detail: `reserved auth bundle (${keys.length} account key${keys.length === 1 ? '' : 's'})` };
|
|
440
518
|
}
|
|
441
519
|
}
|
|
442
520
|
catch (err) {
|
|
443
|
-
return { ready: false, detail: `could not check the
|
|
521
|
+
return { ready: false, detail: `could not check the secret store: ${err.message}` };
|
|
444
522
|
}
|
|
445
|
-
return { ready: false, detail: 'no Claude setup-token minted — agents accounts
|
|
523
|
+
return { ready: false, detail: 'no Claude setup-token minted — agents accounts add claude [name]' };
|
|
446
524
|
}
|
|
447
525
|
function sleep(ms) {
|
|
448
526
|
return new Promise((resolve) => setTimeout(resolve, ms));
|