@phnx-labs/agents-cli 1.20.72 → 1.20.73

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 (219) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +8 -4
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/feed.d.ts +13 -0
  16. package/dist/commands/feed.js +130 -29
  17. package/dist/commands/hq.d.ts +2 -0
  18. package/dist/commands/hq.js +58 -0
  19. package/dist/commands/lease.d.ts +19 -0
  20. package/dist/commands/lease.js +138 -11
  21. package/dist/commands/login.d.ts +2 -0
  22. package/dist/commands/login.js +123 -0
  23. package/dist/commands/logs.js +1 -1
  24. package/dist/commands/mine.d.ts +27 -0
  25. package/dist/commands/mine.js +207 -0
  26. package/dist/commands/monitors.js +79 -49
  27. package/dist/commands/plugins.js +1 -0
  28. package/dist/commands/profiles.js +158 -0
  29. package/dist/commands/repo.js +2 -60
  30. package/dist/commands/resources.d.ts +5 -0
  31. package/dist/commands/resources.js +95 -0
  32. package/dist/commands/routines.d.ts +2 -0
  33. package/dist/commands/routines.js +139 -37
  34. package/dist/commands/secrets.js +234 -47
  35. package/dist/commands/send.d.ts +15 -0
  36. package/dist/commands/send.js +62 -0
  37. package/dist/commands/sessions-tail.d.ts +1 -0
  38. package/dist/commands/sessions-tail.js +20 -9
  39. package/dist/commands/sessions.js +18 -3
  40. package/dist/commands/setup-mine.d.ts +18 -0
  41. package/dist/commands/setup-mine.js +106 -0
  42. package/dist/commands/setup-share.js +2 -2
  43. package/dist/commands/setup.js +3 -1
  44. package/dist/commands/share.d.ts +8 -1
  45. package/dist/commands/share.js +122 -37
  46. package/dist/commands/ssh.d.ts +9 -0
  47. package/dist/commands/ssh.js +196 -26
  48. package/dist/commands/sync.js +1 -1
  49. package/dist/commands/teams.js +11 -2
  50. package/dist/index.js +79 -10
  51. package/dist/lib/activity.d.ts +124 -0
  52. package/dist/lib/activity.js +542 -0
  53. package/dist/lib/agents.js +16 -5
  54. package/dist/lib/artifact-actions.d.ts +1 -1
  55. package/dist/lib/artifact-actions.js +1 -1
  56. package/dist/lib/ask-classifier.d.ts +2 -1
  57. package/dist/lib/ask-classifier.js +3 -3
  58. package/dist/lib/auth-health.d.ts +32 -4
  59. package/dist/lib/auth-health.js +40 -1
  60. package/dist/lib/auto-pull.js +8 -1
  61. package/dist/lib/brand.d.ts +40 -0
  62. package/dist/lib/brand.js +122 -0
  63. package/dist/lib/browser/drivers/ssh.js +4 -7
  64. package/dist/lib/browser/service.js +6 -8
  65. package/dist/lib/channels/providers/index.d.ts +2 -0
  66. package/dist/lib/channels/providers/index.js +20 -0
  67. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  68. package/dist/lib/channels/providers/mailbox.js +26 -0
  69. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  70. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  71. package/dist/lib/channels/providers/rush.d.ts +6 -0
  72. package/dist/lib/channels/providers/rush.js +55 -0
  73. package/dist/lib/channels/registry.d.ts +42 -0
  74. package/dist/lib/channels/registry.js +20 -0
  75. package/dist/lib/channels/resolve.d.ts +11 -0
  76. package/dist/lib/channels/resolve.js +12 -0
  77. package/dist/lib/cli-resources.d.ts +18 -0
  78. package/dist/lib/cli-resources.js +53 -4
  79. package/dist/lib/cloud/codex.js +6 -3
  80. package/dist/lib/cloud/factory.d.ts +1 -0
  81. package/dist/lib/cloud/factory.js +10 -4
  82. package/dist/lib/cloud/rush.d.ts +3 -1
  83. package/dist/lib/cloud/rush.js +6 -1
  84. package/dist/lib/cloud/types.d.ts +2 -0
  85. package/dist/lib/codex-home.d.ts +35 -0
  86. package/dist/lib/codex-home.js +136 -0
  87. package/dist/lib/crabbox/cli.d.ts +44 -0
  88. package/dist/lib/crabbox/cli.js +120 -20
  89. package/dist/lib/crabbox/lease.d.ts +52 -1
  90. package/dist/lib/crabbox/lease.js +117 -16
  91. package/dist/lib/crabbox/progress.d.ts +31 -0
  92. package/dist/lib/crabbox/progress.js +76 -0
  93. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  94. package/dist/lib/crabbox/runtimes.js +33 -8
  95. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  96. package/dist/lib/crabbox/setup-copy.js +127 -0
  97. package/dist/lib/daemon.d.ts +5 -5
  98. package/dist/lib/daemon.js +9 -9
  99. package/dist/lib/drive-sync.js +6 -3
  100. package/dist/lib/event-stream.d.ts +36 -0
  101. package/dist/lib/event-stream.js +68 -0
  102. package/dist/lib/events.d.ts +1 -1
  103. package/dist/lib/exec.d.ts +11 -0
  104. package/dist/lib/exec.js +23 -2
  105. package/dist/lib/feed-policy.d.ts +1 -1
  106. package/dist/lib/feed-policy.js +14 -9
  107. package/dist/lib/feed-ranking.d.ts +32 -0
  108. package/dist/lib/feed-ranking.js +224 -0
  109. package/dist/lib/feed.d.ts +35 -2
  110. package/dist/lib/feed.js +66 -3
  111. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  112. package/dist/lib/fleet/auth-sync.js +92 -0
  113. package/dist/lib/fleet/remote-login.d.ts +187 -0
  114. package/dist/lib/fleet/remote-login.js +556 -0
  115. package/dist/lib/format.d.ts +30 -3
  116. package/dist/lib/format.js +34 -5
  117. package/dist/lib/fs-atomic.d.ts +7 -2
  118. package/dist/lib/fs-atomic.js +8 -12
  119. package/dist/lib/git.d.ts +16 -0
  120. package/dist/lib/git.js +79 -2
  121. package/dist/lib/heal.js +11 -3
  122. package/dist/lib/hosts/progress.d.ts +28 -10
  123. package/dist/lib/hosts/progress.js +79 -22
  124. package/dist/lib/hosts/remote-cmd.js +4 -0
  125. package/dist/lib/hq/floor.d.ts +87 -0
  126. package/dist/lib/hq/floor.js +226 -0
  127. package/dist/lib/menubar/install-menubar.js +14 -20
  128. package/dist/lib/notify.d.ts +1 -0
  129. package/dist/lib/notify.js +3 -3
  130. package/dist/lib/open-url.d.ts +2 -0
  131. package/dist/lib/open-url.js +19 -0
  132. package/dist/lib/openclaw-keychain.d.ts +56 -0
  133. package/dist/lib/openclaw-keychain.js +236 -0
  134. package/dist/lib/overdue.js +10 -0
  135. package/dist/lib/permissions.d.ts +44 -1
  136. package/dist/lib/permissions.js +284 -7
  137. package/dist/lib/project-launch.d.ts +6 -12
  138. package/dist/lib/project-launch.js +13 -228
  139. package/dist/lib/project-resources.d.ts +7 -0
  140. package/dist/lib/project-resources.js +291 -0
  141. package/dist/lib/pty-client.d.ts +27 -0
  142. package/dist/lib/pty-client.js +136 -10
  143. package/dist/lib/refresh.js +14 -10
  144. package/dist/lib/resource-profiles.d.ts +26 -0
  145. package/dist/lib/resource-profiles.js +157 -0
  146. package/dist/lib/resources/permissions.js +7 -1
  147. package/dist/lib/resources/types.d.ts +1 -1
  148. package/dist/lib/resources.d.ts +1 -1
  149. package/dist/lib/resources.js +32 -3
  150. package/dist/lib/routines.d.ts +11 -0
  151. package/dist/lib/routines.js +56 -15
  152. package/dist/lib/runner.d.ts +1 -0
  153. package/dist/lib/runner.js +66 -5
  154. package/dist/lib/secrets/bundles.d.ts +16 -3
  155. package/dist/lib/secrets/bundles.js +206 -37
  156. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  157. package/dist/lib/secrets/icloud-import.js +9 -6
  158. package/dist/lib/secrets/mcp.js +1 -1
  159. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  160. package/dist/lib/secrets/vault-age-helper.js +34 -0
  161. package/dist/lib/secrets/vault.d.ts +49 -0
  162. package/dist/lib/secrets/vault.js +397 -0
  163. package/dist/lib/self-heal/checks/path.js +3 -1
  164. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  165. package/dist/lib/self-heal/checks/shims.js +19 -11
  166. package/dist/lib/session/cloud.d.ts +2 -2
  167. package/dist/lib/session/cloud.js +2 -2
  168. package/dist/lib/session/db.d.ts +4 -0
  169. package/dist/lib/session/db.js +44 -7
  170. package/dist/lib/session/discover.d.ts +2 -0
  171. package/dist/lib/session/discover.js +114 -3
  172. package/dist/lib/session/stream-render.d.ts +3 -0
  173. package/dist/lib/session/stream-render.js +130 -0
  174. package/dist/lib/session/types.d.ts +6 -0
  175. package/dist/lib/share/analytics.d.ts +13 -0
  176. package/dist/lib/share/analytics.js +45 -0
  177. package/dist/lib/share/config.d.ts +19 -5
  178. package/dist/lib/share/config.js +44 -8
  179. package/dist/lib/share/provision.d.ts +58 -6
  180. package/dist/lib/share/provision.js +97 -22
  181. package/dist/lib/share/publish.d.ts +36 -13
  182. package/dist/lib/share/publish.js +55 -8
  183. package/dist/lib/share/worker-template.js +83 -17
  184. package/dist/lib/shims.d.ts +9 -0
  185. package/dist/lib/shims.js +91 -13
  186. package/dist/lib/ssh-exec.d.ts +14 -0
  187. package/dist/lib/ssh-exec.js +57 -0
  188. package/dist/lib/staleness/detectors/hooks.js +25 -1
  189. package/dist/lib/staleness/detectors/permissions.js +66 -0
  190. package/dist/lib/staleness/detectors/workflows.js +20 -0
  191. package/dist/lib/staleness/writers/hooks.js +58 -4
  192. package/dist/lib/staleness/writers/permissions.js +2 -2
  193. package/dist/lib/staleness/writers/skills.js +1 -1
  194. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  195. package/dist/lib/staleness/writers/sources.js +68 -1
  196. package/dist/lib/star-nudge.d.ts +45 -0
  197. package/dist/lib/star-nudge.js +91 -0
  198. package/dist/lib/startup/command-registry.d.ts +6 -1
  199. package/dist/lib/startup/command-registry.js +17 -3
  200. package/dist/lib/state.d.ts +2 -0
  201. package/dist/lib/state.js +3 -0
  202. package/dist/lib/subagents-registry.d.ts +2 -0
  203. package/dist/lib/subagents-registry.js +3 -0
  204. package/dist/lib/teams/parsers.js +214 -6
  205. package/dist/lib/teams/supervisor.d.ts +7 -0
  206. package/dist/lib/teams/supervisor.js +2 -1
  207. package/dist/lib/template.d.ts +1 -1
  208. package/dist/lib/template.js +1 -1
  209. package/dist/lib/triggers/webhook.js +36 -3
  210. package/dist/lib/types.d.ts +73 -0
  211. package/dist/lib/version.d.ts +40 -0
  212. package/dist/lib/version.js +94 -16
  213. package/dist/lib/versions.d.ts +11 -0
  214. package/dist/lib/versions.js +208 -76
  215. package/dist/lib/workflows.d.ts +2 -0
  216. package/dist/lib/workflows.js +88 -0
  217. package/package.json +2 -1
  218. package/dist/commands/daemon.d.ts +0 -10
  219. package/dist/commands/daemon.js +0 -121
@@ -13,9 +13,12 @@
13
13
  * SSH). The item-name scheme is identical, so the only difference is where
14
14
  * bytes land. A file-backed bundle is discovered by the presence of its
15
15
  * metadata item in the file store.
16
+ * - `vault`: a single age-encrypted ~/.agents/vault.age file unlocked by
17
+ * `agents login`; intended for user-managed cross-machine file sync.
16
18
  *
17
- * Cross-machine sync is handled by src/lib/secrets/sync.ts via an explicit
18
- * encrypted export/import flow; the bundle layer is sync-agnostic.
19
+ * Server-backed cross-machine sync is handled by src/lib/secrets/sync.ts via
20
+ * an explicit encrypted export/import flow; the bundle layer also supports the
21
+ * local vault backend for user-managed file sync.
19
22
  */
20
23
  import * as fs from 'fs';
21
24
  import * as os from 'os';
@@ -23,8 +26,10 @@ import * as path from 'path';
23
26
  import * as yaml from 'yaml';
24
27
  import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, isKeychainBackendOverridden, keychainServiceAlias, keychainUsesFileFallback, listKeychainItems, parseBundleValue, resolveRef, secretsKeychainItem, setKeychainToken, } from './index.js';
25
28
  import { fileStore } from './filestore.js';
29
+ import { getVaultSession, vaultDeleteItem, vaultExists, vaultGetItems, vaultGetItem, vaultHasItem, vaultListItems, vaultSetItems, vaultSetItem, } from './vault.js';
26
30
  import { emit } from '../events.js';
27
31
  import { readMeta } from '../state.js';
32
+ import { assertNameActiveInResourceProfile, filterNamesForActiveResourceProfile } from '../resource-profiles.js';
28
33
  import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, agentEvictSync, secretsAgentAutoEnabled, secretsHoldMs } from './agent.js';
29
34
  import { loadSession, deleteSession } from './session-store.js';
30
35
  import { createHash } from 'node:crypto';
@@ -33,6 +38,10 @@ const keychainStore = {
33
38
  get: getKeychainToken,
34
39
  getBatch: getKeychainTokens,
35
40
  set: setKeychainToken,
41
+ setBatch: (items, opts) => {
42
+ for (const [item, value] of items)
43
+ setKeychainToken(item, value, opts);
44
+ },
36
45
  delete: deleteKeychainToken,
37
46
  list: listKeychainItems,
38
47
  };
@@ -59,11 +68,29 @@ const fileItemStore = {
59
68
  return out;
60
69
  },
61
70
  set: (item, value) => fileStore.set(item, value, { allowAutoProvision: FILE_ALLOW_AUTO_PROVISION }),
71
+ setBatch: (items) => {
72
+ for (const [item, value] of items) {
73
+ fileStore.set(item, value, { allowAutoProvision: FILE_ALLOW_AUTO_PROVISION });
74
+ }
75
+ },
62
76
  delete: (item) => fileStore.delete(item),
63
77
  list: (prefix) => fileStore.list(prefix),
64
78
  };
79
+ const vaultStore = {
80
+ has: vaultHasItem,
81
+ get: vaultGetItem,
82
+ getBatch: vaultGetItems,
83
+ set: (item, value) => vaultSetItem(item, value),
84
+ setBatch: (items) => vaultSetItems(items),
85
+ delete: vaultDeleteItem,
86
+ list: vaultListItems,
87
+ };
65
88
  function itemStore(backend) {
66
- return backend === 'file' ? fileItemStore : keychainStore;
89
+ if (backend === 'file')
90
+ return fileItemStore;
91
+ if (backend === 'vault')
92
+ return vaultStore;
93
+ return keychainStore;
67
94
  }
68
95
  /**
69
96
  * Discover a bundle's backend by location: a file-backed bundle's metadata
@@ -72,7 +99,20 @@ function itemStore(backend) {
72
99
  * "read metadata to learn where metadata lives." Absent ⇒ keychain.
73
100
  */
74
101
  export function bundleBackend(name) {
75
- return fileStore.has(BUNDLE_META_PREFIX + name) ? 'file' : 'keychain';
102
+ const item = BUNDLE_META_PREFIX + name;
103
+ if (fileStore.has(item))
104
+ return 'file';
105
+ if (vaultExists() && getVaultSession().loggedIn) {
106
+ try {
107
+ if (vaultHasItem(item))
108
+ return 'vault';
109
+ }
110
+ catch {
111
+ // A vault problem should not hide a keychain/file bundle that already
112
+ // resolved above; exact vault reads surface the decrypt/login error.
113
+ }
114
+ }
115
+ return 'keychain';
76
116
  }
77
117
  /**
78
118
  * Guard a file-backed bundle operation. On macOS the file store must be
@@ -92,6 +132,11 @@ function assertFileBackendUsable(name) {
92
132
  `(no biometry prompt is available headlessly). Set it for this run, e.g.\n` +
93
133
  ` AGENTS_SECRETS_PASSPHRASE=… agents secrets exec ${name} -- <command>`);
94
134
  }
135
+ function assertVaultBackendUsable(name) {
136
+ if (getVaultSession().loggedIn)
137
+ return;
138
+ throw new Error(`Synced bundle '${name}' needs an active login. Run: agents login`);
139
+ }
95
140
  /** Allowed values for a secret's `type` metadata field. */
96
141
  export const SECRET_TYPES = [
97
142
  'api-key',
@@ -108,6 +153,7 @@ export const SECRET_TYPES = [
108
153
  const LAST_USED_THROTTLE_MS = 60_000;
109
154
  export const BUNDLE_NAME_PATTERN = /^[a-z0-9][a-z0-9\-_.]{0,48}$/i;
110
155
  export const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
156
+ export const BUNDLE_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_-]+)?$/;
111
157
  export const BUNDLE_META_PREFIX = 'agents-cli.bundles.';
112
158
  const SECRETS_ITEM_PREFIX = 'agents-cli.secrets.';
113
159
  export const RESERVED_ENV_NAMES = new Set([
@@ -121,6 +167,10 @@ export function bundleToEnvPrefix(name) {
121
167
  export function isReservedEnvName(key) {
122
168
  return RESERVED_ENV_NAMES.has(key.toUpperCase());
123
169
  }
170
+ export function bundleKeyToEnvKey(key) {
171
+ const dot = key.indexOf('.');
172
+ return dot === -1 ? key : key.slice(0, dot);
173
+ }
124
174
  export function isLoaderOrInterpreterEnv(name) {
125
175
  const upper = name.toUpperCase();
126
176
  return upper.startsWith('LD_') ||
@@ -156,10 +206,11 @@ export function validateBundleName(name) {
156
206
  }
157
207
  }
158
208
  export function validateEnvKey(key) {
159
- if (!ENV_KEY_PATTERN.test(key)) {
160
- throw new Error(`Invalid environment variable name '${key}'. Must match [A-Za-z_][A-Za-z0-9_]*.`);
209
+ if (!BUNDLE_KEY_PATTERN.test(key)) {
210
+ throw new Error(`Invalid bundle key '${key}'. Must match [A-Za-z_][A-Za-z0-9_]* with optional .account suffix.`);
161
211
  }
162
- if (isLoaderOrInterpreterEnv(key) || isReservedEnvName(key)) {
212
+ const envKey = bundleKeyToEnvKey(key);
213
+ if (isLoaderOrInterpreterEnv(envKey) || isReservedEnvName(envKey)) {
163
214
  throw new Error(`Env key "${key}" is reserved — cannot be used in a secrets bundle. Reserved keys include PATH, HOME, USER, and dynamic-loader/interpreter vars (LD_*, DYLD_*, NODE_OPTIONS, etc.).`);
164
215
  }
165
216
  }
@@ -197,6 +248,8 @@ export function readBundle(name) {
197
248
  const backend = bundleBackend(name);
198
249
  if (backend === 'file')
199
250
  assertFileBackendUsable(name);
251
+ if (backend === 'vault')
252
+ assertVaultBackendUsable(name);
200
253
  let json;
201
254
  try {
202
255
  json = itemStore(backend).get(bundleMetaItem(name));
@@ -207,6 +260,9 @@ export function readBundle(name) {
207
260
  if (backend === 'file' && fileStore.has(bundleMetaItem(name))) {
208
261
  throw new Error(`Bundle '${name}': failed to decrypt — wrong AGENTS_SECRETS_PASSPHRASE or tampered file store. (${err.message})`);
209
262
  }
263
+ if (vaultExists() && !getVaultSession().loggedIn) {
264
+ throw new Error(`Synced secrets are locked. Run: agents login`);
265
+ }
210
266
  throw new Error(`Secrets bundle '${name}' not found.`);
211
267
  }
212
268
  let parsed;
@@ -226,9 +282,9 @@ export function readBundle(name) {
226
282
  name,
227
283
  description: parsed.description,
228
284
  allow_exec: Boolean(parsed.allow_exec),
229
- // Absent ⇒ keychain; only set when file-backed so a keychain bundle
285
+ // Absent ⇒ keychain; only set when non-keychain so a keychain bundle
230
286
  // round-trips byte-for-byte.
231
- backend: backend === 'file' ? 'file' : undefined,
287
+ backend: backend === 'keychain' ? undefined : backend,
232
288
  // Legacy wire key: the policy is persisted under `tier` (`session` == `daily`).
233
289
  policy: parsePolicy(parsed.tier),
234
290
  vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
@@ -295,11 +351,13 @@ export function shouldEvictAfterBundleWrite(skipRequested, noAgentEnv, backendOv
295
351
  return false;
296
352
  return true;
297
353
  }
298
- export function writeBundle(bundle, opts = {}) {
354
+ function prepareBundleWrite(bundle) {
299
355
  validateBundleName(bundle.name);
300
356
  const backend = bundle.backend ?? 'keychain';
301
357
  if (backend === 'file')
302
358
  assertFileBackendUsable(bundle.name);
359
+ if (backend === 'vault')
360
+ assertVaultBackendUsable(bundle.name);
303
361
  for (const key of Object.keys(bundle.vars)) {
304
362
  validateEnvKey(key);
305
363
  }
@@ -335,7 +393,7 @@ export function writeBundle(bundle, opts = {}) {
335
393
  name: bundle.name,
336
394
  description: bundle.description,
337
395
  allow_exec: bundle.allow_exec ? true : undefined,
338
- backend: backend === 'file' ? 'file' : undefined,
396
+ backend: backend === 'keychain' ? undefined : backend,
339
397
  // Wire format: persist the policy under the legacy `tier` token so older CLI
340
398
  // versions on other synced machines keep reading it — `daily`⇒`session`,
341
399
  // explicit `always`⇒`biometry`, `never`⇒`none`. An absent policy omits the
@@ -352,12 +410,13 @@ export function writeBundle(bundle, opts = {}) {
352
410
  vars: bundle.vars,
353
411
  meta,
354
412
  };
355
- const json = JSON.stringify(payload);
356
- // A `never` bundle's metadata is stored without the biometry ACL too, so
357
- // `view` and the metadata half of a read resolve silently — the whole point
358
- // of the tier. On an un-updated pinned helper this write fails loudly (the
359
- // no-ACL command is missing) rather than silently landing an ACL'd item.
360
- itemStore(backend).set(bundleMetaItem(bundle.name), json, { noAcl: bundle.policy === 'never' });
413
+ return {
414
+ backend,
415
+ metadataItem: bundleMetaItem(bundle.name),
416
+ metadataJson: JSON.stringify(payload),
417
+ };
418
+ }
419
+ function finishBundleWrite(bundle, opts) {
361
420
  emit('secrets.set', { module: 'secrets', bundle: bundle.name });
362
421
  // A broker-held snapshot predates this write; evict it so the next read
363
422
  // re-resolves from the keychain instead of serving stale values.
@@ -368,6 +427,22 @@ export function writeBundle(bundle, opts = {}) {
368
427
  deleteSession(bundle.name);
369
428
  }
370
429
  }
430
+ export function writeBundle(bundle, opts = {}) {
431
+ const prepared = prepareBundleWrite(bundle);
432
+ // A `never` bundle's metadata is stored without the biometry ACL too, so
433
+ // `view` and the metadata half of a read resolve silently — the whole point
434
+ // of the tier. On an un-updated pinned helper this write fails loudly (the
435
+ // no-ACL command is missing) rather than silently landing an ACL'd item.
436
+ itemStore(prepared.backend).set(prepared.metadataItem, prepared.metadataJson, { noAcl: bundle.policy === 'never' });
437
+ finishBundleWrite(bundle, opts);
438
+ }
439
+ export function writeBundleWithItems(bundle, items, opts = {}) {
440
+ const prepared = prepareBundleWrite(bundle);
441
+ const batch = new Map(items);
442
+ batch.set(prepared.metadataItem, prepared.metadataJson);
443
+ itemStore(prepared.backend).setBatch(batch, { noAcl: bundle.policy === 'never' });
444
+ finishBundleWrite(bundle, opts);
445
+ }
371
446
  export function deleteBundle(name) {
372
447
  validateBundleName(name);
373
448
  const deleted = itemStore(bundleBackend(name)).delete(bundleMetaItem(name));
@@ -409,7 +484,7 @@ function parseBundleMeta(nameHint, json, backend) {
409
484
  name,
410
485
  description: parsed.description,
411
486
  allow_exec: Boolean(parsed.allow_exec),
412
- backend: backend === 'file' ? 'file' : undefined,
487
+ backend: backend === 'keychain' ? undefined : backend,
413
488
  // Legacy wire key: the policy is persisted under `tier` (`session` == `daily`).
414
489
  policy: parsePolicy(parsed.tier),
415
490
  vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
@@ -423,7 +498,7 @@ function parseBundleMeta(nameHint, json, backend) {
423
498
  if (parsed.meta && typeof parsed.meta === 'object')
424
499
  bundle.meta = parsed.meta;
425
500
  for (const key of Object.keys(bundle.vars)) {
426
- if (!ENV_KEY_PATTERN.test(key))
501
+ if (!BUNDLE_KEY_PATTERN.test(key))
427
502
  return null;
428
503
  }
429
504
  return bundle;
@@ -534,7 +609,33 @@ export function listBundles() {
534
609
  if (bundle)
535
610
  out.push(bundle);
536
611
  }
537
- return out.sort((a, b) => a.name.localeCompare(b.name));
612
+ if (getVaultSession().loggedIn && vaultExists()) {
613
+ let vaultServices = [];
614
+ try {
615
+ vaultServices = vaultListItems(BUNDLE_META_PREFIX);
616
+ }
617
+ catch {
618
+ vaultServices = [];
619
+ }
620
+ for (const service of vaultServices) {
621
+ const name = service.slice(BUNDLE_META_PREFIX.length);
622
+ if (!BUNDLE_NAME_PATTERN.test(name))
623
+ continue;
624
+ let json;
625
+ try {
626
+ json = vaultStore.get(bundleMetaItem(name));
627
+ }
628
+ catch {
629
+ out.push({ name, backend: 'vault', vars: {} });
630
+ continue;
631
+ }
632
+ const bundle = parseBundleMeta(name, json, 'vault');
633
+ if (bundle)
634
+ out.push(bundle);
635
+ }
636
+ }
637
+ const activeNames = new Set(filterNamesForActiveResourceProfile('secrets', out.map((b) => b.name)));
638
+ return out.filter((bundle) => activeNames.has(bundle.name)).sort((a, b) => a.name.localeCompare(b.name));
538
639
  }
539
640
  export function describeBundle(bundle) {
540
641
  const out = [];
@@ -612,6 +713,67 @@ function selectRequestedKeys(bundle, requested) {
612
713
  }
613
714
  return new Set(req ?? Object.keys(bundle.vars));
614
715
  }
716
+ function assignResolvedEnvValue(env, bundle, storageKey, value, keyMode, owners) {
717
+ const envKey = keyMode === 'storage' ? storageKey : bundleKeyToEnvKey(storageKey);
718
+ const previous = owners.get(envKey);
719
+ if (previous && previous !== storageKey) {
720
+ throw new Error(`Bundle '${bundle.name}' maps multiple keys to '${envKey}': ${previous}, ${storageKey}. ` +
721
+ `Select one account variant with --keys.`);
722
+ }
723
+ owners.set(envKey, storageKey);
724
+ env[envKey] = value;
725
+ }
726
+ function projectResolvedEnv(bundle, env, selectedKeys, keyMode) {
727
+ if (keyMode === 'storage') {
728
+ const out = {};
729
+ for (const key of selectedKeys) {
730
+ if (key in env)
731
+ out[key] = env[key];
732
+ }
733
+ return out;
734
+ }
735
+ let needsProjection = false;
736
+ const owners = new Map();
737
+ for (const key of selectedKeys) {
738
+ const envKey = bundleKeyToEnvKey(key);
739
+ if (envKey !== key)
740
+ needsProjection = true;
741
+ const previous = owners.get(envKey);
742
+ if (previous && previous !== key) {
743
+ throw new Error(`Bundle '${bundle.name}' maps multiple keys to '${envKey}': ${previous}, ${key}. ` +
744
+ `Select one account variant with --keys.`);
745
+ }
746
+ owners.set(envKey, key);
747
+ }
748
+ if (!needsProjection) {
749
+ const envKeys = Object.keys(env);
750
+ if (selectedKeys.size === envKeys.length && envKeys.every((key) => selectedKeys.has(key)))
751
+ return env;
752
+ const out = {};
753
+ for (const key of selectedKeys) {
754
+ if (key in env)
755
+ out[key] = env[key];
756
+ }
757
+ return out;
758
+ }
759
+ const out = {};
760
+ for (const key of selectedKeys) {
761
+ if (key in env)
762
+ out[bundleKeyToEnvKey(key)] = env[key];
763
+ }
764
+ return out;
765
+ }
766
+ export function canCacheResolvedEnv(bundle, selectedKeys, keyMode) {
767
+ if (selectedKeys.size !== Object.keys(bundle.vars).length)
768
+ return false;
769
+ if (keyMode === 'storage')
770
+ return true;
771
+ for (const key of selectedKeys) {
772
+ if (bundleKeyToEnvKey(key) !== key)
773
+ return false;
774
+ }
775
+ return true;
776
+ }
615
777
  /**
616
778
  * Apply the --keys subset + expiry gate to an already-resolved snapshot from
617
779
  * the secrets-agent fast-path. The agent stores the FULL bundle env, so a
@@ -625,15 +787,11 @@ function selectRequestedKeys(bundle, requested) {
625
787
  export function filterAgentHitBySubsetAndExpiry(hit, opts) {
626
788
  const selectedKeys = selectRequestedKeys(hit.bundle, opts.keys);
627
789
  assertNotExpired(hit.bundle, [...selectedKeys], opts.allowExpired ?? false);
628
- // When no subset was requested, return the cached env untouched — same
629
- // reference the agent handed back, so no per-call allocation on the hot path.
630
- if (!opts.keys?.length)
790
+ const env = projectResolvedEnv(hit.bundle, hit.env, selectedKeys, opts.keyMode);
791
+ // When no subset/projection was requested, return the cached env untouched
792
+ // same reference the agent handed back, so no per-call allocation on the hot path.
793
+ if (env === hit.env)
631
794
  return hit;
632
- const env = {};
633
- for (const key of selectedKeys) {
634
- if (key in hit.env)
635
- env[key] = hit.env[key];
636
- }
637
795
  return { bundle: hit.bundle, env };
638
796
  }
639
797
  /**
@@ -681,12 +839,13 @@ export function resolveBundleEnv(bundle, _opts = {}) {
681
839
  ? store.getBatch(keychainItemsToFetch)
682
840
  : new Map();
683
841
  const env = {};
842
+ const owners = new Map();
684
843
  for (const [key, raw] of Object.entries(bundle.vars)) {
685
844
  if (!selectedKeys.has(key))
686
845
  continue;
687
846
  const parsed = parsedByKey.get(key);
688
847
  if ('literal' in parsed) {
689
- env[key] = parsed.literal;
848
+ assignResolvedEnvValue(env, bundle, key, parsed.literal, _opts.keyMode, owners);
690
849
  continue;
691
850
  }
692
851
  if (parsed.ref.provider === 'keychain') {
@@ -696,14 +855,15 @@ export function resolveBundleEnv(bundle, _opts = {}) {
696
855
  throw new Error(`Bundle '${bundle.name}' key '${key}': stored item '${item}' not found. ` +
697
856
  `Run: agents secrets add ${bundle.name} ${key}`);
698
857
  }
699
- env[key] = value;
858
+ assignResolvedEnvValue(env, bundle, key, value, _opts.keyMode, owners);
700
859
  continue;
701
860
  }
702
861
  try {
703
- env[key] = resolveRef(parsed.ref, {
862
+ const value = resolveRef(parsed.ref, {
704
863
  allowExec: bundle.allow_exec,
705
864
  keychainItemFor: (shortId) => secretsKeychainItem(bundle.name, shortId),
706
865
  });
866
+ assignResolvedEnvValue(env, bundle, key, value, _opts.keyMode, owners);
707
867
  }
708
868
  catch (err) {
709
869
  throw new Error(`Bundle '${bundle.name}' key '${key}': ${err.message}`);
@@ -766,6 +926,7 @@ export function isHeadlessSecretsContext(env = process.env, platform = process.p
766
926
  */
767
927
  export function readAndResolveBundleEnv(name, opts = {}) {
768
928
  validateBundleName(name);
929
+ assertNameActiveInResourceProfile('secrets', name);
769
930
  const backend = bundleBackend(name);
770
931
  // Fast-path: if the secrets-agent holds this bundle (user ran
771
932
  // `agents secrets unlock <name>`), return the cached snapshot with no Touch
@@ -828,6 +989,8 @@ export function readAndResolveBundleEnv(name, opts = {}) {
828
989
  }
829
990
  if (backend === 'file')
830
991
  assertFileBackendUsable(name);
992
+ if (backend === 'vault')
993
+ assertVaultBackendUsable(name);
831
994
  const store = itemStore(backend);
832
995
  const metaItem = bundleMetaItem(name);
833
996
  const bundleSecretPrefix = `${SECRETS_ITEM_PREFIX}${name}.`;
@@ -856,6 +1019,9 @@ export function readAndResolveBundleEnv(name, opts = {}) {
856
1019
  if (backend === 'file' && fileStore.has(metaItem)) {
857
1020
  throw new Error(`Bundle '${name}': failed to decrypt — wrong AGENTS_SECRETS_PASSPHRASE or tampered file store.`);
858
1021
  }
1022
+ if (vaultExists() && !getVaultSession().loggedIn) {
1023
+ throw new Error(`Synced secrets are locked. Run: agents login`);
1024
+ }
859
1025
  throw new Error(`Secrets bundle '${name}' not found.`);
860
1026
  }
861
1027
  let parsed;
@@ -872,7 +1038,7 @@ export function readAndResolveBundleEnv(name, opts = {}) {
872
1038
  name,
873
1039
  description: parsed.description,
874
1040
  allow_exec: Boolean(parsed.allow_exec),
875
- backend: backend === 'file' ? 'file' : undefined,
1041
+ backend: backend === 'keychain' ? undefined : backend,
876
1042
  // Legacy wire key: the policy is persisted under `tier` (`session` == `daily`).
877
1043
  policy: parsePolicy(parsed.tier),
878
1044
  vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
@@ -923,12 +1089,13 @@ export function readAndResolveBundleEnv(name, opts = {}) {
923
1089
  };
924
1090
  try {
925
1091
  const env = {};
1092
+ const owners = new Map();
926
1093
  for (const [key] of Object.entries(bundle.vars)) {
927
1094
  if (!selectedKeys.has(key))
928
1095
  continue;
929
1096
  const p = parsedByKey.get(key);
930
1097
  if ('literal' in p) {
931
- env[key] = p.literal;
1098
+ assignResolvedEnvValue(env, bundle, key, p.literal, opts.keyMode, owners);
932
1099
  continue;
933
1100
  }
934
1101
  if (p.ref.provider === 'keychain') {
@@ -941,14 +1108,15 @@ export function readAndResolveBundleEnv(name, opts = {}) {
941
1108
  throw new Error(`Bundle '${bundle.name}' key '${key}': stored item '${item}' not found. ` +
942
1109
  `Run: agents secrets add ${bundle.name} ${key}`);
943
1110
  }
944
- env[key] = value;
1111
+ assignResolvedEnvValue(env, bundle, key, value, opts.keyMode, owners);
945
1112
  continue;
946
1113
  }
947
1114
  try {
948
- env[key] = resolveRef(p.ref, {
1115
+ const value = resolveRef(p.ref, {
949
1116
  allowExec: bundle.allow_exec,
950
1117
  keychainItemFor: (shortId) => secretsKeychainItem(bundle.name, shortId),
951
1118
  });
1119
+ assignResolvedEnvValue(env, bundle, key, value, opts.keyMode, owners);
952
1120
  }
953
1121
  catch (err) {
954
1122
  throw new Error(`Bundle '${bundle.name}' key '${key}': ${err.message}`);
@@ -967,7 +1135,8 @@ export function readAndResolveBundleEnv(name, opts = {}) {
967
1135
  !opts.noAgent &&
968
1136
  process.env.AGENTS_SECRETS_NO_AGENT !== '1' &&
969
1137
  bundlePolicy(bundle) === 'daily' &&
970
- secretsAgentAutoEnabled()) {
1138
+ secretsAgentAutoEnabled() &&
1139
+ canCacheResolvedEnv(bundle, selectedKeys, opts.keyMode)) {
971
1140
  agentAutoLoadSync(name, bundle, env, secretsHoldMs());
972
1141
  }
973
1142
  return { bundle, env };
@@ -1127,7 +1296,7 @@ export function parseDotenv(content) {
1127
1296
  (value.startsWith("'") && value.endsWith("'"))) {
1128
1297
  value = value.slice(1, -1);
1129
1298
  }
1130
- if (ENV_KEY_PATTERN.test(key)) {
1299
+ if (BUNDLE_KEY_PATTERN.test(key)) {
1131
1300
  out[key] = value;
1132
1301
  }
1133
1302
  }
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * The item-name scheme is the same one the modern store uses (see bundles.ts):
13
13
  * metadata under `agents-cli.bundles.<name>`, one value per key under
14
- * `agents-cli.secrets.<bundle>.<key>`. Env keys can never contain a dot
15
- * (ENV_KEY_PATTERN), so splitting a secret service at its LAST dot recovers
14
+ * `agents-cli.secrets.<bundle>.<key>`. Legacy env keys never contained a dot,
15
+ * so metadata-less synced secret services split at their LAST dot to recover
16
16
  * the bundle/key boundary even for dotted bundle names like `hetzner.com`.
17
17
  */
18
18
  import { type SecretsBackend } from './bundles.js';
@@ -11,12 +11,12 @@
11
11
  *
12
12
  * The item-name scheme is the same one the modern store uses (see bundles.ts):
13
13
  * metadata under `agents-cli.bundles.<name>`, one value per key under
14
- * `agents-cli.secrets.<bundle>.<key>`. Env keys can never contain a dot
15
- * (ENV_KEY_PATTERN), so splitting a secret service at its LAST dot recovers
14
+ * `agents-cli.secrets.<bundle>.<key>`. Legacy env keys never contained a dot,
15
+ * so metadata-less synced secret services split at their LAST dot to recover
16
16
  * the bundle/key boundary even for dotted bundle names like `hetzner.com`.
17
17
  */
18
18
  import { deleteSyncedKeychainItem, getSyncedKeychainTokens, listSyncedKeychainItems, parseBundleValue, secretsKeychainItem, serializeRef, SECRETS_ITEM_PREFIX, } from './index.js';
19
- import { BUNDLE_META_PREFIX, BUNDLE_NAME_PATTERN, ENV_KEY_PATTERN, bundleExists, bundleItemStore, bundlePolicy, isLoaderOrInterpreterEnv, isReservedEnvName, keychainRef, readBundle, writeBundle, } from './bundles.js';
19
+ import { BUNDLE_META_PREFIX, BUNDLE_NAME_PATTERN, BUNDLE_KEY_PATTERN, ENV_KEY_PATTERN, bundleKeyToEnvKey, bundleExists, bundleItemStore, bundlePolicy, isLoaderOrInterpreterEnv, isReservedEnvName, keychainRef, readBundle, writeBundle, } from './bundles.js';
20
20
  /**
21
21
  * Group raw synced service names into per-bundle candidates. Pure — separated
22
22
  * from discovery so the parsing rules are unit-testable without a keychain.
@@ -88,7 +88,7 @@ export function importSyncedBundle(candidate, opts = {}) {
88
88
  else {
89
89
  bundle = {
90
90
  name: candidate.name,
91
- backend: opts.backend === 'file' ? 'file' : undefined,
91
+ backend: opts.backend === 'keychain' ? undefined : opts.backend,
92
92
  vars: {},
93
93
  };
94
94
  }
@@ -121,7 +121,10 @@ export function importSyncedBundle(candidate, opts = {}) {
121
121
  // these may be purged; a missing or unimportable key's iCloud item is its
122
122
  // only copy and must survive.
123
123
  const purgeable = new Set();
124
- const rejectedByPolicy = (key) => isLoaderOrInterpreterEnv(key) || isReservedEnvName(key);
124
+ const rejectedByPolicy = (key) => {
125
+ const envKey = bundleKeyToEnvKey(key);
126
+ return isLoaderOrInterpreterEnv(envKey) || isReservedEnvName(envKey);
127
+ };
125
128
  // Keys with a synced secret item: re-store the value device-locally.
126
129
  for (const key of candidate.keys) {
127
130
  // The pre-cutover store accepted keys the modern one refuses (writeBundle
@@ -154,7 +157,7 @@ export function importSyncedBundle(candidate, opts = {}) {
154
157
  // carry everything they need; a keychain ref without its synced item is
155
158
  // unrecoverable.
156
159
  for (const [key, raw] of Object.entries(metaVars)) {
157
- if (!ENV_KEY_PATTERN.test(key))
160
+ if (!BUNDLE_KEY_PATTERN.test(key))
158
161
  continue;
159
162
  if (candidate.keys.includes(key))
160
163
  continue; // the secret item already covered it
@@ -70,7 +70,7 @@ export function resolveSecret(bundle, key) {
70
70
  }
71
71
  // The MCP get_secret tool is typically served by a background/headless agent
72
72
  // process; resolve broker-only there so it never raises an unwatched prompt.
73
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'secrets-mcp', agentOnly: isHeadlessSecretsContext() });
73
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'secrets-mcp', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
74
74
  const value = env[key];
75
75
  if (value === undefined) {
76
76
  throw new Error(`Key '${key}' in bundle '${bundle}' could not be resolved.`);
@@ -0,0 +1 @@
1
+ export declare function runVaultAgeHelperCli(): Promise<void>;
@@ -0,0 +1,34 @@
1
+ import { Decrypter, Encrypter, armor } from 'age-encryption';
2
+ async function readStdin() {
3
+ const chunks = [];
4
+ for await (const chunk of process.stdin) {
5
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
6
+ }
7
+ return Buffer.concat(chunks).toString('utf8');
8
+ }
9
+ async function runAge(input) {
10
+ if (input.action === 'encrypt') {
11
+ const encrypter = new Encrypter();
12
+ if (input.scryptWorkFactor !== undefined) {
13
+ encrypter.setScryptWorkFactor(input.scryptWorkFactor);
14
+ }
15
+ encrypter.setPassphrase(input.passphrase);
16
+ return armor.encode(await encrypter.encrypt(input.plaintext ?? ''));
17
+ }
18
+ if (input.action === 'decrypt') {
19
+ const decrypter = new Decrypter();
20
+ decrypter.addPassphrase(input.passphrase);
21
+ return await decrypter.decrypt(armor.decode(input.blob ?? ''), 'text');
22
+ }
23
+ throw new Error('unknown action');
24
+ }
25
+ export async function runVaultAgeHelperCli() {
26
+ try {
27
+ const input = JSON.parse(await readStdin());
28
+ process.stdout.write(await runAge(input));
29
+ }
30
+ catch (err) {
31
+ console.error(err instanceof Error ? err.message : String(err));
32
+ process.exitCode = 1;
33
+ }
34
+ }
@@ -0,0 +1,49 @@
1
+ export interface VaultKey {
2
+ passphrase: string;
3
+ }
4
+ export interface VaultBundleRecord {
5
+ metadata?: string;
6
+ keys: Record<string, string>;
7
+ }
8
+ export interface VaultData {
9
+ v: 1;
10
+ bundles: Record<string, VaultBundleRecord>;
11
+ }
12
+ export declare const VAULT_SESSION_TTL_MS: number;
13
+ export declare const VAULT_SCRYPT_WORK_FACTOR = 18;
14
+ export declare function vaultPath(): string;
15
+ export declare function vaultExists(): boolean;
16
+ export declare function _setVaultPathForTest(filePath: string | null): void;
17
+ export declare function _clearVaultDataCacheForTest(): void;
18
+ export declare function _resetVaultAgeOperationCountForTest(): void;
19
+ export declare function _getVaultAgeOperationCountForTest(): number;
20
+ export declare function _setVaultAgeHelperLaunchForTest(launch: {
21
+ command: string;
22
+ args: string[];
23
+ } | null): void;
24
+ export declare function encrypt(key: VaultKey, json: VaultData): string;
25
+ export declare function decrypt(key: VaultKey, blob: string): VaultData;
26
+ export declare function createVault(password: string, opts?: {
27
+ overwrite?: boolean;
28
+ }): VaultKey;
29
+ export declare function joinVault(password: string, sourcePath: string, opts?: {
30
+ overwrite?: boolean;
31
+ }): VaultKey;
32
+ export declare function unlock(password: string): VaultKey;
33
+ export declare function cacheVaultKey(key: VaultKey, ttlMs?: number): void;
34
+ export declare function clearVaultKey(): void;
35
+ export declare function getVaultSession(): {
36
+ loggedIn: true;
37
+ key: VaultKey;
38
+ expiresAt: number;
39
+ } | {
40
+ loggedIn: false;
41
+ expiresAt?: number;
42
+ };
43
+ export declare function vaultHasItem(item: string): boolean;
44
+ export declare function vaultGetItem(item: string): string;
45
+ export declare function vaultGetItems(items: string[]): Map<string, string>;
46
+ export declare function vaultSetItem(item: string, value: string): void;
47
+ export declare function vaultSetItems(items: Map<string, string>): void;
48
+ export declare function vaultDeleteItem(item: string): boolean;
49
+ export declare function vaultListItems(prefix: string): string[];