@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
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* secrets-client.ts — the ONE process client through which agents-cli talks to
|
|
3
|
+
* the standalone `secrets` CLI (PHNX-3989).
|
|
4
|
+
*
|
|
5
|
+
* This is the agents-owned half of the secrets extraction: a bounded
|
|
6
|
+
* request/response client over the standalone executable's private consumer
|
|
7
|
+
* pipe. It carries NO storage, provider, or broker implementation — every
|
|
8
|
+
* operation is resolved by spawning `secrets __serve` and exchanging one JSON
|
|
9
|
+
* message over inherited pipes (delta-spec RPC-1). The engine lives entirely in
|
|
10
|
+
* the standalone package; agents-cli never rebundles it (DIST-1), so there is
|
|
11
|
+
* deliberately NO fallback to the in-repo `cli/src/lib/secrets/` engine — a
|
|
12
|
+
* missing executable fails loud with install guidance.
|
|
13
|
+
*
|
|
14
|
+
* Transport (matches secrets-cli/src/protocol-server.ts `runProtocolServer`):
|
|
15
|
+
* - the child reads the request JSON from fd 3 (to EOF) and writes the
|
|
16
|
+
* response JSON to fd 4; nothing is ever written to the child's stdout.
|
|
17
|
+
* - async: `spawn` with stdio ['ignore','ignore','inherit','pipe','pipe'];
|
|
18
|
+
* write+end child.stdio[3], read child.stdio[4] to EOF.
|
|
19
|
+
* - sync: `spawnSync` only wires stdio 0-2 portably (Bun drops numbered fds
|
|
20
|
+
* 3+), so the fd 3/4 wiring is done in a POSIX shell: the request rides a
|
|
21
|
+
* named FIFO fed by a backgrounded `cat` (fd 3), and fd 4 is redirected onto
|
|
22
|
+
* the child's stdout (`4>&1`), which `spawnSync` captures natively on every
|
|
23
|
+
* runtime. Bounded to `SYNC_SERVE_TIMEOUT_MS` so a broken standalone fails
|
|
24
|
+
* fast, never for the server's 60s deadline. POSIX only; Windows has no
|
|
25
|
+
* `mkfifo` and fails loud pointing at the async path.
|
|
26
|
+
*
|
|
27
|
+
* State root (MIG-1): the standalone selects its state root from `SECRETS_HOME`.
|
|
28
|
+
* agents-cli points it at the user agents dir (`~/.agents`) by default so the
|
|
29
|
+
* user's existing stores are adopted in place — no copy, no re-encryption. An
|
|
30
|
+
* explicit `SECRETS_HOME` in the environment wins (test isolation, power users),
|
|
31
|
+
* matching the standalone's own `state.ts` precedence.
|
|
32
|
+
*
|
|
33
|
+
* Policy (CTX-1): agents-cli passes its harness name as the opaque `scope` and,
|
|
34
|
+
* when a request must be bounded, a resource-profile-filtered `allowedBundles`
|
|
35
|
+
* set. Computing that policy stays in agents-cli (the caller supplies `context`);
|
|
36
|
+
* this client only forwards it.
|
|
37
|
+
*
|
|
38
|
+
* The typed shapes are imported `type`-only from `./secrets-types.js`, a pure
|
|
39
|
+
* re-declaration of the standalone's wire types (no runtime code, nothing in
|
|
40
|
+
* the npm tarball beyond the erased type import) — this repo's own in-process
|
|
41
|
+
* engine (`cli/src/lib/secrets/**`) is gone (PHNX-3989 Track D); the standalone
|
|
42
|
+
* `@phnx-labs/secrets-cli` package is the only implementation.
|
|
43
|
+
*/
|
|
44
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
45
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
46
|
+
import { tmpdir } from 'node:os';
|
|
47
|
+
import { join } from 'node:path';
|
|
48
|
+
import { findExecutable } from './platform/exec.js';
|
|
49
|
+
import { getUserAgentsDir } from './state.js';
|
|
50
|
+
/**
|
|
51
|
+
* Wire contract, mirrored from `secrets-cli/src/protocol.ts`. Both sides MUST
|
|
52
|
+
* agree byte-for-byte; this is the shared schema of the seam, the one thing an
|
|
53
|
+
* independent client legitimately re-declares rather than imports.
|
|
54
|
+
*/
|
|
55
|
+
export const PROTOCOL_VERSION = 1;
|
|
56
|
+
const MAX_PROTOCOL_BYTES = 8 * 1024 * 1024;
|
|
57
|
+
/** Just over the server's own 60s deadline, so the server times out first. */
|
|
58
|
+
const SERVE_TIMEOUT_MS = 65_000;
|
|
59
|
+
/**
|
|
60
|
+
* The synchronous path only serves read-only STATUS surfaces — `agents view`,
|
|
61
|
+
* the account-catalog rows, and run-config / account-rotation resolution on the
|
|
62
|
+
* `agents run` hot path. Those must never hang the whole render or launch on a
|
|
63
|
+
* missing or unreachable standalone, so the sync serve carries a short, hard
|
|
64
|
+
* bound instead of the async path's 65s: a broken `secrets` fails loud in a few
|
|
65
|
+
* seconds and the caller renders the rest of its output (or launches on the
|
|
66
|
+
* native login) with one clear line, rather than sitting for the standalone's
|
|
67
|
+
* own 60s deadline (the exact 60s hang PHNX-3989 hit when the child ran under
|
|
68
|
+
* Bun). A real sync op (a handshake, a bundle list, one item read) completes in
|
|
69
|
+
* tens of milliseconds, so this is ~100x headroom. It is NOT a fallback to the
|
|
70
|
+
* embedded engine (DIST-1); the standalone stays the only implementation, it
|
|
71
|
+
* just fails fast.
|
|
72
|
+
*/
|
|
73
|
+
const SYNC_SERVE_TIMEOUT_MS = 3_000;
|
|
74
|
+
/** Serialize `Map`s the way the server's `decodeWire` expects to receive them. */
|
|
75
|
+
export function encodeWire(value) {
|
|
76
|
+
if (value instanceof Map) {
|
|
77
|
+
return { $map: [...value.entries()].map(([key, item]) => [key, encodeWire(item)]) };
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value))
|
|
80
|
+
return value.map(encodeWire);
|
|
81
|
+
if (value !== null && typeof value === 'object') {
|
|
82
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, encodeWire(item)]));
|
|
83
|
+
}
|
|
84
|
+
return value === undefined ? null : value;
|
|
85
|
+
}
|
|
86
|
+
/** Reconstruct `Map`s from the server's `encodeWire`'d reply. */
|
|
87
|
+
export function decodeWire(value) {
|
|
88
|
+
if (Array.isArray(value))
|
|
89
|
+
return value.map(decodeWire);
|
|
90
|
+
if (value !== null && typeof value === 'object') {
|
|
91
|
+
const object = value;
|
|
92
|
+
if (Object.keys(object).length === 1 && Array.isArray(object.$map)) {
|
|
93
|
+
if (object.$map.some((entry) => !Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string')) {
|
|
94
|
+
throw new SecretsClientError('INVALID_RESPONSE', 'Invalid map encoding in secrets response');
|
|
95
|
+
}
|
|
96
|
+
return new Map(object.$map.map(([key, item]) => [key, decodeWire(item)]));
|
|
97
|
+
}
|
|
98
|
+
return Object.fromEntries(Object.entries(object).map(([key, item]) => [key, decodeWire(item)]));
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
/** Carries the server's `{code, message}`, or a client-side transport code. */
|
|
103
|
+
export class SecretsClientError extends Error {
|
|
104
|
+
code;
|
|
105
|
+
constructor(code, message) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.code = code;
|
|
108
|
+
this.name = 'SecretsClientError';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Serialize to a plain `{code, message}`. A consumer that folds a failure into
|
|
112
|
+
* a structure it later `JSON.stringify`s (a teammate's meta.json, a failure
|
|
113
|
+
* record, a spawn env) must never make the serializer chase this Error's
|
|
114
|
+
* internal references and throw `Converting circular structure to JSON`
|
|
115
|
+
* mid-launch — the value here is the two fields a caller actually needs.
|
|
116
|
+
*/
|
|
117
|
+
toJSON() {
|
|
118
|
+
return { code: this.code, message: this.message };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** True when `error` is a {@link SecretsClientError}, optionally with the given code. */
|
|
122
|
+
export function isSecretsClientError(error, code) {
|
|
123
|
+
return error instanceof SecretsClientError && (code === undefined || error.code === code);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Client-side TRANSPORT codes — the standalone could not be reached or spoke a
|
|
127
|
+
* broken protocol. These are the ONLY errors a "degrade when the standalone is
|
|
128
|
+
* unavailable" consumer may swallow: a code from the standalone's own reply
|
|
129
|
+
* (`NOT_FOUND`, `LOCKED`, `WRONG_BACKEND`, `OPERATION_FAILED`, `ACCESS_DENIED`, …)
|
|
130
|
+
* is a real answer and must surface, never be hidden as "unavailable".
|
|
131
|
+
*/
|
|
132
|
+
const SECRETS_TRANSPORT_CODES = new Set([
|
|
133
|
+
'SECRETS_BIN_MISSING',
|
|
134
|
+
'TIMEOUT',
|
|
135
|
+
'SPAWN_FAILED',
|
|
136
|
+
'SYNC_UNSUPPORTED',
|
|
137
|
+
'PROTOCOL_UNSUPPORTED',
|
|
138
|
+
'INVALID_RESPONSE',
|
|
139
|
+
'RESPONSE_TOO_LARGE',
|
|
140
|
+
'IO_ERROR',
|
|
141
|
+
]);
|
|
142
|
+
/** True when `error` is a {@link SecretsClientError} from the transport itself (the
|
|
143
|
+
* standalone was unreachable/unusable), not a data error it answered with. */
|
|
144
|
+
export function isSecretsTransportError(error) {
|
|
145
|
+
return error instanceof SecretsClientError && SECRETS_TRANSPORT_CODES.has(error.code);
|
|
146
|
+
}
|
|
147
|
+
// --- item naming (the shared identifier scheme of the seam) ---------------
|
|
148
|
+
//
|
|
149
|
+
// Raw item identifiers are part of the wire contract, mapped 1:1 across the
|
|
150
|
+
// cutover (delta-spec MIG-1): the standalone stores a bundle's per-key value at
|
|
151
|
+
// `agents-cli.secrets.<bundle>.<KEY>` and a profile provider token at
|
|
152
|
+
// `agents-cli.<provider>.token`, and a bundle var that reads `keychain:<KEY>`
|
|
153
|
+
// refers to the former. agents-cli derives these names whenever it seeds or
|
|
154
|
+
// reads a raw item (account bundles, profile tokens, the reserved `auth`
|
|
155
|
+
// bundle), so they are declared once here beside the protocol schema rather
|
|
156
|
+
// than re-derived per consumer.
|
|
157
|
+
const SERVICE_PREFIX = 'agents-cli';
|
|
158
|
+
export const SECRETS_ITEM_PREFIX = `${SERVICE_PREFIX}.secrets.`;
|
|
159
|
+
/** The raw item holding one bundle key's value. */
|
|
160
|
+
export function secretsKeychainItem(bundle, key) {
|
|
161
|
+
return `${SECRETS_ITEM_PREFIX}${bundle}.${key}`;
|
|
162
|
+
}
|
|
163
|
+
/** The raw item holding a profile provider's token (`agents-cli.<provider>.token`). */
|
|
164
|
+
export function profileKeychainItem(provider) {
|
|
165
|
+
return `${SERVICE_PREFIX}.${provider}.token`;
|
|
166
|
+
}
|
|
167
|
+
/** The bundle-var form that points a key at its own raw item. */
|
|
168
|
+
export function keychainRef(key) {
|
|
169
|
+
return `keychain:${key}`;
|
|
170
|
+
}
|
|
171
|
+
const REF_PATTERN = /^(keychain|env|file|exec):(.+)$/s;
|
|
172
|
+
/**
|
|
173
|
+
* Parse a bundle value into either a literal string or a typed secret ref. A
|
|
174
|
+
* `{ value }` object is an escaped literal (a URL that happens to start with a
|
|
175
|
+
* ref prefix is never misread as a reference).
|
|
176
|
+
*/
|
|
177
|
+
export function parseBundleValue(raw) {
|
|
178
|
+
if (typeof raw === 'object' && raw !== null && typeof raw.value === 'string') {
|
|
179
|
+
return { literal: raw.value };
|
|
180
|
+
}
|
|
181
|
+
if (typeof raw !== 'string') {
|
|
182
|
+
throw new Error(`Invalid bundle value (expected string or {value: string}): ${JSON.stringify(raw)}`);
|
|
183
|
+
}
|
|
184
|
+
const match = REF_PATTERN.exec(raw);
|
|
185
|
+
if (!match)
|
|
186
|
+
return { literal: raw };
|
|
187
|
+
return { ref: { provider: match[1], value: match[2] } };
|
|
188
|
+
}
|
|
189
|
+
// --- executable resolution -------------------------------------------------
|
|
190
|
+
let cachedBin;
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the standalone `secrets` executable: `$SECRETS_BIN` if set, else the
|
|
193
|
+
* `secrets` command on PATH. Cached for the process. A miss throws with install
|
|
194
|
+
* guidance — there is NO fallback to the embedded engine (DIST-1).
|
|
195
|
+
*/
|
|
196
|
+
export function resolveSecretsBin() {
|
|
197
|
+
if (cachedBin)
|
|
198
|
+
return cachedBin;
|
|
199
|
+
const explicit = process.env.SECRETS_BIN?.trim();
|
|
200
|
+
const resolved = explicit && explicit.length > 0 ? explicit : findExecutable('secrets');
|
|
201
|
+
if (!resolved) {
|
|
202
|
+
throw new SecretsClientError('SECRETS_BIN_MISSING', 'The standalone `secrets` CLI was not found. Install it with:\n' +
|
|
203
|
+
' npm i -g @phnx-labs/secrets-cli\n' +
|
|
204
|
+
'or point $SECRETS_BIN at its executable.');
|
|
205
|
+
}
|
|
206
|
+
cachedBin = resolved;
|
|
207
|
+
return resolved;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* How to invoke the resolved binary. A `.js`/`.mjs`/`.cjs` entrypoint (a dev
|
|
211
|
+
* build, or `$SECRETS_BIN` pointing at `dist/index.js`) is run through this
|
|
212
|
+
* process's Node so it works without an executable bit and on Windows; an
|
|
213
|
+
* installed `secrets` shim/binary is spawned directly. Exported so a caller
|
|
214
|
+
* that needs to run an interactive standalone verb directly (e.g. `secrets
|
|
215
|
+
* migrate`, which isn't in this client's op table) can build the same argv.
|
|
216
|
+
*/
|
|
217
|
+
export function invocation(bin) {
|
|
218
|
+
if (/\.[mc]?js$/.test(bin))
|
|
219
|
+
return { command: process.execPath, prefix: [bin] };
|
|
220
|
+
return { command: bin, prefix: [] };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Spawn env for the child, applied to a base env (defaults to `process.env`):
|
|
224
|
+
*
|
|
225
|
+
* - `SECRETS_HOME` defaults to the user agents dir so the standalone adopts the
|
|
226
|
+
* existing stores in place (MIG-1); an explicit value in the base env wins.
|
|
227
|
+
* - The standalone renamed every `AGENTS_SECRETS_*` knob to `SECRETS_*`, so a
|
|
228
|
+
* caller env still carrying the old `AGENTS_SECRETS_PASSPHRASE` (the name
|
|
229
|
+
* agents-cli's own engine reads — filestore/linux/windows) would be invisible
|
|
230
|
+
* to `secrets`, which reads only `SECRETS_PASSPHRASE`. Forward it so the child
|
|
231
|
+
* can decrypt the very file store agents-cli wrote, rather than silently
|
|
232
|
+
* provisioning a fresh machine-local key. An explicit `SECRETS_PASSPHRASE`
|
|
233
|
+
* wins — this only bridges the rename, it never overrides.
|
|
234
|
+
*
|
|
235
|
+
* Exported for the seam test (a pure env mapping, no spawn).
|
|
236
|
+
*/
|
|
237
|
+
export function buildServeEnv(base = process.env) {
|
|
238
|
+
const env = {
|
|
239
|
+
...base,
|
|
240
|
+
SECRETS_HOME: base.SECRETS_HOME ?? getUserAgentsDir(),
|
|
241
|
+
};
|
|
242
|
+
if (!env.SECRETS_PASSPHRASE && base.AGENTS_SECRETS_PASSPHRASE) {
|
|
243
|
+
env.SECRETS_PASSPHRASE = base.AGENTS_SECRETS_PASSPHRASE;
|
|
244
|
+
}
|
|
245
|
+
return env;
|
|
246
|
+
}
|
|
247
|
+
let requestCounter = 0;
|
|
248
|
+
function buildRequest(op, args, context) {
|
|
249
|
+
requestCounter += 1;
|
|
250
|
+
const request = {
|
|
251
|
+
v: PROTOCOL_VERSION,
|
|
252
|
+
id: `${process.pid}-${requestCounter}`,
|
|
253
|
+
op,
|
|
254
|
+
args: encodeWire(args),
|
|
255
|
+
};
|
|
256
|
+
if (context)
|
|
257
|
+
request.context = context;
|
|
258
|
+
return request;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Describe what the child actually wrote on fd 4 when it did not parse as JSON,
|
|
262
|
+
* so a wrong `secrets` on PATH (a shim printing usage, an unrelated binary), a
|
|
263
|
+
* child that ran under the wrong runtime and never answered, or a silent write
|
|
264
|
+
* failure is diagnosable from the error alone rather than a bare "non-JSON
|
|
265
|
+
* response". Bounded to 200 bytes; control bytes are stripped so the message
|
|
266
|
+
* stays one readable line.
|
|
267
|
+
*/
|
|
268
|
+
function describeNonJson(text) {
|
|
269
|
+
const trimmed = text.trim();
|
|
270
|
+
if (!trimmed)
|
|
271
|
+
return ' (the standalone wrote nothing to fd 4)';
|
|
272
|
+
const preview = trimmed.slice(0, 200).replace(/[\u0000-\u001f\u007f]/g, '?');
|
|
273
|
+
return ` (first 200 bytes on fd 4: ${JSON.stringify(preview)})`;
|
|
274
|
+
}
|
|
275
|
+
function parseResponse(raw) {
|
|
276
|
+
const text = raw.toString('utf8');
|
|
277
|
+
let parsed;
|
|
278
|
+
try {
|
|
279
|
+
parsed = JSON.parse(text);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
throw new SecretsClientError('INVALID_RESPONSE', `secrets returned a non-JSON response${describeNonJson(text)}`);
|
|
283
|
+
}
|
|
284
|
+
if (!parsed || parsed.v !== PROTOCOL_VERSION || typeof parsed.id !== 'string') {
|
|
285
|
+
throw new SecretsClientError('INVALID_RESPONSE', 'secrets returned a malformed response envelope');
|
|
286
|
+
}
|
|
287
|
+
if (parsed.ok)
|
|
288
|
+
return decodeWire(parsed.result);
|
|
289
|
+
throw new SecretsClientError(parsed.error.code, parsed.error.message);
|
|
290
|
+
}
|
|
291
|
+
// --- raw transport (one spawn per request) ---------------------------------
|
|
292
|
+
function serveOnce(op, args, context) {
|
|
293
|
+
const { command, prefix } = invocation(resolveSecretsBin());
|
|
294
|
+
const request = Buffer.from(JSON.stringify(buildRequest(op, args, context)));
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
const child = spawn(command, [...prefix, '__serve'], {
|
|
297
|
+
stdio: ['ignore', 'ignore', 'inherit', 'pipe', 'pipe'],
|
|
298
|
+
env: buildServeEnv(),
|
|
299
|
+
});
|
|
300
|
+
let settled = false;
|
|
301
|
+
const fail = (error) => {
|
|
302
|
+
if (settled)
|
|
303
|
+
return;
|
|
304
|
+
settled = true;
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
child.kill('SIGKILL');
|
|
307
|
+
reject(error);
|
|
308
|
+
};
|
|
309
|
+
const timer = setTimeout(() => fail(new SecretsClientError('TIMEOUT', 'secrets request timed out')), SERVE_TIMEOUT_MS);
|
|
310
|
+
child.on('error', (error) => fail(new SecretsClientError('SPAWN_FAILED', `Failed to spawn secrets: ${error.message}`)));
|
|
311
|
+
// A child that dies before reading gives EPIPE here; the outcome surfaces on
|
|
312
|
+
// fd 4 / 'error' instead, so this handler just keeps it from throwing.
|
|
313
|
+
const input = child.stdio[3];
|
|
314
|
+
input.on('error', () => { });
|
|
315
|
+
input.end(request);
|
|
316
|
+
// Settle only when BOTH the response has been read to EOF AND the child has
|
|
317
|
+
// fully exited. The standalone writes its response to fd 4 and closes it
|
|
318
|
+
// *before* it finishes: on the way out it still releases its proper-lockfile
|
|
319
|
+
// lock dir and flushes its meta/events log under the store's `.cache`.
|
|
320
|
+
// Resolving on fd-4 EOF alone let the caller (e.g. a test tearing down its
|
|
321
|
+
// throwaway SECRETS_HOME) race those trailing writes — an ENOTEMPTY rmdir.
|
|
322
|
+
// Awaiting child exit makes the store quiescent the instant secretsRequest
|
|
323
|
+
// resolves, so no consumer needs a cleanup retry.
|
|
324
|
+
const out = child.stdio[4];
|
|
325
|
+
const chunks = [];
|
|
326
|
+
let size = 0;
|
|
327
|
+
let response = null;
|
|
328
|
+
let exited = false;
|
|
329
|
+
const settleWhenReady = () => {
|
|
330
|
+
if (settled || response === null || !exited)
|
|
331
|
+
return;
|
|
332
|
+
settled = true;
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
try {
|
|
335
|
+
resolve(parseResponse(response));
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
reject(error);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
out.on('data', (chunk) => {
|
|
342
|
+
size += chunk.length;
|
|
343
|
+
if (size > MAX_PROTOCOL_BYTES) {
|
|
344
|
+
fail(new SecretsClientError('RESPONSE_TOO_LARGE', 'secrets response exceeds the protocol limit'));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
chunks.push(chunk);
|
|
348
|
+
});
|
|
349
|
+
out.on('error', (error) => fail(new SecretsClientError('IO_ERROR', error.message)));
|
|
350
|
+
out.on('end', () => {
|
|
351
|
+
response = Buffer.concat(chunks);
|
|
352
|
+
settleWhenReady();
|
|
353
|
+
});
|
|
354
|
+
child.on('exit', () => {
|
|
355
|
+
exited = true;
|
|
356
|
+
settleWhenReady();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/** POSIX single-quote a token so the shell passes it to the child verbatim. */
|
|
361
|
+
function shQuote(token) {
|
|
362
|
+
return `'${token.replace(/'/g, `'\\''`)}'`;
|
|
363
|
+
}
|
|
364
|
+
function serveOnceSync(op, args, context) {
|
|
365
|
+
if (process.platform === 'win32') {
|
|
366
|
+
throw new SecretsClientError('SYNC_UNSUPPORTED', 'The synchronous secrets path needs a POSIX shell and FIFO; use secretsRequest (async) on Windows.');
|
|
367
|
+
}
|
|
368
|
+
const { command, prefix } = invocation(resolveSecretsBin());
|
|
369
|
+
const request = Buffer.from(JSON.stringify(buildRequest(op, args, context)));
|
|
370
|
+
// The standalone's private protocol reads the request from fd 3 and writes the
|
|
371
|
+
// response to fd 4, and it REFUSES a plain file or tty for either (it fails with
|
|
372
|
+
// `PRIVATE_PIPE_REQUIRED` so no secret is ever staged to disk). But `spawnSync`
|
|
373
|
+
// only wires stdio 0-2 portably — the Bun runtime (this repo runs its whole CLI
|
|
374
|
+
// suite as `bun src/index.ts`, so the 71 CLI-integration tests spawn agents-cli
|
|
375
|
+
// under Bun) silently DROPS numbered fds 3+, so the old direct-fd wiring left the
|
|
376
|
+
// child blocked on an empty fd 3 for the full timeout. Do the numbered-fd wiring
|
|
377
|
+
// in a POSIX shell against PIPES instead: the request rides a FIFO fed by a
|
|
378
|
+
// backgrounded `cat` (fd 3), and fd 4 is dup'd from the captured stdout pipe
|
|
379
|
+
// (`4>&1`), which `spawnSync` captures natively as an anonymous pipe on every
|
|
380
|
+
// runtime. The child's own fd 1 is then redirected to /dev/null (`1>/dev/null`,
|
|
381
|
+
// applied AFTER `4>&1` so fd 4 keeps the pipe): the response channel (fd 4) is
|
|
382
|
+
// structurally the ONLY writer to the captured stream, so even a stray write to
|
|
383
|
+
// the standalone's own stdout can never corrupt the fd-4 bytes. Both fd 3/4 are
|
|
384
|
+
// S_ISFIFO, satisfying the standalone; the response never touches disk.
|
|
385
|
+
// Identical on Node and Bun.
|
|
386
|
+
const dir = mkdtempSync(join(tmpdir(), 'agents-secrets-'));
|
|
387
|
+
const reqFile = join(dir, 'req');
|
|
388
|
+
const reqFifo = join(dir, 'reqfifo');
|
|
389
|
+
try {
|
|
390
|
+
writeFileSync(reqFile, request);
|
|
391
|
+
const mk = spawnSync('mkfifo', [reqFifo]);
|
|
392
|
+
if (mk.status !== 0) {
|
|
393
|
+
throw new SecretsClientError('SYNC_UNSUPPORTED', 'mkfifo is unavailable for the synchronous secrets path');
|
|
394
|
+
}
|
|
395
|
+
const serve = [command, ...prefix, '__serve'].map(shQuote).join(' ');
|
|
396
|
+
// `exec` so the wrapper `sh` BECOMES the standalone (same PID). Two things ride
|
|
397
|
+
// on that: `spawnSync` waits on the real process, so on return the store is
|
|
398
|
+
// fully flushed and its proper-lockfile lock dir released (the sync path never
|
|
399
|
+
// had the async path's resolve-before-exit race — spawnSync is a hard barrier);
|
|
400
|
+
// and `spawnSync`'s timeout SIGTERM lands on the standalone itself rather than
|
|
401
|
+
// orphaning a wedged child behind a dead wrapper. The backgrounded `cat` only
|
|
402
|
+
// feeds the tiny request into the FIFO and exits the instant the reader (fd 3)
|
|
403
|
+
// opens — long before the standalone finishes — so it is reaped by its own exit
|
|
404
|
+
// and never outlives the call. fd wiring is order-sensitive: `4>&1` dups the
|
|
405
|
+
// response channel from the captured stdout pipe, THEN `1>/dev/null` sends the
|
|
406
|
+
// child's own stdout to the bit bucket, so fd 4 stays the sole writer to the
|
|
407
|
+
// captured stream.
|
|
408
|
+
const script = `cat ${shQuote(reqFile)} > ${shQuote(reqFifo)} & exec ${serve} 3<${shQuote(reqFifo)} 4>&1 1>/dev/null`;
|
|
409
|
+
const result = spawnSync('sh', ['-c', script], {
|
|
410
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
411
|
+
env: buildServeEnv(),
|
|
412
|
+
timeout: SYNC_SERVE_TIMEOUT_MS,
|
|
413
|
+
maxBuffer: MAX_PROTOCOL_BYTES + 4096,
|
|
414
|
+
});
|
|
415
|
+
if (result.error) {
|
|
416
|
+
const err = result.error;
|
|
417
|
+
const code = err.code === 'ETIMEDOUT' ? 'TIMEOUT' : 'SPAWN_FAILED';
|
|
418
|
+
throw new SecretsClientError(code, `secrets request failed: ${err.message}`);
|
|
419
|
+
}
|
|
420
|
+
const raw = result.stdout ?? Buffer.alloc(0);
|
|
421
|
+
if (raw.length > MAX_PROTOCOL_BYTES) {
|
|
422
|
+
throw new SecretsClientError('RESPONSE_TOO_LARGE', 'secrets response exceeds the protocol limit');
|
|
423
|
+
}
|
|
424
|
+
return parseResponse(raw);
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
rmSync(dir, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// --- handshake (once per process) ------------------------------------------
|
|
431
|
+
let handshakeReady = false;
|
|
432
|
+
let handshakePromise = null;
|
|
433
|
+
function checkHandshake(result) {
|
|
434
|
+
const protocol = result?.protocol;
|
|
435
|
+
if (protocol !== PROTOCOL_VERSION) {
|
|
436
|
+
throw new SecretsClientError('PROTOCOL_UNSUPPORTED', `secrets speaks protocol ${String(protocol)}; this agents-cli needs ${PROTOCOL_VERSION}. ` +
|
|
437
|
+
'Update the standalone CLI (npm i -g @phnx-labs/secrets-cli).');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function ensureHandshake() {
|
|
441
|
+
if (handshakeReady)
|
|
442
|
+
return;
|
|
443
|
+
if (!handshakePromise) {
|
|
444
|
+
handshakePromise = (async () => {
|
|
445
|
+
checkHandshake(await serveOnce('handshake', []));
|
|
446
|
+
handshakeReady = true;
|
|
447
|
+
})().catch((error) => {
|
|
448
|
+
handshakePromise = null;
|
|
449
|
+
throw error;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
await handshakePromise;
|
|
453
|
+
}
|
|
454
|
+
function ensureHandshakeSync() {
|
|
455
|
+
if (handshakeReady)
|
|
456
|
+
return;
|
|
457
|
+
checkHandshake(serveOnceSync('handshake', []));
|
|
458
|
+
handshakeReady = true;
|
|
459
|
+
}
|
|
460
|
+
// --- primitives ------------------------------------------------------------
|
|
461
|
+
/**
|
|
462
|
+
* Send one operation to the standalone secrets CLI and await its typed result.
|
|
463
|
+
* Verifies the executable speaks protocol v1 once per process (cached), then
|
|
464
|
+
* spawns `secrets __serve` for the operation. Throws a {@link SecretsClientError}
|
|
465
|
+
* carrying the server's `{code, message}` on failure.
|
|
466
|
+
*/
|
|
467
|
+
export async function secretsRequest(op, args = [], context) {
|
|
468
|
+
await ensureHandshake();
|
|
469
|
+
return (await serveOnce(op, args, context));
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Synchronous sibling of {@link secretsRequest} for the consumers that resolve
|
|
473
|
+
* secrets on a synchronous path (e.g. building a child env before spawn).
|
|
474
|
+
* Bounded by a spawn timeout. POSIX only.
|
|
475
|
+
*/
|
|
476
|
+
export function secretsRequestSync(op, args = [], context) {
|
|
477
|
+
ensureHandshakeSync();
|
|
478
|
+
return serveOnceSync(op, args, context);
|
|
479
|
+
}
|
|
480
|
+
/** Test hook: forget the cached binary + handshake so a new env is re-resolved. */
|
|
481
|
+
export function _resetSecretsClientForTest() {
|
|
482
|
+
cachedBin = undefined;
|
|
483
|
+
handshakeReady = false;
|
|
484
|
+
handshakePromise = null;
|
|
485
|
+
requestCounter = 0;
|
|
486
|
+
}
|
|
487
|
+
// --- typed wrappers -------------------------------------------------------
|
|
488
|
+
//
|
|
489
|
+
// The resolve/read/write/raw-CRUD ops today's consumers hit — one thin, typed
|
|
490
|
+
// forward each onto the two primitives above; the standalone remains the single
|
|
491
|
+
// implementation. Async by default; the read-hot operations agents-cli resolves
|
|
492
|
+
// synchronously carry a `*Sync` sibling.
|
|
493
|
+
//
|
|
494
|
+
// This is deliberately NOT the standalone's full op table. The remaining
|
|
495
|
+
// bundle-metadata ops it also exposes — `bundlePolicy`,
|
|
496
|
+
// `readBundleIfDecryptable`, `keychainItemsForBundle`, `migrateLegacyBundles`
|
|
497
|
+
// — get their wrapper as the consumer-conversion wave (tasks.md item 6) lands
|
|
498
|
+
// each caller that needs it, so a wrapper always ships with a real call site
|
|
499
|
+
// and a test rather than as speculative unused surface. (`describeBundle`,
|
|
500
|
+
// `bundleBackend`, `renameBundle`, `rotateBundleSecret`, and the `sync.*` /
|
|
501
|
+
// `rc-hygiene.*` groups have landed with the consumers that needed them.)
|
|
502
|
+
// bundles.*
|
|
503
|
+
export function readAndResolveBundleEnv(name, opts, context) {
|
|
504
|
+
return secretsRequest('bundles.readAndResolveBundleEnv', [name, opts ?? {}], context);
|
|
505
|
+
}
|
|
506
|
+
export function readAndResolveBundleEnvSync(name, opts, context) {
|
|
507
|
+
return secretsRequestSync('bundles.readAndResolveBundleEnv', [name, opts ?? {}], context);
|
|
508
|
+
}
|
|
509
|
+
export function listBundles(context) {
|
|
510
|
+
return secretsRequest('bundles.listBundles', [], context);
|
|
511
|
+
}
|
|
512
|
+
export function listBundlesSync(context) {
|
|
513
|
+
return secretsRequestSync('bundles.listBundles', [], context);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Per-key kind breakdown (`literal`/`file`/`onepassword`/…) of an already-resolved
|
|
517
|
+
* bundle — the cosmetic `[secrets] Resolved <name>: N keys (…)` summary line. Pure
|
|
518
|
+
* over the bundle document server-side; the wrapper forwards the object the caller
|
|
519
|
+
* already holds from {@link readAndResolveBundleEnv} so the same shape is described.
|
|
520
|
+
*/
|
|
521
|
+
export function describeBundle(bundle, context) {
|
|
522
|
+
return secretsRequest('bundles.describeBundle', [bundle], context);
|
|
523
|
+
}
|
|
524
|
+
export function readBundle(name, context) {
|
|
525
|
+
return secretsRequest('bundles.readBundle', [name], context);
|
|
526
|
+
}
|
|
527
|
+
export function readBundleSync(name, context) {
|
|
528
|
+
return secretsRequestSync('bundles.readBundle', [name], context);
|
|
529
|
+
}
|
|
530
|
+
export function bundleExists(name, context) {
|
|
531
|
+
return secretsRequest('bundles.bundleExists', [name], context);
|
|
532
|
+
}
|
|
533
|
+
export function bundleExistsSync(name, context) {
|
|
534
|
+
return secretsRequestSync('bundles.bundleExists', [name], context);
|
|
535
|
+
}
|
|
536
|
+
export function bundleBackend(name, context) {
|
|
537
|
+
return secretsRequest('bundles.bundleBackend', [name], context);
|
|
538
|
+
}
|
|
539
|
+
export function bundleBackendSync(name, context) {
|
|
540
|
+
return secretsRequestSync('bundles.bundleBackend', [name], context);
|
|
541
|
+
}
|
|
542
|
+
export function writeBundle(bundle, opts, context) {
|
|
543
|
+
return secretsRequest('bundles.writeBundle', [bundle, opts ?? {}], context);
|
|
544
|
+
}
|
|
545
|
+
export function writeBundleWithItems(bundle, items, opts, context) {
|
|
546
|
+
return secretsRequest('bundles.writeBundleWithItems', [bundle, items, opts ?? {}], context);
|
|
547
|
+
}
|
|
548
|
+
export function writeBundleWithItemsSync(bundle, items, opts, context) {
|
|
549
|
+
secretsRequestSync('bundles.writeBundleWithItems', [bundle, items, opts ?? {}], context);
|
|
550
|
+
}
|
|
551
|
+
export function deleteBundle(name, context) {
|
|
552
|
+
return secretsRequest('bundles.deleteBundle', [name], context);
|
|
553
|
+
}
|
|
554
|
+
export function deleteBundleSync(name, context) {
|
|
555
|
+
return secretsRequestSync('bundles.deleteBundle', [name], context);
|
|
556
|
+
}
|
|
557
|
+
/** Rename a bundle: metadata and raw items move together; the source is deleted last. */
|
|
558
|
+
export function renameBundle(oldName, newName, opts, context) {
|
|
559
|
+
return secretsRequest('bundles.renameBundle', [oldName, newName, opts ?? {}], context);
|
|
560
|
+
}
|
|
561
|
+
export function renameBundleSync(oldName, newName, opts, context) {
|
|
562
|
+
secretsRequestSync('bundles.renameBundle', [oldName, newName, opts ?? {}], context);
|
|
563
|
+
}
|
|
564
|
+
/** Rotate one keychain-backed key's value in place, preserving or patching its meta. */
|
|
565
|
+
export function rotateBundleSecret(bundle, key, opts, context) {
|
|
566
|
+
return secretsRequest('bundles.rotateBundleSecret', [bundle, key, opts], context);
|
|
567
|
+
}
|
|
568
|
+
export function rotateBundleSecretSync(bundle, key, opts, context) {
|
|
569
|
+
secretsRequestSync('bundles.rotateBundleSecret', [bundle, key, opts], context);
|
|
570
|
+
}
|
|
571
|
+
// agent.*
|
|
572
|
+
export function agentPing() {
|
|
573
|
+
return secretsRequest('agent.agentPing', []);
|
|
574
|
+
}
|
|
575
|
+
export function agentPingSync() {
|
|
576
|
+
return secretsRequestSync('agent.agentPing', []);
|
|
577
|
+
}
|
|
578
|
+
export function agentStatus() {
|
|
579
|
+
return secretsRequest('agent.agentStatus', []);
|
|
580
|
+
}
|
|
581
|
+
export function agentLock(name) {
|
|
582
|
+
return secretsRequest('agent.agentLock', name === undefined ? [] : [name]);
|
|
583
|
+
}
|
|
584
|
+
export function ensureAgentRunning(timeoutMs) {
|
|
585
|
+
return secretsRequest('agent.ensureAgentRunning', timeoutMs === undefined ? [] : [timeoutMs]);
|
|
586
|
+
}
|
|
587
|
+
// index.* (keychain items)
|
|
588
|
+
export function getKeychainToken(item, context) {
|
|
589
|
+
return secretsRequest('index.getKeychainToken', [item, context ?? {}]);
|
|
590
|
+
}
|
|
591
|
+
export function getKeychainTokenSync(item, context) {
|
|
592
|
+
return secretsRequestSync('index.getKeychainToken', [item, context ?? {}]);
|
|
593
|
+
}
|
|
594
|
+
export function setKeychainToken(item, value, opts) {
|
|
595
|
+
return secretsRequest('index.setKeychainToken', opts === undefined ? [item, value] : [item, value, opts]);
|
|
596
|
+
}
|
|
597
|
+
export function setKeychainTokenSync(item, value, opts) {
|
|
598
|
+
secretsRequestSync('index.setKeychainToken', opts === undefined ? [item, value] : [item, value, opts]);
|
|
599
|
+
}
|
|
600
|
+
export function hasKeychainToken(item) {
|
|
601
|
+
return secretsRequest('index.hasKeychainToken', [item]);
|
|
602
|
+
}
|
|
603
|
+
export function hasKeychainTokenSync(item) {
|
|
604
|
+
return secretsRequestSync('index.hasKeychainToken', [item]);
|
|
605
|
+
}
|
|
606
|
+
export function deleteKeychainToken(item) {
|
|
607
|
+
return secretsRequest('index.deleteKeychainToken', [item]);
|
|
608
|
+
}
|
|
609
|
+
export function deleteKeychainTokenSync(item) {
|
|
610
|
+
return secretsRequestSync('index.deleteKeychainToken', [item]);
|
|
611
|
+
}
|
|
612
|
+
export function listKeychainItems(prefix) {
|
|
613
|
+
return secretsRequest('index.listKeychainItems', [prefix]);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* True when `keychain`-backend items are being routed to the standalone's
|
|
617
|
+
* encrypted file store (headless Linux/Windows with no reachable keyring):
|
|
618
|
+
* on such a host no test or probe can reach a real OS keychain.
|
|
619
|
+
*/
|
|
620
|
+
export function keychainUsesFileFallback() {
|
|
621
|
+
return secretsRequest('index.keychainUsesFileFallback', []);
|
|
622
|
+
}
|
|
623
|
+
// store.* (explicit-backend raw item CRUD)
|
|
624
|
+
export function storeGet(backend, item) {
|
|
625
|
+
return secretsRequest('store.get', [backend, item]);
|
|
626
|
+
}
|
|
627
|
+
export function storeGetSync(backend, item) {
|
|
628
|
+
return secretsRequestSync('store.get', [backend, item]);
|
|
629
|
+
}
|
|
630
|
+
export function storeHas(backend, item) {
|
|
631
|
+
return secretsRequest('store.has', [backend, item]);
|
|
632
|
+
}
|
|
633
|
+
export function storeHasSync(backend, item) {
|
|
634
|
+
return secretsRequestSync('store.has', [backend, item]);
|
|
635
|
+
}
|
|
636
|
+
export function storeSet(backend, item, value) {
|
|
637
|
+
return secretsRequest('store.set', [backend, item, value]);
|
|
638
|
+
}
|
|
639
|
+
export function storeSetSync(backend, item, value) {
|
|
640
|
+
secretsRequestSync('store.set', [backend, item, value]);
|
|
641
|
+
}
|
|
642
|
+
export function storeDelete(backend, item) {
|
|
643
|
+
return secretsRequest('store.delete', [backend, item]);
|
|
644
|
+
}
|
|
645
|
+
// remote.* / push.*
|
|
646
|
+
export function remoteResolveEnv(target, bundle, opts) {
|
|
647
|
+
return secretsRequest('remote.remoteResolveEnv', [target, bundle, opts ?? {}]);
|
|
648
|
+
}
|
|
649
|
+
export function pushBundleToHost(bundle, host, opts) {
|
|
650
|
+
return secretsRequest('push.pushBundleToHost', [bundle, host, opts]);
|
|
651
|
+
}
|
|
652
|
+
export function pushBundleToHostAsync(bundle, host, opts) {
|
|
653
|
+
return secretsRequest('push.pushBundleToHostAsync', [bundle, host, opts]);
|
|
654
|
+
}
|
|
655
|
+
// sync.* (transport pull, used by the `agents sync --secrets` umbrella stage)
|
|
656
|
+
export function listRemoteBundles(context) {
|
|
657
|
+
return secretsRequest('sync.listRemoteBundles', [], context);
|
|
658
|
+
}
|
|
659
|
+
export function pullBundle(name, opts, context) {
|
|
660
|
+
return secretsRequest('sync.pullBundle', [name, opts], context);
|
|
661
|
+
}
|
|
662
|
+
// rc-hygiene.* (shell-rc credential-export advisory, `agents doctor`)
|
|
663
|
+
export function scanUserRcFiles(homeDir, context) {
|
|
664
|
+
return secretsRequest('rc-hygiene.scanUserRcFiles', homeDir === undefined ? [] : [homeDir], context);
|
|
665
|
+
}
|
|
666
|
+
export function scanUserRcFilesSync(homeDir, context) {
|
|
667
|
+
return secretsRequestSync('rc-hygiene.scanUserRcFiles', homeDir === undefined ? [] : [homeDir], context);
|
|
668
|
+
}
|
|
669
|
+
export function masterPassphraseInEnv(context) {
|
|
670
|
+
return secretsRequest('rc-hygiene.masterPassphraseInEnv', [], context);
|
|
671
|
+
}
|
|
672
|
+
export function masterPassphraseInEnvSync(context) {
|
|
673
|
+
return secretsRequestSync('rc-hygiene.masterPassphraseInEnv', [], context);
|
|
674
|
+
}
|
|
675
|
+
// --- env sanitization (EXEC-1: deny loader/interpreter overrides) ----------
|
|
676
|
+
//
|
|
677
|
+
// Pure, engine-free — every spawn boundary strips these before either an
|
|
678
|
+
// injected secrets env or the bare process env reaches a child, so a bundle
|
|
679
|
+
// (or an inherited shell) can never smuggle a loader/interpreter override into
|
|
680
|
+
// an agent's process.
|
|
681
|
+
/** True for a dynamic-loader or language-interpreter override env var. */
|
|
682
|
+
export function isLoaderOrInterpreterEnv(name) {
|
|
683
|
+
const upper = name.toUpperCase();
|
|
684
|
+
return (upper.startsWith('LD_') ||
|
|
685
|
+
upper.startsWith('DYLD_') ||
|
|
686
|
+
[
|
|
687
|
+
'NODE_OPTIONS',
|
|
688
|
+
'PYTHONPATH',
|
|
689
|
+
'PYTHONSTARTUP',
|
|
690
|
+
'BASH_ENV',
|
|
691
|
+
'ENV',
|
|
692
|
+
'PERL5OPT',
|
|
693
|
+
'RUBYOPT',
|
|
694
|
+
'PROMPT_COMMAND',
|
|
695
|
+
'IFS',
|
|
696
|
+
'CDPATH',
|
|
697
|
+
].includes(upper));
|
|
698
|
+
}
|
|
699
|
+
/** Strip loader/interpreter overrides from an env before it reaches a spawned child. */
|
|
700
|
+
export function sanitizeProcessEnv(env = process.env) {
|
|
701
|
+
const out = {};
|
|
702
|
+
for (const [k, v] of Object.entries(env)) {
|
|
703
|
+
if (v === undefined)
|
|
704
|
+
continue;
|
|
705
|
+
if (isLoaderOrInterpreterEnv(k))
|
|
706
|
+
continue;
|
|
707
|
+
out[k] = v;
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
}
|