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