@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,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents-owned fleet policy for the secrets engine boundary (PHNX-3989 CTX-1,
|
|
3
|
+
* delta-spec OWN-1). The standalone `secrets` engine has no concept of a
|
|
4
|
+
* "reserved" per-harness credential store, resource profiles, device roles, or
|
|
5
|
+
* fleet election — none of that is portable secret-storage behavior, so it
|
|
6
|
+
* lives here rather than in the engine. Every actual read/write against a
|
|
7
|
+
* bundle or raw item resolves through the process client (`secrets-client.ts`);
|
|
8
|
+
* this module only decides WHICH bundle/key, WHO may reach it, and WHERE it
|
|
9
|
+
* gets pushed.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { pushBundleToHostAsync, listBundles, } from './secrets-client.js';
|
|
14
|
+
import { filterNamesForActiveResourceProfile, getActiveResourceProfile } from './resource-profiles.js';
|
|
15
|
+
import { isDialableDevice, loadDevicesSync } from './devices/registry.js';
|
|
16
|
+
import { sshTargetFor } from './devices/connect.js';
|
|
17
|
+
import { isHostPinned, isDevicePinned, managedKnownHostsPath } from './devices/known-hosts.js';
|
|
18
|
+
import { machineId, normalizeHost } from './session/sync/config.js';
|
|
19
|
+
import { readFleetSharedDeviceStates, updateFleetSharedDeviceStateAsync, } from './fleet-shared-state.js';
|
|
20
|
+
import { getCacheDir, getUserAgentsDir, readMeta } from './state.js';
|
|
21
|
+
import { listNativeAccounts, readSlots } from './account-registry.js';
|
|
22
|
+
import { claudeAccountTokenKey, provisionWorkerSlot, readReservedCredential } from './claude-account-token.js';
|
|
23
|
+
import { configuredDeviceRole, isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
|
|
24
|
+
import { AUTH_STORE_ALIAS, AUTH_BUNDLE_NAME, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, } from './reserved-stores.js';
|
|
25
|
+
// --- reserved per-harness credential stores (PHNX-3940 / PHNX-3989) --------
|
|
26
|
+
//
|
|
27
|
+
// Re-exported from the leaf module `reserved-stores.ts` rather than declared
|
|
28
|
+
// here: `claude-account-token.ts` needs these constants AND is itself a
|
|
29
|
+
// dependency of this module's fleet-sync helpers below, so declaring them
|
|
30
|
+
// here would form a real circular import (reproduced as a TDZ
|
|
31
|
+
// ReferenceError under real ESM evaluation order, outside vitest's
|
|
32
|
+
// mock-tolerant transform). Keep the public surface unchanged for existing
|
|
33
|
+
// importers of `secrets-policy.js`.
|
|
34
|
+
export { AUTH_STORE_ALIAS, AUTH_BUNDLE_NAME, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, };
|
|
35
|
+
// --- resource-profile -> allowedBundles (CTX-1) -----------------------------
|
|
36
|
+
//
|
|
37
|
+
// A resource profile scopes WHICH bundle names a run may reach. The standalone
|
|
38
|
+
// has no concept of a profile; agents-cli computes the allowed set from the
|
|
39
|
+
// full bundle listing and forwards it as `SecretsContext.allowedBundles` on
|
|
40
|
+
// every bounded request. Absent (no active profile) means full trust, matching
|
|
41
|
+
// the client's own documented default.
|
|
42
|
+
/** Filter `names` down to the ones the active resource profile allows for `secrets`. */
|
|
43
|
+
export function filterBundleNamesForActiveProfile(names) {
|
|
44
|
+
return filterNamesForActiveResourceProfile('secrets', names);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `SecretsContext.allowedBundles` for the active resource profile, or
|
|
48
|
+
* `undefined` when no profile is active (full trust — the client's default).
|
|
49
|
+
* `allNames` is the caller's already-fetched full bundle listing (e.g. from
|
|
50
|
+
* `listBundlesSync()`), so this stays a pure filter with no extra round trip.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveAllowedBundlesForActiveProfile(allNames) {
|
|
53
|
+
const filtered = filterBundleNamesForActiveProfile(allNames);
|
|
54
|
+
return filtered.length === allNames.length ? undefined : filtered;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The `SecretsContext` a run should pass to every bundle resolution it makes:
|
|
58
|
+
* the run's harness as the opaque `scope`, plus `allowedBundles` computed from
|
|
59
|
+
* a fresh full bundle listing when a resource profile is active (a stale
|
|
60
|
+
* listing could exclude a bundle created after the profile was set). No
|
|
61
|
+
* profile active means full trust and no extra round trip.
|
|
62
|
+
*/
|
|
63
|
+
export async function resolveSecretsContextForRun(scope) {
|
|
64
|
+
if (!getActiveResourceProfile())
|
|
65
|
+
return scope ? { scope } : undefined;
|
|
66
|
+
const allNames = (await listBundles()).map((b) => b.name);
|
|
67
|
+
return { allowedBundles: resolveAllowedBundlesForActiveProfile(allNames), scope };
|
|
68
|
+
}
|
|
69
|
+
// --- bundle@host remote-reference parsing (agents' own `--secrets` flag
|
|
70
|
+
// semantics, REMOTE-1) -------------------------------------------------------
|
|
71
|
+
/** Parse a `--secrets` flag value into its bundle name and optional `@host`. */
|
|
72
|
+
export function splitBundleRef(ref) {
|
|
73
|
+
const at = ref.indexOf('@');
|
|
74
|
+
if (at === -1)
|
|
75
|
+
return { bundle: ref };
|
|
76
|
+
const bundle = ref.slice(0, at);
|
|
77
|
+
const host = ref.slice(at + 1);
|
|
78
|
+
if (!bundle || !host) {
|
|
79
|
+
throw new Error(`Invalid remote bundle reference ${JSON.stringify(ref)}. Expected 'bundle@host'.`);
|
|
80
|
+
}
|
|
81
|
+
return { bundle, host };
|
|
82
|
+
}
|
|
83
|
+
/** A remote (bundle@host) reference does not yet support key-subset or expiry overrides. */
|
|
84
|
+
export function assertRemoteBundleFlagsUnsupported(bundleName, host, opts, flagLabels) {
|
|
85
|
+
const hasKeys = Array.isArray(opts.keys) && opts.keys.length > 0;
|
|
86
|
+
if (!hasKeys && !opts.allowExpired)
|
|
87
|
+
return;
|
|
88
|
+
throw new Error(`Bundle '${bundleName}@${host}': ${flagLabels.keysFlag} and ${flagLabels.allowExpiredFlag} are not supported for remote (bundle@host) bundles yet. ` +
|
|
89
|
+
`Drop the flag or resolve the bundle locally.`);
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Fleet sync of the reserved file-backed `auth` bundle (PHNX-2371/PHNX-3609).
|
|
93
|
+
//
|
|
94
|
+
// Every daemon publishes only a safe auth readiness verdict into its owned
|
|
95
|
+
// fleet-shared state file. A deterministic ready publisher reads those
|
|
96
|
+
// verdicts and transfers the actual bundle only to a peer reporting `missing`.
|
|
97
|
+
// Secret values never enter Git; the exceptional provisioning transfer is SSH,
|
|
98
|
+
// async and kill-bounded so it never blocks the daemon event loop.
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
/** Each import/read-back SSH operation gets this deadline plus the SSH hard-kill grace. */
|
|
101
|
+
export const AUTH_SYNC_PUSH_DEADLINE_MS = 20_000;
|
|
102
|
+
export function planAuthBundlePush(localAuthOk, localIsPublisher, devices) {
|
|
103
|
+
if (!localAuthOk) {
|
|
104
|
+
return devices.map((device) => ({ action: 'skip', device: device.name, reason: 'no local file-backed auth bundle' }));
|
|
105
|
+
}
|
|
106
|
+
if (!localIsPublisher) {
|
|
107
|
+
return devices.map((device) => ({ action: 'skip', device: device.name, reason: 'another ready device is the elected auth publisher' }));
|
|
108
|
+
}
|
|
109
|
+
return devices.map((device) => {
|
|
110
|
+
if (!device.reachable)
|
|
111
|
+
return { action: 'skip', device: device.name, reason: 'unreachable' };
|
|
112
|
+
if (!device.pinned) {
|
|
113
|
+
return { action: 'skip', device: device.name, reason: `host key not pinned; run \`agents ssh ${device.name}\` once` };
|
|
114
|
+
}
|
|
115
|
+
if (device.remoteAuth === 'ready')
|
|
116
|
+
return { action: 'skip', device: device.name, reason: 'already present' };
|
|
117
|
+
if (device.remoteAuth === 'invalid')
|
|
118
|
+
return { action: 'skip', device: device.name, reason: 'remote auth bundle uses the wrong backend' };
|
|
119
|
+
if (device.remoteAuth === 'unknown') {
|
|
120
|
+
return { action: 'skip', device: device.name, reason: 'no shared auth verdict has arrived from this peer' };
|
|
121
|
+
}
|
|
122
|
+
return { action: 'push', device: device.name };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function defaultDevices() {
|
|
126
|
+
return Object.values(loadDevicesSync());
|
|
127
|
+
}
|
|
128
|
+
function authStatus(local) {
|
|
129
|
+
if (!local.exists)
|
|
130
|
+
return 'missing';
|
|
131
|
+
return local.ok ? 'ready' : 'invalid';
|
|
132
|
+
}
|
|
133
|
+
/** Publish only safe readiness metadata; useful immediately before a repo push. */
|
|
134
|
+
export async function publishReservedAuthVerdict(options = {}) {
|
|
135
|
+
const local = (options.inspectLocal ?? inspectReservedAuthBundle)();
|
|
136
|
+
const status = authStatus(local);
|
|
137
|
+
const device = options.localName ?? machineId();
|
|
138
|
+
try {
|
|
139
|
+
const write = await updateFleetSharedDeviceStateAsync(device, { auth: { status } }, options.userAgentsDir ?? getUserAgentsDir());
|
|
140
|
+
return { device, status, changed: write.changed, error: null };
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
return { device, status, changed: false, error: err.message };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Publish local readiness, elect one ready source, then asynchronously provision
|
|
148
|
+
* only peers whose shared verdict says the bundle is missing.
|
|
149
|
+
*/
|
|
150
|
+
export async function syncReservedAuthBundle(deps = {}) {
|
|
151
|
+
const result = { publisher: null, stateChanged: false, pushed: [], skipped: [], errors: [] };
|
|
152
|
+
const published = await publishReservedAuthVerdict(deps);
|
|
153
|
+
const localStatus = published.status;
|
|
154
|
+
const localName = published.device;
|
|
155
|
+
const localNorm = normalizeHost(localName);
|
|
156
|
+
const root = deps.userAgentsDir ?? getUserAgentsDir();
|
|
157
|
+
const devices = (deps.listDevices ?? defaultDevices)().filter((device) => normalizeHost(device.name) !== localNorm);
|
|
158
|
+
result.stateChanged = published.changed;
|
|
159
|
+
if (published.error)
|
|
160
|
+
result.errors.push({ device: localName, message: `could not publish auth verdict: ${published.error}` });
|
|
161
|
+
const read = readFleetSharedDeviceStates(root);
|
|
162
|
+
result.errors.push(...read.errors);
|
|
163
|
+
const stateByDevice = new Map(read.states.map((state) => [normalizeHost(state.device), state]));
|
|
164
|
+
const registered = new Map(devices.map((device) => [normalizeHost(device.name), device]));
|
|
165
|
+
const readyPublishers = read.states
|
|
166
|
+
.filter((state) => {
|
|
167
|
+
if (state.auth?.status !== 'ready')
|
|
168
|
+
return false;
|
|
169
|
+
if (normalizeHost(state.device) === localNorm)
|
|
170
|
+
return localStatus === 'ready';
|
|
171
|
+
const device = registered.get(normalizeHost(state.device));
|
|
172
|
+
return !!device && isDialableDevice(device);
|
|
173
|
+
})
|
|
174
|
+
.map((state) => state.device);
|
|
175
|
+
if (localStatus === 'ready' && !readyPublishers.some((name) => normalizeHost(name) === localNorm)) {
|
|
176
|
+
readyPublishers.push(localName);
|
|
177
|
+
}
|
|
178
|
+
readyPublishers.sort((a, b) => normalizeHost(a).localeCompare(normalizeHost(b)));
|
|
179
|
+
result.publisher = readyPublishers[0] ?? null;
|
|
180
|
+
const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
|
|
181
|
+
const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
|
|
182
|
+
const plan = planAuthBundlePush(localStatus === 'ready', localIsPublisher, devices.map((device) => ({
|
|
183
|
+
name: device.name,
|
|
184
|
+
reachable: isDialableDevice(device),
|
|
185
|
+
pinned: isDevicePinned(device, pinned),
|
|
186
|
+
remoteAuth: stateByDevice.get(normalizeHost(device.name))?.auth?.status ?? 'unknown',
|
|
187
|
+
})));
|
|
188
|
+
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
189
|
+
const push = deps.push ?? ((bundle, host) => pushBundleToHostAsync(bundle, host, {
|
|
190
|
+
remoteBackend: 'file',
|
|
191
|
+
operation: 'auth-sync',
|
|
192
|
+
agentOnly: true,
|
|
193
|
+
timeoutMs: AUTH_SYNC_PUSH_DEADLINE_MS,
|
|
194
|
+
}));
|
|
195
|
+
const targetOf = deps.sshTarget ?? sshTargetFor;
|
|
196
|
+
const outcomes = await Promise.all(plan.map(async (item) => {
|
|
197
|
+
if (item.action === 'skip')
|
|
198
|
+
return { kind: 'skip', device: item.device, message: item.reason };
|
|
199
|
+
const profile = byName.get(item.device);
|
|
200
|
+
if (!profile)
|
|
201
|
+
return { kind: 'skip', device: item.device, message: 'not in registry' };
|
|
202
|
+
try {
|
|
203
|
+
const out = await push(AUTH_STORE_ALIAS, targetOf(profile));
|
|
204
|
+
return out.ok
|
|
205
|
+
? { kind: 'pushed', device: item.device, message: out.message }
|
|
206
|
+
: { kind: 'error', device: item.device, message: out.message };
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
return { kind: 'error', device: item.device, message: err.message };
|
|
210
|
+
}
|
|
211
|
+
}));
|
|
212
|
+
for (const outcome of outcomes) {
|
|
213
|
+
if (outcome.kind === 'pushed')
|
|
214
|
+
result.pushed.push(outcome.device);
|
|
215
|
+
else if (outcome.kind === 'skip')
|
|
216
|
+
result.skipped.push({ device: outcome.device, reason: outcome.message });
|
|
217
|
+
else
|
|
218
|
+
result.errors.push({ device: outcome.device, message: outcome.message });
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Reserved-store sync, generalized to every portable account (PHNX-3940 T6).
|
|
224
|
+
//
|
|
225
|
+
// The path above pushes ONE bundle (`auth`) to peers reporting a bundle-coarse
|
|
226
|
+
// `missing` verdict. That under-pushes: a peer already holding `auth` (verdict
|
|
227
|
+
// `ready`) never receives a newly-added account's key, so a new account never
|
|
228
|
+
// propagates. It also pushes to any peer regardless of role, and a headed peer
|
|
229
|
+
// must never receive a durable key (owner invariant 7).
|
|
230
|
+
//
|
|
231
|
+
// The functions below fix both. The plan is per ACCOUNT and per KEY
|
|
232
|
+
// (`<ENV>_<accountId>`), not per bundle, and targets `role=worker` peers only.
|
|
233
|
+
//
|
|
234
|
+
// INVARIANT 1 (transport, retain nothing): materializing a worker slot happens
|
|
235
|
+
// on the box where the key LANDED (`reconcileLocalWorkerSlots` ->
|
|
236
|
+
// `provisionWorkerSlot`); the credential itself only ever moves over the
|
|
237
|
+
// existing encrypted SSH bundle push (the client's `pushBundleToHostAsync`),
|
|
238
|
+
// which writes nothing on the sender beyond its existing store. A native
|
|
239
|
+
// OAuth/session file is never transported (`fleet/auth-sync.ts`
|
|
240
|
+
// `isCredentialSafeToPropagate` stays false).
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
const EMPTY_KEY_SET = new Set();
|
|
243
|
+
/**
|
|
244
|
+
* Pure plan: for each worker peer, push each bundle it is missing at least one
|
|
245
|
+
* key of. Deterministic (peers and bundles sorted) so it pins exactly in tests.
|
|
246
|
+
* A headed peer is skipped BEFORE any key comparison -- it never receives a key.
|
|
247
|
+
*/
|
|
248
|
+
export function planReservedStoreSync(accounts, peers) {
|
|
249
|
+
const keysByBundle = new Map();
|
|
250
|
+
for (const account of accounts) {
|
|
251
|
+
let keys = keysByBundle.get(account.bundle);
|
|
252
|
+
if (!keys)
|
|
253
|
+
keysByBundle.set(account.bundle, (keys = new Set()));
|
|
254
|
+
keys.add(account.key);
|
|
255
|
+
}
|
|
256
|
+
const bundles = [...keysByBundle.keys()].sort();
|
|
257
|
+
const items = [];
|
|
258
|
+
for (const peer of [...peers].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
259
|
+
if (peer.headed) {
|
|
260
|
+
items.push({ action: 'skip', device: peer.name, reason: 'headed device receives the account row, never a durable key' });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (!peer.reachable) {
|
|
264
|
+
items.push({ action: 'skip', device: peer.name, reason: 'unreachable' });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (!peer.pinned) {
|
|
268
|
+
items.push({ action: 'skip', device: peer.name, reason: `host key not pinned; run \`agents ssh ${peer.name}\` once` });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
let missingAny = false;
|
|
272
|
+
for (const bundle of bundles) {
|
|
273
|
+
const wanted = keysByBundle.get(bundle);
|
|
274
|
+
const present = peer.presentKeys[bundle] ?? EMPTY_KEY_SET;
|
|
275
|
+
const missing = [...wanted].filter((key) => !present.has(key)).sort();
|
|
276
|
+
if (missing.length > 0) {
|
|
277
|
+
items.push({ action: 'push', device: peer.name, bundle, keys: missing });
|
|
278
|
+
missingAny = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!missingAny)
|
|
282
|
+
items.push({ action: 'skip', device: peer.name, reason: 'all reserved credentials present' });
|
|
283
|
+
}
|
|
284
|
+
return items;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Every portable account's durable worker credential, resolved to (bundle, key).
|
|
288
|
+
* A T1 row carries `workerCredential`; a claude row predating T1 has none, so it
|
|
289
|
+
* falls back to the legacy `auth` bundle keyed by the account email -- the
|
|
290
|
+
* legacy fallback for claude rows that predate T1. An account with no derivable
|
|
291
|
+
* durable credential (per-device harness, or a non-claude row with no
|
|
292
|
+
* `workerCredential`) is not a sync target.
|
|
293
|
+
*/
|
|
294
|
+
export function reservedSyncTargets(meta) {
|
|
295
|
+
const out = [];
|
|
296
|
+
for (const account of listNativeAccounts(meta)) {
|
|
297
|
+
const cred = account.workerCredential;
|
|
298
|
+
if (cred) {
|
|
299
|
+
out.push({ accountId: account.id, harness: account.agent, bundle: cred.bundle, key: cred.key, fingerprint: cred.mintedAt });
|
|
300
|
+
}
|
|
301
|
+
else if (account.agent === 'claude' && account.identityLabel) {
|
|
302
|
+
out.push({ accountId: account.id, harness: 'claude', bundle: AUTH_STORE_ALIAS, key: claudeAccountTokenKey(account.identityLabel), fingerprint: 'legacy' });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
// --- publisher-side delivery memo -----------------------------------------
|
|
308
|
+
// A push sends a WHOLE bundle; the receiver reports no per-key inventory today
|
|
309
|
+
// (that is T3's per-account verdict seam). So the publisher records what it has
|
|
310
|
+
// delivered to each peer, keyed by the credential's fingerprint, to answer "does
|
|
311
|
+
// the peer already have this key" without re-pushing an unchanged bundle every
|
|
312
|
+
// tick. A new account (no memo entry) or a re-mint (fingerprint change) is not a
|
|
313
|
+
// hit, so it propagates within one tick. The memo is LOCAL publisher bookkeeping,
|
|
314
|
+
// never synced.
|
|
315
|
+
function deliveryMemoPath(root = getCacheDir()) {
|
|
316
|
+
return path.join(root, 'reserved-sync-delivered.json');
|
|
317
|
+
}
|
|
318
|
+
function memoKey(peer, bundle, key) {
|
|
319
|
+
return `${normalizeHost(peer)} ${bundle} ${key}`;
|
|
320
|
+
}
|
|
321
|
+
export function readDeliveryMemo(root) {
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(fs.readFileSync(deliveryMemoPath(root), 'utf-8'));
|
|
324
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
325
|
+
return parsed;
|
|
326
|
+
}
|
|
327
|
+
catch { /* missing/malformed → empty memo */ }
|
|
328
|
+
return {};
|
|
329
|
+
}
|
|
330
|
+
function writeDeliveryMemo(memo, root = getCacheDir()) {
|
|
331
|
+
fs.mkdirSync(root, { recursive: true });
|
|
332
|
+
fs.writeFileSync(deliveryMemoPath(root), `${JSON.stringify(memo, null, 2)}\n`, 'utf-8');
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The keys a peer is KNOWN to hold, per bundle, from what we have delivered to it
|
|
336
|
+
* (memo, matched by fingerprint) plus the coarse legacy-`auth` verdict: a peer
|
|
337
|
+
* reporting `auth.status === 'ready'` is known to hold every legacy `auth` key.
|
|
338
|
+
* Reserved `__<harness>__` bundles have no coarse verdict, so their presence comes
|
|
339
|
+
* from the memo alone -- which is what makes a newly-added reserved account push.
|
|
340
|
+
*/
|
|
341
|
+
export function peerPresentKeys(peer, accounts, memo, legacyAuthReady) {
|
|
342
|
+
const present = {};
|
|
343
|
+
for (const account of accounts) {
|
|
344
|
+
const known = memo[memoKey(peer, account.bundle, account.key)] === account.fingerprint
|
|
345
|
+
|| (legacyAuthReady && account.bundle === AUTH_STORE_ALIAS);
|
|
346
|
+
if (!known)
|
|
347
|
+
continue;
|
|
348
|
+
(present[account.bundle] ??= new Set()).add(account.key);
|
|
349
|
+
}
|
|
350
|
+
return present;
|
|
351
|
+
}
|
|
352
|
+
function defaultHasLocalKey(bundle, key) {
|
|
353
|
+
// Reserved-store aware: readReservedCredential reads a `__<harness>__` item
|
|
354
|
+
// directly and a legacy/provider bundle through the normal resolver.
|
|
355
|
+
return readReservedCredential(bundle, key) !== null;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Push every portable account's reserved store to the worker peers that are
|
|
359
|
+
* missing it, per key and per role. Reuses the same election as
|
|
360
|
+
* {@link syncReservedAuthBundle} (one deterministic ready publisher pushes) so
|
|
361
|
+
* there is no second scheduler. The daemon runs this each tick.
|
|
362
|
+
*/
|
|
363
|
+
export async function syncReservedStores(deps = {}) {
|
|
364
|
+
const result = { publisher: null, pushed: [], skipped: [], errors: [] };
|
|
365
|
+
const localName = deps.localName ?? machineId();
|
|
366
|
+
const localNorm = normalizeHost(localName);
|
|
367
|
+
const root = deps.userAgentsDir ?? getUserAgentsDir();
|
|
368
|
+
const meta = (deps.readMetaFn ?? readMeta)();
|
|
369
|
+
const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
|
|
370
|
+
const targets = reservedSyncTargets(meta).filter((t) => hasLocalKey(t.bundle, t.key));
|
|
371
|
+
const devices = (deps.listDevices ?? defaultDevices)().filter((d) => normalizeHost(d.name) !== localNorm);
|
|
372
|
+
const read = readFleetSharedDeviceStates(root);
|
|
373
|
+
result.errors.push(...read.errors);
|
|
374
|
+
const stateByDevice = new Map(read.states.map((s) => [normalizeHost(s.device), s]));
|
|
375
|
+
const registered = new Map(devices.map((d) => [normalizeHost(d.name), d]));
|
|
376
|
+
const localReady = deps.localReady ?? inspectReservedAuthBundle().ok;
|
|
377
|
+
const readyPublishers = read.states
|
|
378
|
+
.filter((s) => {
|
|
379
|
+
if (s.auth?.status !== 'ready')
|
|
380
|
+
return false;
|
|
381
|
+
if (normalizeHost(s.device) === localNorm)
|
|
382
|
+
return localReady;
|
|
383
|
+
const d = registered.get(normalizeHost(s.device));
|
|
384
|
+
return !!d && isDialableDevice(d);
|
|
385
|
+
})
|
|
386
|
+
.map((s) => s.device);
|
|
387
|
+
if (localReady && !readyPublishers.some((n) => normalizeHost(n) === localNorm))
|
|
388
|
+
readyPublishers.push(localName);
|
|
389
|
+
readyPublishers.sort((a, b) => normalizeHost(a).localeCompare(normalizeHost(b)));
|
|
390
|
+
result.publisher = readyPublishers[0] ?? null;
|
|
391
|
+
const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
|
|
392
|
+
if (targets.length === 0 || !localIsPublisher) {
|
|
393
|
+
for (const d of devices) {
|
|
394
|
+
result.skipped.push({ device: d.name, reason: targets.length === 0 ? 'no portable account with a durable credential' : 'another ready device is the elected publisher' });
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
const memo = readDeliveryMemo(deps.cacheDir);
|
|
399
|
+
const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
|
|
400
|
+
const peerRole = deps.peerRole ?? configuredDeviceRole;
|
|
401
|
+
const peers = devices.map((d) => ({
|
|
402
|
+
name: d.name,
|
|
403
|
+
headed: isHeadedDeviceRole(peerRole(d.name)),
|
|
404
|
+
reachable: isDialableDevice(d),
|
|
405
|
+
pinned: isDevicePinned(d, pinned),
|
|
406
|
+
presentKeys: peerPresentKeys(d.name, targets, memo, stateByDevice.get(normalizeHost(d.name))?.auth?.status === 'ready'),
|
|
407
|
+
}));
|
|
408
|
+
const plan = planReservedStoreSync(targets, peers);
|
|
409
|
+
const byName = new Map(devices.map((d) => [d.name, d]));
|
|
410
|
+
const push = deps.push ?? ((bundle, host) => pushBundleToHostAsync(bundle, host, {
|
|
411
|
+
remoteBackend: 'file', operation: 'reserved-sync', agentOnly: true, timeoutMs: AUTH_SYNC_PUSH_DEADLINE_MS,
|
|
412
|
+
}));
|
|
413
|
+
const sshTarget = deps.sshTarget ?? sshTargetFor;
|
|
414
|
+
for (const item of plan) {
|
|
415
|
+
if (item.action === 'skip') {
|
|
416
|
+
result.skipped.push({ device: item.device, reason: item.reason });
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const profile = byName.get(item.device);
|
|
420
|
+
if (!profile) {
|
|
421
|
+
result.skipped.push({ device: item.device, reason: 'not in registry' });
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const out = await push(item.bundle, sshTarget(profile));
|
|
426
|
+
if (out.ok) {
|
|
427
|
+
result.pushed.push({ device: item.device, bundle: item.bundle, keys: item.keys });
|
|
428
|
+
for (const key of item.keys) {
|
|
429
|
+
const fp = targets.find((t) => t.bundle === item.bundle && t.key === key)?.fingerprint;
|
|
430
|
+
if (fp)
|
|
431
|
+
memo[memoKey(item.device, item.bundle, key)] = fp;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
result.errors.push({ device: item.device, message: out.message });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
result.errors.push({ device: item.device, message: err.message });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
writeDeliveryMemo(memo, deps.cacheDir ?? getCacheDir());
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Worker-side: after a durable key lands, materialize a slot for each portable
|
|
447
|
+
* account whose credential is now present locally. Runs only on a NON-headed
|
|
448
|
+
* (worker or unmarked) device -- a headed device provisions its slots from an
|
|
449
|
+
* interactive native login (`accounts add`), never from an injected durable key
|
|
450
|
+
* (invariant 7). Idempotent: an account already backed by a `durable` slot is
|
|
451
|
+
* skipped.
|
|
452
|
+
*/
|
|
453
|
+
export function reconcileLocalWorkerSlots(deps = {}) {
|
|
454
|
+
const result = { provisioned: [], skipped: [], errors: [] };
|
|
455
|
+
const role = deps.selfRole ?? selfConfiguredDeviceRole();
|
|
456
|
+
if (isHeadedDeviceRole(role))
|
|
457
|
+
return result; // headed boxes provision via native login
|
|
458
|
+
const meta = (deps.readMetaFn ?? readMeta)();
|
|
459
|
+
const slots = readSlots(meta);
|
|
460
|
+
const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
|
|
461
|
+
const provision = deps.provision ?? provisionWorkerSlot;
|
|
462
|
+
for (const account of listNativeAccounts(meta)) {
|
|
463
|
+
const cred = account.workerCredential;
|
|
464
|
+
if (!cred)
|
|
465
|
+
continue; // legacy claude worker resolves its token at spawn
|
|
466
|
+
if (!hasLocalKey(cred.bundle, cred.key)) {
|
|
467
|
+
result.skipped.push({ accountId: account.id, reason: 'durable key not synced yet' });
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (slots[account.id]?.authMode === 'durable') {
|
|
471
|
+
result.skipped.push({ accountId: account.id, reason: 'slot already provisioned' });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
provision(account);
|
|
476
|
+
result.provisioned.push(account.id);
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
result.errors.push({ accountId: account.id, message: err.message });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-shared types for the standalone `secrets` CLI (PHNX-3989).
|
|
3
|
+
*
|
|
4
|
+
* The standalone engine (`@phnx-labs/secrets-cli`, extracted from what used to
|
|
5
|
+
* be `cli/src/lib/secrets/**`) owns every one of these shapes; this module only
|
|
6
|
+
* re-declares them so `secrets-client.ts` and its consumers keep exact static
|
|
7
|
+
* types across the process-client boundary with no runtime dependency on the
|
|
8
|
+
* (now deleted) in-repo engine. Keep byte-for-byte in sync with
|
|
9
|
+
* `secrets-cli/src/schema.ts` / `src/core/bundles.ts` — this is the one seam
|
|
10
|
+
* both sides legitimately re-declare rather than share a package for (see the
|
|
11
|
+
* `secrets-client.ts` docblock).
|
|
12
|
+
*/
|
|
13
|
+
/** Supported bundle storage backends. */
|
|
14
|
+
export type SecretsBackend = 'keychain' | 'file' | 'vault';
|
|
15
|
+
/** Supported secret resolution providers for a bundle var. */
|
|
16
|
+
export type SecretProvider = 'keychain' | 'env' | 'file' | 'exec';
|
|
17
|
+
/** A typed reference to a secret: a provider plus a provider-specific value. */
|
|
18
|
+
export interface SecretRef {
|
|
19
|
+
provider: SecretProvider;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A bundle value: either a string (literal or provider-prefixed ref) or an
|
|
24
|
+
* object `{value: string}` used to escape a literal that would otherwise be
|
|
25
|
+
* parsed as a ref (e.g. a URL that happens to start with `env:`).
|
|
26
|
+
*/
|
|
27
|
+
export type BundleValue = string | {
|
|
28
|
+
value: string;
|
|
29
|
+
};
|
|
30
|
+
/** Remote (bundle@host) push destination backend. */
|
|
31
|
+
export type RemoteBackend = 'keychain' | 'file';
|
|
32
|
+
export declare const SECRET_TYPES: readonly ["api-key", "token", "password", "url", "database-url", "ssh-key", "certificate", "webhook", "note"];
|
|
33
|
+
export type SecretType = (typeof SECRET_TYPES)[number];
|
|
34
|
+
/** Per-secret metadata; absent fields are omitted at write time. */
|
|
35
|
+
export interface VarMeta {
|
|
36
|
+
type?: SecretType;
|
|
37
|
+
/** Future-dated ISO date ('YYYY-MM-DD'). */
|
|
38
|
+
expires?: string;
|
|
39
|
+
note?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Bundle prompt policy. `hold` (default): one Touch ID per hold window (~7d),
|
|
43
|
+
* then silent via the secrets-agent. `always`: prompt every read. `never`: no
|
|
44
|
+
* biometry ACL — least-safe, automation-only.
|
|
45
|
+
*/
|
|
46
|
+
export type SecretsPolicy = 'always' | 'hold' | 'never';
|
|
47
|
+
/** A named set of environment variable definitions backed by secret stores. */
|
|
48
|
+
export interface SecretsBundle {
|
|
49
|
+
name: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
allow_exec?: boolean;
|
|
52
|
+
/** Absent ⇒ `keychain`. */
|
|
53
|
+
backend?: SecretsBackend;
|
|
54
|
+
/** Absent ⇒ configured default (`hold`). */
|
|
55
|
+
policy?: SecretsPolicy;
|
|
56
|
+
created_at?: string;
|
|
57
|
+
updated_at?: string;
|
|
58
|
+
last_used?: string;
|
|
59
|
+
vars: Record<string, BundleValue>;
|
|
60
|
+
meta?: Record<string, VarMeta>;
|
|
61
|
+
}
|
|
62
|
+
/** Per-key kind breakdown of an already-resolved bundle. */
|
|
63
|
+
export interface BundleEntryInfo {
|
|
64
|
+
key: string;
|
|
65
|
+
kind: 'literal' | 'keychain' | 'env' | 'file' | 'exec';
|
|
66
|
+
/** Ref target, or empty for a literal. Never a resolved value. */
|
|
67
|
+
detail: string;
|
|
68
|
+
}
|
|
69
|
+
/** Options for `writeBundle` / `writeBundleWithItems`. */
|
|
70
|
+
export interface WriteBundleOptions {
|
|
71
|
+
/** Skip evicting the broker-held copy (no-op writers such as `stampLastUsed`). */
|
|
72
|
+
skipBrokerEviction?: boolean;
|
|
73
|
+
/** Allow writing a reserved `__<harness>__` store. */
|
|
74
|
+
allowReservedStore?: boolean;
|
|
75
|
+
}
|
|
76
|
+
/** Options for `readAndResolveBundleEnv`. */
|
|
77
|
+
export interface ResolveBundleOptions {
|
|
78
|
+
caller?: string;
|
|
79
|
+
/** Harness type whose unlock may be reused. */
|
|
80
|
+
agent?: string;
|
|
81
|
+
/** Duration shown in the Touch ID prompt. */
|
|
82
|
+
duration?: string;
|
|
83
|
+
/** Allow this call to raise an interactive biometric prompt. */
|
|
84
|
+
interactiveUnlock?: boolean;
|
|
85
|
+
/** Skip the broker fast-path and read from the keychain directly. */
|
|
86
|
+
noAgent?: boolean;
|
|
87
|
+
/** Resolve only from an already-unlocked broker snapshot; fail before prompting. */
|
|
88
|
+
agentOnly?: boolean;
|
|
89
|
+
/** Inject only these keys. Errors if any requested key is absent. */
|
|
90
|
+
keys?: string[];
|
|
91
|
+
/** Skip the per-key expiry gate. */
|
|
92
|
+
allowExpired?: boolean;
|
|
93
|
+
/** `process` projects dotted keys to shell-safe env names; `storage` preserves them. */
|
|
94
|
+
keyMode?: 'process' | 'storage';
|
|
95
|
+
/** Read a reserved `__<harness>__` store (system readers only). */
|
|
96
|
+
allowReservedStore?: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** Options for `renameBundle`. */
|
|
99
|
+
export interface RenameOptions {
|
|
100
|
+
/** Overwrite an existing destination bundle. */
|
|
101
|
+
force?: boolean;
|
|
102
|
+
}
|
|
103
|
+
/** Options for `rotateBundleSecret`. */
|
|
104
|
+
export interface RotateOptions {
|
|
105
|
+
/** New plaintext value to write into the store (replaces the old one). */
|
|
106
|
+
newValue: string;
|
|
107
|
+
/** When true, drop existing meta for this key. Mutually exclusive with `meta`. */
|
|
108
|
+
clearMeta?: boolean;
|
|
109
|
+
/** Patch to merge into existing meta. Undefined fields preserve current values. */
|
|
110
|
+
meta?: Partial<VarMeta>;
|
|
111
|
+
}
|
|
112
|
+
/** Reading a keychain-backed item through the standalone. */
|
|
113
|
+
export interface KeychainReadContext {
|
|
114
|
+
agent?: string;
|
|
115
|
+
bundle?: string;
|
|
116
|
+
sessionId?: string;
|
|
117
|
+
reason?: string;
|
|
118
|
+
duration?: string;
|
|
119
|
+
defaultPolicy?: 'hold' | 'always' | 'never';
|
|
120
|
+
forceDuration?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* The caller attests the item(s) carry NO biometry ACL — so the read is
|
|
123
|
+
* silent even when no one is at the screen. Never pass this for an
|
|
124
|
+
* ACL-protected item.
|
|
125
|
+
*/
|
|
126
|
+
silentNoAcl?: boolean;
|
|
127
|
+
}
|
|
128
|
+
/** One held bundle unlock reported by `agent.agentStatus`. */
|
|
129
|
+
export interface AgentStatusEntry {
|
|
130
|
+
name: string;
|
|
131
|
+
expiresAt: number;
|
|
132
|
+
keyCount: number;
|
|
133
|
+
harness: string;
|
|
134
|
+
leaseId?: string;
|
|
135
|
+
keys?: string[];
|
|
136
|
+
}
|
|
137
|
+
/** Options for `pushBundleToHost` / `pushBundleToHostAsync`. */
|
|
138
|
+
export interface PushBundleOptions {
|
|
139
|
+
remoteBackend: RemoteBackend;
|
|
140
|
+
/** Overwrite a key that already exists on the remote. */
|
|
141
|
+
force?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Ignored. File-backend export never forwards the passphrase (PHNX-2371);
|
|
144
|
+
* the remote auto-provisions its own machine-local key. Kept on the options
|
|
145
|
+
* type so existing callers that passed one still compile.
|
|
146
|
+
*/
|
|
147
|
+
passphrase?: string;
|
|
148
|
+
/** Label for the audit trail — `export --device` vs `fleet apply`. */
|
|
149
|
+
operation: string;
|
|
150
|
+
/** Preserve an automation account's permanent prompt-free policy remotely. */
|
|
151
|
+
policyNever?: boolean;
|
|
152
|
+
/** Permit a human-invoked push to read locally without requiring the agent broker. */
|
|
153
|
+
agentOnly?: boolean;
|
|
154
|
+
/** Non-secret literals whose bundle value kind must survive the dotenv transport. */
|
|
155
|
+
literalValues?: Record<string, string>;
|
|
156
|
+
/** Per-SSH-operation deadline. Async daemon callers must set this explicitly. */
|
|
157
|
+
timeoutMs?: number;
|
|
158
|
+
}
|
|
159
|
+
export interface PushBundleResult {
|
|
160
|
+
ok: boolean;
|
|
161
|
+
host: string;
|
|
162
|
+
bundle: string;
|
|
163
|
+
keyCount: number;
|
|
164
|
+
/** One line for the caller to render. Never contains a secret value. */
|
|
165
|
+
message: string;
|
|
166
|
+
}
|
|
167
|
+
/** Lightweight listing entry returned by the managed sync transport. */
|
|
168
|
+
export interface RemoteBundleSummary {
|
|
169
|
+
name: string;
|
|
170
|
+
updated_at: string;
|
|
171
|
+
}
|
|
172
|
+
/** Options for `pullBundle` (the `agents sync --secrets` umbrella stage). */
|
|
173
|
+
export interface PullOptions {
|
|
174
|
+
passphrase: string;
|
|
175
|
+
/** When true, overwrite an existing local bundle. */
|
|
176
|
+
force?: boolean;
|
|
177
|
+
}
|
|
178
|
+
/** One credential-shaped shell-rc export finding. */
|
|
179
|
+
export interface RcSecretFinding {
|
|
180
|
+
/** Basename of the rc file, e.g. `.zshenv`. */
|
|
181
|
+
file: string;
|
|
182
|
+
/** 1-based line number of the export. */
|
|
183
|
+
line: number;
|
|
184
|
+
/** The exported variable name. Never the value. */
|
|
185
|
+
name: string;
|
|
186
|
+
/** The file-store master passphrase gets called out separately. */
|
|
187
|
+
isMasterPassphrase: boolean;
|
|
188
|
+
}
|