@phnx-labs/agents-cli 1.22.84 → 1.22.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +80 -53
  3. package/dist/bootstrap.d.ts +7 -6
  4. package/dist/bootstrap.js +20 -27
  5. package/dist/cli/command-registry.js +5 -1
  6. package/dist/commands/accounts.d.ts +6 -13
  7. package/dist/commands/accounts.js +330 -143
  8. package/dist/commands/alias.js +7 -3
  9. package/dist/commands/apply.js +1 -1
  10. package/dist/commands/auth-mint.d.ts +7 -3
  11. package/dist/commands/auth-mint.js +21 -9
  12. package/dist/commands/auth.js +2 -3
  13. package/dist/commands/browser.js +10 -10
  14. package/dist/commands/daemon.d.ts +7 -3
  15. package/dist/commands/daemon.js +40 -25
  16. package/dist/commands/doctor.js +28 -2
  17. package/dist/commands/exec.js +87 -39
  18. package/dist/commands/fleet-capture.js +21 -2
  19. package/dist/commands/harness-wizard.js +2 -2
  20. package/dist/commands/lease.js +7 -12
  21. package/dist/commands/profiles.d.ts +2 -2
  22. package/dist/commands/profiles.js +20 -13
  23. package/dist/commands/repo.js +3 -3
  24. package/dist/commands/run-account-picker.d.ts +3 -3
  25. package/dist/commands/run-account-picker.js +29 -30
  26. package/dist/commands/secrets-passthrough.d.ts +20 -0
  27. package/dist/commands/secrets-passthrough.js +43 -0
  28. package/dist/commands/setup-accounts.d.ts +3 -2
  29. package/dist/commands/setup-accounts.js +4 -4
  30. package/dist/commands/setup-secrets.d.ts +16 -21
  31. package/dist/commands/setup-secrets.js +43 -224
  32. package/dist/commands/ssh.js +2 -2
  33. package/dist/commands/sync.js +1 -1
  34. package/dist/commands/update.d.ts +10 -0
  35. package/dist/commands/update.js +43 -40
  36. package/dist/commands/versions.d.ts +11 -11
  37. package/dist/commands/versions.js +131 -83
  38. package/dist/commands/view.d.ts +1 -9
  39. package/dist/commands/view.js +41 -55
  40. package/dist/commands/webhook.js +5 -5
  41. package/dist/commands/workflows.js +2 -1
  42. package/dist/index.d.ts +12 -12
  43. package/dist/index.js +13 -42
  44. package/dist/lib/account-capabilities.d.ts +1 -1
  45. package/dist/lib/account-capabilities.js +1 -1
  46. package/dist/lib/account-catalog.d.ts +121 -6
  47. package/dist/lib/account-catalog.js +421 -7
  48. package/dist/lib/account-registry.d.ts +8 -30
  49. package/dist/lib/account-registry.js +36 -79
  50. package/dist/lib/account-schema.d.ts +1 -1
  51. package/dist/lib/account-schema.js +1 -1
  52. package/dist/lib/accounting/account-pool-collect.js +16 -4
  53. package/dist/lib/accounting/rotate.d.ts +20 -7
  54. package/dist/lib/accounting/rotate.js +89 -13
  55. package/dist/lib/accounting/usage.js +11 -16
  56. package/dist/lib/accounts/add.d.ts +138 -0
  57. package/dist/lib/accounts/add.js +651 -0
  58. package/dist/lib/accounts/migrate.d.ts +117 -0
  59. package/dist/lib/accounts/migrate.js +536 -0
  60. package/dist/lib/accounts/slots.d.ts +13 -0
  61. package/dist/lib/accounts/slots.js +100 -0
  62. package/dist/lib/agent-spec/agents.d.ts +16 -0
  63. package/dist/lib/agent-spec/agents.js +38 -4
  64. package/dist/lib/app-bundle-install.js +5 -4
  65. package/dist/lib/auth-health.d.ts +3 -0
  66. package/dist/lib/auth-health.js +11 -0
  67. package/dist/lib/auth-mint.d.ts +52 -16
  68. package/dist/lib/auth-mint.js +117 -39
  69. package/dist/lib/browser/chrome.d.ts +1 -1
  70. package/dist/lib/browser/chrome.js +5 -5
  71. package/dist/lib/byok-usage.js +3 -3
  72. package/dist/lib/claude-account-token.d.ts +47 -3
  73. package/dist/lib/claude-account-token.js +172 -50
  74. package/dist/lib/cloud/antigravity.js +4 -4
  75. package/dist/lib/cloud/cursor.js +4 -4
  76. package/dist/lib/crabbox/cli.d.ts +1 -1
  77. package/dist/lib/crabbox/cli.js +6 -6
  78. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  79. package/dist/lib/crabbox/runtimes.js +5 -5
  80. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  81. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  82. package/dist/lib/daemon/auth-sync-service.js +32 -1
  83. package/dist/lib/daemon/daemon.d.ts +6 -16
  84. package/dist/lib/daemon/daemon.js +52 -97
  85. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  86. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  87. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  88. package/dist/lib/daemon/harness-update-service.js +1 -1
  89. package/dist/lib/daemon/runner.d.ts +15 -3
  90. package/dist/lib/daemon/runner.js +49 -24
  91. package/dist/lib/daemon-health.d.ts +1 -2
  92. package/dist/lib/daemon-health.js +7 -6
  93. package/dist/lib/daemon-services.d.ts +1 -1
  94. package/dist/lib/daemon-services.js +1 -11
  95. package/dist/lib/daemon-webhooks.d.ts +8 -7
  96. package/dist/lib/daemon-webhooks.js +10 -9
  97. package/dist/lib/device-config.js +1 -1
  98. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  99. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  100. package/dist/lib/devices/harness-inventory.js +126 -4
  101. package/dist/lib/doctor-diff.js +2 -1
  102. package/dist/lib/exec-account-home.d.ts +38 -0
  103. package/dist/lib/exec-account-home.js +164 -0
  104. package/dist/lib/exec.d.ts +30 -0
  105. package/dist/lib/exec.js +101 -31
  106. package/dist/lib/fleet/apply.d.ts +2 -2
  107. package/dist/lib/fleet/apply.js +4 -4
  108. package/dist/lib/fleet/auth-sync.js +4 -3
  109. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  110. package/dist/lib/harness/adapter.d.ts +7 -7
  111. package/dist/lib/harness/adapter.js +10 -6
  112. package/dist/lib/harness/adapters/grok.js +8 -3
  113. package/dist/lib/harness/adapters/muse.js +1 -1
  114. package/dist/lib/harness/adapters/opencode.js +12 -3
  115. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  116. package/dist/lib/harness-auth-capabilities.js +52 -0
  117. package/dist/lib/helper-versions.d.ts +6 -4
  118. package/dist/lib/helper-versions.js +5 -4
  119. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  120. package/dist/lib/hosts/credential-transport.js +37 -0
  121. package/dist/lib/hosts/dispatch.d.ts +7 -0
  122. package/dist/lib/hosts/dispatch.js +13 -6
  123. package/dist/lib/identity/client.d.ts +3 -3
  124. package/dist/lib/identity/client.js +3 -3
  125. package/dist/lib/installations/index.d.ts +1 -1
  126. package/dist/lib/installations/index.js +1 -1
  127. package/dist/lib/installations/migrate.js +9 -0
  128. package/dist/lib/installations/resolve.js +1 -1
  129. package/dist/lib/installations/shims.d.ts +21 -4
  130. package/dist/lib/installations/shims.js +86 -5
  131. package/dist/lib/installations/store.d.ts +43 -0
  132. package/dist/lib/installations/store.js +75 -7
  133. package/dist/lib/installations/versions.js +1 -1
  134. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  135. package/dist/lib/menubar/install-menubar.js +3 -3
  136. package/dist/lib/native-accounts.d.ts +35 -0
  137. package/dist/lib/native-accounts.js +41 -0
  138. package/dist/lib/net-close.d.ts +11 -0
  139. package/dist/lib/net-close.js +25 -0
  140. package/dist/lib/openclaw-keychain.js +27 -1
  141. package/dist/lib/profiles.js +22 -9
  142. package/dist/lib/project-resources.js +1 -1
  143. package/dist/lib/reserved-stores.d.ts +53 -0
  144. package/dist/lib/reserved-stores.js +120 -0
  145. package/dist/lib/secrets-client.d.ts +199 -0
  146. package/dist/lib/secrets-client.js +718 -0
  147. package/dist/lib/secrets-policy.d.ts +235 -0
  148. package/dist/lib/secrets-policy.js +505 -0
  149. package/dist/lib/secrets-types.d.ts +188 -0
  150. package/dist/lib/secrets-types.js +24 -0
  151. package/dist/lib/self-heal/checks/shims.js +5 -3
  152. package/dist/lib/service-manifest.js +3 -4
  153. package/dist/lib/session/db.d.ts +18 -0
  154. package/dist/lib/session/db.js +47 -14
  155. package/dist/lib/session/sync/config.js +2 -2
  156. package/dist/lib/sha256-asset.d.ts +11 -16
  157. package/dist/lib/sha256-asset.js +11 -16
  158. package/dist/lib/share/config.js +14 -5
  159. package/dist/lib/signin-badge.d.ts +26 -0
  160. package/dist/lib/signin-badge.js +42 -0
  161. package/dist/lib/staleness/detectors/workflows.js +3 -105
  162. package/dist/lib/staleness/writers/workflows.js +2 -1
  163. package/dist/lib/state.d.ts +7 -4
  164. package/dist/lib/state.js +18 -7
  165. package/dist/lib/sync-umbrella.js +2 -2
  166. package/dist/lib/teams/agents.js +1 -1
  167. package/dist/lib/types.d.ts +74 -33
  168. package/dist/lib/view-types.d.ts +6 -3
  169. package/dist/lib/workflows-registry.d.ts +71 -0
  170. package/dist/lib/workflows-registry.js +280 -0
  171. package/dist/lib/workflows.d.ts +21 -21
  172. package/dist/lib/workflows.js +12 -327
  173. package/package.json +2 -3
  174. package/scripts/postinstall.js +35 -30
  175. package/dist/commands/secrets-import.d.ts +0 -18
  176. package/dist/commands/secrets-import.js +0 -74
  177. package/dist/commands/secrets-migrate.d.ts +0 -25
  178. package/dist/commands/secrets-migrate.js +0 -334
  179. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  180. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  181. package/dist/commands/secrets-sync.d.ts +0 -11
  182. package/dist/commands/secrets-sync.js +0 -153
  183. package/dist/commands/secrets-vault.d.ts +0 -10
  184. package/dist/commands/secrets-vault.js +0 -130
  185. package/dist/commands/secrets.d.ts +0 -212
  186. package/dist/commands/secrets.js +0 -3030
  187. package/dist/lib/accounts/connect.d.ts +0 -176
  188. package/dist/lib/accounts/connect.js +0 -453
  189. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  190. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  191. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  192. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  193. package/dist/lib/secrets/agent.d.ts +0 -358
  194. package/dist/lib/secrets/agent.js +0 -1291
  195. package/dist/lib/secrets/audit.d.ts +0 -46
  196. package/dist/lib/secrets/audit.js +0 -101
  197. package/dist/lib/secrets/bundles.d.ts +0 -290
  198. package/dist/lib/secrets/bundles.js +0 -1547
  199. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  200. package/dist/lib/secrets/download-keychain.js +0 -70
  201. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  202. package/dist/lib/secrets/drivers/rush.js +0 -90
  203. package/dist/lib/secrets/fallback.d.ts +0 -48
  204. package/dist/lib/secrets/fallback.js +0 -48
  205. package/dist/lib/secrets/filestore.d.ts +0 -222
  206. package/dist/lib/secrets/filestore.js +0 -1099
  207. package/dist/lib/secrets/headless.d.ts +0 -43
  208. package/dist/lib/secrets/headless.js +0 -56
  209. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  210. package/dist/lib/secrets/icloud-import.js +0 -206
  211. package/dist/lib/secrets/index.d.ts +0 -451
  212. package/dist/lib/secrets/index.js +0 -1568
  213. package/dist/lib/secrets/install-helper.d.ts +0 -72
  214. package/dist/lib/secrets/install-helper.js +0 -245
  215. package/dist/lib/secrets/lease.d.ts +0 -25
  216. package/dist/lib/secrets/lease.js +0 -44
  217. package/dist/lib/secrets/linux.d.ts +0 -77
  218. package/dist/lib/secrets/linux.js +0 -393
  219. package/dist/lib/secrets/list-filter.d.ts +0 -109
  220. package/dist/lib/secrets/list-filter.js +0 -261
  221. package/dist/lib/secrets/mcp.d.ts +0 -93
  222. package/dist/lib/secrets/mcp.js +0 -211
  223. package/dist/lib/secrets/profiles.d.ts +0 -10
  224. package/dist/lib/secrets/profiles.js +0 -13
  225. package/dist/lib/secrets/push.d.ts +0 -133
  226. package/dist/lib/secrets/push.js +0 -273
  227. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  228. package/dist/lib/secrets/rc-hygiene.js +0 -143
  229. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  230. package/dist/lib/secrets/read-backoff.js +0 -64
  231. package/dist/lib/secrets/reaper.d.ts +0 -90
  232. package/dist/lib/secrets/reaper.js +0 -243
  233. package/dist/lib/secrets/remote.d.ts +0 -152
  234. package/dist/lib/secrets/remote.js +0 -344
  235. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  236. package/dist/lib/secrets/reserved-sync.js +0 -147
  237. package/dist/lib/secrets/scope.d.ts +0 -26
  238. package/dist/lib/secrets/scope.js +0 -29
  239. package/dist/lib/secrets/session-store.d.ts +0 -107
  240. package/dist/lib/secrets/session-store.js +0 -342
  241. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  242. package/dist/lib/secrets/sync-backend.js +0 -13
  243. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  244. package/dist/lib/secrets/sync-commands.js +0 -21
  245. package/dist/lib/secrets/sync.d.ts +0 -48
  246. package/dist/lib/secrets/sync.js +0 -237
  247. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  248. package/dist/lib/secrets/unlock-hints.js +0 -36
  249. package/dist/lib/secrets/usage-db.d.ts +0 -46
  250. package/dist/lib/secrets/usage-db.js +0 -96
  251. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  252. package/dist/lib/secrets/vault-age-helper.js +0 -34
  253. package/dist/lib/secrets/vault.d.ts +0 -49
  254. package/dist/lib/secrets/vault.js +0 -399
  255. package/dist/lib/secrets/windows.d.ts +0 -81
  256. package/dist/lib/secrets/windows.js +0 -556
  257. package/scripts/install-helper.js +0 -97
  258. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  259. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -1,334 +0,0 @@
1
- /**
2
- * `agents secrets migrate-acl` — refresh existing keychain items so they pick
3
- * up the new SecAccess ACL written by the signed Agents CLI.app helper.
4
- *
5
- * Items created before 1.19.2 (or by `security add-generic-password` directly)
6
- * may carry the legacy "this-app-only" ACL that prompts the user for a
7
- * password on every read. Re-writing them through the helper bakes in the
8
- * empty trusted-app ACL that suppresses the prompt and lets the helper read
9
- * them under LocalAuthentication instead.
10
- *
11
- * Sequence per item:
12
- * 1. Read the current value (no auth prompt path — uses the unauthenticated
13
- * `security` CLI for non-sync items, helper `get` for sync items).
14
- * 2. Append (item, value, sync) to an encrypted backup before any writes.
15
- * 3. Delete + rewrite via the helper so macOS hands us a fresh ACL on the
16
- * new item.
17
- * 4. Read back via the helper to verify the value round-trips.
18
- *
19
- * `--dry-run` (default) reports the planned actions. `--commit` performs the
20
- * writes and produces the backup.
21
- */
22
- import chalk from 'chalk';
23
- import * as crypto from 'crypto';
24
- import * as fs from 'fs';
25
- import * as path from 'path';
26
- import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, rekeyServiceNames, rekeyStatus, setKeychainToken, withRawKeychainServiceNames, } from '../lib/secrets/index.js';
27
- import { getBackupsDir } from '../lib/state.js';
28
- import { encryptBlob, MIN_PASSPHRASE_LEN } from '../lib/secrets/sync.js';
29
- import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
30
- const ITEM_PREFIX = 'agents-cli.';
31
- function enumerateItems() {
32
- const seen = new Map();
33
- // Raw scope: operate on the literal enumerated names (see #316).
34
- withRawKeychainServiceNames(() => {
35
- for (const item of listKeychainItems(ITEM_PREFIX)) {
36
- // hasKeychainToken with sync=false probes the non-synced keychain; the
37
- // helper's list returns both. We don't try to distinguish — re-write with
38
- // sync=false by default and only flip to sync=true if the value is only
39
- // readable via the synced-only probe.
40
- const localExists = hasKeychainToken(item);
41
- seen.set(item, { item, sync: !localExists });
42
- }
43
- });
44
- return [...seen.values()];
45
- }
46
- function writeEncryptedBackup(records, passphrase) {
47
- const dir = getBackupsDir();
48
- fs.mkdirSync(dir, { recursive: true });
49
- const iso = new Date().toISOString().replace(/[:.]/g, '-');
50
- const file = path.join(dir, `keychain-pre-migrate-${iso}.json.enc`);
51
- const envelope = encryptBlob(JSON.stringify({ v: 1, records }), passphrase);
52
- fs.writeFileSync(file, JSON.stringify(envelope), { mode: 0o600 });
53
- return file;
54
- }
55
- async function promptPassphrase() {
56
- if (!isInteractiveTerminal()) {
57
- throw new Error('A backup passphrase is required. Run from a TTY or set AGENTS_BACKUP_PASSPHRASE.');
58
- }
59
- const { password } = await import('@inquirer/prompts');
60
- const first = await password({ message: 'Backup passphrase (used to encrypt the pre-migration snapshot)', mask: true });
61
- if (first.length < MIN_PASSPHRASE_LEN)
62
- throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
63
- const second = await password({ message: 'Confirm passphrase', mask: true });
64
- if (first !== second)
65
- throw new Error('Passphrases do not match.');
66
- return first;
67
- }
68
- function migrateOne(record) {
69
- // Raw scope: `item` is a literal service name enumerated from the helper —
70
- // possibly a pre-re-key cleartext leftover — and this rewrite must target
71
- // exactly that item, never its hashed transform (see #316).
72
- return withRawKeychainServiceNames(() => migrateOneRaw(record));
73
- }
74
- function migrateOneRaw(record) {
75
- const { item, value } = record;
76
- // Delete + re-add to force macOS to bind a fresh ACL on the new item.
77
- // SecItemUpdate preserves the existing ACL, so an in-place rewrite would
78
- // not fix the legacy "enter password" prompt.
79
- try {
80
- deleteKeychainToken(item);
81
- }
82
- catch (err) {
83
- return { item, status: 'write-failed', detail: `delete: ${err.message}` };
84
- }
85
- try {
86
- setKeychainToken(item, value);
87
- }
88
- catch (err) {
89
- // Try to restore the old value so we don't lose data on a write failure.
90
- // The backup is the durable safety net; this is best-effort UX.
91
- try {
92
- setKeychainToken(item, value);
93
- }
94
- catch { /* swallow */ }
95
- return { item, status: 'write-failed', detail: `set: ${err.message}` };
96
- }
97
- let readBack;
98
- try {
99
- readBack = getKeychainToken(item);
100
- }
101
- catch (err) {
102
- return { item, status: 'verify-failed', detail: `read-back: ${err.message}` };
103
- }
104
- if (readBack !== value) {
105
- return { item, status: 'verify-failed', detail: 'value mismatch after rewrite' };
106
- }
107
- return { item, status: 'ok' };
108
- }
109
- /** Register the keychain-migration commands (`migrate-acl`, `rekey`) on the
110
- * parent secrets Command. */
111
- export function registerSecretsMigrateAclCommand(secrets) {
112
- registerSecretsRekeyCommand(secrets);
113
- secrets
114
- .command('migrate-acl')
115
- .description('Refresh legacy keychain ACLs and re-home items stranded in a stale access group. Dry-run by default.')
116
- .option('--commit', 'Perform writes (default is dry-run reporting only)')
117
- .option('--prefix <p>', `Restrict to items beginning with PREFIX (default ${ITEM_PREFIX})`, ITEM_PREFIX)
118
- .option('--all', 'Rewrite EVERY matching item, not just legacy stragglers (slower; a Touch ID prompt per item)')
119
- .option('--passphrase-env <var>', 'Read the backup passphrase from this env var instead of prompting')
120
- .action(async (opts) => {
121
- try {
122
- if (process.platform !== 'darwin') {
123
- throw new Error('migrate-acl is macOS-only. Linux items already use the keyring-native ACL model.');
124
- }
125
- const prefix = opts.prefix ?? ITEM_PREFIX;
126
- if (!prefix.startsWith(ITEM_PREFIX)) {
127
- throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${prefix}').`);
128
- }
129
- // Two independent classes of work under one command:
130
- // (a) Legacy ACL stragglers — items still in the file-based keychain
131
- // with a pre-migration trusted-app ACL. Rewritten value-by-value.
132
- // (b) Orphaned access groups — data-protection items filed under a
133
- // pre-#279 non-concrete group, invisible to the pinned queries.
134
- // Re-homed by the helper behind a single Touch ID.
135
- // Default: only legacy stragglers for (a); `--all` forces a full rewrite.
136
- // Raw scope: migrate-acl operates on the literal names the helper
137
- // enumerates (which may include pre-re-key cleartext leftovers) — the
138
- // #316 name-hashing transform must not rewrite them.
139
- const names = opts.all
140
- ? withRawKeychainServiceNames(() => listKeychainItems(prefix))
141
- : listLegacyKeychainItems(prefix);
142
- const items = names.map((item) => {
143
- const localExists = withRawKeychainServiceNames(() => hasKeychainToken(item));
144
- return { item, sync: !localExists };
145
- });
146
- const orphans = listOrphanedKeychainItems(prefix);
147
- if (items.length === 0 && orphans.length === 0) {
148
- console.log(opts.all
149
- ? chalk.gray(`No keychain items with prefix '${prefix}'.`)
150
- : chalk.green(`Nothing to migrate — all items under '${prefix}' use the modern ACL and access group.`));
151
- return;
152
- }
153
- const label = opts.all ? 'item' : 'legacy item';
154
- if (items.length > 0) {
155
- console.log(chalk.bold(`Found ${items.length} ${label}(s) under '${prefix}' with an outdated ACL.`));
156
- }
157
- if (orphans.length > 0) {
158
- console.log(chalk.bold(`Found ${orphans.length} orphaned item(s) under '${prefix}' in a stale access group.`));
159
- }
160
- if (!opts.commit) {
161
- for (const { item, sync } of items) {
162
- console.log(` ${chalk.cyan(item)} ${chalk.gray(sync ? '(synced)' : '(local)')}`);
163
- }
164
- for (const item of orphans) {
165
- console.log(` ${chalk.cyan(item)} ${chalk.yellow('(orphaned access group)')}`);
166
- }
167
- console.log();
168
- console.log(chalk.gray('Dry-run — pass --commit to perform the migration.'));
169
- return;
170
- }
171
- // ---- Commit phase ----
172
- let okCount = 0;
173
- let failCount = 0;
174
- // (a) Legacy ACL rewrite. Snapshot every value first (one batched read
175
- // behind a single helper process), encrypt, then delete + re-add. A total
176
- // read failure here counts the legacy items as failed but does NOT abort —
177
- // the orphan sweep (b) is independent and must still run.
178
- if (items.length > 0) {
179
- const fetched = withRawKeychainServiceNames(() => getKeychainTokens(items.map((i) => i.item)));
180
- const records = [];
181
- for (const { item, sync } of items) {
182
- const value = fetched.get(item);
183
- if (value === undefined) {
184
- console.error(chalk.red(`Skipping '${item}': read failed or item absent.`));
185
- continue;
186
- }
187
- records.push({ item, sync, value });
188
- }
189
- if (records.length === 0) {
190
- console.error(chalk.red('No legacy items could be read — skipping legacy rewrite.'));
191
- failCount += items.length;
192
- }
193
- else {
194
- const passphrase = opts.passphraseEnv
195
- ? (() => {
196
- const v = process.env[opts.passphraseEnv];
197
- if (!v)
198
- throw new Error(`Env var '${opts.passphraseEnv}' not set.`);
199
- if (v.length < MIN_PASSPHRASE_LEN)
200
- throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
201
- return v;
202
- })()
203
- : await promptPassphrase();
204
- const backupPath = writeEncryptedBackup(records, passphrase);
205
- // Quick fingerprint so the user can sanity-check recovery without decrypting.
206
- const fingerprint = crypto
207
- .createHash('sha256')
208
- .update(records.length + '\n' + records.map((r) => r.item).sort().join('\n'))
209
- .digest('hex')
210
- .slice(0, 12);
211
- console.log(chalk.green(`Encrypted backup written to ${backupPath} (sha256-12: ${fingerprint}).`));
212
- const results = [];
213
- for (const record of records) {
214
- const r = migrateOne(record);
215
- results.push(r);
216
- if (r.status === 'ok') {
217
- console.log(` ${chalk.green('ok')} ${record.item}`);
218
- }
219
- else {
220
- console.log(` ${chalk.red(r.status)} ${record.item} ${chalk.gray(r.detail ?? '')}`);
221
- }
222
- }
223
- const ok = results.filter((r) => r.status === 'ok').length;
224
- okCount += ok;
225
- failCount += results.length - ok;
226
- if (results.length - ok > 0) {
227
- console.error(chalk.gray(`Restore from ${backupPath} using the backup passphrase if needed.`));
228
- }
229
- }
230
- }
231
- // (b) Orphan re-home — one helper call, one Touch ID for the batch. The
232
- // helper adds the pinned copy before deleting the orphan, so no pre-write
233
- // backup is needed (a failed add leaves the orphan intact and readable).
234
- if (orphans.length > 0) {
235
- console.log(chalk.bold(`Re-homing ${orphans.length} orphaned item(s) into the current access group (one Touch ID)…`));
236
- const results = migrateOrphanedKeychainItems(prefix);
237
- const healed = new Set();
238
- for (const r of results) {
239
- healed.add(r.item);
240
- if (r.status === 'ok') {
241
- okCount += 1;
242
- console.log(` ${chalk.green('ok')} ${r.item}`);
243
- }
244
- else {
245
- failCount += 1;
246
- console.log(` ${chalk.red(r.status)} ${r.item} ${chalk.gray(r.detail ?? '')}`);
247
- }
248
- }
249
- // Any orphan we listed but the helper couldn't reach (e.g. under a
250
- // different signing team) — surface it, never drop it silently.
251
- for (const item of orphans) {
252
- if (!healed.has(item)) {
253
- failCount += 1;
254
- console.log(` ${chalk.red('fail')} ${item} ${chalk.gray('unreachable (different signing team?)')}`);
255
- }
256
- }
257
- }
258
- const total = okCount + failCount;
259
- console.log();
260
- if (failCount === 0) {
261
- console.log(chalk.green(`Migrated ${okCount}/${total} item(s).`));
262
- }
263
- else {
264
- console.error(chalk.yellow(`Migrated ${okCount}/${total} item(s); ${failCount} failed.`));
265
- process.exit(1);
266
- }
267
- }
268
- catch (err) {
269
- if (isPromptCancelled(err))
270
- return;
271
- console.error(chalk.red(err.message));
272
- process.exit(1);
273
- }
274
- });
275
- }
276
- /**
277
- * `agents secrets rekey` — the one-time #316 migration: replace enumerable
278
- * cleartext keychain service names (`agents-cli.secrets.<bundle>.<KEY>`, …)
279
- * with opaque HMAC-hashed names. Normally runs automatically on the first
280
- * interactive keychain use after upgrade; this command exists for headless
281
- * machines, retrying after a cancelled Touch ID, and prefix-restricted test
282
- * runs. Idempotent — safe to re-run any time.
283
- */
284
- function registerSecretsRekeyCommand(secrets) {
285
- secrets
286
- .command('rekey')
287
- .description('Replace enumerable keychain service names with opaque HMAC-hashed names (one-time; idempotent). macOS only.')
288
- .option('--status', 'Report re-key state without changing anything')
289
- .option('--prefix <p...>', 'Restrict to cleartext services starting with PREFIX (partial run: moves matching items but does NOT activate hashed naming)')
290
- .action((opts) => {
291
- try {
292
- if (process.platform !== 'darwin') {
293
- throw new Error('secrets rekey is macOS-only. Linux/Windows service names are not enumerable via the keychain helper.');
294
- }
295
- if (opts.status) {
296
- const st = rekeyStatus();
297
- console.log(st.migrated
298
- ? chalk.green('Hashed service names: ACTIVE')
299
- : chalk.yellow('Hashed service names: not yet active'));
300
- console.log(` HMAC key present: ${st.hasKey ? 'yes' : 'no'}`);
301
- if (!st.enumerationOk) {
302
- console.log(chalk.red(' Keychain enumeration unavailable (locked keybag?) — counts below are unreliable.'));
303
- }
304
- console.log(` Cleartext-named items remaining: ${st.cleartext.length}`);
305
- if (st.pendingDeletes > 0) {
306
- console.log(chalk.yellow(` Pending deletes from an interrupted run: ${st.pendingDeletes} (finished automatically on next use)`));
307
- }
308
- return;
309
- }
310
- for (const p of opts.prefix ?? []) {
311
- if (!p.startsWith(ITEM_PREFIX)) {
312
- throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${p}').`);
313
- }
314
- }
315
- const report = rekeyServiceNames({
316
- prefixes: opts.prefix,
317
- announce: true,
318
- log: (line) => console.log(line),
319
- });
320
- if (report.failed.length > 0)
321
- process.exit(1);
322
- if (report.nothingToDo && report.activated) {
323
- console.log(chalk.green('Nothing to re-key — hashed service names are active.'));
324
- }
325
- }
326
- catch (err) {
327
- const msg = err.message;
328
- console.error(chalk.red(msg));
329
- // Touch ID cancel exits 4 (matching the helper's contract); re-running
330
- // resumes where it left off.
331
- process.exit(/touch id cancelled/i.test(msg) ? 4 : 1);
332
- }
333
- });
334
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * `agents secrets rotate-passphrase` — rotate the encrypted file store's
3
- * machine-local master passphrase (RUSH-1975).
4
- *
5
- * Re-encrypts every `<item>.enc` under a freshly generated key and rewrites the
6
- * 0600 key file in place, atomically: the new store is staged, verified
7
- * (round-trip + count), fsync'd, then swapped by directory rename, so a crash
8
- * leaves the old store intact and readable. Headless-safe and Linux-first — the
9
- * remediation path for a compromised passphrase (RUSH-1968), where the two
10
- * supported alternatives are unacceptable (a hand-rolled non-atomic script, or
11
- * export-to-plaintext which is the exposure being fixed).
12
- *
13
- * Dry-run by default, matching `import-keyring`; `--commit` performs the swap.
14
- */
15
- import type { Command } from 'commander';
16
- /** Register `agents secrets rotate-passphrase` on the parent secrets Command. */
17
- export declare function registerSecretsRotatePassphraseCommand(secrets: Command): void;
@@ -1,96 +0,0 @@
1
- /**
2
- * `agents secrets rotate-passphrase` — rotate the encrypted file store's
3
- * machine-local master passphrase (RUSH-1975).
4
- *
5
- * Re-encrypts every `<item>.enc` under a freshly generated key and rewrites the
6
- * 0600 key file in place, atomically: the new store is staged, verified
7
- * (round-trip + count), fsync'd, then swapped by directory rename, so a crash
8
- * leaves the old store intact and readable. Headless-safe and Linux-first — the
9
- * remediation path for a compromised passphrase (RUSH-1968), where the two
10
- * supported alternatives are unacceptable (a hand-rolled non-atomic script, or
11
- * export-to-plaintext which is the exposure being fixed).
12
- *
13
- * Dry-run by default, matching `import-keyring`; `--commit` performs the swap.
14
- */
15
- import chalk from 'chalk';
16
- import { machinePassphraseExists, rotatePassphrase } from '../lib/secrets/filestore.js';
17
- import { agentStatus } from '../lib/secrets/agent.js';
18
- /** Register `agents secrets rotate-passphrase` on the parent secrets Command. */
19
- export function registerSecretsRotatePassphraseCommand(secrets) {
20
- secrets
21
- .command('rotate-passphrase')
22
- .description('Re-key the encrypted file store under a new machine-local passphrase (atomic, headless-safe). Dry-run by default.')
23
- .option('--commit', 'Perform the rotation (default is dry-run reporting only)')
24
- .option('--dry-run', 'Report bundle count and round-trip result without re-keying (the default). Still heals an interrupted rotation — that is the one thing it writes.')
25
- .option('--force', 'Override the safety refusals (held broker unlocks, or a passphrase exported in the environment)')
26
- .addHelpText('after', `
27
- Rotates the auto-provisioned file-store key at ~/.agents/.secrets-key/passphrase:
28
- decrypts every item under the current key, re-encrypts under a new one, verifies
29
- every item round-trips, then swaps the store and key file atomically. A crash
30
- before the swap leaves the old store readable with the old key; a crash inside the
31
- swap self-heals on the next rotate-passphrase run (not on an ordinary get). No
32
- plaintext secret value or passphrase is ever written to disk, argv, or a log.
33
-
34
- Examples:
35
- # Report what would rotate (no writes)
36
- agents secrets rotate-passphrase
37
-
38
- # Perform the rotation
39
- agents secrets rotate-passphrase --commit`)
40
- .action(async (opts) => {
41
- try {
42
- if (opts.commit && opts.dryRun) {
43
- throw new Error('--commit and --dry-run are mutually exclusive.');
44
- }
45
- const dryRun = !opts.commit;
46
- if (!machinePassphraseExists()) {
47
- throw new Error('No machine-local passphrase is provisioned on this box, so there is nothing to rotate. ' +
48
- 'This command re-keys the file store\'s auto-provisioned key at ~/.agents/.secrets-key/passphrase.');
49
- }
50
- // Guard 1: refuse while the secrets-agent holds live unlocks (macOS), so a
51
- // concurrent read cannot land against a half-swapped store. --force overrides.
52
- if (!dryRun && !opts.force) {
53
- const held = await agentStatus();
54
- if (held.length > 0) {
55
- throw new Error(`The secrets-agent is holding ${held.length} unlocked bundle(s) (${held.map((e) => e.name).join(', ')}). ` +
56
- 'Lock them first (`agents secrets lock --all`) so no concurrent read races the rotation, or pass --force.');
57
- }
58
- }
59
- // Guard 2: a passphrase exported in the environment shadows the on-disk key
60
- // file with a now-stale value after the rotation, breaking every read. This
61
- // is the exact RUSH-1968 footgun — refuse the commit and point at the fix.
62
- if (!dryRun && !opts.force && (process.env.AGENTS_SECRETS_PASSPHRASE ?? '').length > 0) {
63
- throw new Error('AGENTS_SECRETS_PASSPHRASE is set in this environment; it would shadow the rotated key file ' +
64
- 'with a stale value and break every read. Unset it (this is the RUSH-1968 fix) before rotating, or pass --force.');
65
- }
66
- const report = rotatePassphrase({ dryRun });
67
- console.log(`${chalk.bold(String(report.bundleCount))} item(s) decrypt under the current key and ` +
68
- `${report.roundTripOk ? chalk.green('round-trip cleanly') : chalk.red('failed to round-trip')} under a new key.`);
69
- if (report.skipped.length > 0) {
70
- console.log(chalk.gray(`skipped ${report.skipped.length} orphan file(s) (not re-keyed):`));
71
- for (const s of report.skipped)
72
- console.log(chalk.gray(` - ${s}`));
73
- }
74
- if (report.dryRun) {
75
- if (report.recoveredInterruptedRotation) {
76
- // Be precise: this run DID write. Recovery is deliberately not gated on
77
- // --commit, because healing is how a crashed store becomes readable
78
- // again without re-keying it.
79
- console.log(chalk.yellow('Recovered an interrupted rotation: the store was healed back to a single ' +
80
- 'readable state. That is the only thing this dry run wrote — no re-keying happened.'));
81
- }
82
- else {
83
- console.log(chalk.gray('Dry-run: nothing written.'));
84
- }
85
- console.log(chalk.gray(`Pass --commit to re-encrypt the store and swap the key file (${report.keyFilePath}).`));
86
- return;
87
- }
88
- console.log(chalk.green(`Rotated: re-encrypted ${report.bundleCount} item(s) and rewrote ${report.keyFilePath} (mode 0600).`));
89
- console.log(chalk.gray('The previous key and ciphertext were removed. Any process still holding the old passphrase in its environment must be restarted.'));
90
- }
91
- catch (err) {
92
- console.error(chalk.red(err.message));
93
- process.exit(1);
94
- }
95
- });
96
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * `agents secrets push|pull` subcommands.
3
- *
4
- * Replaces iCloud Keychain as the cross-device sync mechanism with explicit
5
- * encrypted-at-rest sync against api.prix.dev. Plaintext never leaves the
6
- * machine — bundle contents are sealed with AES-256-GCM under a user-supplied
7
- * passphrase before upload.
8
- */
9
- import type { Command } from 'commander';
10
- /** Register `agents secrets push|pull|remote-list` on the parent secrets Command. */
11
- export declare function registerSecretsSyncCommands(secrets: Command): void;
@@ -1,153 +0,0 @@
1
- /**
2
- * `agents secrets push|pull` subcommands.
3
- *
4
- * Replaces iCloud Keychain as the cross-device sync mechanism with explicit
5
- * encrypted-at-rest sync against api.prix.dev. Plaintext never leaves the
6
- * machine — bundle contents are sealed with AES-256-GCM under a user-supplied
7
- * passphrase before upload.
8
- */
9
- import chalk from 'chalk';
10
- import { listRemoteBundles, MIN_PASSPHRASE_LEN, pullBundle, pushBundle, } from '../lib/secrets/sync.js';
11
- import { bundleExists, listBundles } from '../lib/secrets/bundles.js';
12
- import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
13
- import { missingSyncPassphraseMessage, resolveSyncPassphraseFromEnv, warnEnvPassphraseReadableOnce, } from '../lib/secrets/sync-passphrase.js';
14
- async function promptPassphrase(message, confirm = false) {
15
- if (!isInteractiveTerminal()) {
16
- throw new Error(missingSyncPassphraseMessage());
17
- }
18
- const { password } = await import('@inquirer/prompts');
19
- const first = await password({ message, mask: true });
20
- if (first.length < MIN_PASSPHRASE_LEN)
21
- throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
22
- if (!confirm)
23
- return first;
24
- const second = await password({ message: 'Confirm passphrase', mask: true });
25
- if (first !== second)
26
- throw new Error('Passphrases do not match.');
27
- return first;
28
- }
29
- function passphraseFromEnvOrPrompt(confirm) {
30
- // Env resolution (current name, then the deprecated master-key name) lives in
31
- // lib/secrets/sync-passphrase.ts so this command and `agents sync` share one
32
- // chokepoint — otherwise each call site warns separately and the "once per
33
- // process" promise is a lie.
34
- const { value } = resolveSyncPassphraseFromEnv();
35
- if (value) {
36
- if (value.length < MIN_PASSPHRASE_LEN) {
37
- return Promise.reject(new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`));
38
- }
39
- warnEnvPassphraseReadableOnce();
40
- return Promise.resolve(value);
41
- }
42
- return promptPassphrase('Sync passphrase', confirm);
43
- }
44
- /** Strip C0/C1 control bytes from server-supplied strings before terminal print. */
45
- function safePrint(s) {
46
- return s.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, '');
47
- }
48
- /** Register `agents secrets push|pull|remote-list` on the parent secrets Command. */
49
- export function registerSecretsSyncCommands(secrets) {
50
- secrets
51
- .command('push [name]')
52
- .description('Encrypt a local bundle and upload it to api.prix.dev (replaces iCloud Keychain sync).')
53
- .option('--all', 'Push every local bundle (each prompts independently if no passphrase env var is set)')
54
- .action(async (name, opts) => {
55
- try {
56
- if (opts.all) {
57
- const bundles = listBundles();
58
- if (bundles.length === 0) {
59
- console.log(chalk.gray('No local bundles to push.'));
60
- return;
61
- }
62
- const passphrase = await passphraseFromEnvOrPrompt(true);
63
- for (const b of bundles) {
64
- try {
65
- const { updated_at } = await pushBundle(b.name, { passphrase });
66
- console.log(chalk.green(`Pushed '${b.name}' (updated_at=${updated_at}).`));
67
- }
68
- catch (err) {
69
- console.error(chalk.red(`Failed to push '${b.name}': ${err.message}`));
70
- }
71
- }
72
- return;
73
- }
74
- if (!name) {
75
- throw new Error('Bundle name required. Try: agents secrets push <name> | agents secrets push --all');
76
- }
77
- if (!bundleExists(name)) {
78
- throw new Error(`Bundle '${name}' not found locally.`);
79
- }
80
- const passphrase = await passphraseFromEnvOrPrompt(true);
81
- const { updated_at } = await pushBundle(name, { passphrase });
82
- console.log(chalk.green(`Pushed '${name}' to api.prix.dev (updated_at=${updated_at}).`));
83
- console.log(chalk.gray('Remember the passphrase — it is required to pull this bundle on another machine.'));
84
- }
85
- catch (err) {
86
- if (isPromptCancelled(err))
87
- return;
88
- console.error(chalk.red(err.message));
89
- process.exit(1);
90
- }
91
- });
92
- secrets
93
- .command('pull [name]')
94
- .description('Decrypt a remote bundle from api.prix.dev and restore it into the local keychain.')
95
- .option('--all', 'Pull every bundle visible on the remote')
96
- .option('--force', 'Overwrite a local bundle with the same name')
97
- .action(async (name, opts) => {
98
- try {
99
- if (opts.all) {
100
- const remote = await listRemoteBundles();
101
- if (remote.length === 0) {
102
- console.log(chalk.gray('No remote bundles found.'));
103
- return;
104
- }
105
- const passphrase = await passphraseFromEnvOrPrompt(false);
106
- for (const r of remote) {
107
- try {
108
- await pullBundle(r.name, { passphrase, force: opts.force });
109
- console.log(chalk.green(`Pulled '${r.name}'.`));
110
- }
111
- catch (err) {
112
- console.error(chalk.red(`Failed to pull '${r.name}': ${err.message}`));
113
- }
114
- }
115
- return;
116
- }
117
- if (!name) {
118
- throw new Error('Bundle name required. Try: agents secrets pull <name> | agents secrets pull --all');
119
- }
120
- const passphrase = await passphraseFromEnvOrPrompt(false);
121
- const bundle = await pullBundle(name, { passphrase, force: opts.force });
122
- const keyCount = Object.keys(bundle.vars).length;
123
- console.log(chalk.green(`Pulled '${name}' (${keyCount} key${keyCount === 1 ? '' : 's'}) into local keychain.`));
124
- }
125
- catch (err) {
126
- if (isPromptCancelled(err))
127
- return;
128
- console.error(chalk.red(err.message));
129
- process.exit(1);
130
- }
131
- });
132
- secrets
133
- .command('remote-list')
134
- .alias('remote-ls')
135
- .description('List bundles currently stored on api.prix.dev for this account.')
136
- .action(async () => {
137
- try {
138
- const remote = await listRemoteBundles();
139
- if (remote.length === 0) {
140
- console.log(chalk.gray('No remote bundles found.'));
141
- return;
142
- }
143
- console.log(chalk.bold(`${'NAME'.padEnd(24)} UPDATED_AT`));
144
- for (const r of remote) {
145
- console.log(`${chalk.cyan(safePrint(r.name).padEnd(24))} ${chalk.gray(safePrint(r.updated_at))}`);
146
- }
147
- }
148
- catch (err) {
149
- console.error(chalk.red(err.message));
150
- process.exit(1);
151
- }
152
- });
153
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * `agents secrets vault` — unlock/lock the age-encrypted synced-secrets file
3
- * at ~/.agents/vault.age (distinct from `agents secrets unlock|lock`, which
4
- * holds keychain bundles in the secrets-agent).
5
- *
6
- * Formerly top-level `agents login` / `agents logout`.
7
- */
8
- import type { Command } from 'commander';
9
- /** Register `agents secrets vault unlock|lock` under the secrets parent. */
10
- export declare function registerSecretsVaultCommands(secretsCmd: Command): void;