@phnx-labs/agents-cli 1.22.84 → 1.22.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/CHANGELOG.md +57 -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/setup-accounts.d.ts +3 -2
  28. package/dist/commands/setup-accounts.js +4 -4
  29. package/dist/commands/setup-secrets.d.ts +16 -21
  30. package/dist/commands/setup-secrets.js +43 -224
  31. package/dist/commands/ssh.js +2 -2
  32. package/dist/commands/sync.js +1 -1
  33. package/dist/commands/update.d.ts +10 -0
  34. package/dist/commands/update.js +43 -40
  35. package/dist/commands/versions.d.ts +11 -11
  36. package/dist/commands/versions.js +131 -83
  37. package/dist/commands/view.d.ts +1 -9
  38. package/dist/commands/view.js +41 -55
  39. package/dist/commands/webhook.js +5 -5
  40. package/dist/commands/workflows.js +2 -1
  41. package/dist/index.d.ts +12 -12
  42. package/dist/index.js +13 -42
  43. package/dist/lib/account-capabilities.d.ts +1 -1
  44. package/dist/lib/account-capabilities.js +1 -1
  45. package/dist/lib/account-catalog.d.ts +121 -6
  46. package/dist/lib/account-catalog.js +421 -7
  47. package/dist/lib/account-registry.d.ts +8 -30
  48. package/dist/lib/account-registry.js +36 -79
  49. package/dist/lib/account-schema.d.ts +1 -1
  50. package/dist/lib/account-schema.js +1 -1
  51. package/dist/lib/accounting/account-pool-collect.js +16 -4
  52. package/dist/lib/accounting/rotate.d.ts +20 -7
  53. package/dist/lib/accounting/rotate.js +89 -13
  54. package/dist/lib/accounting/usage.js +11 -16
  55. package/dist/lib/accounts/add.d.ts +138 -0
  56. package/dist/lib/accounts/add.js +651 -0
  57. package/dist/lib/accounts/migrate.d.ts +117 -0
  58. package/dist/lib/accounts/migrate.js +536 -0
  59. package/dist/lib/accounts/slots.d.ts +13 -0
  60. package/dist/lib/accounts/slots.js +100 -0
  61. package/dist/lib/agent-spec/agents.d.ts +16 -0
  62. package/dist/lib/agent-spec/agents.js +32 -4
  63. package/dist/lib/app-bundle-install.js +5 -4
  64. package/dist/lib/auth-health.d.ts +3 -0
  65. package/dist/lib/auth-health.js +11 -0
  66. package/dist/lib/auth-mint.d.ts +52 -16
  67. package/dist/lib/auth-mint.js +117 -39
  68. package/dist/lib/browser/chrome.d.ts +1 -1
  69. package/dist/lib/browser/chrome.js +5 -5
  70. package/dist/lib/byok-usage.js +3 -3
  71. package/dist/lib/claude-account-token.d.ts +47 -3
  72. package/dist/lib/claude-account-token.js +172 -50
  73. package/dist/lib/cloud/antigravity.js +4 -4
  74. package/dist/lib/cloud/cursor.js +4 -4
  75. package/dist/lib/crabbox/cli.d.ts +1 -1
  76. package/dist/lib/crabbox/cli.js +6 -6
  77. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  78. package/dist/lib/crabbox/runtimes.js +5 -5
  79. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  80. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  81. package/dist/lib/daemon/auth-sync-service.js +28 -1
  82. package/dist/lib/daemon/daemon.d.ts +6 -16
  83. package/dist/lib/daemon/daemon.js +52 -97
  84. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  85. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  86. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  87. package/dist/lib/daemon/harness-update-service.js +1 -1
  88. package/dist/lib/daemon/runner.d.ts +15 -3
  89. package/dist/lib/daemon/runner.js +49 -24
  90. package/dist/lib/daemon-health.d.ts +1 -2
  91. package/dist/lib/daemon-health.js +7 -6
  92. package/dist/lib/daemon-services.d.ts +1 -1
  93. package/dist/lib/daemon-services.js +1 -11
  94. package/dist/lib/daemon-webhooks.d.ts +8 -7
  95. package/dist/lib/daemon-webhooks.js +10 -9
  96. package/dist/lib/device-config.js +1 -1
  97. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  98. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  99. package/dist/lib/devices/harness-inventory.js +126 -4
  100. package/dist/lib/doctor-diff.js +2 -1
  101. package/dist/lib/exec-account-home.d.ts +38 -0
  102. package/dist/lib/exec-account-home.js +164 -0
  103. package/dist/lib/exec.d.ts +30 -0
  104. package/dist/lib/exec.js +101 -31
  105. package/dist/lib/fleet/apply.d.ts +2 -2
  106. package/dist/lib/fleet/apply.js +4 -4
  107. package/dist/lib/fleet/auth-sync.js +4 -3
  108. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  109. package/dist/lib/harness/adapter.d.ts +7 -7
  110. package/dist/lib/harness/adapter.js +10 -6
  111. package/dist/lib/harness/adapters/grok.js +8 -3
  112. package/dist/lib/harness/adapters/muse.js +1 -1
  113. package/dist/lib/harness/adapters/opencode.js +12 -3
  114. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  115. package/dist/lib/harness-auth-capabilities.js +52 -0
  116. package/dist/lib/helper-versions.d.ts +6 -4
  117. package/dist/lib/helper-versions.js +5 -4
  118. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  119. package/dist/lib/hosts/credential-transport.js +37 -0
  120. package/dist/lib/hosts/dispatch.d.ts +7 -0
  121. package/dist/lib/hosts/dispatch.js +13 -6
  122. package/dist/lib/identity/client.d.ts +3 -3
  123. package/dist/lib/identity/client.js +3 -3
  124. package/dist/lib/installations/index.d.ts +1 -1
  125. package/dist/lib/installations/index.js +1 -1
  126. package/dist/lib/installations/migrate.js +9 -0
  127. package/dist/lib/installations/resolve.js +1 -1
  128. package/dist/lib/installations/shims.d.ts +15 -0
  129. package/dist/lib/installations/shims.js +69 -0
  130. package/dist/lib/installations/store.d.ts +43 -0
  131. package/dist/lib/installations/store.js +75 -7
  132. package/dist/lib/installations/versions.js +1 -1
  133. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  134. package/dist/lib/menubar/install-menubar.js +3 -3
  135. package/dist/lib/native-accounts.d.ts +35 -0
  136. package/dist/lib/native-accounts.js +41 -0
  137. package/dist/lib/net-close.d.ts +11 -0
  138. package/dist/lib/net-close.js +25 -0
  139. package/dist/lib/openclaw-keychain.js +27 -1
  140. package/dist/lib/profiles.js +22 -9
  141. package/dist/lib/project-resources.js +1 -1
  142. package/dist/lib/reserved-stores.d.ts +53 -0
  143. package/dist/lib/reserved-stores.js +120 -0
  144. package/dist/lib/secrets-client.d.ts +191 -0
  145. package/dist/lib/secrets-client.js +710 -0
  146. package/dist/lib/secrets-policy.d.ts +222 -0
  147. package/dist/lib/secrets-policy.js +483 -0
  148. package/dist/lib/secrets-types.d.ts +188 -0
  149. package/dist/lib/secrets-types.js +24 -0
  150. package/dist/lib/service-manifest.js +3 -4
  151. package/dist/lib/session/db.d.ts +18 -0
  152. package/dist/lib/session/db.js +47 -14
  153. package/dist/lib/session/sync/config.js +2 -2
  154. package/dist/lib/sha256-asset.d.ts +11 -16
  155. package/dist/lib/sha256-asset.js +11 -16
  156. package/dist/lib/share/config.js +14 -5
  157. package/dist/lib/signin-badge.d.ts +26 -0
  158. package/dist/lib/signin-badge.js +42 -0
  159. package/dist/lib/staleness/detectors/workflows.js +3 -105
  160. package/dist/lib/staleness/writers/workflows.js +2 -1
  161. package/dist/lib/state.d.ts +7 -4
  162. package/dist/lib/state.js +18 -7
  163. package/dist/lib/sync-umbrella.js +2 -2
  164. package/dist/lib/teams/agents.js +1 -1
  165. package/dist/lib/types.d.ts +74 -33
  166. package/dist/lib/view-types.d.ts +6 -3
  167. package/dist/lib/workflows-registry.d.ts +71 -0
  168. package/dist/lib/workflows-registry.js +280 -0
  169. package/dist/lib/workflows.d.ts +21 -21
  170. package/dist/lib/workflows.js +12 -327
  171. package/package.json +2 -3
  172. package/scripts/postinstall.js +4 -29
  173. package/dist/commands/secrets-import.d.ts +0 -18
  174. package/dist/commands/secrets-import.js +0 -74
  175. package/dist/commands/secrets-migrate.d.ts +0 -25
  176. package/dist/commands/secrets-migrate.js +0 -334
  177. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  178. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  179. package/dist/commands/secrets-sync.d.ts +0 -11
  180. package/dist/commands/secrets-sync.js +0 -153
  181. package/dist/commands/secrets-vault.d.ts +0 -10
  182. package/dist/commands/secrets-vault.js +0 -130
  183. package/dist/commands/secrets.d.ts +0 -212
  184. package/dist/commands/secrets.js +0 -3030
  185. package/dist/lib/accounts/connect.d.ts +0 -176
  186. package/dist/lib/accounts/connect.js +0 -453
  187. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  188. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  189. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  190. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  191. package/dist/lib/secrets/agent.d.ts +0 -358
  192. package/dist/lib/secrets/agent.js +0 -1291
  193. package/dist/lib/secrets/audit.d.ts +0 -46
  194. package/dist/lib/secrets/audit.js +0 -101
  195. package/dist/lib/secrets/bundles.d.ts +0 -290
  196. package/dist/lib/secrets/bundles.js +0 -1547
  197. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  198. package/dist/lib/secrets/download-keychain.js +0 -70
  199. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  200. package/dist/lib/secrets/drivers/rush.js +0 -90
  201. package/dist/lib/secrets/fallback.d.ts +0 -48
  202. package/dist/lib/secrets/fallback.js +0 -48
  203. package/dist/lib/secrets/filestore.d.ts +0 -222
  204. package/dist/lib/secrets/filestore.js +0 -1099
  205. package/dist/lib/secrets/headless.d.ts +0 -43
  206. package/dist/lib/secrets/headless.js +0 -56
  207. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  208. package/dist/lib/secrets/icloud-import.js +0 -206
  209. package/dist/lib/secrets/index.d.ts +0 -451
  210. package/dist/lib/secrets/index.js +0 -1568
  211. package/dist/lib/secrets/install-helper.d.ts +0 -72
  212. package/dist/lib/secrets/install-helper.js +0 -245
  213. package/dist/lib/secrets/lease.d.ts +0 -25
  214. package/dist/lib/secrets/lease.js +0 -44
  215. package/dist/lib/secrets/linux.d.ts +0 -77
  216. package/dist/lib/secrets/linux.js +0 -393
  217. package/dist/lib/secrets/list-filter.d.ts +0 -109
  218. package/dist/lib/secrets/list-filter.js +0 -261
  219. package/dist/lib/secrets/mcp.d.ts +0 -93
  220. package/dist/lib/secrets/mcp.js +0 -211
  221. package/dist/lib/secrets/profiles.d.ts +0 -10
  222. package/dist/lib/secrets/profiles.js +0 -13
  223. package/dist/lib/secrets/push.d.ts +0 -133
  224. package/dist/lib/secrets/push.js +0 -273
  225. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  226. package/dist/lib/secrets/rc-hygiene.js +0 -143
  227. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  228. package/dist/lib/secrets/read-backoff.js +0 -64
  229. package/dist/lib/secrets/reaper.d.ts +0 -90
  230. package/dist/lib/secrets/reaper.js +0 -243
  231. package/dist/lib/secrets/remote.d.ts +0 -152
  232. package/dist/lib/secrets/remote.js +0 -344
  233. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  234. package/dist/lib/secrets/reserved-sync.js +0 -147
  235. package/dist/lib/secrets/scope.d.ts +0 -26
  236. package/dist/lib/secrets/scope.js +0 -29
  237. package/dist/lib/secrets/session-store.d.ts +0 -107
  238. package/dist/lib/secrets/session-store.js +0 -342
  239. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  240. package/dist/lib/secrets/sync-backend.js +0 -13
  241. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  242. package/dist/lib/secrets/sync-commands.js +0 -21
  243. package/dist/lib/secrets/sync.d.ts +0 -48
  244. package/dist/lib/secrets/sync.js +0 -237
  245. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  246. package/dist/lib/secrets/unlock-hints.js +0 -36
  247. package/dist/lib/secrets/usage-db.d.ts +0 -46
  248. package/dist/lib/secrets/usage-db.js +0 -96
  249. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  250. package/dist/lib/secrets/vault-age-helper.js +0 -34
  251. package/dist/lib/secrets/vault.d.ts +0 -49
  252. package/dist/lib/secrets/vault.js +0 -399
  253. package/dist/lib/secrets/windows.d.ts +0 -81
  254. package/dist/lib/secrets/windows.js +0 -556
  255. package/scripts/install-helper.js +0 -97
  256. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  257. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Agents-owned fleet policy for the secrets engine boundary (PHNX-3989 CTX-1,
3
+ * delta-spec OWN-1). The standalone `secrets` engine has no concept of a
4
+ * "reserved" per-harness credential store, resource profiles, device roles, or
5
+ * fleet election — none of that is portable secret-storage behavior, so it
6
+ * lives here rather than in the engine. Every actual read/write against a
7
+ * bundle or raw item resolves through the process client (`secrets-client.ts`);
8
+ * this module only decides WHICH bundle/key, WHO may reach it, and WHERE it
9
+ * gets pushed.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { pushBundleToHostAsync, listBundles, } from './secrets-client.js';
14
+ import { filterNamesForActiveResourceProfile, getActiveResourceProfile } from './resource-profiles.js';
15
+ import { isDialableDevice, loadDevicesSync } from './devices/registry.js';
16
+ import { sshTargetFor } from './devices/connect.js';
17
+ import { isHostPinned, isDevicePinned, managedKnownHostsPath } from './devices/known-hosts.js';
18
+ import { machineId, normalizeHost } from './session/sync/config.js';
19
+ import { readFleetSharedDeviceStates, updateFleetSharedDeviceStateAsync, } from './fleet-shared-state.js';
20
+ import { getCacheDir, getUserAgentsDir, readMeta } from './state.js';
21
+ import { listNativeAccounts, readSlots } from './account-registry.js';
22
+ import { claudeAccountTokenKey, provisionWorkerSlot, readReservedCredential } from './claude-account-token.js';
23
+ import { configuredDeviceRole, isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
24
+ import { AUTH_STORE_ALIAS, AUTH_BUNDLE_NAME, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, } from './reserved-stores.js';
25
+ // --- reserved per-harness credential stores (PHNX-3940 / PHNX-3989) --------
26
+ //
27
+ // Re-exported from the leaf module `reserved-stores.ts` rather than declared
28
+ // here: `claude-account-token.ts` needs these constants AND is itself a
29
+ // dependency of this module's fleet-sync helpers below, so declaring them
30
+ // here would form a real circular import (reproduced as a TDZ
31
+ // ReferenceError under real ESM evaluation order, outside vitest's
32
+ // mock-tolerant transform). Keep the public surface unchanged for existing
33
+ // importers of `secrets-policy.js`.
34
+ export { AUTH_STORE_ALIAS, AUTH_BUNDLE_NAME, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, };
35
+ // --- resource-profile -> allowedBundles (CTX-1) -----------------------------
36
+ //
37
+ // A resource profile scopes WHICH bundle names a run may reach. The standalone
38
+ // has no concept of a profile; agents-cli computes the allowed set from the
39
+ // full bundle listing and forwards it as `SecretsContext.allowedBundles` on
40
+ // every bounded request. Absent (no active profile) means full trust, matching
41
+ // the client's own documented default.
42
+ /** Filter `names` down to the ones the active resource profile allows for `secrets`. */
43
+ export function filterBundleNamesForActiveProfile(names) {
44
+ return filterNamesForActiveResourceProfile('secrets', names);
45
+ }
46
+ /**
47
+ * `SecretsContext.allowedBundles` for the active resource profile, or
48
+ * `undefined` when no profile is active (full trust — the client's default).
49
+ * `allNames` is the caller's already-fetched full bundle listing (e.g. from
50
+ * `listBundlesSync()`), so this stays a pure filter with no extra round trip.
51
+ */
52
+ export function resolveAllowedBundlesForActiveProfile(allNames) {
53
+ const filtered = filterBundleNamesForActiveProfile(allNames);
54
+ return filtered.length === allNames.length ? undefined : filtered;
55
+ }
56
+ /**
57
+ * The `SecretsContext` a run should pass to every bundle resolution it makes:
58
+ * the run's harness as the opaque `scope`, plus `allowedBundles` computed from
59
+ * a fresh full bundle listing when a resource profile is active (a stale
60
+ * listing could exclude a bundle created after the profile was set). No
61
+ * profile active means full trust and no extra round trip.
62
+ */
63
+ export async function resolveSecretsContextForRun(scope) {
64
+ if (!getActiveResourceProfile())
65
+ return scope ? { scope } : undefined;
66
+ const allNames = (await listBundles()).map((b) => b.name);
67
+ return { allowedBundles: resolveAllowedBundlesForActiveProfile(allNames), scope };
68
+ }
69
+ // --- bundle@host remote-reference parsing (agents' own `--secrets` flag
70
+ // semantics, REMOTE-1) -------------------------------------------------------
71
+ /** Parse a `--secrets` flag value into its bundle name and optional `@host`. */
72
+ export function splitBundleRef(ref) {
73
+ const at = ref.indexOf('@');
74
+ if (at === -1)
75
+ return { bundle: ref };
76
+ const bundle = ref.slice(0, at);
77
+ const host = ref.slice(at + 1);
78
+ if (!bundle || !host) {
79
+ throw new Error(`Invalid remote bundle reference ${JSON.stringify(ref)}. Expected 'bundle@host'.`);
80
+ }
81
+ return { bundle, host };
82
+ }
83
+ /** A remote (bundle@host) reference does not yet support key-subset or expiry overrides. */
84
+ export function assertRemoteBundleFlagsUnsupported(bundleName, host, opts, flagLabels) {
85
+ const hasKeys = Array.isArray(opts.keys) && opts.keys.length > 0;
86
+ if (!hasKeys && !opts.allowExpired)
87
+ return;
88
+ throw new Error(`Bundle '${bundleName}@${host}': ${flagLabels.keysFlag} and ${flagLabels.allowExpiredFlag} are not supported for remote (bundle@host) bundles yet. ` +
89
+ `Drop the flag or resolve the bundle locally.`);
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Fleet sync of the reserved file-backed `auth` bundle (PHNX-2371/PHNX-3609).
93
+ //
94
+ // Every daemon publishes only a safe auth readiness verdict into its owned
95
+ // fleet-shared state file. A deterministic ready publisher reads those
96
+ // verdicts and transfers the actual bundle only to a peer reporting `missing`.
97
+ // Secret values never enter Git; the exceptional provisioning transfer is SSH,
98
+ // async and kill-bounded so it never blocks the daemon event loop.
99
+ // ---------------------------------------------------------------------------
100
+ /** Each import/read-back SSH operation gets this deadline plus the SSH hard-kill grace. */
101
+ export const AUTH_SYNC_PUSH_DEADLINE_MS = 20_000;
102
+ export function planAuthBundlePush(localAuthOk, localIsPublisher, devices) {
103
+ if (!localAuthOk) {
104
+ return devices.map((device) => ({ action: 'skip', device: device.name, reason: 'no local file-backed auth bundle' }));
105
+ }
106
+ if (!localIsPublisher) {
107
+ return devices.map((device) => ({ action: 'skip', device: device.name, reason: 'another ready device is the elected auth publisher' }));
108
+ }
109
+ return devices.map((device) => {
110
+ if (!device.reachable)
111
+ return { action: 'skip', device: device.name, reason: 'unreachable' };
112
+ if (!device.pinned) {
113
+ return { action: 'skip', device: device.name, reason: `host key not pinned; run \`agents ssh ${device.name}\` once` };
114
+ }
115
+ if (device.remoteAuth === 'ready')
116
+ return { action: 'skip', device: device.name, reason: 'already present' };
117
+ if (device.remoteAuth === 'invalid')
118
+ return { action: 'skip', device: device.name, reason: 'remote auth bundle uses the wrong backend' };
119
+ if (device.remoteAuth === 'unknown') {
120
+ return { action: 'skip', device: device.name, reason: 'no shared auth verdict has arrived from this peer' };
121
+ }
122
+ return { action: 'push', device: device.name };
123
+ });
124
+ }
125
+ function defaultDevices() {
126
+ return Object.values(loadDevicesSync());
127
+ }
128
+ function authStatus(local) {
129
+ if (!local.exists)
130
+ return 'missing';
131
+ return local.ok ? 'ready' : 'invalid';
132
+ }
133
+ /** Publish only safe readiness metadata; useful immediately before a repo push. */
134
+ export async function publishReservedAuthVerdict(options = {}) {
135
+ const local = (options.inspectLocal ?? inspectReservedAuthBundle)();
136
+ const status = authStatus(local);
137
+ const device = options.localName ?? machineId();
138
+ try {
139
+ const write = await updateFleetSharedDeviceStateAsync(device, { auth: { status } }, options.userAgentsDir ?? getUserAgentsDir());
140
+ return { device, status, changed: write.changed, error: null };
141
+ }
142
+ catch (err) {
143
+ return { device, status, changed: false, error: err.message };
144
+ }
145
+ }
146
+ /**
147
+ * Publish local readiness, elect one ready source, then asynchronously provision
148
+ * only peers whose shared verdict says the bundle is missing.
149
+ */
150
+ export async function syncReservedAuthBundle(deps = {}) {
151
+ const result = { publisher: null, stateChanged: false, pushed: [], skipped: [], errors: [] };
152
+ const published = await publishReservedAuthVerdict(deps);
153
+ const localStatus = published.status;
154
+ const localName = published.device;
155
+ const localNorm = normalizeHost(localName);
156
+ const root = deps.userAgentsDir ?? getUserAgentsDir();
157
+ const devices = (deps.listDevices ?? defaultDevices)().filter((device) => normalizeHost(device.name) !== localNorm);
158
+ result.stateChanged = published.changed;
159
+ if (published.error)
160
+ result.errors.push({ device: localName, message: `could not publish auth verdict: ${published.error}` });
161
+ const read = readFleetSharedDeviceStates(root);
162
+ result.errors.push(...read.errors);
163
+ const stateByDevice = new Map(read.states.map((state) => [normalizeHost(state.device), state]));
164
+ const registered = new Map(devices.map((device) => [normalizeHost(device.name), device]));
165
+ const readyPublishers = read.states
166
+ .filter((state) => {
167
+ if (state.auth?.status !== 'ready')
168
+ return false;
169
+ if (normalizeHost(state.device) === localNorm)
170
+ return localStatus === 'ready';
171
+ const device = registered.get(normalizeHost(state.device));
172
+ return !!device && isDialableDevice(device);
173
+ })
174
+ .map((state) => state.device);
175
+ if (localStatus === 'ready' && !readyPublishers.some((name) => normalizeHost(name) === localNorm)) {
176
+ readyPublishers.push(localName);
177
+ }
178
+ readyPublishers.sort((a, b) => normalizeHost(a).localeCompare(normalizeHost(b)));
179
+ result.publisher = readyPublishers[0] ?? null;
180
+ const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
181
+ const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
182
+ const plan = planAuthBundlePush(localStatus === 'ready', localIsPublisher, devices.map((device) => ({
183
+ name: device.name,
184
+ reachable: isDialableDevice(device),
185
+ pinned: isDevicePinned(device, pinned),
186
+ remoteAuth: stateByDevice.get(normalizeHost(device.name))?.auth?.status ?? 'unknown',
187
+ })));
188
+ const byName = new Map(devices.map((device) => [device.name, device]));
189
+ const push = deps.push ?? ((bundle, host) => pushBundleToHostAsync(bundle, host, {
190
+ remoteBackend: 'file',
191
+ operation: 'auth-sync',
192
+ agentOnly: true,
193
+ timeoutMs: AUTH_SYNC_PUSH_DEADLINE_MS,
194
+ }));
195
+ const targetOf = deps.sshTarget ?? sshTargetFor;
196
+ const outcomes = await Promise.all(plan.map(async (item) => {
197
+ if (item.action === 'skip')
198
+ return { kind: 'skip', device: item.device, message: item.reason };
199
+ const profile = byName.get(item.device);
200
+ if (!profile)
201
+ return { kind: 'skip', device: item.device, message: 'not in registry' };
202
+ try {
203
+ const out = await push(AUTH_STORE_ALIAS, targetOf(profile));
204
+ return out.ok
205
+ ? { kind: 'pushed', device: item.device, message: out.message }
206
+ : { kind: 'error', device: item.device, message: out.message };
207
+ }
208
+ catch (err) {
209
+ return { kind: 'error', device: item.device, message: err.message };
210
+ }
211
+ }));
212
+ for (const outcome of outcomes) {
213
+ if (outcome.kind === 'pushed')
214
+ result.pushed.push(outcome.device);
215
+ else if (outcome.kind === 'skip')
216
+ result.skipped.push({ device: outcome.device, reason: outcome.message });
217
+ else
218
+ result.errors.push({ device: outcome.device, message: outcome.message });
219
+ }
220
+ return result;
221
+ }
222
+ // ---------------------------------------------------------------------------
223
+ // Reserved-store sync, generalized to every portable account (PHNX-3940 T6).
224
+ //
225
+ // The path above pushes ONE bundle (`auth`) to peers reporting a bundle-coarse
226
+ // `missing` verdict. That under-pushes: a peer already holding `auth` (verdict
227
+ // `ready`) never receives a newly-added account's key, so a new account never
228
+ // propagates. It also pushes to any peer regardless of role, and a headed peer
229
+ // must never receive a durable key (owner invariant 7).
230
+ //
231
+ // The functions below fix both. The plan is per ACCOUNT and per KEY
232
+ // (`<ENV>_<accountId>`), not per bundle, and targets `role=worker` peers only.
233
+ //
234
+ // INVARIANT 1 (transport, retain nothing): materializing a worker slot happens
235
+ // on the box where the key LANDED (`reconcileLocalWorkerSlots` ->
236
+ // `provisionWorkerSlot`); the credential itself only ever moves over the
237
+ // existing encrypted SSH bundle push (the client's `pushBundleToHostAsync`),
238
+ // which writes nothing on the sender beyond its existing store. A native
239
+ // OAuth/session file is never transported (`fleet/auth-sync.ts`
240
+ // `isCredentialSafeToPropagate` stays false).
241
+ // ---------------------------------------------------------------------------
242
+ const EMPTY_KEY_SET = new Set();
243
+ /**
244
+ * Pure plan: for each worker peer, push each bundle it is missing at least one
245
+ * key of. Deterministic (peers and bundles sorted) so it pins exactly in tests.
246
+ * A headed peer is skipped BEFORE any key comparison -- it never receives a key.
247
+ */
248
+ export function planReservedStoreSync(accounts, peers) {
249
+ const keysByBundle = new Map();
250
+ for (const account of accounts) {
251
+ let keys = keysByBundle.get(account.bundle);
252
+ if (!keys)
253
+ keysByBundle.set(account.bundle, (keys = new Set()));
254
+ keys.add(account.key);
255
+ }
256
+ const bundles = [...keysByBundle.keys()].sort();
257
+ const items = [];
258
+ for (const peer of [...peers].sort((a, b) => a.name.localeCompare(b.name))) {
259
+ if (peer.headed) {
260
+ items.push({ action: 'skip', device: peer.name, reason: 'headed device receives the account row, never a durable key' });
261
+ continue;
262
+ }
263
+ if (!peer.reachable) {
264
+ items.push({ action: 'skip', device: peer.name, reason: 'unreachable' });
265
+ continue;
266
+ }
267
+ if (!peer.pinned) {
268
+ items.push({ action: 'skip', device: peer.name, reason: `host key not pinned; run \`agents ssh ${peer.name}\` once` });
269
+ continue;
270
+ }
271
+ let missingAny = false;
272
+ for (const bundle of bundles) {
273
+ const wanted = keysByBundle.get(bundle);
274
+ const present = peer.presentKeys[bundle] ?? EMPTY_KEY_SET;
275
+ const missing = [...wanted].filter((key) => !present.has(key)).sort();
276
+ if (missing.length > 0) {
277
+ items.push({ action: 'push', device: peer.name, bundle, keys: missing });
278
+ missingAny = true;
279
+ }
280
+ }
281
+ if (!missingAny)
282
+ items.push({ action: 'skip', device: peer.name, reason: 'all reserved credentials present' });
283
+ }
284
+ return items;
285
+ }
286
+ /**
287
+ * Every portable account's durable worker credential, resolved to (bundle, key).
288
+ * A T1 row carries `workerCredential`; a claude row predating T1 has none, so it
289
+ * falls back to the legacy `auth` bundle keyed by the account email -- the
290
+ * legacy fallback for claude rows that predate T1. An account with no derivable
291
+ * durable credential (per-device harness, or a non-claude row with no
292
+ * `workerCredential`) is not a sync target.
293
+ */
294
+ export function reservedSyncTargets(meta) {
295
+ const out = [];
296
+ for (const account of listNativeAccounts(meta)) {
297
+ const cred = account.workerCredential;
298
+ if (cred) {
299
+ out.push({ accountId: account.id, harness: account.agent, bundle: cred.bundle, key: cred.key, fingerprint: cred.mintedAt });
300
+ }
301
+ else if (account.agent === 'claude' && account.identityLabel) {
302
+ out.push({ accountId: account.id, harness: 'claude', bundle: AUTH_STORE_ALIAS, key: claudeAccountTokenKey(account.identityLabel), fingerprint: 'legacy' });
303
+ }
304
+ }
305
+ return out;
306
+ }
307
+ // --- publisher-side delivery memo -----------------------------------------
308
+ // A push sends a WHOLE bundle; the receiver reports no per-key inventory today
309
+ // (that is T3's per-account verdict seam). So the publisher records what it has
310
+ // delivered to each peer, keyed by the credential's fingerprint, to answer "does
311
+ // the peer already have this key" without re-pushing an unchanged bundle every
312
+ // tick. A new account (no memo entry) or a re-mint (fingerprint change) is not a
313
+ // hit, so it propagates within one tick. The memo is LOCAL publisher bookkeeping,
314
+ // never synced.
315
+ function deliveryMemoPath(root = getCacheDir()) {
316
+ return path.join(root, 'reserved-sync-delivered.json');
317
+ }
318
+ function memoKey(peer, bundle, key) {
319
+ return `${normalizeHost(peer)} ${bundle} ${key}`;
320
+ }
321
+ export function readDeliveryMemo(root) {
322
+ try {
323
+ const parsed = JSON.parse(fs.readFileSync(deliveryMemoPath(root), 'utf-8'));
324
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
325
+ return parsed;
326
+ }
327
+ catch { /* missing/malformed → empty memo */ }
328
+ return {};
329
+ }
330
+ function writeDeliveryMemo(memo, root = getCacheDir()) {
331
+ fs.mkdirSync(root, { recursive: true });
332
+ fs.writeFileSync(deliveryMemoPath(root), `${JSON.stringify(memo, null, 2)}\n`, 'utf-8');
333
+ }
334
+ /**
335
+ * The keys a peer is KNOWN to hold, per bundle, from what we have delivered to it
336
+ * (memo, matched by fingerprint) plus the coarse legacy-`auth` verdict: a peer
337
+ * reporting `auth.status === 'ready'` is known to hold every legacy `auth` key.
338
+ * Reserved `__<harness>__` bundles have no coarse verdict, so their presence comes
339
+ * from the memo alone -- which is what makes a newly-added reserved account push.
340
+ */
341
+ export function peerPresentKeys(peer, accounts, memo, legacyAuthReady) {
342
+ const present = {};
343
+ for (const account of accounts) {
344
+ const known = memo[memoKey(peer, account.bundle, account.key)] === account.fingerprint
345
+ || (legacyAuthReady && account.bundle === AUTH_STORE_ALIAS);
346
+ if (!known)
347
+ continue;
348
+ (present[account.bundle] ??= new Set()).add(account.key);
349
+ }
350
+ return present;
351
+ }
352
+ function defaultHasLocalKey(bundle, key) {
353
+ // Reserved-store aware: readReservedCredential reads a `__<harness>__` item
354
+ // directly and a legacy/provider bundle through the normal resolver.
355
+ return readReservedCredential(bundle, key) !== null;
356
+ }
357
+ /**
358
+ * Push every portable account's reserved store to the worker peers that are
359
+ * missing it, per key and per role. Reuses the same election as
360
+ * {@link syncReservedAuthBundle} (one deterministic ready publisher pushes) so
361
+ * there is no second scheduler. The daemon runs this each tick.
362
+ */
363
+ export async function syncReservedStores(deps = {}) {
364
+ const result = { publisher: null, pushed: [], skipped: [], errors: [] };
365
+ const localName = deps.localName ?? machineId();
366
+ const localNorm = normalizeHost(localName);
367
+ const root = deps.userAgentsDir ?? getUserAgentsDir();
368
+ const meta = (deps.readMetaFn ?? readMeta)();
369
+ const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
370
+ const targets = reservedSyncTargets(meta).filter((t) => hasLocalKey(t.bundle, t.key));
371
+ const devices = (deps.listDevices ?? defaultDevices)().filter((d) => normalizeHost(d.name) !== localNorm);
372
+ const read = readFleetSharedDeviceStates(root);
373
+ result.errors.push(...read.errors);
374
+ const stateByDevice = new Map(read.states.map((s) => [normalizeHost(s.device), s]));
375
+ const registered = new Map(devices.map((d) => [normalizeHost(d.name), d]));
376
+ const localReady = deps.localReady ?? inspectReservedAuthBundle().ok;
377
+ const readyPublishers = read.states
378
+ .filter((s) => {
379
+ if (s.auth?.status !== 'ready')
380
+ return false;
381
+ if (normalizeHost(s.device) === localNorm)
382
+ return localReady;
383
+ const d = registered.get(normalizeHost(s.device));
384
+ return !!d && isDialableDevice(d);
385
+ })
386
+ .map((s) => s.device);
387
+ if (localReady && !readyPublishers.some((n) => normalizeHost(n) === localNorm))
388
+ readyPublishers.push(localName);
389
+ readyPublishers.sort((a, b) => normalizeHost(a).localeCompare(normalizeHost(b)));
390
+ result.publisher = readyPublishers[0] ?? null;
391
+ const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
392
+ if (targets.length === 0 || !localIsPublisher) {
393
+ for (const d of devices) {
394
+ result.skipped.push({ device: d.name, reason: targets.length === 0 ? 'no portable account with a durable credential' : 'another ready device is the elected publisher' });
395
+ }
396
+ return result;
397
+ }
398
+ const memo = readDeliveryMemo(deps.cacheDir);
399
+ const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
400
+ const peerRole = deps.peerRole ?? configuredDeviceRole;
401
+ const peers = devices.map((d) => ({
402
+ name: d.name,
403
+ headed: isHeadedDeviceRole(peerRole(d.name)),
404
+ reachable: isDialableDevice(d),
405
+ pinned: isDevicePinned(d, pinned),
406
+ presentKeys: peerPresentKeys(d.name, targets, memo, stateByDevice.get(normalizeHost(d.name))?.auth?.status === 'ready'),
407
+ }));
408
+ const plan = planReservedStoreSync(targets, peers);
409
+ const byName = new Map(devices.map((d) => [d.name, d]));
410
+ const push = deps.push ?? ((bundle, host) => pushBundleToHostAsync(bundle, host, {
411
+ remoteBackend: 'file', operation: 'reserved-sync', agentOnly: true, timeoutMs: AUTH_SYNC_PUSH_DEADLINE_MS,
412
+ }));
413
+ const sshTarget = deps.sshTarget ?? sshTargetFor;
414
+ for (const item of plan) {
415
+ if (item.action === 'skip') {
416
+ result.skipped.push({ device: item.device, reason: item.reason });
417
+ continue;
418
+ }
419
+ const profile = byName.get(item.device);
420
+ if (!profile) {
421
+ result.skipped.push({ device: item.device, reason: 'not in registry' });
422
+ continue;
423
+ }
424
+ try {
425
+ const out = await push(item.bundle, sshTarget(profile));
426
+ if (out.ok) {
427
+ result.pushed.push({ device: item.device, bundle: item.bundle, keys: item.keys });
428
+ for (const key of item.keys) {
429
+ const fp = targets.find((t) => t.bundle === item.bundle && t.key === key)?.fingerprint;
430
+ if (fp)
431
+ memo[memoKey(item.device, item.bundle, key)] = fp;
432
+ }
433
+ }
434
+ else {
435
+ result.errors.push({ device: item.device, message: out.message });
436
+ }
437
+ }
438
+ catch (err) {
439
+ result.errors.push({ device: item.device, message: err.message });
440
+ }
441
+ }
442
+ writeDeliveryMemo(memo, deps.cacheDir ?? getCacheDir());
443
+ return result;
444
+ }
445
+ /**
446
+ * Worker-side: after a durable key lands, materialize a slot for each portable
447
+ * account whose credential is now present locally. Runs only on a NON-headed
448
+ * (worker or unmarked) device -- a headed device provisions its slots from an
449
+ * interactive native login (`accounts add`), never from an injected durable key
450
+ * (invariant 7). Idempotent: an account already backed by a `durable` slot is
451
+ * skipped.
452
+ */
453
+ export function reconcileLocalWorkerSlots(deps = {}) {
454
+ const result = { provisioned: [], skipped: [], errors: [] };
455
+ const role = deps.selfRole ?? selfConfiguredDeviceRole();
456
+ if (isHeadedDeviceRole(role))
457
+ return result; // headed boxes provision via native login
458
+ const meta = (deps.readMetaFn ?? readMeta)();
459
+ const slots = readSlots(meta);
460
+ const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
461
+ const provision = deps.provision ?? provisionWorkerSlot;
462
+ for (const account of listNativeAccounts(meta)) {
463
+ const cred = account.workerCredential;
464
+ if (!cred)
465
+ continue; // legacy claude worker resolves its token at spawn
466
+ if (!hasLocalKey(cred.bundle, cred.key)) {
467
+ result.skipped.push({ accountId: account.id, reason: 'durable key not synced yet' });
468
+ continue;
469
+ }
470
+ if (slots[account.id]?.authMode === 'durable') {
471
+ result.skipped.push({ accountId: account.id, reason: 'slot already provisioned' });
472
+ continue;
473
+ }
474
+ try {
475
+ provision(account);
476
+ result.provisioned.push(account.id);
477
+ }
478
+ catch (err) {
479
+ result.errors.push({ accountId: account.id, message: err.message });
480
+ }
481
+ }
482
+ return result;
483
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Wire-shared types for the standalone `secrets` CLI (PHNX-3989).
3
+ *
4
+ * The standalone engine (`@phnx-labs/secrets-cli`, extracted from what used to
5
+ * be `cli/src/lib/secrets/**`) owns every one of these shapes; this module only
6
+ * re-declares them so `secrets-client.ts` and its consumers keep exact static
7
+ * types across the process-client boundary with no runtime dependency on the
8
+ * (now deleted) in-repo engine. Keep byte-for-byte in sync with
9
+ * `secrets-cli/src/schema.ts` / `src/core/bundles.ts` — this is the one seam
10
+ * both sides legitimately re-declare rather than share a package for (see the
11
+ * `secrets-client.ts` docblock).
12
+ */
13
+ /** Supported bundle storage backends. */
14
+ export type SecretsBackend = 'keychain' | 'file' | 'vault';
15
+ /** Supported secret resolution providers for a bundle var. */
16
+ export type SecretProvider = 'keychain' | 'env' | 'file' | 'exec';
17
+ /** A typed reference to a secret: a provider plus a provider-specific value. */
18
+ export interface SecretRef {
19
+ provider: SecretProvider;
20
+ value: string;
21
+ }
22
+ /**
23
+ * A bundle value: either a string (literal or provider-prefixed ref) or an
24
+ * object `{value: string}` used to escape a literal that would otherwise be
25
+ * parsed as a ref (e.g. a URL that happens to start with `env:`).
26
+ */
27
+ export type BundleValue = string | {
28
+ value: string;
29
+ };
30
+ /** Remote (bundle@host) push destination backend. */
31
+ export type RemoteBackend = 'keychain' | 'file';
32
+ export declare const SECRET_TYPES: readonly ["api-key", "token", "password", "url", "database-url", "ssh-key", "certificate", "webhook", "note"];
33
+ export type SecretType = (typeof SECRET_TYPES)[number];
34
+ /** Per-secret metadata; absent fields are omitted at write time. */
35
+ export interface VarMeta {
36
+ type?: SecretType;
37
+ /** Future-dated ISO date ('YYYY-MM-DD'). */
38
+ expires?: string;
39
+ note?: string;
40
+ }
41
+ /**
42
+ * Bundle prompt policy. `hold` (default): one Touch ID per hold window (~7d),
43
+ * then silent via the secrets-agent. `always`: prompt every read. `never`: no
44
+ * biometry ACL — least-safe, automation-only.
45
+ */
46
+ export type SecretsPolicy = 'always' | 'hold' | 'never';
47
+ /** A named set of environment variable definitions backed by secret stores. */
48
+ export interface SecretsBundle {
49
+ name: string;
50
+ description?: string;
51
+ allow_exec?: boolean;
52
+ /** Absent ⇒ `keychain`. */
53
+ backend?: SecretsBackend;
54
+ /** Absent ⇒ configured default (`hold`). */
55
+ policy?: SecretsPolicy;
56
+ created_at?: string;
57
+ updated_at?: string;
58
+ last_used?: string;
59
+ vars: Record<string, BundleValue>;
60
+ meta?: Record<string, VarMeta>;
61
+ }
62
+ /** Per-key kind breakdown of an already-resolved bundle. */
63
+ export interface BundleEntryInfo {
64
+ key: string;
65
+ kind: 'literal' | 'keychain' | 'env' | 'file' | 'exec';
66
+ /** Ref target, or empty for a literal. Never a resolved value. */
67
+ detail: string;
68
+ }
69
+ /** Options for `writeBundle` / `writeBundleWithItems`. */
70
+ export interface WriteBundleOptions {
71
+ /** Skip evicting the broker-held copy (no-op writers such as `stampLastUsed`). */
72
+ skipBrokerEviction?: boolean;
73
+ /** Allow writing a reserved `__<harness>__` store. */
74
+ allowReservedStore?: boolean;
75
+ }
76
+ /** Options for `readAndResolveBundleEnv`. */
77
+ export interface ResolveBundleOptions {
78
+ caller?: string;
79
+ /** Harness type whose unlock may be reused. */
80
+ agent?: string;
81
+ /** Duration shown in the Touch ID prompt. */
82
+ duration?: string;
83
+ /** Allow this call to raise an interactive biometric prompt. */
84
+ interactiveUnlock?: boolean;
85
+ /** Skip the broker fast-path and read from the keychain directly. */
86
+ noAgent?: boolean;
87
+ /** Resolve only from an already-unlocked broker snapshot; fail before prompting. */
88
+ agentOnly?: boolean;
89
+ /** Inject only these keys. Errors if any requested key is absent. */
90
+ keys?: string[];
91
+ /** Skip the per-key expiry gate. */
92
+ allowExpired?: boolean;
93
+ /** `process` projects dotted keys to shell-safe env names; `storage` preserves them. */
94
+ keyMode?: 'process' | 'storage';
95
+ /** Read a reserved `__<harness>__` store (system readers only). */
96
+ allowReservedStore?: boolean;
97
+ }
98
+ /** Options for `renameBundle`. */
99
+ export interface RenameOptions {
100
+ /** Overwrite an existing destination bundle. */
101
+ force?: boolean;
102
+ }
103
+ /** Options for `rotateBundleSecret`. */
104
+ export interface RotateOptions {
105
+ /** New plaintext value to write into the store (replaces the old one). */
106
+ newValue: string;
107
+ /** When true, drop existing meta for this key. Mutually exclusive with `meta`. */
108
+ clearMeta?: boolean;
109
+ /** Patch to merge into existing meta. Undefined fields preserve current values. */
110
+ meta?: Partial<VarMeta>;
111
+ }
112
+ /** Reading a keychain-backed item through the standalone. */
113
+ export interface KeychainReadContext {
114
+ agent?: string;
115
+ bundle?: string;
116
+ sessionId?: string;
117
+ reason?: string;
118
+ duration?: string;
119
+ defaultPolicy?: 'hold' | 'always' | 'never';
120
+ forceDuration?: boolean;
121
+ /**
122
+ * The caller attests the item(s) carry NO biometry ACL — so the read is
123
+ * silent even when no one is at the screen. Never pass this for an
124
+ * ACL-protected item.
125
+ */
126
+ silentNoAcl?: boolean;
127
+ }
128
+ /** One held bundle unlock reported by `agent.agentStatus`. */
129
+ export interface AgentStatusEntry {
130
+ name: string;
131
+ expiresAt: number;
132
+ keyCount: number;
133
+ harness: string;
134
+ leaseId?: string;
135
+ keys?: string[];
136
+ }
137
+ /** Options for `pushBundleToHost` / `pushBundleToHostAsync`. */
138
+ export interface PushBundleOptions {
139
+ remoteBackend: RemoteBackend;
140
+ /** Overwrite a key that already exists on the remote. */
141
+ force?: boolean;
142
+ /**
143
+ * Ignored. File-backend export never forwards the passphrase (PHNX-2371);
144
+ * the remote auto-provisions its own machine-local key. Kept on the options
145
+ * type so existing callers that passed one still compile.
146
+ */
147
+ passphrase?: string;
148
+ /** Label for the audit trail — `export --device` vs `fleet apply`. */
149
+ operation: string;
150
+ /** Preserve an automation account's permanent prompt-free policy remotely. */
151
+ policyNever?: boolean;
152
+ /** Permit a human-invoked push to read locally without requiring the agent broker. */
153
+ agentOnly?: boolean;
154
+ /** Non-secret literals whose bundle value kind must survive the dotenv transport. */
155
+ literalValues?: Record<string, string>;
156
+ /** Per-SSH-operation deadline. Async daemon callers must set this explicitly. */
157
+ timeoutMs?: number;
158
+ }
159
+ export interface PushBundleResult {
160
+ ok: boolean;
161
+ host: string;
162
+ bundle: string;
163
+ keyCount: number;
164
+ /** One line for the caller to render. Never contains a secret value. */
165
+ message: string;
166
+ }
167
+ /** Lightweight listing entry returned by the managed sync transport. */
168
+ export interface RemoteBundleSummary {
169
+ name: string;
170
+ updated_at: string;
171
+ }
172
+ /** Options for `pullBundle` (the `agents sync --secrets` umbrella stage). */
173
+ export interface PullOptions {
174
+ passphrase: string;
175
+ /** When true, overwrite an existing local bundle. */
176
+ force?: boolean;
177
+ }
178
+ /** One credential-shaped shell-rc export finding. */
179
+ export interface RcSecretFinding {
180
+ /** Basename of the rc file, e.g. `.zshenv`. */
181
+ file: string;
182
+ /** 1-based line number of the export. */
183
+ line: number;
184
+ /** The exported variable name. Never the value. */
185
+ name: string;
186
+ /** The file-store master passphrase gets called out separately. */
187
+ isMasterPassphrase: boolean;
188
+ }