@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
|
@@ -60,6 +60,42 @@ export declare function listInstallationLabels(agent: AgentId): string[];
|
|
|
60
60
|
* that disappears mid-scan is skipped rather than failing the whole listing.
|
|
61
61
|
*/
|
|
62
62
|
export declare function listInstallations(agent: AgentId): Installation[];
|
|
63
|
+
/**
|
|
64
|
+
* The label of the single managed installation every harness gets under the
|
|
65
|
+
* account model v2 (PHNX-3940). One managed install per harness per device;
|
|
66
|
+
* accounts live in credential slots beside it, and a second home is an expert
|
|
67
|
+
* `--isolated` copy, never another managed install.
|
|
68
|
+
*/
|
|
69
|
+
export declare const MANAGED_INSTALLATION_LABEL = "main";
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the ONE managed installation of a harness, or null when the harness
|
|
72
|
+
* has no managed install yet. The winner is deterministic: the `main` label
|
|
73
|
+
* first, then the recorded global default (the install the user actually
|
|
74
|
+
* launches), then the first remaining non-isolated installation by label.
|
|
75
|
+
* Isolated copies (`agents add --isolated`) never count — they are expert
|
|
76
|
+
* second homes, not the managed install.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveManagedInstallation(agent: AgentId): Installation | null;
|
|
79
|
+
export interface EnsureHarnessInstallationResult {
|
|
80
|
+
installation: Installation;
|
|
81
|
+
/** True when this call installed the harness just now; false when the managed installation already existed and was reused unchanged. */
|
|
82
|
+
installed: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The taught entry point for "make this harness runnable" (PHNX-3940): return
|
|
86
|
+
* the harness's single managed installation, installing the current release
|
|
87
|
+
* into the `main` label when the harness is not installed yet. `release` pins
|
|
88
|
+
* the fresh install to a concrete release (recorded as a pinned update policy
|
|
89
|
+
* by `installVersion`); it has no effect when an installation already exists —
|
|
90
|
+
* moving an existing install is `agents update <agent> --to <release>`'s job.
|
|
91
|
+
*
|
|
92
|
+
* Throws (fail loud) when the install fails; never returns a half-installed
|
|
93
|
+
* record.
|
|
94
|
+
*/
|
|
95
|
+
export declare function ensureHarnessInstallation(agent: AgentId, opts?: {
|
|
96
|
+
release?: string;
|
|
97
|
+
onProgress?: (message: string) => void;
|
|
98
|
+
}): Promise<EnsureHarnessInstallationResult>;
|
|
63
99
|
/**
|
|
64
100
|
* Get the directory where a specific version is installed.
|
|
65
101
|
*/
|
|
@@ -154,6 +190,13 @@ export declare function pickCanonicalGlobalBinaryVersion(agent: AgentId, version
|
|
|
154
190
|
* For a self-updating global-binary agent (droid) every version dir resolves to
|
|
155
191
|
* the SAME binary, so this collapses them to a single canonical entry — one
|
|
156
192
|
* install, one row in `agents view`, never the phantom set of semver dir names.
|
|
193
|
+
*
|
|
194
|
+
* A directory is an installation only when it has a working launch binary.
|
|
195
|
+
* Account credential slots (PHNX-3940, `~/.agents/.history/accounts/**`) are
|
|
196
|
+
* HOME-shaped dirs with no binary and no `installation.json`, so they can
|
|
197
|
+
* never appear here. Record-less pre-frozen installs are reported as-is; their
|
|
198
|
+
* migration belongs to the record-based paths (`listInstallations`), never to
|
|
199
|
+
* this read surface.
|
|
157
200
|
*/
|
|
158
201
|
export declare function listInstalledVersions(agent: AgentId): string[];
|
|
159
202
|
/**
|
|
@@ -224,6 +224,62 @@ export function listInstallations(agent) {
|
|
|
224
224
|
}
|
|
225
225
|
return out;
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* The label of the single managed installation every harness gets under the
|
|
229
|
+
* account model v2 (PHNX-3940). One managed install per harness per device;
|
|
230
|
+
* accounts live in credential slots beside it, and a second home is an expert
|
|
231
|
+
* `--isolated` copy, never another managed install.
|
|
232
|
+
*/
|
|
233
|
+
export const MANAGED_INSTALLATION_LABEL = 'main';
|
|
234
|
+
/**
|
|
235
|
+
* Resolve the ONE managed installation of a harness, or null when the harness
|
|
236
|
+
* has no managed install yet. The winner is deterministic: the `main` label
|
|
237
|
+
* first, then the recorded global default (the install the user actually
|
|
238
|
+
* launches), then the first remaining non-isolated installation by label.
|
|
239
|
+
* Isolated copies (`agents add --isolated`) never count — they are expert
|
|
240
|
+
* second homes, not the managed install.
|
|
241
|
+
*/
|
|
242
|
+
export function resolveManagedInstallation(agent) {
|
|
243
|
+
const managed = listInstallations(agent).filter((i) => !isVersionIsolated(agent, i.label));
|
|
244
|
+
if (managed.length === 0)
|
|
245
|
+
return null;
|
|
246
|
+
const main = managed.find((i) => i.label === MANAGED_INSTALLATION_LABEL);
|
|
247
|
+
if (main)
|
|
248
|
+
return main;
|
|
249
|
+
const globalDefault = getGlobalDefault(agent);
|
|
250
|
+
const def = globalDefault ? managed.find((i) => i.label === globalDefault) : undefined;
|
|
251
|
+
return def ?? managed[0];
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The taught entry point for "make this harness runnable" (PHNX-3940): return
|
|
255
|
+
* the harness's single managed installation, installing the current release
|
|
256
|
+
* into the `main` label when the harness is not installed yet. `release` pins
|
|
257
|
+
* the fresh install to a concrete release (recorded as a pinned update policy
|
|
258
|
+
* by `installVersion`); it has no effect when an installation already exists —
|
|
259
|
+
* moving an existing install is `agents update <agent> --to <release>`'s job.
|
|
260
|
+
*
|
|
261
|
+
* Throws (fail loud) when the install fails; never returns a half-installed
|
|
262
|
+
* record.
|
|
263
|
+
*/
|
|
264
|
+
export async function ensureHarnessInstallation(agent, opts = {}) {
|
|
265
|
+
const existing = resolveManagedInstallation(agent);
|
|
266
|
+
if (existing)
|
|
267
|
+
return { installation: existing, installed: false };
|
|
268
|
+
const release = opts.release ?? 'latest';
|
|
269
|
+
// installVersion lives in ./versions.js, which imports this file. A dynamic
|
|
270
|
+
// import keeps the static module graph acyclic (see the docblock at the top
|
|
271
|
+
// of this file) while the call itself runs after both modules are loaded.
|
|
272
|
+
const { installVersion } = await import('./versions.js');
|
|
273
|
+
const result = await installVersion(agent, release, opts.onProgress, { installationLabel: MANAGED_INSTALLATION_LABEL });
|
|
274
|
+
if (!result.success) {
|
|
275
|
+
throw new Error(result.error || `Failed to install ${agent}@${release}.`);
|
|
276
|
+
}
|
|
277
|
+
const record = readInstallation(agent, MANAGED_INSTALLATION_LABEL);
|
|
278
|
+
if (!record) {
|
|
279
|
+
throw new Error(`Install of ${agent}@${release} reported success but left no installation record at ${MANAGED_INSTALLATION_LABEL}.`);
|
|
280
|
+
}
|
|
281
|
+
return { installation: record, installed: true };
|
|
282
|
+
}
|
|
227
283
|
/**
|
|
228
284
|
* Get the directory where a specific version is installed.
|
|
229
285
|
*/
|
|
@@ -534,6 +590,13 @@ function collapseGlobalBinaryVersions(agent, versions) {
|
|
|
534
590
|
* For a self-updating global-binary agent (droid) every version dir resolves to
|
|
535
591
|
* the SAME binary, so this collapses them to a single canonical entry — one
|
|
536
592
|
* install, one row in `agents view`, never the phantom set of semver dir names.
|
|
593
|
+
*
|
|
594
|
+
* A directory is an installation only when it has a working launch binary.
|
|
595
|
+
* Account credential slots (PHNX-3940, `~/.agents/.history/accounts/**`) are
|
|
596
|
+
* HOME-shaped dirs with no binary and no `installation.json`, so they can
|
|
597
|
+
* never appear here. Record-less pre-frozen installs are reported as-is; their
|
|
598
|
+
* migration belongs to the record-based paths (`listInstallations`), never to
|
|
599
|
+
* this read surface.
|
|
537
600
|
*/
|
|
538
601
|
export function listInstalledVersions(agent) {
|
|
539
602
|
const agentVersionsDir = path.join(getVersionsDir(), agent);
|
|
@@ -554,13 +617,18 @@ export function listInstalledVersions(agent) {
|
|
|
554
617
|
const entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
|
|
555
618
|
const versions = [];
|
|
556
619
|
for (const entry of entries) {
|
|
557
|
-
if (entry.isDirectory())
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
620
|
+
if (!entry.isDirectory())
|
|
621
|
+
continue;
|
|
622
|
+
// Probe the real launch binary (isVersionInstalled), not just the
|
|
623
|
+
// node_modules/.bin wrapper — a gutted install must not count as healthy
|
|
624
|
+
// in the balanced account/version picker. A dir with no working binary
|
|
625
|
+
// (a slot, a partial download) is not an installation at all. Read-only:
|
|
626
|
+
// `agents view` enumerates through here and must leave every version home
|
|
627
|
+
// byte-identical (issue #2058), so a record-less pre-frozen install is
|
|
628
|
+
// reported as-is — its migration belongs to the record-based paths
|
|
629
|
+
// (listInstallations), never to a read surface.
|
|
630
|
+
if (isVersionInstalled(agent, entry.name)) {
|
|
631
|
+
versions.push(entry.name);
|
|
564
632
|
}
|
|
565
633
|
}
|
|
566
634
|
versions.sort(compareVersions);
|
|
@@ -1044,7 +1044,7 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1044
1044
|
throw new Error(`Invalid version: ${JSON.stringify(version)}`);
|
|
1045
1045
|
}
|
|
1046
1046
|
// A caller-supplied installation label decouples the addressable version-dir
|
|
1047
|
-
// name from the vendor release (PHNX-3940): `agents accounts
|
|
1047
|
+
// name from the vendor release (PHNX-3940): `agents accounts add` mints an
|
|
1048
1048
|
// opaque stable label so ten accounts can share the same upstream `latest`
|
|
1049
1049
|
// under distinct isolated homes, each with its own native login. It follows
|
|
1050
1050
|
// the same identity/release split the installScript branch already uses via
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Install + lifecycle for the macOS menu-bar helper (`MenubarHelper.app`).
|
|
3
3
|
*
|
|
4
|
-
* Mirrors
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Mirrors the stable-Application-Support-path-survives-npm-re-sign pattern and
|
|
5
|
+
* a RunAtLoad + KeepAlive user service, the same shape the deleted embedded
|
|
6
|
+
* secrets-agent used before it moved to the standalone secrets-cli (PHNX-3989).
|
|
7
7
|
*
|
|
8
8
|
* The helper is a no-Dock `.accessory` status-bar app. It reads live agent
|
|
9
9
|
* state directly from disk and shells `agents` only for actions, so the plist
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Install + lifecycle for the macOS menu-bar helper (`MenubarHelper.app`).
|
|
3
3
|
*
|
|
4
|
-
* Mirrors
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Mirrors the stable-Application-Support-path-survives-npm-re-sign pattern and
|
|
5
|
+
* a RunAtLoad + KeepAlive user service, the same shape the deleted embedded
|
|
6
|
+
* secrets-agent used before it moved to the standalone secrets-cli (PHNX-3989).
|
|
7
7
|
*
|
|
8
8
|
* The helper is a no-Dock `.accessory` status-bar app. It reads live agent
|
|
9
9
|
* state directly from disk and shells `agents` only for actions, so the plist
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native-account lookup leaf (PHNX-3940).
|
|
3
|
+
*
|
|
4
|
+
* A PURE, types-only module: it answers "which registered account is this
|
|
5
|
+
* email / what does its identity key decode to" from a `Meta` snapshot passed
|
|
6
|
+
* in by the caller — it never reads state, the keychain, or the network, and it
|
|
7
|
+
* imports nothing from `account-registry` (which imports the agent-spec engine).
|
|
8
|
+
* That import-graph constraint is load-bearing: `agent-spec/agents.ts`
|
|
9
|
+
* completes an email-only worker home from the registry row here, and importing
|
|
10
|
+
* `account-registry` there would be a cycle. `account-registry` re-exports both
|
|
11
|
+
* functions so existing callers keep one import site.
|
|
12
|
+
*
|
|
13
|
+
* The merge of central (`accounts.native`, `scope:'version'`) and device-scoped
|
|
14
|
+
* (`deviceAccounts.native`) rows mirrors `listNativeAccounts`, inlined so this
|
|
15
|
+
* module stays a leaf.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentId, Meta, NativeAccountRecord } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* The one native row for `agent` whose `identityLabel` (email) matches, or null.
|
|
20
|
+
*
|
|
21
|
+
* Returns null when zero OR several rows match — never guesses. Several rows can
|
|
22
|
+
* share one email (a Team seat AND a personal Max under the same address are two
|
|
23
|
+
* accounts, two orgs), and picking one would attribute a worker home to the
|
|
24
|
+
* wrong org. The read-side caller falls closed to email-only identity in that
|
|
25
|
+
* case, which is correct: an ambiguous identity is better left unresolved than
|
|
26
|
+
* resolved wrong.
|
|
27
|
+
*/
|
|
28
|
+
export declare function registeredNativeAccountForEmail(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, agent: AgentId, email: string): NativeAccountRecord | null;
|
|
29
|
+
/**
|
|
30
|
+
* Decode an `<agent>:<label>=<value>[:<label>=<value>…]` identity key into its
|
|
31
|
+
* parts, or null when it does not belong to `agent` or is malformed. The key
|
|
32
|
+
* shape is what `buildIdentityKey` in `agent-spec/agents.ts` writes — e.g.
|
|
33
|
+
* `claude:account=<uuid>:org=<uuid>` → `{ account, org }`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseNativeIdentityKey(agent: AgentId, identityKey: string): Record<string, string> | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Central + device-scoped native rows, device winning on a shared id. */
|
|
2
|
+
function mergedNativeAccounts(meta) {
|
|
3
|
+
const merged = { ...meta.accounts?.native, ...meta.deviceAccounts?.native };
|
|
4
|
+
return Object.values(merged);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The one native row for `agent` whose `identityLabel` (email) matches, or null.
|
|
8
|
+
*
|
|
9
|
+
* Returns null when zero OR several rows match — never guesses. Several rows can
|
|
10
|
+
* share one email (a Team seat AND a personal Max under the same address are two
|
|
11
|
+
* accounts, two orgs), and picking one would attribute a worker home to the
|
|
12
|
+
* wrong org. The read-side caller falls closed to email-only identity in that
|
|
13
|
+
* case, which is correct: an ambiguous identity is better left unresolved than
|
|
14
|
+
* resolved wrong.
|
|
15
|
+
*/
|
|
16
|
+
export function registeredNativeAccountForEmail(meta, agent, email) {
|
|
17
|
+
const needle = email.trim().toLowerCase();
|
|
18
|
+
if (!needle)
|
|
19
|
+
return null;
|
|
20
|
+
const matches = mergedNativeAccounts(meta).filter((account) => account.agent === agent && account.identityLabel?.trim().toLowerCase() === needle);
|
|
21
|
+
return matches.length === 1 ? matches[0] : null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decode an `<agent>:<label>=<value>[:<label>=<value>…]` identity key into its
|
|
25
|
+
* parts, or null when it does not belong to `agent` or is malformed. The key
|
|
26
|
+
* shape is what `buildIdentityKey` in `agent-spec/agents.ts` writes — e.g.
|
|
27
|
+
* `claude:account=<uuid>:org=<uuid>` → `{ account, org }`.
|
|
28
|
+
*/
|
|
29
|
+
export function parseNativeIdentityKey(agent, identityKey) {
|
|
30
|
+
const prefix = `${agent}:`;
|
|
31
|
+
if (!identityKey.startsWith(prefix))
|
|
32
|
+
return null;
|
|
33
|
+
const parts = {};
|
|
34
|
+
for (const segment of identityKey.slice(prefix.length).split(':')) {
|
|
35
|
+
const eq = segment.indexOf('=');
|
|
36
|
+
if (eq < 1 || eq === segment.length - 1)
|
|
37
|
+
return null;
|
|
38
|
+
parts[segment.slice(0, eq)] = segment.slice(eq + 1);
|
|
39
|
+
}
|
|
40
|
+
return Object.keys(parts).length > 0 ? parts : null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded-close helper for a local `net.Server`. Generic — no secrets/engine
|
|
3
|
+
* dependency — so consumers that only needed it for that reason (the webhook
|
|
4
|
+
* receiver, the daemon's webhook service) no longer import `lib/secrets/*`.
|
|
5
|
+
*/
|
|
6
|
+
import type * as net from 'node:net';
|
|
7
|
+
/**
|
|
8
|
+
* Close `server`, resolving once closed or after `timeoutMs`, whichever comes
|
|
9
|
+
* first — a shutdown must never hang on a client that never disconnects.
|
|
10
|
+
*/
|
|
11
|
+
export declare function closeServerBounded(server: net.Server, timeoutMs?: number): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const SERVER_CLOSE_TIMEOUT_MS = 5000;
|
|
2
|
+
/**
|
|
3
|
+
* Close `server`, resolving once closed or after `timeoutMs`, whichever comes
|
|
4
|
+
* first — a shutdown must never hang on a client that never disconnects.
|
|
5
|
+
*/
|
|
6
|
+
export function closeServerBounded(server, timeoutMs = SERVER_CLOSE_TIMEOUT_MS) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let settled = false;
|
|
9
|
+
const finish = () => {
|
|
10
|
+
if (settled)
|
|
11
|
+
return;
|
|
12
|
+
settled = true;
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
resolve();
|
|
15
|
+
};
|
|
16
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
17
|
+
try {
|
|
18
|
+
server.close(() => finish());
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Already closed / not listening — treat as released.
|
|
22
|
+
finish();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'child_process';
|
|
2
2
|
import { getCliLaunch } from './cli-entry.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* argv for writing a bare (non-`agents-cli.`) keychain item via
|
|
5
|
+
* `/usr/bin/security add-generic-password`, deliberately WITHOUT the value: the
|
|
6
|
+
* secret travels over stdin (see below) so it never lands in argv or a `ps`
|
|
7
|
+
* snapshot. Pure — no agents-cli secrets engine involved (this migration writes
|
|
8
|
+
* directly to the OpenClaw account's OWN macOS keychain entries, bypassing the
|
|
9
|
+
* `agents secrets` bundle store entirely), so it's a local helper rather than a
|
|
10
|
+
* process-client call.
|
|
11
|
+
*/
|
|
12
|
+
function buildAddGenericPasswordArgs(account, item) {
|
|
13
|
+
return ['add-generic-password', '-U', '-a', account, '-s', item, '-w'];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* spawnSync options for the bare `-w` keychain write. `input` pipes the value
|
|
17
|
+
* TWICE (bare `-w` prompts enter+confirm; one line fails the confirm and stores
|
|
18
|
+
* an empty secret). `detached: true` runs `security` in a new session with no
|
|
19
|
+
* controlling terminal, so readpassphrase(3) falls back to our piped stdin
|
|
20
|
+
* instead of prompting the user's `/dev/tty` in an interactive shell.
|
|
21
|
+
*/
|
|
22
|
+
function buildAddGenericPasswordSpawnOptions(value) {
|
|
23
|
+
return {
|
|
24
|
+
input: `${value}\n${value}\n`,
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
26
|
+
timeout: 10_000,
|
|
27
|
+
detached: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
4
30
|
export const OPENCLAW_KEYCHAIN_ACCOUNT = 'openclaw';
|
|
5
31
|
export const OPENCLAW_KEYCHAIN_PROVIDER = 'agents_keychain';
|
|
6
32
|
export const OPENCLAW_KEYCHAIN_ENV_SERVICES = {
|
package/dist/lib/profiles.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as crypto from 'node:crypto';
|
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { ALL_AGENT_IDS } from './agents.js';
|
|
13
13
|
import { getUserAgentsDir } from './state.js';
|
|
14
|
-
import {
|
|
14
|
+
import { deleteKeychainTokenSync, getKeychainTokenSync, hasKeychainTokenSync, isSecretsClientError, profileKeychainItem } from './secrets-client.js';
|
|
15
15
|
import { getPreset } from './profiles-presets.js';
|
|
16
16
|
import { MODEL_TIERS, isTierToken } from './model-tiers.js';
|
|
17
17
|
import { addAccount, findAccount, resolveCredentialAccount } from './account-registry.js';
|
|
@@ -85,7 +85,7 @@ function migrateLegacyProfileAuth(profile, file) {
|
|
|
85
85
|
const accountName = `legacy-${profile.provider}-${suffix}`;
|
|
86
86
|
if (!findAccount(accountName)) {
|
|
87
87
|
const kind = profile.auth.envVar.includes('BEARER_TOKEN') ? 'bearer-token' : 'api-key';
|
|
88
|
-
addAccount(accountName, provider, kind,
|
|
88
|
+
addAccount(accountName, provider, kind, getKeychainTokenSync(profile.auth.keychainItem));
|
|
89
89
|
}
|
|
90
90
|
const migratedAccount = findAccount(accountName);
|
|
91
91
|
const oldItem = profile.auth.keychainItem;
|
|
@@ -110,7 +110,7 @@ function migrateLegacyProfileAuth(profile, file) {
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
if (!stillReferenced)
|
|
113
|
-
|
|
113
|
+
deleteKeychainTokenSync(oldItem);
|
|
114
114
|
}
|
|
115
115
|
/** Write a profile to disk atomically (write-to-tmp then rename). */
|
|
116
116
|
export function writeProfile(profile) {
|
|
@@ -250,7 +250,7 @@ export function profileAuthLabel(profile) {
|
|
|
250
250
|
return `${provider} ${maskToken(token)}`;
|
|
251
251
|
}
|
|
252
252
|
if (profile.auth) {
|
|
253
|
-
return `${provider} ${
|
|
253
|
+
return `${provider} ${hasKeychainTokenSync(profile.auth.keychainItem) ? 'stored' : 'missing'}`;
|
|
254
254
|
}
|
|
255
255
|
return provider;
|
|
256
256
|
}
|
|
@@ -324,7 +324,7 @@ export function profileFromPreset(profileName, preset, version) {
|
|
|
324
324
|
env: { ...preset.env },
|
|
325
325
|
auth: {
|
|
326
326
|
envVar: preset.authEnvVar,
|
|
327
|
-
keychainItem:
|
|
327
|
+
keychainItem: profileKeychainItem(preset.provider),
|
|
328
328
|
},
|
|
329
329
|
authOptional: preset.authOptional,
|
|
330
330
|
description: preset.description,
|
|
@@ -392,7 +392,7 @@ export function profileFromHostModel(name, host, model, opts = {}) {
|
|
|
392
392
|
forkedFrom: host,
|
|
393
393
|
};
|
|
394
394
|
if (opts.provider && opts.authEnvVar) {
|
|
395
|
-
profile.auth = { envVar: opts.authEnvVar, keychainItem:
|
|
395
|
+
profile.auth = { envVar: opts.authEnvVar, keychainItem: profileKeychainItem(opts.provider) };
|
|
396
396
|
profile.authOptional = false;
|
|
397
397
|
}
|
|
398
398
|
return profile;
|
|
@@ -455,7 +455,7 @@ export function forkProfile(source, name, opts = {}) {
|
|
|
455
455
|
throw new Error(`Host '${host}' has no known auth env var; --provider cannot be attached to this fork.`);
|
|
456
456
|
}
|
|
457
457
|
forked.provider = opts.provider;
|
|
458
|
-
forked.auth = { envVar, keychainItem:
|
|
458
|
+
forked.auth = { envVar, keychainItem: profileKeychainItem(opts.provider) };
|
|
459
459
|
forked.authOptional = source.authOptional ?? false;
|
|
460
460
|
}
|
|
461
461
|
return forked;
|
|
@@ -529,10 +529,23 @@ export function resolveProfileEnv(profile) {
|
|
|
529
529
|
// Optional auth (host manages its own login) with no stored token: inject
|
|
530
530
|
// nothing and let the host use its own credentials. Only required auth
|
|
531
531
|
// hard-fails on a missing keychain item.
|
|
532
|
-
if (profile.authOptional && !
|
|
532
|
+
if (profile.authOptional && !hasKeychainTokenSync(profile.auth.keychainItem)) {
|
|
533
533
|
return env;
|
|
534
534
|
}
|
|
535
|
-
|
|
535
|
+
let token;
|
|
536
|
+
try {
|
|
537
|
+
token = getKeychainTokenSync(profile.auth.keychainItem);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
// The standalone deliberately reports only a code, so name the item and
|
|
541
|
+
// the repair here — a bare `NOT_FOUND` gives the user nothing to act on.
|
|
542
|
+
if (!isSecretsClientError(err, 'NOT_FOUND'))
|
|
543
|
+
throw err;
|
|
544
|
+
throw new Error(`Harness '${profile.name}' needs a ${profile.provider ?? 'provider'} key, but its keychain item ` +
|
|
545
|
+
`'${profile.auth.keychainItem}' is missing on this device. Store one with ` +
|
|
546
|
+
`'agents harness edit ${profile.name} --auth-provider ${profile.provider ?? '<provider>'}' ` +
|
|
547
|
+
`(or --from-secrets <bundle>:<key>), or bind an account with 'agents harness edit ${profile.name} --account <name>'.`);
|
|
548
|
+
}
|
|
536
549
|
env[profile.auth.envVar] = token;
|
|
537
550
|
}
|
|
538
551
|
return env;
|
|
@@ -9,7 +9,7 @@ import { markdownToToml } from './convert.js';
|
|
|
9
9
|
import { safeJoin } from './paths.js';
|
|
10
10
|
import { subagentTarget } from './subagents-registry.js';
|
|
11
11
|
import { parseSubagentFrontmatter } from './subagents.js';
|
|
12
|
-
import { syncWorkflowToVersion } from './workflows.js';
|
|
12
|
+
import { syncWorkflowToVersion } from './workflows-registry.js';
|
|
13
13
|
const MANIFEST_FILE = '.agents-managed.json';
|
|
14
14
|
const MANIFEST_VERSION = 1;
|
|
15
15
|
const COPY_IGNORE = new Set(['.DS_Store', '.git', '.gitignore', '.venv', '__pycache__', 'node_modules']);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SecretsBackend } from './secrets-types.js';
|
|
2
|
+
import { type AgentId } from './types.js';
|
|
3
|
+
/** Legacy readable alias for `__claude__`. Not migrated in this track. */
|
|
4
|
+
export declare const AUTH_STORE_ALIAS = "auth";
|
|
5
|
+
/** The one reserved bundle name most callers actually need. */
|
|
6
|
+
export declare const AUTH_BUNDLE_NAME = "auth";
|
|
7
|
+
/**
|
|
8
|
+
* The reserved `auth` bundle holds per-account Claude setup-tokens for
|
|
9
|
+
* unattended usage/probe and MUST be file-backed (headless, fleet-shareable —
|
|
10
|
+
* credential-management.md invariant 7). The standalone enforces the same
|
|
11
|
+
* rule on its write path (`WRONG_BACKEND`); agents-cli asserts it on every
|
|
12
|
+
* read so a keychain/vault-backed `auth` left over from an older layout fails
|
|
13
|
+
* loud instead of silently being ignored by usage/probe (SEC-GAP-3).
|
|
14
|
+
*/
|
|
15
|
+
export declare const AUTH_BUNDLE_BACKEND: SecretsBackend;
|
|
16
|
+
export declare const RESERVED_BUNDLE_NAMES: Set<string>;
|
|
17
|
+
export declare function isReservedBundleName(name: string): boolean;
|
|
18
|
+
/** Thrown when the reserved `auth` bundle is found on the wrong backend. */
|
|
19
|
+
export declare class ReservedBundleWrongBackendError extends Error {
|
|
20
|
+
readonly bundle: string;
|
|
21
|
+
readonly backend: SecretsBackend;
|
|
22
|
+
constructor(bundle: string, backend: SecretsBackend);
|
|
23
|
+
}
|
|
24
|
+
/** Fail loud when the reserved `auth` bundle is on any backend but `file`. */
|
|
25
|
+
export declare function assertReservedAuthBackend(backend: SecretsBackend): void;
|
|
26
|
+
/**
|
|
27
|
+
* True for the wrong-backend refusal in either shape: raised here from a
|
|
28
|
+
* read-side check, or returned by the standalone (`WRONG_BACKEND`) when its
|
|
29
|
+
* own write/resolve guard fired.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isReservedBundleBackendError(error: unknown): boolean;
|
|
32
|
+
export declare const RESERVED_STORES: Record<AgentId, string>;
|
|
33
|
+
export declare function reservedStoreName(harness: AgentId): string;
|
|
34
|
+
export declare function isReservedStoreName(name: string): boolean;
|
|
35
|
+
export type StorableCredentialKind = 'setup-token' | 'api-key';
|
|
36
|
+
/**
|
|
37
|
+
* Fail loud at the write boundary: only a setup-token or an API key may enter
|
|
38
|
+
* a reserved store. Rotating OAuth/session credentials stay in their slot on
|
|
39
|
+
* the device that minted them.
|
|
40
|
+
*/
|
|
41
|
+
export declare function assertStorableCredentialKind(kind: string, harness?: AgentId): asserts kind is StorableCredentialKind;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the local reserved `auth` bundle exists and is on the expected
|
|
44
|
+
* (file) backend. `ok: true` on a missing bundle — nothing to be wrong about.
|
|
45
|
+
* Read-only status probe (`agents doctor`, `agents fleet apply`); an
|
|
46
|
+
* unreachable standalone is treated the same as "bundle absent" so a
|
|
47
|
+
* diagnostic never crashes over one optional finding.
|
|
48
|
+
*/
|
|
49
|
+
export declare function inspectReservedAuthBundle(): {
|
|
50
|
+
exists: boolean;
|
|
51
|
+
backend: SecretsBackend | null;
|
|
52
|
+
ok: boolean;
|
|
53
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved per-harness credential stores (PHNX-3940 / PHNX-3989).
|
|
3
|
+
*
|
|
4
|
+
* One store name per `ALL_AGENT_IDS` entry: `__<harness>__`. A user-created
|
|
5
|
+
* bundle can never collide (the standalone rejects `__`-prefixed names unless
|
|
6
|
+
* explicitly allowed). The legacy `auth` bundle remains a readable alias for
|
|
7
|
+
* `__claude__` — this module does not migrate data.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately a LEAF module (no import of `claude-account-token.ts` or
|
|
10
|
+
* `secrets-policy.ts`): both of those need these constants, and
|
|
11
|
+
* `claude-account-token.ts` is itself a dependency of `secrets-policy.ts`'s
|
|
12
|
+
* fleet-sync helpers — importing this module's exports from `secrets-policy.js`
|
|
13
|
+
* instead would form a real circular import that throws
|
|
14
|
+
* `ReferenceError: Cannot access '...' before initialization` under real ESM
|
|
15
|
+
* evaluation order (reproduced running this suite outside vitest's
|
|
16
|
+
* mock-tolerant transform). Keep it a leaf; `secrets-policy.ts` re-exports it
|
|
17
|
+
* for the existing public surface.
|
|
18
|
+
*/
|
|
19
|
+
import { bundleExistsSync, bundleBackendSync, isSecretsClientError } from './secrets-client.js';
|
|
20
|
+
import { AGENT_IDS } from './types.js';
|
|
21
|
+
/** Legacy readable alias for `__claude__`. Not migrated in this track. */
|
|
22
|
+
export const AUTH_STORE_ALIAS = 'auth';
|
|
23
|
+
/** The one reserved bundle name most callers actually need. */
|
|
24
|
+
export const AUTH_BUNDLE_NAME = AUTH_STORE_ALIAS;
|
|
25
|
+
/**
|
|
26
|
+
* The reserved `auth` bundle holds per-account Claude setup-tokens for
|
|
27
|
+
* unattended usage/probe and MUST be file-backed (headless, fleet-shareable —
|
|
28
|
+
* credential-management.md invariant 7). The standalone enforces the same
|
|
29
|
+
* rule on its write path (`WRONG_BACKEND`); agents-cli asserts it on every
|
|
30
|
+
* read so a keychain/vault-backed `auth` left over from an older layout fails
|
|
31
|
+
* loud instead of silently being ignored by usage/probe (SEC-GAP-3).
|
|
32
|
+
*/
|
|
33
|
+
export const AUTH_BUNDLE_BACKEND = 'file';
|
|
34
|
+
export const RESERVED_BUNDLE_NAMES = new Set([AUTH_BUNDLE_NAME]);
|
|
35
|
+
export function isReservedBundleName(name) {
|
|
36
|
+
return RESERVED_BUNDLE_NAMES.has(name.trim().toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
/** Thrown when the reserved `auth` bundle is found on the wrong backend. */
|
|
39
|
+
export class ReservedBundleWrongBackendError extends Error {
|
|
40
|
+
bundle;
|
|
41
|
+
backend;
|
|
42
|
+
constructor(bundle, backend) {
|
|
43
|
+
super(`Bundle '${bundle}' is reserved for file-backed setup-tokens (headless, fleet-shareable). ` +
|
|
44
|
+
`A ${backend}-backed '${bundle}' bundle is ignored by usage/probe instead of authenticating. ` +
|
|
45
|
+
`Recreate it as file-backed: agents secrets delete ${bundle} --yes && agents secrets create ${bundle} --backend file`);
|
|
46
|
+
this.name = 'ReservedBundleWrongBackendError';
|
|
47
|
+
this.bundle = bundle;
|
|
48
|
+
this.backend = backend;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Fail loud when the reserved `auth` bundle is on any backend but `file`. */
|
|
52
|
+
export function assertReservedAuthBackend(backend) {
|
|
53
|
+
if (backend !== AUTH_BUNDLE_BACKEND)
|
|
54
|
+
throw new ReservedBundleWrongBackendError(AUTH_STORE_ALIAS, backend);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* True for the wrong-backend refusal in either shape: raised here from a
|
|
58
|
+
* read-side check, or returned by the standalone (`WRONG_BACKEND`) when its
|
|
59
|
+
* own write/resolve guard fired.
|
|
60
|
+
*/
|
|
61
|
+
export function isReservedBundleBackendError(error) {
|
|
62
|
+
return error instanceof ReservedBundleWrongBackendError || isSecretsClientError(error, 'WRONG_BACKEND');
|
|
63
|
+
}
|
|
64
|
+
export const RESERVED_STORES = Object.fromEntries(AGENT_IDS.map((id) => [id, `__${id}__`]));
|
|
65
|
+
export function reservedStoreName(harness) {
|
|
66
|
+
const name = RESERVED_STORES[harness];
|
|
67
|
+
if (!name)
|
|
68
|
+
throw new Error(`No reserved store for '${harness}'.`);
|
|
69
|
+
return name;
|
|
70
|
+
}
|
|
71
|
+
export function isReservedStoreName(name) {
|
|
72
|
+
const n = name.trim().toLowerCase();
|
|
73
|
+
if (n === AUTH_STORE_ALIAS)
|
|
74
|
+
return true;
|
|
75
|
+
return AGENT_IDS.some((id) => RESERVED_STORES[id].toLowerCase() === n);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fail loud at the write boundary: only a setup-token or an API key may enter
|
|
79
|
+
* a reserved store. Rotating OAuth/session credentials stay in their slot on
|
|
80
|
+
* the device that minted them.
|
|
81
|
+
*/
|
|
82
|
+
export function assertStorableCredentialKind(kind, harness) {
|
|
83
|
+
if (kind === 'setup-token' || kind === 'api-key')
|
|
84
|
+
return;
|
|
85
|
+
throw new Error(storableCredentialRefusal(kind, harness));
|
|
86
|
+
}
|
|
87
|
+
function storableCredentialRefusal(kind, harness) {
|
|
88
|
+
if (harness === 'codex')
|
|
89
|
+
return 'codex: auth.json is a rotating session; add an API key instead';
|
|
90
|
+
if (harness === 'droid')
|
|
91
|
+
return 'droid: refresh tokens are single-use and collapse the fleet when copied; add a FACTORY_API_KEY instead';
|
|
92
|
+
if (harness === 'claude')
|
|
93
|
+
return 'claude: native OAuth (.credentials.json) is a rotating session; mint a setup-token instead';
|
|
94
|
+
if (harness === 'grok')
|
|
95
|
+
return 'grok: auth.json is a rotating session; add an XAI_API_KEY instead';
|
|
96
|
+
if (harness === 'kimi' || harness === 'antigravity')
|
|
97
|
+
return `${harness}: no portable worker credential; log in per device`;
|
|
98
|
+
const who = harness ?? 'this harness';
|
|
99
|
+
return `${who}: reserved stores accept only a setup-token or an API key, not '${kind}'`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether the local reserved `auth` bundle exists and is on the expected
|
|
103
|
+
* (file) backend. `ok: true` on a missing bundle — nothing to be wrong about.
|
|
104
|
+
* Read-only status probe (`agents doctor`, `agents fleet apply`); an
|
|
105
|
+
* unreachable standalone is treated the same as "bundle absent" so a
|
|
106
|
+
* diagnostic never crashes over one optional finding.
|
|
107
|
+
*/
|
|
108
|
+
export function inspectReservedAuthBundle() {
|
|
109
|
+
let exists;
|
|
110
|
+
try {
|
|
111
|
+
exists = bundleExistsSync(AUTH_BUNDLE_NAME);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return { exists: false, backend: null, ok: true };
|
|
115
|
+
}
|
|
116
|
+
if (!exists)
|
|
117
|
+
return { exists: false, backend: null, ok: true };
|
|
118
|
+
const backend = bundleBackendSync(AUTH_BUNDLE_NAME);
|
|
119
|
+
return { exists: true, backend, ok: backend === AUTH_BUNDLE_BACKEND };
|
|
120
|
+
}
|