@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.
Files changed (291) 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/apply.js +1 -1
  9. package/dist/commands/auth-mint.d.ts +7 -3
  10. package/dist/commands/auth-mint.js +21 -9
  11. package/dist/commands/auth.js +2 -3
  12. package/dist/commands/browser.js +10 -10
  13. package/dist/commands/daemon.d.ts +7 -3
  14. package/dist/commands/daemon.js +40 -25
  15. package/dist/commands/doctor.js +28 -2
  16. package/dist/commands/exec.js +87 -39
  17. package/dist/commands/fleet-capture.js +21 -2
  18. package/dist/commands/harness-wizard.js +2 -2
  19. package/dist/commands/lease.js +7 -12
  20. package/dist/commands/profiles.d.ts +2 -2
  21. package/dist/commands/profiles.js +20 -13
  22. package/dist/commands/repo.js +3 -3
  23. package/dist/commands/run-account-picker.d.ts +3 -3
  24. package/dist/commands/run-account-picker.js +29 -30
  25. package/dist/commands/secrets-passthrough.d.ts +20 -0
  26. package/dist/commands/secrets-passthrough.js +43 -0
  27. package/dist/commands/sessions-trace.d.ts +11 -0
  28. package/dist/commands/sessions-trace.js +71 -0
  29. package/dist/commands/setup-accounts.d.ts +3 -2
  30. package/dist/commands/setup-accounts.js +4 -4
  31. package/dist/commands/setup-secrets.d.ts +16 -21
  32. package/dist/commands/setup-secrets.js +43 -224
  33. package/dist/commands/ssh.js +2 -2
  34. package/dist/commands/sync.js +1 -1
  35. package/dist/commands/update.d.ts +10 -0
  36. package/dist/commands/update.js +43 -40
  37. package/dist/commands/versions.d.ts +11 -11
  38. package/dist/commands/versions.js +131 -83
  39. package/dist/commands/view.d.ts +1 -9
  40. package/dist/commands/view.js +41 -55
  41. package/dist/commands/webhook.js +5 -5
  42. package/dist/commands/workflows.js +2 -1
  43. package/dist/index.d.ts +12 -12
  44. package/dist/index.js +13 -42
  45. package/dist/lib/account-capabilities.d.ts +1 -1
  46. package/dist/lib/account-capabilities.js +1 -1
  47. package/dist/lib/account-catalog.d.ts +121 -6
  48. package/dist/lib/account-catalog.js +421 -7
  49. package/dist/lib/account-registry.d.ts +8 -30
  50. package/dist/lib/account-registry.js +36 -79
  51. package/dist/lib/account-schema.d.ts +1 -1
  52. package/dist/lib/account-schema.js +1 -1
  53. package/dist/lib/accounting/account-pool-collect.js +16 -4
  54. package/dist/lib/accounting/rotate.d.ts +20 -7
  55. package/dist/lib/accounting/rotate.js +89 -13
  56. package/dist/lib/accounting/usage.js +11 -16
  57. package/dist/lib/accounts/add.d.ts +138 -0
  58. package/dist/lib/accounts/add.js +651 -0
  59. package/dist/lib/accounts/migrate.d.ts +117 -0
  60. package/dist/lib/accounts/migrate.js +536 -0
  61. package/dist/lib/accounts/slots.d.ts +13 -0
  62. package/dist/lib/accounts/slots.js +100 -0
  63. package/dist/lib/agent-spec/agents.d.ts +16 -0
  64. package/dist/lib/agent-spec/agents.js +32 -4
  65. package/dist/lib/app-bundle-install.js +5 -4
  66. package/dist/lib/auth-health.d.ts +3 -0
  67. package/dist/lib/auth-health.js +11 -0
  68. package/dist/lib/auth-mint.d.ts +52 -16
  69. package/dist/lib/auth-mint.js +117 -39
  70. package/dist/lib/browser/chrome.d.ts +1 -1
  71. package/dist/lib/browser/chrome.js +5 -5
  72. package/dist/lib/byok-usage.js +3 -3
  73. package/dist/lib/claude-account-token.d.ts +47 -3
  74. package/dist/lib/claude-account-token.js +172 -50
  75. package/dist/lib/cloud/antigravity.js +4 -4
  76. package/dist/lib/cloud/cursor.js +4 -4
  77. package/dist/lib/crabbox/cli.d.ts +1 -1
  78. package/dist/lib/crabbox/cli.js +6 -6
  79. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  80. package/dist/lib/crabbox/runtimes.js +5 -5
  81. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  82. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  83. package/dist/lib/daemon/auth-sync-service.js +28 -1
  84. package/dist/lib/daemon/daemon.d.ts +6 -16
  85. package/dist/lib/daemon/daemon.js +52 -97
  86. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  87. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  88. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  89. package/dist/lib/daemon/harness-update-service.js +1 -1
  90. package/dist/lib/daemon/runner.d.ts +15 -3
  91. package/dist/lib/daemon/runner.js +49 -24
  92. package/dist/lib/daemon-health.d.ts +1 -2
  93. package/dist/lib/daemon-health.js +7 -6
  94. package/dist/lib/daemon-services.d.ts +1 -1
  95. package/dist/lib/daemon-services.js +1 -11
  96. package/dist/lib/daemon-ticks.js +23 -2
  97. package/dist/lib/daemon-webhooks.d.ts +8 -7
  98. package/dist/lib/daemon-webhooks.js +10 -9
  99. package/dist/lib/device-config.js +1 -1
  100. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  101. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  102. package/dist/lib/devices/harness-inventory.js +126 -4
  103. package/dist/lib/doctor-diff.js +2 -1
  104. package/dist/lib/exec-account-home.d.ts +38 -0
  105. package/dist/lib/exec-account-home.js +164 -0
  106. package/dist/lib/exec.d.ts +30 -0
  107. package/dist/lib/exec.js +101 -31
  108. package/dist/lib/feed/activity-stream.d.ts +60 -0
  109. package/dist/lib/feed/activity-stream.js +271 -0
  110. package/dist/lib/feed/activity.d.ts +7 -0
  111. package/dist/lib/feed/activity.js +10 -4
  112. package/dist/lib/feed/watch.d.ts +9 -3
  113. package/dist/lib/feed/watch.js +72 -36
  114. package/dist/lib/fleet/apply.d.ts +2 -2
  115. package/dist/lib/fleet/apply.js +4 -4
  116. package/dist/lib/fleet/auth-sync.js +4 -3
  117. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  118. package/dist/lib/fleet-shared-state.d.ts +6 -0
  119. package/dist/lib/harness/adapter.d.ts +7 -7
  120. package/dist/lib/harness/adapter.js +10 -6
  121. package/dist/lib/harness/adapters/grok.js +8 -3
  122. package/dist/lib/harness/adapters/muse.js +1 -1
  123. package/dist/lib/harness/adapters/opencode.js +12 -3
  124. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  125. package/dist/lib/harness-auth-capabilities.js +52 -0
  126. package/dist/lib/helper-versions.d.ts +6 -4
  127. package/dist/lib/helper-versions.js +5 -4
  128. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  129. package/dist/lib/hosts/credential-transport.js +37 -0
  130. package/dist/lib/hosts/dispatch.d.ts +7 -0
  131. package/dist/lib/hosts/dispatch.js +13 -6
  132. package/dist/lib/identity/client.d.ts +3 -3
  133. package/dist/lib/identity/client.js +3 -3
  134. package/dist/lib/installations/index.d.ts +1 -1
  135. package/dist/lib/installations/index.js +1 -1
  136. package/dist/lib/installations/migrate.js +9 -0
  137. package/dist/lib/installations/resolve.js +1 -1
  138. package/dist/lib/installations/shims.d.ts +15 -0
  139. package/dist/lib/installations/shims.js +69 -0
  140. package/dist/lib/installations/store.d.ts +43 -0
  141. package/dist/lib/installations/store.js +75 -7
  142. package/dist/lib/installations/versions.js +1 -1
  143. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  144. package/dist/lib/menubar/install-menubar.js +3 -3
  145. package/dist/lib/native-accounts.d.ts +35 -0
  146. package/dist/lib/native-accounts.js +41 -0
  147. package/dist/lib/net-close.d.ts +11 -0
  148. package/dist/lib/net-close.js +25 -0
  149. package/dist/lib/openclaw-keychain.js +27 -1
  150. package/dist/lib/profiles.js +22 -9
  151. package/dist/lib/project-resources.js +1 -1
  152. package/dist/lib/reserved-stores.d.ts +53 -0
  153. package/dist/lib/reserved-stores.js +120 -0
  154. package/dist/lib/secrets-client.d.ts +191 -0
  155. package/dist/lib/secrets-client.js +710 -0
  156. package/dist/lib/secrets-policy.d.ts +222 -0
  157. package/dist/lib/secrets-policy.js +483 -0
  158. package/dist/lib/secrets-types.d.ts +188 -0
  159. package/dist/lib/secrets-types.js +24 -0
  160. package/dist/lib/service-manifest.js +3 -4
  161. package/dist/lib/session/active.d.ts +38 -4
  162. package/dist/lib/session/active.js +36 -4
  163. package/dist/lib/session/bash-command.js +10 -0
  164. package/dist/lib/session/db.d.ts +65 -2
  165. package/dist/lib/session/db.js +178 -18
  166. package/dist/lib/session/mirror.d.ts +5 -0
  167. package/dist/lib/session/mirror.js +166 -0
  168. package/dist/lib/session/parse.d.ts +49 -0
  169. package/dist/lib/session/parse.js +324 -30
  170. package/dist/lib/session/prompt.d.ts +93 -2
  171. package/dist/lib/session/prompt.js +266 -15
  172. package/dist/lib/session/remote/peer-stream.d.ts +47 -0
  173. package/dist/lib/session/remote/peer-stream.js +142 -0
  174. package/dist/lib/session/remote/watch.d.ts +1 -1
  175. package/dist/lib/session/remote/watch.js +41 -42
  176. package/dist/lib/session/session-cache.d.ts +9 -0
  177. package/dist/lib/session/session-cache.js +40 -2
  178. package/dist/lib/session/sync/config.js +2 -2
  179. package/dist/lib/session/timeline-pass.d.ts +129 -0
  180. package/dist/lib/session/timeline-pass.js +323 -0
  181. package/dist/lib/session/timeline.d.ts +182 -0
  182. package/dist/lib/session/timeline.js +636 -0
  183. package/dist/lib/session/types.d.ts +155 -1
  184. package/dist/lib/sha256-asset.d.ts +11 -16
  185. package/dist/lib/sha256-asset.js +11 -16
  186. package/dist/lib/share/config.js +14 -5
  187. package/dist/lib/signin-badge.d.ts +26 -0
  188. package/dist/lib/signin-badge.js +42 -0
  189. package/dist/lib/staleness/detectors/workflows.js +3 -105
  190. package/dist/lib/staleness/writers/workflows.js +2 -1
  191. package/dist/lib/state.d.ts +7 -4
  192. package/dist/lib/state.js +18 -7
  193. package/dist/lib/summarizer/pass.d.ts +2 -0
  194. package/dist/lib/summarizer/pass.js +14 -1
  195. package/dist/lib/summarizer/summarize.d.ts +7 -0
  196. package/dist/lib/summarizer/summarize.js +3 -0
  197. package/dist/lib/sync-umbrella.js +2 -2
  198. package/dist/lib/teams/agents.js +1 -1
  199. package/dist/lib/types.d.ts +74 -33
  200. package/dist/lib/view-types.d.ts +6 -3
  201. package/dist/lib/workflows-registry.d.ts +71 -0
  202. package/dist/lib/workflows-registry.js +280 -0
  203. package/dist/lib/workflows.d.ts +21 -21
  204. package/dist/lib/workflows.js +12 -327
  205. package/package.json +2 -3
  206. package/scripts/postinstall.js +4 -29
  207. package/dist/commands/secrets-import.d.ts +0 -18
  208. package/dist/commands/secrets-import.js +0 -74
  209. package/dist/commands/secrets-migrate.d.ts +0 -25
  210. package/dist/commands/secrets-migrate.js +0 -334
  211. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  212. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  213. package/dist/commands/secrets-sync.d.ts +0 -11
  214. package/dist/commands/secrets-sync.js +0 -153
  215. package/dist/commands/secrets-vault.d.ts +0 -10
  216. package/dist/commands/secrets-vault.js +0 -130
  217. package/dist/commands/secrets.d.ts +0 -212
  218. package/dist/commands/secrets.js +0 -3030
  219. package/dist/lib/accounts/connect.d.ts +0 -170
  220. package/dist/lib/accounts/connect.js +0 -383
  221. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  222. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  223. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  224. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  225. package/dist/lib/secrets/agent.d.ts +0 -358
  226. package/dist/lib/secrets/agent.js +0 -1291
  227. package/dist/lib/secrets/audit.d.ts +0 -46
  228. package/dist/lib/secrets/audit.js +0 -101
  229. package/dist/lib/secrets/bundles.d.ts +0 -290
  230. package/dist/lib/secrets/bundles.js +0 -1547
  231. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  232. package/dist/lib/secrets/download-keychain.js +0 -70
  233. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  234. package/dist/lib/secrets/drivers/rush.js +0 -90
  235. package/dist/lib/secrets/fallback.d.ts +0 -48
  236. package/dist/lib/secrets/fallback.js +0 -48
  237. package/dist/lib/secrets/filestore.d.ts +0 -222
  238. package/dist/lib/secrets/filestore.js +0 -1099
  239. package/dist/lib/secrets/headless.d.ts +0 -43
  240. package/dist/lib/secrets/headless.js +0 -56
  241. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  242. package/dist/lib/secrets/icloud-import.js +0 -206
  243. package/dist/lib/secrets/index.d.ts +0 -451
  244. package/dist/lib/secrets/index.js +0 -1568
  245. package/dist/lib/secrets/install-helper.d.ts +0 -72
  246. package/dist/lib/secrets/install-helper.js +0 -245
  247. package/dist/lib/secrets/lease.d.ts +0 -25
  248. package/dist/lib/secrets/lease.js +0 -44
  249. package/dist/lib/secrets/linux.d.ts +0 -77
  250. package/dist/lib/secrets/linux.js +0 -393
  251. package/dist/lib/secrets/list-filter.d.ts +0 -109
  252. package/dist/lib/secrets/list-filter.js +0 -261
  253. package/dist/lib/secrets/mcp.d.ts +0 -93
  254. package/dist/lib/secrets/mcp.js +0 -211
  255. package/dist/lib/secrets/profiles.d.ts +0 -10
  256. package/dist/lib/secrets/profiles.js +0 -13
  257. package/dist/lib/secrets/push.d.ts +0 -133
  258. package/dist/lib/secrets/push.js +0 -273
  259. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  260. package/dist/lib/secrets/rc-hygiene.js +0 -143
  261. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  262. package/dist/lib/secrets/read-backoff.js +0 -64
  263. package/dist/lib/secrets/reaper.d.ts +0 -90
  264. package/dist/lib/secrets/reaper.js +0 -243
  265. package/dist/lib/secrets/remote.d.ts +0 -152
  266. package/dist/lib/secrets/remote.js +0 -344
  267. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  268. package/dist/lib/secrets/reserved-sync.js +0 -147
  269. package/dist/lib/secrets/scope.d.ts +0 -26
  270. package/dist/lib/secrets/scope.js +0 -29
  271. package/dist/lib/secrets/session-store.d.ts +0 -107
  272. package/dist/lib/secrets/session-store.js +0 -342
  273. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  274. package/dist/lib/secrets/sync-backend.js +0 -13
  275. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  276. package/dist/lib/secrets/sync-commands.js +0 -21
  277. package/dist/lib/secrets/sync.d.ts +0 -48
  278. package/dist/lib/secrets/sync.js +0 -237
  279. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  280. package/dist/lib/secrets/unlock-hints.js +0 -36
  281. package/dist/lib/secrets/usage-db.d.ts +0 -46
  282. package/dist/lib/secrets/usage-db.js +0 -96
  283. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  284. package/dist/lib/secrets/vault-age-helper.js +0 -34
  285. package/dist/lib/secrets/vault.d.ts +0 -49
  286. package/dist/lib/secrets/vault.js +0 -399
  287. package/dist/lib/secrets/windows.d.ts +0 -81
  288. package/dist/lib/secrets/windows.js +0 -556
  289. package/scripts/install-helper.js +0 -97
  290. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  291. /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
+ }