@phnx-labs/agents-cli 1.22.83 → 1.22.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +80 -53
- package/dist/bootstrap.d.ts +7 -6
- package/dist/bootstrap.js +20 -27
- package/dist/cli/command-registry.js +5 -1
- package/dist/commands/accounts.d.ts +6 -13
- package/dist/commands/accounts.js +330 -143
- package/dist/commands/apply.js +1 -1
- package/dist/commands/auth-mint.d.ts +7 -3
- package/dist/commands/auth-mint.js +21 -9
- package/dist/commands/auth.js +2 -3
- package/dist/commands/browser.js +10 -10
- package/dist/commands/daemon.d.ts +7 -3
- package/dist/commands/daemon.js +40 -25
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/exec.js +87 -39
- package/dist/commands/fleet-capture.js +21 -2
- package/dist/commands/harness-wizard.js +2 -2
- package/dist/commands/lease.js +7 -12
- package/dist/commands/profiles.d.ts +2 -2
- package/dist/commands/profiles.js +20 -13
- package/dist/commands/repo.js +3 -3
- package/dist/commands/run-account-picker.d.ts +3 -3
- package/dist/commands/run-account-picker.js +29 -30
- package/dist/commands/secrets-passthrough.d.ts +20 -0
- package/dist/commands/secrets-passthrough.js +43 -0
- package/dist/commands/sessions-trace.d.ts +11 -0
- package/dist/commands/sessions-trace.js +71 -0
- package/dist/commands/setup-accounts.d.ts +3 -2
- package/dist/commands/setup-accounts.js +4 -4
- package/dist/commands/setup-secrets.d.ts +16 -21
- package/dist/commands/setup-secrets.js +43 -224
- package/dist/commands/ssh.js +2 -2
- package/dist/commands/sync.js +1 -1
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.js +43 -40
- package/dist/commands/versions.d.ts +11 -11
- package/dist/commands/versions.js +131 -83
- package/dist/commands/view.d.ts +1 -9
- package/dist/commands/view.js +41 -55
- package/dist/commands/webhook.js +5 -5
- package/dist/commands/workflows.js +2 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +13 -42
- package/dist/lib/account-capabilities.d.ts +1 -1
- package/dist/lib/account-capabilities.js +1 -1
- package/dist/lib/account-catalog.d.ts +121 -6
- package/dist/lib/account-catalog.js +421 -7
- package/dist/lib/account-registry.d.ts +8 -30
- package/dist/lib/account-registry.js +36 -79
- package/dist/lib/account-schema.d.ts +1 -1
- package/dist/lib/account-schema.js +1 -1
- package/dist/lib/accounting/account-pool-collect.js +16 -4
- package/dist/lib/accounting/rotate.d.ts +20 -7
- package/dist/lib/accounting/rotate.js +89 -13
- package/dist/lib/accounting/usage.js +11 -16
- package/dist/lib/accounts/add.d.ts +138 -0
- package/dist/lib/accounts/add.js +651 -0
- package/dist/lib/accounts/migrate.d.ts +117 -0
- package/dist/lib/accounts/migrate.js +536 -0
- package/dist/lib/accounts/slots.d.ts +13 -0
- package/dist/lib/accounts/slots.js +100 -0
- package/dist/lib/agent-spec/agents.d.ts +16 -0
- package/dist/lib/agent-spec/agents.js +32 -4
- package/dist/lib/app-bundle-install.js +5 -4
- package/dist/lib/auth-health.d.ts +3 -0
- package/dist/lib/auth-health.js +11 -0
- package/dist/lib/auth-mint.d.ts +52 -16
- package/dist/lib/auth-mint.js +117 -39
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +5 -5
- package/dist/lib/byok-usage.js +3 -3
- package/dist/lib/claude-account-token.d.ts +47 -3
- package/dist/lib/claude-account-token.js +172 -50
- package/dist/lib/cloud/antigravity.js +4 -4
- package/dist/lib/cloud/cursor.js +4 -4
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +6 -6
- package/dist/lib/crabbox/runtimes.d.ts +3 -3
- package/dist/lib/crabbox/runtimes.js +5 -5
- package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
- package/dist/lib/daemon/account-state-daemon-service.js +199 -3
- package/dist/lib/daemon/auth-sync-service.js +28 -1
- package/dist/lib/daemon/daemon.d.ts +6 -16
- package/dist/lib/daemon/daemon.js +52 -97
- package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
- package/dist/lib/daemon/daemon.test-fixture.js +22 -40
- package/dist/lib/daemon/harness-update-service.d.ts +1 -1
- package/dist/lib/daemon/harness-update-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +15 -3
- package/dist/lib/daemon/runner.js +49 -24
- package/dist/lib/daemon-health.d.ts +1 -2
- package/dist/lib/daemon-health.js +7 -6
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +1 -11
- package/dist/lib/daemon-ticks.js +23 -2
- package/dist/lib/daemon-webhooks.d.ts +8 -7
- package/dist/lib/daemon-webhooks.js +10 -9
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/harness-inventory.d.ts +39 -0
- package/dist/lib/devices/harness-inventory.js +126 -4
- package/dist/lib/doctor-diff.js +2 -1
- package/dist/lib/exec-account-home.d.ts +38 -0
- package/dist/lib/exec-account-home.js +164 -0
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +101 -31
- package/dist/lib/feed/activity-stream.d.ts +60 -0
- package/dist/lib/feed/activity-stream.js +271 -0
- package/dist/lib/feed/activity.d.ts +7 -0
- package/dist/lib/feed/activity.js +10 -4
- package/dist/lib/feed/watch.d.ts +9 -3
- package/dist/lib/feed/watch.js +72 -36
- package/dist/lib/fleet/apply.d.ts +2 -2
- package/dist/lib/fleet/apply.js +4 -4
- package/dist/lib/fleet/auth-sync.js +4 -3
- package/dist/lib/fleet-shared-repo-sync.js +6 -9
- package/dist/lib/fleet-shared-state.d.ts +6 -0
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapter.js +10 -6
- package/dist/lib/harness/adapters/grok.js +8 -3
- package/dist/lib/harness/adapters/muse.js +1 -1
- package/dist/lib/harness/adapters/opencode.js +12 -3
- package/dist/lib/harness-auth-capabilities.d.ts +61 -0
- package/dist/lib/harness-auth-capabilities.js +52 -0
- package/dist/lib/helper-versions.d.ts +6 -4
- package/dist/lib/helper-versions.js +5 -4
- package/dist/lib/hosts/credential-transport.d.ts +10 -0
- package/dist/lib/hosts/credential-transport.js +37 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +13 -6
- package/dist/lib/identity/client.d.ts +3 -3
- package/dist/lib/identity/client.js +3 -3
- package/dist/lib/installations/index.d.ts +1 -1
- package/dist/lib/installations/index.js +1 -1
- package/dist/lib/installations/migrate.js +9 -0
- package/dist/lib/installations/resolve.js +1 -1
- package/dist/lib/installations/shims.d.ts +15 -0
- package/dist/lib/installations/shims.js +69 -0
- package/dist/lib/installations/store.d.ts +43 -0
- package/dist/lib/installations/store.js +75 -7
- package/dist/lib/installations/versions.js +1 -1
- package/dist/lib/menubar/install-menubar.d.ts +3 -3
- package/dist/lib/menubar/install-menubar.js +3 -3
- package/dist/lib/native-accounts.d.ts +35 -0
- package/dist/lib/native-accounts.js +41 -0
- package/dist/lib/net-close.d.ts +11 -0
- package/dist/lib/net-close.js +25 -0
- package/dist/lib/openclaw-keychain.js +27 -1
- package/dist/lib/profiles.js +22 -9
- package/dist/lib/project-resources.js +1 -1
- package/dist/lib/reserved-stores.d.ts +53 -0
- package/dist/lib/reserved-stores.js +120 -0
- package/dist/lib/secrets-client.d.ts +191 -0
- package/dist/lib/secrets-client.js +710 -0
- package/dist/lib/secrets-policy.d.ts +222 -0
- package/dist/lib/secrets-policy.js +483 -0
- package/dist/lib/secrets-types.d.ts +188 -0
- package/dist/lib/secrets-types.js +24 -0
- package/dist/lib/service-manifest.js +3 -4
- package/dist/lib/session/active.d.ts +38 -4
- package/dist/lib/session/active.js +36 -4
- package/dist/lib/session/bash-command.js +10 -0
- package/dist/lib/session/db.d.ts +65 -2
- package/dist/lib/session/db.js +178 -18
- package/dist/lib/session/mirror.d.ts +5 -0
- package/dist/lib/session/mirror.js +166 -0
- package/dist/lib/session/parse.d.ts +49 -0
- package/dist/lib/session/parse.js +324 -30
- package/dist/lib/session/prompt.d.ts +93 -2
- package/dist/lib/session/prompt.js +266 -15
- package/dist/lib/session/remote/peer-stream.d.ts +47 -0
- package/dist/lib/session/remote/peer-stream.js +142 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +41 -42
- package/dist/lib/session/session-cache.d.ts +9 -0
- package/dist/lib/session/session-cache.js +40 -2
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/timeline-pass.d.ts +129 -0
- package/dist/lib/session/timeline-pass.js +323 -0
- package/dist/lib/session/timeline.d.ts +182 -0
- package/dist/lib/session/timeline.js +636 -0
- package/dist/lib/session/types.d.ts +155 -1
- package/dist/lib/sha256-asset.d.ts +11 -16
- package/dist/lib/sha256-asset.js +11 -16
- package/dist/lib/share/config.js +14 -5
- package/dist/lib/signin-badge.d.ts +26 -0
- package/dist/lib/signin-badge.js +42 -0
- package/dist/lib/staleness/detectors/workflows.js +3 -105
- package/dist/lib/staleness/writers/workflows.js +2 -1
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +18 -7
- package/dist/lib/summarizer/pass.d.ts +2 -0
- package/dist/lib/summarizer/pass.js +14 -1
- package/dist/lib/summarizer/summarize.d.ts +7 -0
- package/dist/lib/summarizer/summarize.js +3 -0
- package/dist/lib/sync-umbrella.js +2 -2
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +74 -33
- package/dist/lib/view-types.d.ts +6 -3
- package/dist/lib/workflows-registry.d.ts +71 -0
- package/dist/lib/workflows-registry.js +280 -0
- package/dist/lib/workflows.d.ts +21 -21
- package/dist/lib/workflows.js +12 -327
- package/package.json +2 -3
- package/scripts/postinstall.js +4 -29
- package/dist/commands/secrets-import.d.ts +0 -18
- package/dist/commands/secrets-import.js +0 -74
- package/dist/commands/secrets-migrate.d.ts +0 -25
- package/dist/commands/secrets-migrate.js +0 -334
- package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
- package/dist/commands/secrets-rotate-passphrase.js +0 -96
- package/dist/commands/secrets-sync.d.ts +0 -11
- package/dist/commands/secrets-sync.js +0 -153
- package/dist/commands/secrets-vault.d.ts +0 -10
- package/dist/commands/secrets-vault.js +0 -130
- package/dist/commands/secrets.d.ts +0 -212
- package/dist/commands/secrets.js +0 -3030
- package/dist/lib/accounts/connect.d.ts +0 -170
- package/dist/lib/accounts/connect.js +0 -383
- package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
- package/dist/lib/daemon/keychain-reap-service.js +0 -32
- package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
- package/dist/lib/daemon/secrets-broker-service.js +0 -51
- package/dist/lib/secrets/agent.d.ts +0 -358
- package/dist/lib/secrets/agent.js +0 -1291
- package/dist/lib/secrets/audit.d.ts +0 -46
- package/dist/lib/secrets/audit.js +0 -101
- package/dist/lib/secrets/bundles.d.ts +0 -290
- package/dist/lib/secrets/bundles.js +0 -1547
- package/dist/lib/secrets/download-keychain.d.ts +0 -47
- package/dist/lib/secrets/download-keychain.js +0 -70
- package/dist/lib/secrets/drivers/rush.d.ts +0 -14
- package/dist/lib/secrets/drivers/rush.js +0 -90
- package/dist/lib/secrets/fallback.d.ts +0 -48
- package/dist/lib/secrets/fallback.js +0 -48
- package/dist/lib/secrets/filestore.d.ts +0 -222
- package/dist/lib/secrets/filestore.js +0 -1099
- package/dist/lib/secrets/headless.d.ts +0 -43
- package/dist/lib/secrets/headless.js +0 -56
- package/dist/lib/secrets/icloud-import.d.ts +0 -79
- package/dist/lib/secrets/icloud-import.js +0 -206
- package/dist/lib/secrets/index.d.ts +0 -451
- package/dist/lib/secrets/index.js +0 -1568
- package/dist/lib/secrets/install-helper.d.ts +0 -72
- package/dist/lib/secrets/install-helper.js +0 -245
- package/dist/lib/secrets/lease.d.ts +0 -25
- package/dist/lib/secrets/lease.js +0 -44
- package/dist/lib/secrets/linux.d.ts +0 -77
- package/dist/lib/secrets/linux.js +0 -393
- package/dist/lib/secrets/list-filter.d.ts +0 -109
- package/dist/lib/secrets/list-filter.js +0 -261
- package/dist/lib/secrets/mcp.d.ts +0 -93
- package/dist/lib/secrets/mcp.js +0 -211
- package/dist/lib/secrets/profiles.d.ts +0 -10
- package/dist/lib/secrets/profiles.js +0 -13
- package/dist/lib/secrets/push.d.ts +0 -133
- package/dist/lib/secrets/push.js +0 -273
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
- package/dist/lib/secrets/rc-hygiene.js +0 -143
- package/dist/lib/secrets/read-backoff.d.ts +0 -27
- package/dist/lib/secrets/read-backoff.js +0 -64
- package/dist/lib/secrets/reaper.d.ts +0 -90
- package/dist/lib/secrets/reaper.js +0 -243
- package/dist/lib/secrets/remote.d.ts +0 -152
- package/dist/lib/secrets/remote.js +0 -344
- package/dist/lib/secrets/reserved-sync.d.ts +0 -66
- package/dist/lib/secrets/reserved-sync.js +0 -147
- package/dist/lib/secrets/scope.d.ts +0 -26
- package/dist/lib/secrets/scope.js +0 -29
- package/dist/lib/secrets/session-store.d.ts +0 -107
- package/dist/lib/secrets/session-store.js +0 -342
- package/dist/lib/secrets/sync-backend.d.ts +0 -48
- package/dist/lib/secrets/sync-backend.js +0 -13
- package/dist/lib/secrets/sync-commands.d.ts +0 -21
- package/dist/lib/secrets/sync-commands.js +0 -21
- package/dist/lib/secrets/sync.d.ts +0 -48
- package/dist/lib/secrets/sync.js +0 -237
- package/dist/lib/secrets/unlock-hints.d.ts +0 -27
- package/dist/lib/secrets/unlock-hints.js +0 -36
- package/dist/lib/secrets/usage-db.d.ts +0 -46
- package/dist/lib/secrets/usage-db.js +0 -96
- package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
- package/dist/lib/secrets/vault-age-helper.js +0 -34
- package/dist/lib/secrets/vault.d.ts +0 -49
- package/dist/lib/secrets/vault.js +0 -399
- package/dist/lib/secrets/windows.d.ts +0 -81
- package/dist/lib/secrets/windows.js +0 -556
- package/scripts/install-helper.js +0 -97
- /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
- /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote secrets — read and use `agents secrets` bundles that live on another
|
|
3
|
-
* host over the same SSH path that `agents secrets export --device` uses.
|
|
4
|
-
*
|
|
5
|
-
* Browse streams remote stdout verbatim; use resolves a bundle to an env map
|
|
6
|
-
* and injects it ephemerally without persisting it locally.
|
|
7
|
-
*/
|
|
8
|
-
import { sshExec, sshExecAsync, sshStream, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
9
|
-
import { resolveHost } from '../hosts/registry.js';
|
|
10
|
-
import { emitSecretAudit } from './audit.js';
|
|
11
|
-
import { sshTargetFor } from '../hosts/types.js';
|
|
12
|
-
import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
13
|
-
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
14
|
-
import { isLoaderOrInterpreterEnv } from './bundles.js';
|
|
15
|
-
import { isHostPinned, hostKeyCheckingOpts } from '../devices/known-hosts.js';
|
|
16
|
-
export const REMOTE_TIMEOUT_MS = 30_000;
|
|
17
|
-
/** The host part of an ssh target (`user@host` -> `host`) for known_hosts matching. */
|
|
18
|
-
function hostKeyLookupName(target) {
|
|
19
|
-
return target.split('@').pop() ?? target;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* SSH options for secret-carrying transport. Pins the managed host key and
|
|
23
|
-
* disables multiplex so no reusable control socket lingers for other invocations.
|
|
24
|
-
*/
|
|
25
|
-
export function credentialTransportSshOpts(target) {
|
|
26
|
-
return { hostKeyOpts: hostKeyCheckingOpts(isHostPinned(hostKeyLookupName(target))), multiplex: false };
|
|
27
|
-
}
|
|
28
|
-
/** Refuse durable credential transfer until the destination SSH key is pinned. */
|
|
29
|
-
export function assertCredentialTransportHostPinned(target, pinned = isHostPinned(hostKeyLookupName(target))) {
|
|
30
|
-
if (pinned)
|
|
31
|
-
return;
|
|
32
|
-
throw new Error(`Refusing to transfer provider credentials to '${target}' before its SSH host key is pinned. ` +
|
|
33
|
-
`Connect once with 'agents ssh ${target}' and verify the host, then retry.`);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Trust boundary for a remote-resolved env map. A peer's export output is
|
|
37
|
-
* untrusted input that could reshape this process once merged into the env, so
|
|
38
|
-
* block dangerous override classes here for every consumer:
|
|
39
|
-
* loader/interpreter vars, GIT_*, *_PROXY, and *_BASE_URL.
|
|
40
|
-
*/
|
|
41
|
-
export function isDangerousRemoteEnvKey(name) {
|
|
42
|
-
const upper = name.toUpperCase();
|
|
43
|
-
if (isLoaderOrInterpreterEnv(upper))
|
|
44
|
-
return true;
|
|
45
|
-
if (upper.startsWith('GIT_'))
|
|
46
|
-
return true; // GIT_SSH_COMMAND hijacks git subprocesses
|
|
47
|
-
if (upper.endsWith('_PROXY'))
|
|
48
|
-
return true; // *_PROXY = MITM
|
|
49
|
-
if (upper.endsWith('_BASE_URL'))
|
|
50
|
-
return true; // *_BASE_URL = model API redirect
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
/** Remote OS for a host name or target string. Prefer the original host name
|
|
54
|
-
* because enrolled inline hosts resolve to `user@address`, while the OS
|
|
55
|
-
* registry is keyed by the host name. */
|
|
56
|
-
function osForTarget(target, lookupName) {
|
|
57
|
-
const byName = lookupName ? resolveRemoteOsSync(lookupName) : undefined;
|
|
58
|
-
return byName ?? resolveRemoteOsSync(target.split('@').pop() ?? target);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Resolve a `--device` value to an ssh target string for the remote-secrets
|
|
62
|
-
* path. Delegates to the same resolver `run --device` uses; on a miss, treats
|
|
63
|
-
* the value as a raw ssh target and validates it.
|
|
64
|
-
*/
|
|
65
|
-
export async function resolveHostSshTarget(nameOrAlias) {
|
|
66
|
-
const host = await resolveHost(nameOrAlias);
|
|
67
|
-
if (host)
|
|
68
|
-
return sshTargetFor(host);
|
|
69
|
-
assertValidSshTarget(nameOrAlias);
|
|
70
|
-
return nameOrAlias;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Merge `--host` / `--hosts` (and their `--device` / `--devices` aliases) into
|
|
74
|
-
* an ordered, de-duplicated list. Empty when none is set.
|
|
75
|
-
*/
|
|
76
|
-
export function parseHostsOption(opts) {
|
|
77
|
-
const out = [];
|
|
78
|
-
const seen = new Set();
|
|
79
|
-
const push = (h) => {
|
|
80
|
-
const t = h.trim();
|
|
81
|
-
if (t && !seen.has(t)) {
|
|
82
|
-
seen.add(t);
|
|
83
|
-
out.push(t);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
if (opts.host)
|
|
87
|
-
push(opts.host);
|
|
88
|
-
if (opts.device)
|
|
89
|
-
push(opts.device);
|
|
90
|
-
if (opts.hosts)
|
|
91
|
-
for (const h of opts.hosts.split(','))
|
|
92
|
-
push(h);
|
|
93
|
-
if (opts.devices)
|
|
94
|
-
for (const h of opts.devices.split(','))
|
|
95
|
-
push(h);
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Split a `bundle@host` reference. No `@` → a local bundle. Bundle names can't
|
|
100
|
-
* contain `@`, so the FIRST `@` separates bundle from ssh target; the target
|
|
101
|
-
* itself may be `user@host` (e.g. `r2.backups@muqsit@box`).
|
|
102
|
-
*/
|
|
103
|
-
export function splitBundleRef(ref) {
|
|
104
|
-
const at = ref.indexOf('@');
|
|
105
|
-
if (at === -1)
|
|
106
|
-
return { bundle: ref };
|
|
107
|
-
const bundle = ref.slice(0, at);
|
|
108
|
-
const host = ref.slice(at + 1);
|
|
109
|
-
if (!bundle || !host) {
|
|
110
|
-
throw new Error(`Invalid remote bundle reference ${JSON.stringify(ref)}. Expected 'bundle@host'.`);
|
|
111
|
-
}
|
|
112
|
-
return { bundle, host };
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Run `agents secrets <args>` on a remote host over ssh and return the raw
|
|
116
|
-
* result. Used by browse commands; `tty` forces `-tt` so remote passphrase
|
|
117
|
-
* prompts can surface.
|
|
118
|
-
*/
|
|
119
|
-
export function remoteSecretsRaw(target, args, opts = {}) {
|
|
120
|
-
const remoteCmd = buildRemoteAgentsInvocation(['secrets', ...args], undefined, osForTarget(target, opts.osLookupName));
|
|
121
|
-
// `secret: true` pins the managed host key and refuses multiplex; `-tt`
|
|
122
|
-
// additionally allocates a PTY and never multiplexes. The two compose.
|
|
123
|
-
const posture = opts.secret ? credentialTransportSshOpts(target) : {};
|
|
124
|
-
const conn = opts.tty
|
|
125
|
-
? { ...posture, extraSshArgs: ['-tt'], multiplex: false }
|
|
126
|
-
: posture;
|
|
127
|
-
return sshExec(target, remoteCmd, {
|
|
128
|
-
timeoutMs: opts.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
129
|
-
input: opts.input,
|
|
130
|
-
...conn,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/** Async, kill-bounded inverse of {@link remoteSecretsRaw} for daemon callers. */
|
|
134
|
-
export function remoteSecretsRawAsync(target, args, opts = {}) {
|
|
135
|
-
const remoteCmd = buildRemoteAgentsInvocation(['secrets', ...args], undefined, osForTarget(target, opts.osLookupName));
|
|
136
|
-
const posture = opts.secret ? credentialTransportSshOpts(target) : {};
|
|
137
|
-
const conn = opts.tty
|
|
138
|
-
? { ...posture, extraSshArgs: ['-tt'], multiplex: false }
|
|
139
|
-
: posture;
|
|
140
|
-
return sshExecAsync(target, remoteCmd, {
|
|
141
|
-
timeoutMs: opts.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
142
|
-
input: opts.input,
|
|
143
|
-
...conn,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Run a remote `agents secrets <args>` foreground with local stdio inherited,
|
|
148
|
-
* so the remote sees a real TTY and its passphrase prompt surfaces and reads
|
|
149
|
-
* keystrokes. Output streams to the terminal; only the exit code is returned.
|
|
150
|
-
*/
|
|
151
|
-
export function remoteSecretsStream(target, args, opts = {}) {
|
|
152
|
-
const remoteCmd = buildRemoteAgentsInvocation(['secrets', ...args], undefined, osForTarget(target, opts.osLookupName));
|
|
153
|
-
return sshStream(target, remoteCmd, { tty: true, ...credentialTransportSshOpts(target) });
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Resolve a remote bundle to a plaintext env map by driving the remote's
|
|
157
|
-
* `agents secrets export <bundle> --plaintext --format json`. Values cross over
|
|
158
|
-
* ssh stdout, parsed in memory, never persisted.
|
|
159
|
-
*
|
|
160
|
-
* The remote unlocks the bundle with its own credentials; this machine does NOT
|
|
161
|
-
* forward its passphrase because the remote bundle is encrypted with the
|
|
162
|
-
* remote's passphrase.
|
|
163
|
-
*/
|
|
164
|
-
export async function remoteResolveEnv(target, bundle, opts = {}) {
|
|
165
|
-
assertValidSshTarget(target);
|
|
166
|
-
// AGENTS_SECRETS_REMOTE_TRANSPORT marks this as the machine-to-machine JSON
|
|
167
|
-
// resolve path (the public shell-eval export mode was removed).
|
|
168
|
-
const remoteCmd = buildRemoteAgentsInvocation(['secrets', 'export', bundle, '--plaintext', '--format', 'json'], undefined, osForTarget(target, opts.osLookupName), { AGENTS_SECRETS_REMOTE_TRANSPORT: '1' });
|
|
169
|
-
const res = sshExec(target, remoteCmd, {
|
|
170
|
-
timeoutMs: REMOTE_TIMEOUT_MS,
|
|
171
|
-
...credentialTransportSshOpts(target),
|
|
172
|
-
});
|
|
173
|
-
if (res.code !== 0) {
|
|
174
|
-
const msg = (res.stderr || res.stdout || '').trim();
|
|
175
|
-
const why = res.timedOut ? 'timed out' : res.code === null ? 'ssh failed' : `exit ${res.code}`;
|
|
176
|
-
throw new Error(`Failed to resolve '${bundle}' on ${target} (${why})${msg ? `: ${msg}` : ''}`);
|
|
177
|
-
}
|
|
178
|
-
// Tolerate login-shell banner noise on stdout: take the outer { … } object.
|
|
179
|
-
const raw = res.stdout;
|
|
180
|
-
const start = raw.indexOf('{');
|
|
181
|
-
const end = raw.lastIndexOf('}');
|
|
182
|
-
const jsonText = start >= 0 && end >= start ? raw.slice(start, end + 1) : raw.trim();
|
|
183
|
-
let parsed;
|
|
184
|
-
try {
|
|
185
|
-
parsed = JSON.parse(jsonText);
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
throw new Error(`Could not parse secrets JSON from '${bundle}' on ${target}. ` +
|
|
189
|
-
`Is the remote agents-cli new enough for 'secrets export --format json'?`);
|
|
190
|
-
}
|
|
191
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
192
|
-
throw new Error(`Unexpected payload resolving '${bundle}' on ${target}.`);
|
|
193
|
-
}
|
|
194
|
-
const env = {};
|
|
195
|
-
const blocked = [];
|
|
196
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
197
|
-
// Drop dangerous-override keys returned by the (untrusted) peer.
|
|
198
|
-
if (isDangerousRemoteEnvKey(k)) {
|
|
199
|
-
blocked.push(k);
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
env[k] = typeof v === 'string' ? v : String(v);
|
|
203
|
-
}
|
|
204
|
-
if (blocked.length > 0) {
|
|
205
|
-
process.stderr.write(`[secrets] Dropped ${blocked.length} dangerous key(s) from '${bundle}'@${target} ` +
|
|
206
|
-
`(remote override blocked): ${blocked.join(', ')}\n`);
|
|
207
|
-
}
|
|
208
|
-
// Audit the event on the initiating host too; values never enter the payload.
|
|
209
|
-
emitSecretAudit({
|
|
210
|
-
event: 'secrets.get',
|
|
211
|
-
bundle,
|
|
212
|
-
operation: 'remote resolve',
|
|
213
|
-
source: 'remote',
|
|
214
|
-
host: target,
|
|
215
|
-
status: 'success',
|
|
216
|
-
keyCount: Object.keys(env).length,
|
|
217
|
-
});
|
|
218
|
-
return env;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* True when the remote's headless read-back raised the locked-login-keychain
|
|
222
|
-
* signature. Anything else must NOT be mislabeled as a locked keychain.
|
|
223
|
-
*/
|
|
224
|
-
function isLockedKeychainReadBackError(stderr) {
|
|
225
|
-
const s = stderr.toLowerCase();
|
|
226
|
-
return (s.includes('not unlocked') ||
|
|
227
|
-
s.includes('secrets agent') ||
|
|
228
|
-
s.includes('stored item') ||
|
|
229
|
-
s.includes('not found'));
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Decide whether a keychain-backed push to a remote actually persisted its
|
|
233
|
-
* secret value items by reading the bundle back the same way a later resolve
|
|
234
|
-
* will. Catches the silent failure where headless SSH writes metadata but not
|
|
235
|
-
* readable value items to a locked macOS login keychain.
|
|
236
|
-
*/
|
|
237
|
-
export function evaluateKeychainWriteVerification(pushedKeys, readBack) {
|
|
238
|
-
if (!readBack.ok) {
|
|
239
|
-
const stderr = readBack.stderr.trim();
|
|
240
|
-
if (isLockedKeychainReadBackError(stderr)) {
|
|
241
|
-
return {
|
|
242
|
-
ok: false,
|
|
243
|
-
kind: 'locked-keychain',
|
|
244
|
-
reason: `the remote could not read it back${stderr ? ` (${stderr})` : ''}`,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
return {
|
|
248
|
-
ok: false,
|
|
249
|
-
kind: 'error',
|
|
250
|
-
reason: stderr || 'read-back failed',
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
const present = new Set(readBack.keys);
|
|
254
|
-
const missing = pushedKeys.filter((k) => !present.has(k));
|
|
255
|
-
if (missing.length > 0) {
|
|
256
|
-
return {
|
|
257
|
-
ok: false,
|
|
258
|
-
kind: 'locked-keychain',
|
|
259
|
-
reason: `${missing.length} of ${pushedKeys.length} key(s) did not persist on the remote ` +
|
|
260
|
-
`(missing: ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ', …' : ''})`,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
return { ok: true };
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Actionable error message for a failed keychain-over-SSH push. Names the cause
|
|
267
|
-
* (locked remote login keychain) and steers to the two real fixes.
|
|
268
|
-
*/
|
|
269
|
-
export function keychainWriteFailureMessage(host, bundle, reason) {
|
|
270
|
-
return (`${host}: pushed '${bundle}' but the keychain items did not persist — ${reason}. ` +
|
|
271
|
-
`A macOS login keychain is LOCKED under headless SSH, so a keychain-backed write ` +
|
|
272
|
-
`lands the bundle metadata but no readable secret items, and later reads fail with ` +
|
|
273
|
-
`"stored item '…' not found". Re-run with a headless-readable backend:\n` +
|
|
274
|
-
` agents secrets export ${bundle} --device ${host} --remote-backend file\n` +
|
|
275
|
-
`(needs AGENTS_SECRETS_PASSPHRASE set locally), or unlock the remote keychain first ` +
|
|
276
|
-
`(e.g. an interactive login / \`agents secrets unlock\` on ${host}) and retry.`);
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Read a bundle back from a remote over SSH and confirm the pushed keys
|
|
280
|
-
* materialized. Drives the marker-gated json transport but keeps only KEY NAMES;
|
|
281
|
-
* plaintext values are discarded immediately. Returns a verification verdict.
|
|
282
|
-
*/
|
|
283
|
-
export function verifyRemoteKeychainPush(target, bundle, pushedKeys, opts = {}) {
|
|
284
|
-
const remoteCmd = buildRemoteAgentsInvocation(['secrets', 'export', bundle, '--plaintext', '--format', 'json'], undefined, osForTarget(target, opts.osLookupName), { AGENTS_SECRETS_REMOTE_TRANSPORT: '1' });
|
|
285
|
-
const res = sshExec(target, remoteCmd, {
|
|
286
|
-
timeoutMs: opts.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
287
|
-
...(opts.secret ? credentialTransportSshOpts(target) : {}),
|
|
288
|
-
});
|
|
289
|
-
return evaluateRemoteKeychainPushResult(pushedKeys, res);
|
|
290
|
-
}
|
|
291
|
-
/** Async, kill-bounded read-back verification for daemon-originated pushes. */
|
|
292
|
-
export async function verifyRemoteKeychainPushAsync(target, bundle, pushedKeys, opts = {}) {
|
|
293
|
-
const remoteCmd = buildRemoteAgentsInvocation(['secrets', 'export', bundle, '--plaintext', '--format', 'json'], undefined, osForTarget(target, opts.osLookupName), { AGENTS_SECRETS_REMOTE_TRANSPORT: '1' });
|
|
294
|
-
const res = await sshExecAsync(target, remoteCmd, {
|
|
295
|
-
timeoutMs: opts.timeoutMs ?? REMOTE_TIMEOUT_MS,
|
|
296
|
-
...(opts.secret ? credentialTransportSshOpts(target) : {}),
|
|
297
|
-
});
|
|
298
|
-
return evaluateRemoteKeychainPushResult(pushedKeys, res);
|
|
299
|
-
}
|
|
300
|
-
function evaluateRemoteKeychainPushResult(pushedKeys, res) {
|
|
301
|
-
if (res.code !== 0) {
|
|
302
|
-
const why = res.timedOut ? 'timed out' : res.code === null ? 'ssh failed' : `exit ${res.code}`;
|
|
303
|
-
const stderr = `${why}${(res.stderr || res.stdout || '').trim() ? `: ${(res.stderr || res.stdout).trim()}` : ''}`;
|
|
304
|
-
return evaluateKeychainWriteVerification(pushedKeys, { ok: false, stderr });
|
|
305
|
-
}
|
|
306
|
-
// Tolerate login-shell banner noise; keep only key names and discard values.
|
|
307
|
-
const raw = res.stdout;
|
|
308
|
-
const start = raw.indexOf('{');
|
|
309
|
-
const end = raw.lastIndexOf('}');
|
|
310
|
-
const jsonText = start >= 0 && end >= start ? raw.slice(start, end + 1) : raw.trim();
|
|
311
|
-
let keys;
|
|
312
|
-
try {
|
|
313
|
-
const parsed = JSON.parse(jsonText);
|
|
314
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
315
|
-
return evaluateKeychainWriteVerification(pushedKeys, {
|
|
316
|
-
ok: false,
|
|
317
|
-
stderr: 'unexpected read-back payload',
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
keys = Object.keys(parsed);
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
323
|
-
return evaluateKeychainWriteVerification(pushedKeys, {
|
|
324
|
-
ok: false,
|
|
325
|
-
stderr: 'could not parse read-back JSON',
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
return evaluateKeychainWriteVerification(pushedKeys, { ok: true, keys });
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* `bash -lc` command + stdin payload for a file-backed remote import.
|
|
332
|
-
*
|
|
333
|
-
* The file store is passphrase-free: the remote auto-provisions its own machine-
|
|
334
|
-
* local key, so reads are headless. AGENTS_SECRETS_PASSPHRASE must NOT be
|
|
335
|
-
* forwarded (PHNX-2371): a remote keyed to a secret its daemon does not hold
|
|
336
|
-
* reports "Imported N key(s)" then fails every later decrypt.
|
|
337
|
-
*/
|
|
338
|
-
export function buildRemoteFileImportCommand(bundle, dotenv, opts = {}) {
|
|
339
|
-
const force = opts.force ? ' --force' : '';
|
|
340
|
-
const policy = opts.policyNever ? ' --policy never --i-understand' : '';
|
|
341
|
-
const importCmd = `agents secrets import ${shellQuote(bundle)} --from - --backend file${force}${policy}`;
|
|
342
|
-
// No prologue — AGENTS_SECRETS_PASSPHRASE stays unset on the remote.
|
|
343
|
-
return { remoteCmd: `bash -lc ${shellQuote(importCmd)}`, input: dotenv };
|
|
344
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { type PushBundleResult } from './push.js';
|
|
2
|
-
import { type DeviceProfile } from '../devices/registry.js';
|
|
3
|
-
import { type SharedAuthStatus } from '../fleet-shared-state.js';
|
|
4
|
-
/** Each import/read-back SSH operation gets this deadline plus the SSH hard-kill grace. */
|
|
5
|
-
export declare const AUTH_SYNC_PUSH_DEADLINE_MS = 20000;
|
|
6
|
-
export interface AuthSyncDevice {
|
|
7
|
-
name: string;
|
|
8
|
-
reachable: boolean;
|
|
9
|
-
pinned: boolean;
|
|
10
|
-
remoteAuth: SharedAuthStatus | 'unknown';
|
|
11
|
-
}
|
|
12
|
-
export type AuthSyncPlanItem = {
|
|
13
|
-
action: 'push';
|
|
14
|
-
device: string;
|
|
15
|
-
} | {
|
|
16
|
-
action: 'skip';
|
|
17
|
-
device: string;
|
|
18
|
-
reason: string;
|
|
19
|
-
};
|
|
20
|
-
export declare function planAuthBundlePush(localAuthOk: boolean, localIsPublisher: boolean, devices: AuthSyncDevice[]): AuthSyncPlanItem[];
|
|
21
|
-
export interface AuthSyncResult {
|
|
22
|
-
publisher: string | null;
|
|
23
|
-
stateChanged: boolean;
|
|
24
|
-
pushed: string[];
|
|
25
|
-
skipped: Array<{
|
|
26
|
-
device: string;
|
|
27
|
-
reason: string;
|
|
28
|
-
}>;
|
|
29
|
-
errors: Array<{
|
|
30
|
-
device: string;
|
|
31
|
-
message: string;
|
|
32
|
-
}>;
|
|
33
|
-
}
|
|
34
|
-
export interface AuthSyncDeps {
|
|
35
|
-
inspectLocal?: () => {
|
|
36
|
-
exists: boolean;
|
|
37
|
-
ok: boolean;
|
|
38
|
-
};
|
|
39
|
-
listDevices?: () => DeviceProfile[];
|
|
40
|
-
localName?: string;
|
|
41
|
-
userAgentsDir?: string;
|
|
42
|
-
isPinned?: (name: string) => boolean;
|
|
43
|
-
push?: (bundle: string, host: string) => Promise<PushBundleResult>;
|
|
44
|
-
sshTarget?: (device: DeviceProfile) => string;
|
|
45
|
-
}
|
|
46
|
-
export interface PublishAuthVerdictOptions {
|
|
47
|
-
inspectLocal?: () => {
|
|
48
|
-
exists: boolean;
|
|
49
|
-
ok: boolean;
|
|
50
|
-
};
|
|
51
|
-
localName?: string;
|
|
52
|
-
userAgentsDir?: string;
|
|
53
|
-
}
|
|
54
|
-
export interface PublishAuthVerdictResult {
|
|
55
|
-
device: string;
|
|
56
|
-
status: SharedAuthStatus;
|
|
57
|
-
changed: boolean;
|
|
58
|
-
error: string | null;
|
|
59
|
-
}
|
|
60
|
-
/** Publish only safe readiness metadata; useful immediately before a repo push. */
|
|
61
|
-
export declare function publishReservedAuthVerdict(options?: PublishAuthVerdictOptions): Promise<PublishAuthVerdictResult>;
|
|
62
|
-
/**
|
|
63
|
-
* Publish local readiness, elect one ready source, then asynchronously provision
|
|
64
|
-
* only peers whose shared verdict says the bundle is missing.
|
|
65
|
-
*/
|
|
66
|
-
export declare function syncReservedAuthBundle(deps?: AuthSyncDeps): Promise<AuthSyncResult>;
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fleet sync of the reserved file-backed `auth` bundle (PHNX-2371/PHNX-3609).
|
|
3
|
-
*
|
|
4
|
-
* Every daemon publishes only a safe auth readiness verdict into its owned
|
|
5
|
-
* fleet-shared state file. A deterministic ready publisher reads those verdicts
|
|
6
|
-
* and transfers the actual bundle only to a peer reporting `missing`. Secret
|
|
7
|
-
* values never enter Git; the exceptional provisioning transfer remains SSH,
|
|
8
|
-
* now async and kill-bounded instead of blocking the daemon event loop.
|
|
9
|
-
*/
|
|
10
|
-
import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle } from './bundles.js';
|
|
11
|
-
import { pushBundleToHostAsync } from './push.js';
|
|
12
|
-
import { isDialableDevice, loadDevicesSync } from '../devices/registry.js';
|
|
13
|
-
import { sshTargetFor } from '../devices/connect.js';
|
|
14
|
-
import { isHostPinned, isDevicePinned, managedKnownHostsPath } from '../devices/known-hosts.js';
|
|
15
|
-
import { machineId, normalizeHost } from '../session/sync/config.js';
|
|
16
|
-
import { readFleetSharedDeviceStates, updateFleetSharedDeviceStateAsync, } from '../fleet-shared-state.js';
|
|
17
|
-
import { getUserAgentsDir } from '../state.js';
|
|
18
|
-
/** Each import/read-back SSH operation gets this deadline plus the SSH hard-kill grace. */
|
|
19
|
-
export const AUTH_SYNC_PUSH_DEADLINE_MS = 20_000;
|
|
20
|
-
export function planAuthBundlePush(localAuthOk, localIsPublisher, devices) {
|
|
21
|
-
if (!localAuthOk) {
|
|
22
|
-
return devices.map((device) => ({ action: 'skip', device: device.name, reason: 'no local file-backed auth bundle' }));
|
|
23
|
-
}
|
|
24
|
-
if (!localIsPublisher) {
|
|
25
|
-
return devices.map((device) => ({ action: 'skip', device: device.name, reason: 'another ready device is the elected auth publisher' }));
|
|
26
|
-
}
|
|
27
|
-
return devices.map((device) => {
|
|
28
|
-
if (!device.reachable)
|
|
29
|
-
return { action: 'skip', device: device.name, reason: 'unreachable' };
|
|
30
|
-
if (!device.pinned) {
|
|
31
|
-
return { action: 'skip', device: device.name, reason: `host key not pinned; run \`agents ssh ${device.name}\` once` };
|
|
32
|
-
}
|
|
33
|
-
if (device.remoteAuth === 'ready')
|
|
34
|
-
return { action: 'skip', device: device.name, reason: 'already present' };
|
|
35
|
-
if (device.remoteAuth === 'invalid')
|
|
36
|
-
return { action: 'skip', device: device.name, reason: 'remote auth bundle uses the wrong backend' };
|
|
37
|
-
if (device.remoteAuth === 'unknown') {
|
|
38
|
-
return { action: 'skip', device: device.name, reason: 'no shared auth verdict has arrived from this peer' };
|
|
39
|
-
}
|
|
40
|
-
return { action: 'push', device: device.name };
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
function defaultDevices() {
|
|
44
|
-
return Object.values(loadDevicesSync());
|
|
45
|
-
}
|
|
46
|
-
function authStatus(local) {
|
|
47
|
-
if (!local.exists)
|
|
48
|
-
return 'missing';
|
|
49
|
-
return local.ok ? 'ready' : 'invalid';
|
|
50
|
-
}
|
|
51
|
-
/** Publish only safe readiness metadata; useful immediately before a repo push. */
|
|
52
|
-
export async function publishReservedAuthVerdict(options = {}) {
|
|
53
|
-
const local = (options.inspectLocal ?? inspectReservedAuthBundle)();
|
|
54
|
-
const status = authStatus(local);
|
|
55
|
-
const device = options.localName ?? machineId();
|
|
56
|
-
try {
|
|
57
|
-
const write = await updateFleetSharedDeviceStateAsync(device, { auth: { status } }, options.userAgentsDir ?? getUserAgentsDir());
|
|
58
|
-
return { device, status, changed: write.changed, error: null };
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
return { device, status, changed: false, error: err.message };
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Publish local readiness, elect one ready source, then asynchronously provision
|
|
66
|
-
* only peers whose shared verdict says the bundle is missing.
|
|
67
|
-
*/
|
|
68
|
-
export async function syncReservedAuthBundle(deps = {}) {
|
|
69
|
-
const result = {
|
|
70
|
-
publisher: null,
|
|
71
|
-
stateChanged: false,
|
|
72
|
-
pushed: [],
|
|
73
|
-
skipped: [],
|
|
74
|
-
errors: [],
|
|
75
|
-
};
|
|
76
|
-
const published = await publishReservedAuthVerdict(deps);
|
|
77
|
-
const localStatus = published.status;
|
|
78
|
-
const localName = published.device;
|
|
79
|
-
const localNorm = normalizeHost(localName);
|
|
80
|
-
const root = deps.userAgentsDir ?? getUserAgentsDir();
|
|
81
|
-
const devices = (deps.listDevices ?? defaultDevices)().filter((device) => normalizeHost(device.name) !== localNorm);
|
|
82
|
-
result.stateChanged = published.changed;
|
|
83
|
-
if (published.error)
|
|
84
|
-
result.errors.push({ device: localName, message: `could not publish auth verdict: ${published.error}` });
|
|
85
|
-
const read = readFleetSharedDeviceStates(root);
|
|
86
|
-
result.errors.push(...read.errors);
|
|
87
|
-
const stateByDevice = new Map(read.states.map((state) => [normalizeHost(state.device), state]));
|
|
88
|
-
const registered = new Map(devices.map((device) => [normalizeHost(device.name), device]));
|
|
89
|
-
const readyPublishers = read.states
|
|
90
|
-
.filter((state) => {
|
|
91
|
-
if (state.auth?.status !== 'ready')
|
|
92
|
-
return false;
|
|
93
|
-
if (normalizeHost(state.device) === localNorm)
|
|
94
|
-
return localStatus === 'ready';
|
|
95
|
-
const device = registered.get(normalizeHost(state.device));
|
|
96
|
-
// Removed and known-offline device files are stale observations, not live
|
|
97
|
-
// executors. Letting one win election would suppress provisioning forever.
|
|
98
|
-
return !!device && isDialableDevice(device);
|
|
99
|
-
})
|
|
100
|
-
.map((state) => state.device);
|
|
101
|
-
if (localStatus === 'ready' && !readyPublishers.some((name) => normalizeHost(name) === localNorm)) {
|
|
102
|
-
readyPublishers.push(localName);
|
|
103
|
-
}
|
|
104
|
-
readyPublishers.sort((a, b) => normalizeHost(a).localeCompare(normalizeHost(b)));
|
|
105
|
-
result.publisher = readyPublishers[0] ?? null;
|
|
106
|
-
const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
|
|
107
|
-
const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
|
|
108
|
-
const plan = planAuthBundlePush(localStatus === 'ready', localIsPublisher, devices.map((device) => ({
|
|
109
|
-
name: device.name,
|
|
110
|
-
reachable: isDialableDevice(device),
|
|
111
|
-
pinned: isDevicePinned(device, pinned),
|
|
112
|
-
remoteAuth: stateByDevice.get(normalizeHost(device.name))?.auth?.status ?? 'unknown',
|
|
113
|
-
})));
|
|
114
|
-
const byName = new Map(devices.map((device) => [device.name, device]));
|
|
115
|
-
const push = deps.push ?? ((bundle, host) => pushBundleToHostAsync(bundle, host, {
|
|
116
|
-
remoteBackend: 'file',
|
|
117
|
-
operation: 'auth-sync',
|
|
118
|
-
agentOnly: true,
|
|
119
|
-
timeoutMs: AUTH_SYNC_PUSH_DEADLINE_MS,
|
|
120
|
-
}));
|
|
121
|
-
const targetOf = deps.sshTarget ?? sshTargetFor;
|
|
122
|
-
const outcomes = await Promise.all(plan.map(async (item) => {
|
|
123
|
-
if (item.action === 'skip')
|
|
124
|
-
return { kind: 'skip', device: item.device, message: item.reason };
|
|
125
|
-
const profile = byName.get(item.device);
|
|
126
|
-
if (!profile)
|
|
127
|
-
return { kind: 'skip', device: item.device, message: 'not in registry' };
|
|
128
|
-
try {
|
|
129
|
-
const out = await push(AUTH_BUNDLE_NAME, targetOf(profile));
|
|
130
|
-
return out.ok
|
|
131
|
-
? { kind: 'pushed', device: item.device, message: out.message }
|
|
132
|
-
: { kind: 'error', device: item.device, message: out.message };
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
return { kind: 'error', device: item.device, message: err.message };
|
|
136
|
-
}
|
|
137
|
-
}));
|
|
138
|
-
for (const outcome of outcomes) {
|
|
139
|
-
if (outcome.kind === 'pushed')
|
|
140
|
-
result.pushed.push(outcome.device);
|
|
141
|
-
else if (outcome.kind === 'skip')
|
|
142
|
-
result.skipped.push({ device: outcome.device, reason: outcome.message });
|
|
143
|
-
else
|
|
144
|
-
result.errors.push({ device: outcome.device, message: outcome.message });
|
|
145
|
-
}
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Harness scoping for an unlocked bundle — the shared vocabulary of the broker
|
|
3
|
-
* (agent.ts), the durable session store (session-store.ts), and the read path
|
|
4
|
-
* (bundles.ts).
|
|
5
|
-
*
|
|
6
|
-
* A grant is stored under a scope and read under a scope; the two must agree or
|
|
7
|
-
* the bundle is invisible. `agents secrets unlock --agent <agent>` exists to NARROW
|
|
8
|
-
* a grant to one harness, so an unlock without it is global by definition.
|
|
9
|
-
*
|
|
10
|
-
* This module deliberately has NO imports: agent.ts and session-store.ts already
|
|
11
|
-
* import each other, and hanging the scope constants off either one would close
|
|
12
|
-
* that cycle — under ESM a cyclic `const` read can land in the temporal dead zone
|
|
13
|
-
* and throw at runtime even though tsc is happy.
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Scope of an unlock that was not narrowed with `--agent`: readable by every
|
|
17
|
-
* harness. Not a valid harness name, so it can never collide with one.
|
|
18
|
-
*/
|
|
19
|
-
export declare const GLOBAL_HARNESS = "*";
|
|
20
|
-
/**
|
|
21
|
-
* Scopes a reader consults, most specific first: its own harness, then the global
|
|
22
|
-
* grant. This is the resolution order of the scoped-grant model — a narrow
|
|
23
|
-
* `--agent claude` unlock stays claude-only while an unscoped unlock serves
|
|
24
|
-
* everyone — not a fallback papering over a miss.
|
|
25
|
-
*/
|
|
26
|
-
export declare function bundleScopeChain(harness: string | undefined): string[];
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Harness scoping for an unlocked bundle — the shared vocabulary of the broker
|
|
3
|
-
* (agent.ts), the durable session store (session-store.ts), and the read path
|
|
4
|
-
* (bundles.ts).
|
|
5
|
-
*
|
|
6
|
-
* A grant is stored under a scope and read under a scope; the two must agree or
|
|
7
|
-
* the bundle is invisible. `agents secrets unlock --agent <agent>` exists to NARROW
|
|
8
|
-
* a grant to one harness, so an unlock without it is global by definition.
|
|
9
|
-
*
|
|
10
|
-
* This module deliberately has NO imports: agent.ts and session-store.ts already
|
|
11
|
-
* import each other, and hanging the scope constants off either one would close
|
|
12
|
-
* that cycle — under ESM a cyclic `const` read can land in the temporal dead zone
|
|
13
|
-
* and throw at runtime even though tsc is happy.
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Scope of an unlock that was not narrowed with `--agent`: readable by every
|
|
17
|
-
* harness. Not a valid harness name, so it can never collide with one.
|
|
18
|
-
*/
|
|
19
|
-
export const GLOBAL_HARNESS = '*';
|
|
20
|
-
/**
|
|
21
|
-
* Scopes a reader consults, most specific first: its own harness, then the global
|
|
22
|
-
* grant. This is the resolution order of the scoped-grant model — a narrow
|
|
23
|
-
* `--agent claude` unlock stays claude-only while an unscoped unlock serves
|
|
24
|
-
* everyone — not a fallback papering over a miss.
|
|
25
|
-
*/
|
|
26
|
-
export function bundleScopeChain(harness) {
|
|
27
|
-
const own = harness || GLOBAL_HARNESS;
|
|
28
|
-
return own === GLOBAL_HARNESS ? [GLOBAL_HARNESS] : [own, GLOBAL_HARNESS];
|
|
29
|
-
}
|