@phnx-labs/agents-cli 1.22.84 → 1.22.86

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