@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
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
3
|
-
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
4
|
-
* quote pair and takes the inner bytes verbatim (no unescaping), so any single-line
|
|
5
|
-
* value survives unchanged with no escaping. Newlines would break its line-based
|
|
6
|
-
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
7
|
-
*/
|
|
8
|
-
export declare function bundleEnvToDotenv(env: Record<string, string>): string;
|
|
9
|
-
/** Where the bundle should live ON THE REMOTE. */
|
|
10
|
-
export type RemoteBackend = 'keychain' | 'file';
|
|
11
|
-
/** A bundle read once, ready to push to any number of hosts. */
|
|
12
|
-
export interface ResolvedBundleForPush {
|
|
13
|
-
/** key -> value. Never logged; only its KEY NAMES are ever surfaced. */
|
|
14
|
-
env: Record<string, string>;
|
|
15
|
-
/** The same values as a dotenv blob, shipped over ssh stdin (never argv). */
|
|
16
|
-
dotenv: string;
|
|
17
|
-
keyCount: number;
|
|
18
|
-
}
|
|
19
|
-
export interface PushBundleOptions {
|
|
20
|
-
remoteBackend: RemoteBackend;
|
|
21
|
-
/** Overwrite a key that already exists on the remote. */
|
|
22
|
-
force?: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Ignored. File-backend export never forwards AGENTS_SECRETS_PASSPHRASE
|
|
25
|
-
* (PHNX-2371); the remote auto-provisions its own machine-local key.
|
|
26
|
-
* Kept on the options type so existing callers that passed one still compile.
|
|
27
|
-
*/
|
|
28
|
-
passphrase?: string;
|
|
29
|
-
/** Label for the audit trail — `export --device` vs `fleet apply`. */
|
|
30
|
-
operation: string;
|
|
31
|
-
/** Preserve an automation account's permanent prompt-free policy remotely. */
|
|
32
|
-
policyNever?: boolean;
|
|
33
|
-
/** Permit a human-invoked push to read locally without requiring the agent broker. */
|
|
34
|
-
agentOnly?: boolean;
|
|
35
|
-
/** Non-secret literals whose bundle value kind must survive the dotenv transport. */
|
|
36
|
-
literalValues?: Record<string, string>;
|
|
37
|
-
/** Per-SSH-operation deadline. Async daemon callers must set this explicitly. */
|
|
38
|
-
timeoutMs?: number;
|
|
39
|
-
}
|
|
40
|
-
export interface PushBundleResult {
|
|
41
|
-
ok: boolean;
|
|
42
|
-
host: string;
|
|
43
|
-
bundle: string;
|
|
44
|
-
keyCount: number;
|
|
45
|
-
/** One line for the caller to render. Never contains a secret value. */
|
|
46
|
-
message: string;
|
|
47
|
-
}
|
|
48
|
-
export interface LiteralRestorePlan {
|
|
49
|
-
key: string;
|
|
50
|
-
removeArgs: string[];
|
|
51
|
-
addArgs: string[];
|
|
52
|
-
}
|
|
53
|
-
/** Preserve the literal-vs-secret schema that dotenv transport cannot encode. */
|
|
54
|
-
export declare function planLiteralRestoration(bundle: string, literalValues?: Record<string, string>): LiteralRestorePlan[];
|
|
55
|
-
/**
|
|
56
|
-
* Read and resolve a bundle once, for pushing to one or more hosts.
|
|
57
|
-
*
|
|
58
|
-
* `keyMode: 'storage'` is fixed — a push always sends storage-shaped values.
|
|
59
|
-
*
|
|
60
|
-
* `agentOnly` is the caller's to decide, and it is NOT a security level: it
|
|
61
|
-
* selects the broker-only read (fail fast with the `agents secrets unlock` hint)
|
|
62
|
-
* over the interactive read (macOS may raise a Touch ID sheet). The headless
|
|
63
|
-
* guard exists because an AGENT LAUNCH must never enumerate the keychain — a
|
|
64
|
-
* per-bundle prefix becomes a broad `agents-cli.` scan after service-name
|
|
65
|
-
* hashing, and macOS evaluates unrelated biometry ACLs during it, one sheet per
|
|
66
|
-
* installed version (bundles.ts, RUSH-2440). A human at a TTY is not that case:
|
|
67
|
-
* they can answer one prompt, and the interactive sibling reads (`view --reveal`,
|
|
68
|
-
* `exec`) already let them. NOT `get` — it is unconditionally `agentOnly` on
|
|
69
|
-
* purpose, because prompting would hang a `$(agents secrets get …)` capture
|
|
70
|
-
* mid-pipeline (SEC-13b). It defaults to `true` so an automated caller that says
|
|
71
|
-
* nothing stays broker-only.
|
|
72
|
-
*/
|
|
73
|
-
export declare function resolveBundleForPush(bundle: string, caller: string, opts?: {
|
|
74
|
-
agentOnly?: boolean;
|
|
75
|
-
}): ResolvedBundleForPush;
|
|
76
|
-
/**
|
|
77
|
-
* WHICH transport a (backend, target-OS) pair selects, and the exact bytes it
|
|
78
|
-
* will send. One of four outcomes, and picking the wrong one is silent: a
|
|
79
|
-
* Windows target handed a POSIX `bash -lc` produces garbage, and a Windows
|
|
80
|
-
* target handed `--from -` hangs forever on a stdin the `agents.ps1` shim never
|
|
81
|
-
* forwards. Neither shows up as a failed ssh, so the selection is the thing
|
|
82
|
-
* worth pinning.
|
|
83
|
-
*
|
|
84
|
-
* Separated from execution so it is decidable without a network: the branch is
|
|
85
|
-
* chosen from the device registry alone, and `pushResolvedBundleToHost` below
|
|
86
|
-
* does nothing but run what this returns.
|
|
87
|
-
*/
|
|
88
|
-
export type PushTransport =
|
|
89
|
-
/** No supported command exists for this pair — fail loud, never a wrong path. */
|
|
90
|
-
{
|
|
91
|
-
kind: 'refuse';
|
|
92
|
-
message: string;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* A command sent over the raw ssh engine, with the .env on stdin. `multiplex`
|
|
96
|
-
* is always `false`: a push ships credential bytes, so its connection must not
|
|
97
|
-
* reuse — or leave behind — a persistent authenticated control master to the
|
|
98
|
-
* destination the way the default fan-out connections do (RUSH-2527). This
|
|
99
|
-
* mirrors the `--copy-creds` dispatch posture (`multiplex: !opts.copyCreds`).
|
|
100
|
-
*/
|
|
101
|
-
| {
|
|
102
|
-
kind: 'ssh';
|
|
103
|
-
remoteCmd: string;
|
|
104
|
-
input: string;
|
|
105
|
-
multiplex: false;
|
|
106
|
-
}
|
|
107
|
-
/** The OS-aware `agents secrets` wrapper, the READ inverse's own path. Never multiplexed — see the `ssh` kind. */
|
|
108
|
-
| {
|
|
109
|
-
kind: 'remote-secrets';
|
|
110
|
-
args: string[];
|
|
111
|
-
input: string;
|
|
112
|
-
multiplex: false;
|
|
113
|
-
};
|
|
114
|
-
/** Choose the transport for one push. Pure: registry read in, plan out. */
|
|
115
|
-
export declare function planPushTransport(resolved: ResolvedBundleForPush, bundle: string, host: string, opts: PushBundleOptions): PushTransport;
|
|
116
|
-
/**
|
|
117
|
-
* Push an already-resolved bundle to ONE host.
|
|
118
|
-
*
|
|
119
|
-
* Drives the remote's own `agents secrets import --from -`, so the values land
|
|
120
|
-
* in the remote's chosen backend and the .env is read off ssh stdin rather than
|
|
121
|
-
* parsed by a remote shell. `import` auto-creates the bundle.
|
|
122
|
-
*/
|
|
123
|
-
export declare function pushResolvedBundleToHost(resolved: ResolvedBundleForPush, bundle: string, host: string, opts: PushBundleOptions): PushBundleResult;
|
|
124
|
-
/** Resolve and push in one call — for a single host. */
|
|
125
|
-
export declare function pushBundleToHost(bundle: string, host: string, opts: PushBundleOptions): PushBundleResult;
|
|
126
|
-
/**
|
|
127
|
-
* Async, kill-bounded push for daemon services. It preserves the foreground
|
|
128
|
-
* primitive's transport, read-back verification, and literal restoration; only
|
|
129
|
-
* the SSH execution strategy differs.
|
|
130
|
-
*/
|
|
131
|
-
export declare function pushResolvedBundleToHostAsync(resolved: ResolvedBundleForPush, bundle: string, host: string, opts: PushBundleOptions): Promise<PushBundleResult>;
|
|
132
|
-
/** Resolve locally once, then run the daemon-safe async push. */
|
|
133
|
-
export declare function pushBundleToHostAsync(bundle: string, host: string, opts: PushBundleOptions): Promise<PushBundleResult>;
|
package/dist/lib/secrets/push.js
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Push one bundle's values to a remote host over SSH — the provisioning
|
|
3
|
-
* primitive behind `agents secrets export --device` and, from RUSH-1968, behind
|
|
4
|
-
* `agents fleet apply --provision-secrets`.
|
|
5
|
-
*
|
|
6
|
-
* This logic used to live inline in the `export --device` command action. It moved
|
|
7
|
-
* here because `lib/fleet/apply.ts` needs it and a lib MUST NOT import a command
|
|
8
|
-
* module — and because the absence of a callable primitive is part of why
|
|
9
|
-
* `fleet apply` never provisioned secrets at all, leaving an operator to
|
|
10
|
-
* hand-export the file store's master key across the fleet.
|
|
11
|
-
*
|
|
12
|
-
* Two rules shape the shape of this module:
|
|
13
|
-
*
|
|
14
|
-
* - **It never prints.** The lib layer stays `console.*`-free (SEC-14/SEC-16), so
|
|
15
|
-
* every outcome is returned as data and the CALLER renders it. That is also
|
|
16
|
-
* what lets `fleet apply` fold a push into its own per-device report instead of
|
|
17
|
-
* interleaving stray lines into it.
|
|
18
|
-
* - **Resolve once, push N times.** `resolveBundleForPush` is separate because
|
|
19
|
-
* reading a bundle can prompt (Touch ID); doing it per host would prompt per
|
|
20
|
-
* host. `export --device a,b,c` resolves once and pushes three times.
|
|
21
|
-
*/
|
|
22
|
-
import { sshExec, sshExecAsync } from '../ssh-exec.js';
|
|
23
|
-
import { remoteShellFor, buildWindowsStdinImportCommand } from '../hosts/remote-cmd.js';
|
|
24
|
-
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
25
|
-
import { remoteSecretsRaw, remoteSecretsRawAsync, verifyRemoteKeychainPush, verifyRemoteKeychainPushAsync, keychainWriteFailureMessage, buildRemoteFileImportCommand, credentialTransportSshOpts, } from './remote.js';
|
|
26
|
-
import { readAndResolveBundleEnv } from './bundles.js';
|
|
27
|
-
/**
|
|
28
|
-
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
29
|
-
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
30
|
-
* quote pair and takes the inner bytes verbatim (no unescaping), so any single-line
|
|
31
|
-
* value survives unchanged with no escaping. Newlines would break its line-based
|
|
32
|
-
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
33
|
-
*/
|
|
34
|
-
export function bundleEnvToDotenv(env) {
|
|
35
|
-
const lines = [];
|
|
36
|
-
for (const [k, v] of Object.entries(env)) {
|
|
37
|
-
if (/[\r\n]/.test(v)) {
|
|
38
|
-
throw new Error(`Key '${k}' has a multi-line value; the SSH .env transport can't carry newlines. ` +
|
|
39
|
-
`Set it directly on the remote with 'agents secrets add ${k} --value-stdin'.`);
|
|
40
|
-
}
|
|
41
|
-
lines.push(`${k}="${v}"`);
|
|
42
|
-
}
|
|
43
|
-
return lines.join('\n') + '\n';
|
|
44
|
-
}
|
|
45
|
-
/** Preserve the literal-vs-secret schema that dotenv transport cannot encode. */
|
|
46
|
-
export function planLiteralRestoration(bundle, literalValues = {}) {
|
|
47
|
-
return Object.entries(literalValues).map(([key, value]) => ({
|
|
48
|
-
key,
|
|
49
|
-
removeArgs: ['remove', bundle, key, '--yes'],
|
|
50
|
-
addArgs: ['add', bundle, key, '--value', value],
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Read and resolve a bundle once, for pushing to one or more hosts.
|
|
55
|
-
*
|
|
56
|
-
* `keyMode: 'storage'` is fixed — a push always sends storage-shaped values.
|
|
57
|
-
*
|
|
58
|
-
* `agentOnly` is the caller's to decide, and it is NOT a security level: it
|
|
59
|
-
* selects the broker-only read (fail fast with the `agents secrets unlock` hint)
|
|
60
|
-
* over the interactive read (macOS may raise a Touch ID sheet). The headless
|
|
61
|
-
* guard exists because an AGENT LAUNCH must never enumerate the keychain — a
|
|
62
|
-
* per-bundle prefix becomes a broad `agents-cli.` scan after service-name
|
|
63
|
-
* hashing, and macOS evaluates unrelated biometry ACLs during it, one sheet per
|
|
64
|
-
* installed version (bundles.ts, RUSH-2440). A human at a TTY is not that case:
|
|
65
|
-
* they can answer one prompt, and the interactive sibling reads (`view --reveal`,
|
|
66
|
-
* `exec`) already let them. NOT `get` — it is unconditionally `agentOnly` on
|
|
67
|
-
* purpose, because prompting would hang a `$(agents secrets get …)` capture
|
|
68
|
-
* mid-pipeline (SEC-13b). It defaults to `true` so an automated caller that says
|
|
69
|
-
* nothing stays broker-only.
|
|
70
|
-
*/
|
|
71
|
-
export function resolveBundleForPush(bundle, caller, opts = {}) {
|
|
72
|
-
const agentOnly = opts.agentOnly ?? true;
|
|
73
|
-
const { env } = readAndResolveBundleEnv(bundle, { caller, keyMode: 'storage', agentOnly });
|
|
74
|
-
return { env, dotenv: bundleEnvToDotenv(env), keyCount: Object.keys(env).length };
|
|
75
|
-
}
|
|
76
|
-
function isPowershellTarget(host) {
|
|
77
|
-
return remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell';
|
|
78
|
-
}
|
|
79
|
-
/** Choose the transport for one push. Pure: registry read in, plan out. */
|
|
80
|
-
export function planPushTransport(resolved, bundle, host, opts) {
|
|
81
|
-
const powershell = isPowershellTarget(host);
|
|
82
|
-
if (opts.remoteBackend === 'file') {
|
|
83
|
-
// Both file-backend paths build a POSIX `bash -lc` command. Refuse a Windows
|
|
84
|
-
// target cleanly rather than emit broken PowerShell (fail loud at the
|
|
85
|
-
// boundary, never a silent wrong path).
|
|
86
|
-
if (powershell) {
|
|
87
|
-
return { kind: 'refuse', message: 'file backend export to a Windows target is not yet supported' };
|
|
88
|
-
}
|
|
89
|
-
const { remoteCmd, input } = buildRemoteFileImportCommand(bundle, resolved.dotenv, {
|
|
90
|
-
force: opts.force,
|
|
91
|
-
policyNever: opts.policyNever,
|
|
92
|
-
});
|
|
93
|
-
return { kind: 'ssh', remoteCmd, input, multiplex: false };
|
|
94
|
-
}
|
|
95
|
-
if (powershell) {
|
|
96
|
-
// Keychain on a Windows target: the `agents.ps1` shim doesn't forward
|
|
97
|
-
// ssh-piped stdin to node, so `--from -` would hang. Bridge the piped .env
|
|
98
|
-
// through PowerShell into a temp file and import `--from <file>` (deleted
|
|
99
|
-
// afterwards). Same hardened ssh engine; the .env still only ever crosses
|
|
100
|
-
// the wire over ssh stdin.
|
|
101
|
-
return {
|
|
102
|
-
kind: 'ssh',
|
|
103
|
-
remoteCmd: buildWindowsStdinImportCommand(bundle, { force: opts.force, policyNever: opts.policyNever }),
|
|
104
|
-
input: resolved.dotenv,
|
|
105
|
-
multiplex: false,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
// Keychain on a POSIX target: OS-aware wrapping + the hardened ssh engine
|
|
109
|
-
// (BatchMode, ConnectTimeout, keepalive, control-socket reuse) via the same
|
|
110
|
-
// path the READ inverse (`remoteResolveEnv`) uses.
|
|
111
|
-
return {
|
|
112
|
-
kind: 'remote-secrets',
|
|
113
|
-
args: [
|
|
114
|
-
'import', bundle, '--from', '-',
|
|
115
|
-
...(opts.force ? ['--force'] : []),
|
|
116
|
-
...(opts.policyNever ? ['--policy', 'never', '--i-understand'] : []),
|
|
117
|
-
],
|
|
118
|
-
input: resolved.dotenv,
|
|
119
|
-
multiplex: false,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Push an already-resolved bundle to ONE host.
|
|
124
|
-
*
|
|
125
|
-
* Drives the remote's own `agents secrets import --from -`, so the values land
|
|
126
|
-
* in the remote's chosen backend and the .env is read off ssh stdin rather than
|
|
127
|
-
* parsed by a remote shell. `import` auto-creates the bundle.
|
|
128
|
-
*/
|
|
129
|
-
export function pushResolvedBundleToHost(resolved, bundle, host, opts) {
|
|
130
|
-
const fail = (message) => ({ ok: false, host, bundle, keyCount: resolved.keyCount, message });
|
|
131
|
-
const plan = planPushTransport(resolved, bundle, host, opts);
|
|
132
|
-
if (plan.kind === 'refuse')
|
|
133
|
-
return fail(plan.message);
|
|
134
|
-
// Every ssh below rides the credential-transport posture: the managed host key
|
|
135
|
-
// is pinned (a changed key is refused) and the connection never reuses or
|
|
136
|
-
// leaves a persistent authenticated control master to the destination
|
|
137
|
-
// (RUSH-2527, `credentialTransportSshOpts`). The plan's `multiplex: false` is
|
|
138
|
-
// the raw-`ssh` branch's half of that; the `remote-secrets` branch and the
|
|
139
|
-
// read-back/policy/literal follow-ups pass `secret: true` for the same posture.
|
|
140
|
-
const res = plan.kind === 'ssh'
|
|
141
|
-
? sshExec(host, plan.remoteCmd, { input: plan.input, timeoutMs: opts.timeoutMs, hostKeyOpts: credentialTransportSshOpts(host).hostKeyOpts, multiplex: plan.multiplex })
|
|
142
|
-
: remoteSecretsRaw(host, plan.args, { input: plan.input, timeoutMs: opts.timeoutMs, osLookupName: host, secret: true });
|
|
143
|
-
if (res.code === null) {
|
|
144
|
-
return fail(res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed'));
|
|
145
|
-
}
|
|
146
|
-
if (res.code !== 0) {
|
|
147
|
-
const msg = (res.stderr || res.stdout || '').trim();
|
|
148
|
-
return fail(`remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`);
|
|
149
|
-
}
|
|
150
|
-
// A successful remote import is not proof the keys are readable. Two silent
|
|
151
|
-
// failures hide behind exit 0:
|
|
152
|
-
// - keychain: a macOS login keychain is locked under headless SSH, so the
|
|
153
|
-
// write lands metadata but no readable value items.
|
|
154
|
-
// - file: a forwarded AGENTS_SECRETS_PASSPHRASE keys ciphertext to a secret
|
|
155
|
-
// the destination daemon does not hold (PHNX-2371).
|
|
156
|
-
// Read the bundle back the way a later resolve will and FAIL LOUDLY.
|
|
157
|
-
const verdict = verifyRemoteKeychainPush(host, bundle, Object.keys(resolved.env), { osLookupName: host, secret: true, timeoutMs: opts.timeoutMs });
|
|
158
|
-
if (!verdict.ok) {
|
|
159
|
-
if (opts.remoteBackend === 'keychain' && verdict.kind === 'locked-keychain') {
|
|
160
|
-
return fail(keychainWriteFailureMessage(host, bundle, verdict.reason));
|
|
161
|
-
}
|
|
162
|
-
if (opts.remoteBackend === 'file') {
|
|
163
|
-
return fail(`${host}: pushed '${bundle}' but the remote could not decrypt it (${verdict.reason}). ` +
|
|
164
|
-
`File-backend export never forwards AGENTS_SECRETS_PASSPHRASE — the remote keys ` +
|
|
165
|
-
`the bundle under its own machine-local key. If the destination still cannot read, ` +
|
|
166
|
-
`unset a stale AGENTS_SECRETS_PASSPHRASE there (\`agents doctor\`).`);
|
|
167
|
-
}
|
|
168
|
-
return fail(`pushed '${bundle}' but could not verify it on the remote: ${verdict.reason}`);
|
|
169
|
-
}
|
|
170
|
-
for (const step of planLiteralRestoration(bundle, opts.literalValues)) {
|
|
171
|
-
const removed = remoteSecretsRaw(host, step.removeArgs, { osLookupName: host, secret: true, timeoutMs: opts.timeoutMs });
|
|
172
|
-
if (removed.code !== 0) {
|
|
173
|
-
const msg = (removed.stderr || removed.stdout || '').trim();
|
|
174
|
-
return fail(`pushed '${bundle}' but could not replace transported ${step.key}${msg ? `: ${msg}` : ''}`);
|
|
175
|
-
}
|
|
176
|
-
const literal = remoteSecretsRaw(host, step.addArgs, { osLookupName: host, secret: true, timeoutMs: opts.timeoutMs });
|
|
177
|
-
if (literal.code !== 0) {
|
|
178
|
-
const msg = (literal.stderr || literal.stdout || '').trim();
|
|
179
|
-
return fail(`pushed '${bundle}' but could not preserve literal ${step.key}${msg ? `: ${msg}` : ''}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const remoteMsg = (res.stdout || '').trim().split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
183
|
-
return {
|
|
184
|
-
ok: true,
|
|
185
|
-
host,
|
|
186
|
-
bundle,
|
|
187
|
-
keyCount: resolved.keyCount,
|
|
188
|
-
message: remoteMsg || `${resolved.keyCount} key(s) exported`,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
/** Resolve and push in one call — for a single host. */
|
|
192
|
-
export function pushBundleToHost(bundle, host, opts) {
|
|
193
|
-
return pushResolvedBundleToHost(resolveBundleForPush(bundle, opts.operation, { agentOnly: opts.agentOnly }), bundle, host, opts);
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Async, kill-bounded push for daemon services. It preserves the foreground
|
|
197
|
-
* primitive's transport, read-back verification, and literal restoration; only
|
|
198
|
-
* the SSH execution strategy differs.
|
|
199
|
-
*/
|
|
200
|
-
export async function pushResolvedBundleToHostAsync(resolved, bundle, host, opts) {
|
|
201
|
-
const fail = (message) => ({ ok: false, host, bundle, keyCount: resolved.keyCount, message });
|
|
202
|
-
const plan = planPushTransport(resolved, bundle, host, opts);
|
|
203
|
-
if (plan.kind === 'refuse')
|
|
204
|
-
return fail(plan.message);
|
|
205
|
-
const res = plan.kind === 'ssh'
|
|
206
|
-
? await sshExecAsync(host, plan.remoteCmd, {
|
|
207
|
-
input: plan.input,
|
|
208
|
-
timeoutMs: opts.timeoutMs,
|
|
209
|
-
hostKeyOpts: credentialTransportSshOpts(host).hostKeyOpts,
|
|
210
|
-
multiplex: plan.multiplex,
|
|
211
|
-
})
|
|
212
|
-
: await remoteSecretsRawAsync(host, plan.args, {
|
|
213
|
-
input: plan.input,
|
|
214
|
-
timeoutMs: opts.timeoutMs,
|
|
215
|
-
osLookupName: host,
|
|
216
|
-
secret: true,
|
|
217
|
-
});
|
|
218
|
-
if (res.code === null)
|
|
219
|
-
return fail(res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed'));
|
|
220
|
-
if (res.code !== 0) {
|
|
221
|
-
const msg = (res.stderr || res.stdout || '').trim();
|
|
222
|
-
return fail(`remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`);
|
|
223
|
-
}
|
|
224
|
-
const verdict = await verifyRemoteKeychainPushAsync(host, bundle, Object.keys(resolved.env), {
|
|
225
|
-
osLookupName: host,
|
|
226
|
-
secret: true,
|
|
227
|
-
timeoutMs: opts.timeoutMs,
|
|
228
|
-
});
|
|
229
|
-
if (!verdict.ok) {
|
|
230
|
-
if (opts.remoteBackend === 'keychain' && verdict.kind === 'locked-keychain') {
|
|
231
|
-
return fail(keychainWriteFailureMessage(host, bundle, verdict.reason));
|
|
232
|
-
}
|
|
233
|
-
if (opts.remoteBackend === 'file') {
|
|
234
|
-
return fail(`${host}: pushed '${bundle}' but the remote could not decrypt it (${verdict.reason}). ` +
|
|
235
|
-
`File-backend export never forwards AGENTS_SECRETS_PASSPHRASE — the remote keys ` +
|
|
236
|
-
`the bundle under its own machine-local key. If the destination still cannot read, ` +
|
|
237
|
-
`unset a stale AGENTS_SECRETS_PASSPHRASE there (\`agents doctor\`).`);
|
|
238
|
-
}
|
|
239
|
-
return fail(`pushed '${bundle}' but could not verify it on the remote: ${verdict.reason}`);
|
|
240
|
-
}
|
|
241
|
-
for (const step of planLiteralRestoration(bundle, opts.literalValues)) {
|
|
242
|
-
const removed = await remoteSecretsRawAsync(host, step.removeArgs, {
|
|
243
|
-
osLookupName: host,
|
|
244
|
-
secret: true,
|
|
245
|
-
timeoutMs: opts.timeoutMs,
|
|
246
|
-
});
|
|
247
|
-
if (removed.code !== 0) {
|
|
248
|
-
const msg = (removed.stderr || removed.stdout || '').trim();
|
|
249
|
-
return fail(`pushed '${bundle}' but could not replace transported ${step.key}${msg ? `: ${msg}` : ''}`);
|
|
250
|
-
}
|
|
251
|
-
const literal = await remoteSecretsRawAsync(host, step.addArgs, {
|
|
252
|
-
osLookupName: host,
|
|
253
|
-
secret: true,
|
|
254
|
-
timeoutMs: opts.timeoutMs,
|
|
255
|
-
});
|
|
256
|
-
if (literal.code !== 0) {
|
|
257
|
-
const msg = (literal.stderr || literal.stdout || '').trim();
|
|
258
|
-
return fail(`pushed '${bundle}' but could not preserve literal ${step.key}${msg ? `: ${msg}` : ''}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const remoteMsg = (res.stdout || '').trim().split('\n').map((line) => line.trim()).filter(Boolean).pop();
|
|
262
|
-
return {
|
|
263
|
-
ok: true,
|
|
264
|
-
host,
|
|
265
|
-
bundle,
|
|
266
|
-
keyCount: resolved.keyCount,
|
|
267
|
-
message: remoteMsg || `${resolved.keyCount} key(s) exported`,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
/** Resolve locally once, then run the daemon-safe async push. */
|
|
271
|
-
export function pushBundleToHostAsync(bundle, host, opts) {
|
|
272
|
-
return pushResolvedBundleToHostAsync(resolveBundleForPush(bundle, opts.operation, { agentOnly: opts.agentOnly }), bundle, host, opts);
|
|
273
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detect credentials exported from shell rc files.
|
|
3
|
-
*
|
|
4
|
-
* A secret exported from `~/.zshenv`, `~/.zshrc`, `~/.bashrc`, `~/.profile`, …
|
|
5
|
-
* is inherited by every process the login shell spawns and is readable from
|
|
6
|
-
* `/proc/<pid>/environ` by any same-user process — the master passphrase leak
|
|
7
|
-
* behind RUSH-1968. `.zshenv` in particular is sourced by *every* zsh
|
|
8
|
-
* invocation, including non-interactive `ssh host 'cmd'`, so the value lands in
|
|
9
|
-
* the environment of essentially everything the box runs.
|
|
10
|
-
*
|
|
11
|
-
* The keychain-backed store (`agents secrets`) is the sanctioned home for
|
|
12
|
-
* credentials; the file-store master passphrase belongs in the off-env 0600 key
|
|
13
|
-
* file (`~/.agents/.secrets-key/passphrase`), never a shell rc export.
|
|
14
|
-
*
|
|
15
|
-
* This scanner powers a warn-level advisory in `agents doctor`. It reads only
|
|
16
|
-
* the exported variable *name* and its line number — never the value — so a
|
|
17
|
-
* finding can be printed, logged, or shipped without leaking the secret.
|
|
18
|
-
*/
|
|
19
|
-
/** Shell rc files sourced at login/startup, in the order a report lists them. */
|
|
20
|
-
export declare const RC_FILENAMES: readonly [".zshenv", ".zprofile", ".zshrc", ".zlogin", ".bash_profile", ".bash_login", ".bashrc", ".profile", ".kshrc"];
|
|
21
|
-
/**
|
|
22
|
-
* A single credential-shaped export found in an rc file. Carries the variable
|
|
23
|
-
* NAME and location only — the value is never captured.
|
|
24
|
-
*/
|
|
25
|
-
export interface RcSecretFinding {
|
|
26
|
-
/** Basename of the rc file, e.g. `.zshenv`. */
|
|
27
|
-
file: string;
|
|
28
|
-
/** 1-based line number of the export. */
|
|
29
|
-
line: number;
|
|
30
|
-
/** The exported variable name (e.g. `AGENTS_SECRETS_PASSPHRASE`). Never the value. */
|
|
31
|
-
name: string;
|
|
32
|
-
/** The file-store master passphrase gets called out separately — it is the highest-severity case. */
|
|
33
|
-
isMasterPassphrase: boolean;
|
|
34
|
-
}
|
|
35
|
-
/** The file-store master key. Its own resolution prefers an off-env 0600 file, so
|
|
36
|
-
* a shell-rc export is always wrong once the store exists (RUSH-1968). */
|
|
37
|
-
export declare const MASTER_PASSPHRASE = "AGENTS_SECRETS_PASSPHRASE";
|
|
38
|
-
/**
|
|
39
|
-
* True when the file-store master key is live in THIS process's environment.
|
|
40
|
-
*
|
|
41
|
-
* The scanner above reads FILES, and that leaves a hole: a value inherited by a
|
|
42
|
-
* long-lived process outlives the rc line that set it, so deleting the export
|
|
43
|
-
* makes `scanRcExports` report clean while every shell, editor and agent started
|
|
44
|
-
* beforehand still carries the key and passes it to everything it spawns.
|
|
45
|
-
* Returns a boolean — never the value — so a finding built from it can be
|
|
46
|
-
* printed, logged, or shipped without leaking the secret.
|
|
47
|
-
*/
|
|
48
|
-
export declare function masterPassphraseInEnv(): boolean;
|
|
49
|
-
/** True if a variable name looks like it holds a credential value. */
|
|
50
|
-
export declare function isCredentialName(name: string): boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Scan one rc file's contents for credential-shaped exports. Pure — takes the
|
|
53
|
-
* text, returns findings (names + line numbers only, never values). A trailing
|
|
54
|
-
* `# comment` is stripped before matching so a commented-out export is ignored;
|
|
55
|
-
* the variable name always precedes any inline `#`, so stripping never loses it.
|
|
56
|
-
*/
|
|
57
|
-
export declare function scanRcExports(basename: string, content: string): RcSecretFinding[];
|
|
58
|
-
/**
|
|
59
|
-
* Scan the current user's shell rc files. Reads each file that exists under
|
|
60
|
-
* `homeDir` (default `os.homedir()`) and aggregates the findings. Missing or
|
|
61
|
-
* unreadable files are skipped silently — an advisory, not a hard requirement.
|
|
62
|
-
*/
|
|
63
|
-
export declare function scanUserRcFiles(homeDir?: string): RcSecretFinding[];
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detect credentials exported from shell rc files.
|
|
3
|
-
*
|
|
4
|
-
* A secret exported from `~/.zshenv`, `~/.zshrc`, `~/.bashrc`, `~/.profile`, …
|
|
5
|
-
* is inherited by every process the login shell spawns and is readable from
|
|
6
|
-
* `/proc/<pid>/environ` by any same-user process — the master passphrase leak
|
|
7
|
-
* behind RUSH-1968. `.zshenv` in particular is sourced by *every* zsh
|
|
8
|
-
* invocation, including non-interactive `ssh host 'cmd'`, so the value lands in
|
|
9
|
-
* the environment of essentially everything the box runs.
|
|
10
|
-
*
|
|
11
|
-
* The keychain-backed store (`agents secrets`) is the sanctioned home for
|
|
12
|
-
* credentials; the file-store master passphrase belongs in the off-env 0600 key
|
|
13
|
-
* file (`~/.agents/.secrets-key/passphrase`), never a shell rc export.
|
|
14
|
-
*
|
|
15
|
-
* This scanner powers a warn-level advisory in `agents doctor`. It reads only
|
|
16
|
-
* the exported variable *name* and its line number — never the value — so a
|
|
17
|
-
* finding can be printed, logged, or shipped without leaking the secret.
|
|
18
|
-
*/
|
|
19
|
-
import * as fs from 'fs';
|
|
20
|
-
import * as os from 'os';
|
|
21
|
-
import * as path from 'path';
|
|
22
|
-
/** Shell rc files sourced at login/startup, in the order a report lists them. */
|
|
23
|
-
export const RC_FILENAMES = [
|
|
24
|
-
'.zshenv',
|
|
25
|
-
'.zprofile',
|
|
26
|
-
'.zshrc',
|
|
27
|
-
'.zlogin',
|
|
28
|
-
'.bash_profile',
|
|
29
|
-
'.bash_login',
|
|
30
|
-
'.bashrc',
|
|
31
|
-
'.profile',
|
|
32
|
-
'.kshrc',
|
|
33
|
-
];
|
|
34
|
-
/** The file-store master key. Its own resolution prefers an off-env 0600 file, so
|
|
35
|
-
* a shell-rc export is always wrong once the store exists (RUSH-1968). */
|
|
36
|
-
export const MASTER_PASSPHRASE = 'AGENTS_SECRETS_PASSPHRASE';
|
|
37
|
-
/**
|
|
38
|
-
* True when the file-store master key is live in THIS process's environment.
|
|
39
|
-
*
|
|
40
|
-
* The scanner above reads FILES, and that leaves a hole: a value inherited by a
|
|
41
|
-
* long-lived process outlives the rc line that set it, so deleting the export
|
|
42
|
-
* makes `scanRcExports` report clean while every shell, editor and agent started
|
|
43
|
-
* beforehand still carries the key and passes it to everything it spawns.
|
|
44
|
-
* Returns a boolean — never the value — so a finding built from it can be
|
|
45
|
-
* printed, logged, or shipped without leaking the secret.
|
|
46
|
-
*/
|
|
47
|
-
export function masterPassphraseInEnv() {
|
|
48
|
-
return (process.env[MASTER_PASSPHRASE] ?? '').length > 0;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Last `_`-delimited segment values that mark a variable as credential-shaped.
|
|
52
|
-
* Matched against the FINAL segment (segment equality, not substring) so
|
|
53
|
-
* `X_API_KEY` fires on `KEY` while `SOME_KEY_PATH` (ends `PATH`) and `MONKEY`
|
|
54
|
-
* (single segment, not equal to `KEY`) do not.
|
|
55
|
-
*/
|
|
56
|
-
const CREDENTIAL_SEGMENTS = new Set([
|
|
57
|
-
'PASSPHRASE',
|
|
58
|
-
'PASSWORD',
|
|
59
|
-
'PASSWD',
|
|
60
|
-
'SECRET',
|
|
61
|
-
'SECRETS',
|
|
62
|
-
'TOKEN',
|
|
63
|
-
'KEY',
|
|
64
|
-
'APIKEY',
|
|
65
|
-
'PAT',
|
|
66
|
-
'CREDENTIAL',
|
|
67
|
-
'CREDENTIALS',
|
|
68
|
-
]);
|
|
69
|
-
/**
|
|
70
|
-
* Final segments that look credential-shaped but name a file path, socket, or
|
|
71
|
-
* config knob rather than a secret value — excluded to cut false positives.
|
|
72
|
-
* Applied to the whole name's final segment before the credential check.
|
|
73
|
-
*/
|
|
74
|
-
const BENIGN_FINAL_SEGMENTS = new Set([
|
|
75
|
-
'PATH',
|
|
76
|
-
'FILE',
|
|
77
|
-
'DIR',
|
|
78
|
-
'SOCK',
|
|
79
|
-
'SOCKET',
|
|
80
|
-
'ID',
|
|
81
|
-
'NAME',
|
|
82
|
-
'PARALLELISM', // TOKENIZERS_PARALLELISM and friends
|
|
83
|
-
]);
|
|
84
|
-
/** True if a variable name looks like it holds a credential value. */
|
|
85
|
-
export function isCredentialName(name) {
|
|
86
|
-
if (name === MASTER_PASSPHRASE)
|
|
87
|
-
return true;
|
|
88
|
-
const segments = name.toUpperCase().split('_');
|
|
89
|
-
const last = segments[segments.length - 1];
|
|
90
|
-
if (BENIGN_FINAL_SEGMENTS.has(last))
|
|
91
|
-
return false;
|
|
92
|
-
return CREDENTIAL_SEGMENTS.has(last);
|
|
93
|
-
}
|
|
94
|
-
// Matches an env assignment that puts a value in the environment:
|
|
95
|
-
// export NAME=... (sh/bash/zsh)
|
|
96
|
-
// typeset -x NAME=... / declare -x NAME=... (bash/zsh export-attribute form)
|
|
97
|
-
const EXPORT_RE = /^\s*(?:export\s+|(?:typeset|declare)\s+-[A-Za-z]*x[A-Za-z]*\s+)([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
98
|
-
/**
|
|
99
|
-
* Scan one rc file's contents for credential-shaped exports. Pure — takes the
|
|
100
|
-
* text, returns findings (names + line numbers only, never values). A trailing
|
|
101
|
-
* `# comment` is stripped before matching so a commented-out export is ignored;
|
|
102
|
-
* the variable name always precedes any inline `#`, so stripping never loses it.
|
|
103
|
-
*/
|
|
104
|
-
export function scanRcExports(basename, content) {
|
|
105
|
-
const out = [];
|
|
106
|
-
const lines = content.split('\n');
|
|
107
|
-
for (let i = 0; i < lines.length; i++) {
|
|
108
|
-
const code = lines[i].replace(/#.*$/, '');
|
|
109
|
-
const m = EXPORT_RE.exec(code);
|
|
110
|
-
if (!m)
|
|
111
|
-
continue;
|
|
112
|
-
const name = m[1];
|
|
113
|
-
if (!isCredentialName(name))
|
|
114
|
-
continue;
|
|
115
|
-
out.push({
|
|
116
|
-
file: basename,
|
|
117
|
-
line: i + 1,
|
|
118
|
-
name,
|
|
119
|
-
isMasterPassphrase: name === MASTER_PASSPHRASE,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Scan the current user's shell rc files. Reads each file that exists under
|
|
126
|
-
* `homeDir` (default `os.homedir()`) and aggregates the findings. Missing or
|
|
127
|
-
* unreadable files are skipped silently — an advisory, not a hard requirement.
|
|
128
|
-
*/
|
|
129
|
-
export function scanUserRcFiles(homeDir = os.homedir()) {
|
|
130
|
-
const out = [];
|
|
131
|
-
for (const name of RC_FILENAMES) {
|
|
132
|
-
const fp = path.join(homeDir, name);
|
|
133
|
-
let content;
|
|
134
|
-
try {
|
|
135
|
-
content = fs.readFileSync(fp, 'utf8');
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
continue; // absent or unreadable — nothing to report
|
|
139
|
-
}
|
|
140
|
-
out.push(...scanRcExports(name, content));
|
|
141
|
-
}
|
|
142
|
-
return out;
|
|
143
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Negative memo for failed/cancelled macOS keychain reads (the "back-off").
|
|
3
|
-
*
|
|
4
|
-
* A cancelled Touch ID sheet is the user saying "not now" — but a polling
|
|
5
|
-
* caller (AGI EXT host's `agents view` loop, a watch script)
|
|
6
|
-
* retries the same read a few seconds later and pops the sheet again, forever.
|
|
7
|
-
* The headless guard (index.ts `assertRawKeychainReadAllowed`) covers the
|
|
8
|
-
* no-TTY case; this memo covers a context that CAN prompt but just had its
|
|
9
|
-
* prompt cancelled or fail: the next read of the same item within the TTL
|
|
10
|
-
* throws the back-off error instead of re-prompting. Any successful read (or
|
|
11
|
-
* write) of the item clears the memo.
|
|
12
|
-
*
|
|
13
|
-
* Stored as regenerable state under `~/.agents/.cache/keychain-read-backoff/`,
|
|
14
|
-
* one file per item (filename is a hash of the item name; the file carries no
|
|
15
|
-
* secret material — a name and a deadline only). All operations are
|
|
16
|
-
* best-effort: a lost memo costs at most one extra prompt, never a read.
|
|
17
|
-
*/
|
|
18
|
-
/** How long a failed/cancelled read suppresses retries of the same item. */
|
|
19
|
-
export declare const KEYCHAIN_READ_BACKOFF_TTL_MS: number;
|
|
20
|
-
/** Test seam: point the memo at a temp dir so tests never touch the real cache. */
|
|
21
|
-
export declare function setKeychainReadBackoffDirForTest(dir: string | null): void;
|
|
22
|
-
/** True while `key` is inside the back-off window opened by a failed/cancelled read. */
|
|
23
|
-
export declare function isKeychainReadBackedOff(key: string, now?: number): boolean;
|
|
24
|
-
/** Open (or refresh) the back-off window for `key` after a failed/cancelled read. */
|
|
25
|
-
export declare function noteKeychainReadFailure(key: string, now?: number): void;
|
|
26
|
-
/** Clear the memo: a successful read or write of the item resets the back-off. */
|
|
27
|
-
export declare function clearKeychainReadBackoff(key: string): void;
|