@phnx-labs/agents-cli 1.20.17 → 1.20.19

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 (66) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +1 -1
  3. package/dist/commands/budget.d.ts +14 -0
  4. package/dist/commands/budget.js +137 -0
  5. package/dist/commands/cost.d.ts +12 -0
  6. package/dist/commands/cost.js +139 -0
  7. package/dist/commands/exec.d.ts +20 -0
  8. package/dist/commands/exec.js +382 -5
  9. package/dist/commands/secrets.d.ts +15 -0
  10. package/dist/commands/secrets.js +343 -16
  11. package/dist/commands/sessions.js +4 -0
  12. package/dist/index.js +4 -0
  13. package/dist/lib/budget/config.d.ts +9 -0
  14. package/dist/lib/budget/config.js +115 -0
  15. package/dist/lib/budget/enforce.d.ts +94 -0
  16. package/dist/lib/budget/enforce.js +151 -0
  17. package/dist/lib/budget/ledger.d.ts +61 -0
  18. package/dist/lib/budget/ledger.js +107 -0
  19. package/dist/lib/budget/preflight.d.ts +110 -0
  20. package/dist/lib/budget/preflight.js +200 -0
  21. package/dist/lib/checkpoint.d.ts +54 -0
  22. package/dist/lib/checkpoint.js +56 -0
  23. package/dist/lib/cloud/rush.js +18 -0
  24. package/dist/lib/exec.d.ts +36 -0
  25. package/dist/lib/exec.js +192 -4
  26. package/dist/lib/git.d.ts +18 -0
  27. package/dist/lib/git.js +67 -4
  28. package/dist/lib/loop.d.ts +145 -0
  29. package/dist/lib/loop.js +330 -0
  30. package/dist/lib/mcp.d.ts +7 -0
  31. package/dist/lib/mcp.js +24 -0
  32. package/dist/lib/models.d.ts +11 -0
  33. package/dist/lib/models.js +21 -0
  34. package/dist/lib/plugins.js +5 -2
  35. package/dist/lib/pricing/cost.d.ts +46 -0
  36. package/dist/lib/pricing/cost.js +71 -0
  37. package/dist/lib/pricing/index.d.ts +8 -0
  38. package/dist/lib/pricing/index.js +8 -0
  39. package/dist/lib/pricing/prices.json +138 -0
  40. package/dist/lib/pricing/table.d.ts +17 -0
  41. package/dist/lib/pricing/table.js +73 -0
  42. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  43. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  44. package/dist/lib/secrets/agent.d.ts +147 -0
  45. package/dist/lib/secrets/agent.js +500 -0
  46. package/dist/lib/secrets/bundles.d.ts +58 -7
  47. package/dist/lib/secrets/bundles.js +264 -75
  48. package/dist/lib/secrets/filestore.d.ts +82 -0
  49. package/dist/lib/secrets/filestore.js +295 -0
  50. package/dist/lib/secrets/linux.d.ts +6 -24
  51. package/dist/lib/secrets/linux.js +22 -265
  52. package/dist/lib/session/db.d.ts +40 -0
  53. package/dist/lib/session/db.js +84 -2
  54. package/dist/lib/session/discover.d.ts +2 -0
  55. package/dist/lib/session/discover.js +126 -2
  56. package/dist/lib/session/render.d.ts +2 -0
  57. package/dist/lib/session/render.js +1 -1
  58. package/dist/lib/session/types.d.ts +4 -0
  59. package/dist/lib/teams/agents.d.ts +32 -0
  60. package/dist/lib/teams/agents.js +66 -3
  61. package/dist/lib/teams/api.js +20 -0
  62. package/dist/lib/teams/parsers.js +16 -4
  63. package/dist/lib/types.d.ts +48 -0
  64. package/dist/lib/workflows.d.ts +56 -0
  65. package/dist/lib/workflows.js +72 -5
  66. package/package.json +2 -1
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Passphrase-encrypted file store for secrets — platform-neutral.
3
+ *
4
+ * An AES-256-GCM encrypted-file store under `~/.agents/.cache/secrets/`. The
5
+ * encryption key is scrypt-derived from a passphrase read from
6
+ * `AGENTS_SECRETS_PASSPHRASE` (preferred), a machine-local provisioned key, or
7
+ * a TTY prompt. One `<item>.enc` JSON file per item, mode 0600.
8
+ *
9
+ * Two callers:
10
+ * - Linux (src/lib/secrets/linux.ts): the headless fallback when the default
11
+ * Secret Service collection is locked. Auto-provisions a machine-local
12
+ * passphrase so `agents secrets` works out of the box on a server.
13
+ * - macOS file-backed bundles (src/lib/secrets/bundles.ts): an explicit,
14
+ * opt-in non-biometry backend for headless/remote release runs. The bundle
15
+ * layer guards this path so it only activates with an explicit
16
+ * AGENTS_SECRETS_PASSPHRASE (or TTY) — never the silent machine-local
17
+ * auto-provision — so a remote box holds ciphertext only.
18
+ *
19
+ * The item-name scheme is shared with the keychain backend so a file-backed
20
+ * item and its keychain twin carry identical names:
21
+ * `agents-cli.bundles.<name>` and `agents-cli.secrets.<bundle>.<key>`.
22
+ */
23
+ import { execSync } from 'child_process';
24
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
25
+ import * as fs from 'fs';
26
+ import * as os from 'os';
27
+ import * as path from 'path';
28
+ // ---------- file store location ----------
29
+ let fileDirOverride = null;
30
+ let cachedPassphrase = null;
31
+ let warnedAutoPassphrase = false;
32
+ export function fileDir() {
33
+ return fileDirOverride ?? path.join(os.homedir(), '.agents', '.cache', 'secrets');
34
+ }
35
+ function ensureFileDir() {
36
+ fs.mkdirSync(fileDir(), { recursive: true, mode: 0o700 });
37
+ }
38
+ // ---------- passphrase ----------
39
+ function readPassphraseFromTty() {
40
+ const fd = fs.openSync('/dev/tty', 'r+');
41
+ let echoDisabled = false;
42
+ try {
43
+ fs.writeSync(fd, 'Enter AGENTS_SECRETS_PASSPHRASE: ');
44
+ try {
45
+ execSync('stty -echo < /dev/tty', { stdio: 'ignore' });
46
+ echoDisabled = true;
47
+ }
48
+ catch {
49
+ // stty not available — fall through; passphrase will echo. Better
50
+ // than refusing to function.
51
+ }
52
+ let pass = '';
53
+ const buf = Buffer.alloc(1);
54
+ while (true) {
55
+ const n = fs.readSync(fd, buf, 0, 1, null);
56
+ if (n === 0)
57
+ break;
58
+ const ch = buf.toString('utf8', 0, n);
59
+ if (ch === '\n' || ch === '\r')
60
+ break;
61
+ pass += ch;
62
+ }
63
+ return pass;
64
+ }
65
+ finally {
66
+ if (echoDisabled) {
67
+ try {
68
+ execSync('stty echo < /dev/tty', { stdio: 'ignore' });
69
+ }
70
+ catch { /* best effort */ }
71
+ }
72
+ try {
73
+ fs.writeSync(fd, '\n');
74
+ }
75
+ catch { /* best effort */ }
76
+ fs.closeSync(fd);
77
+ }
78
+ }
79
+ /** Path of the auto-provisioned machine-local passphrase. Lives alongside the
80
+ * encrypted items but is never itself an item (no `.enc` suffix, so it's
81
+ * excluded from list/has/get and from fileFallbackPreviouslyActivated). */
82
+ function passphraseFilePath() {
83
+ return path.join(fileDir(), '.passphrase');
84
+ }
85
+ /** True if a machine-local passphrase has already been provisioned. */
86
+ export function machinePassphraseExists() {
87
+ try {
88
+ return fs.readFileSync(passphraseFilePath(), 'utf8').trim().length > 0;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ function readMachinePassphrase() {
95
+ try {
96
+ const p = fs.readFileSync(passphraseFilePath(), 'utf8').trim();
97
+ return p.length > 0 ? p : null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ /**
104
+ * Provision (or read back) a stable machine-local passphrase for the encrypted
105
+ * file store, so `agents secrets` works out of the box on a headless box where
106
+ * the keyring is locked and no AGENTS_SECRETS_PASSPHRASE is set.
107
+ *
108
+ * Security model: this is encryption-at-rest with the key held in a 0600 file —
109
+ * the same posture as an SSH private key, and identical to the common
110
+ * "export AGENTS_SECRETS_PASSPHRASE=… in ~/.zshenv (chmod 600)" workaround. The
111
+ * keyring (key in a daemon's locked memory) is stronger but is unavailable
112
+ * without a graphical/unlocked session. For an off-disk key, set
113
+ * AGENTS_SECRETS_PASSPHRASE (it always takes precedence) or unlock the keyring.
114
+ */
115
+ function provisionMachinePassphrase() {
116
+ const existing = readMachinePassphrase();
117
+ if (existing)
118
+ return existing;
119
+ ensureFileDir();
120
+ const generated = randomBytes(32).toString('base64');
121
+ const fp = passphraseFilePath();
122
+ try {
123
+ // wx: fail if a concurrent process created it first (then we read theirs).
124
+ fs.writeFileSync(fp, generated, { mode: 0o600, flag: 'wx' });
125
+ }
126
+ catch {
127
+ const raced = readMachinePassphrase();
128
+ if (raced)
129
+ return raced;
130
+ throw new Error(`Failed to provision machine-local passphrase at ${fp}.`);
131
+ }
132
+ if (!warnedAutoPassphrase) {
133
+ warnedAutoPassphrase = true;
134
+ process.stderr.write(`[agents] keyring locked and no AGENTS_SECRETS_PASSPHRASE set; provisioned a ` +
135
+ `machine-local passphrase at ${fp} (mode 0600). Set AGENTS_SECRETS_PASSPHRASE ` +
136
+ `for a key held off disk.\n`);
137
+ }
138
+ return generated;
139
+ }
140
+ /**
141
+ * Resolve the passphrase for the encrypted file store.
142
+ *
143
+ * Order: AGENTS_SECRETS_PASSPHRASE > previously-provisioned machine-local key >
144
+ * (interactive) TTY prompt > (headless) auto-provisioned machine-local key.
145
+ *
146
+ * `allowAutoProvision` (default true, used by the Linux fallback) controls the
147
+ * last two steps. macOS file-backed bundles pass `false` so a missing
148
+ * passphrase is a hard, explicit error instead of a silently provisioned
149
+ * on-disk key — the caller (bundles.ts) guards this before we get here.
150
+ */
151
+ export function getPassphrase(opts = {}) {
152
+ const allowAutoProvision = opts.allowAutoProvision ?? true;
153
+ if (cachedPassphrase !== null)
154
+ return cachedPassphrase;
155
+ const env = process.env.AGENTS_SECRETS_PASSPHRASE;
156
+ if (env && env.length > 0) {
157
+ cachedPassphrase = env;
158
+ return env;
159
+ }
160
+ // A previously-provisioned machine-local passphrase is this machine's stable
161
+ // file-store key — prefer it for both interactive and headless runs so they
162
+ // always agree (a TTY run won't re-prompt once the file exists).
163
+ const onDisk = readMachinePassphrase();
164
+ if (onDisk) {
165
+ cachedPassphrase = onDisk;
166
+ return onDisk;
167
+ }
168
+ if (!allowAutoProvision) {
169
+ throw new Error('AGENTS_SECRETS_PASSPHRASE is not set. A passphrase is required to decrypt ' +
170
+ 'this file-backed secret store.');
171
+ }
172
+ // First run, no env, no provisioned key: prompt when interactive, otherwise
173
+ // (headless — the reported bug) auto-provision instead of hard-failing.
174
+ if (process.stdin.isTTY) {
175
+ const p = readPassphraseFromTty();
176
+ if (!p)
177
+ throw new Error('No passphrase entered.');
178
+ cachedPassphrase = p;
179
+ return p;
180
+ }
181
+ cachedPassphrase = provisionMachinePassphrase();
182
+ return cachedPassphrase;
183
+ }
184
+ function deriveKey(passphrase, salt) {
185
+ return scryptSync(passphrase, salt, 32);
186
+ }
187
+ /** Encrypt plaintext under a passphrase using AES-256-GCM with a random
188
+ * scrypt salt and a random 96-bit IV. Exported for tests. */
189
+ export function encryptForFallback(plaintext, passphrase) {
190
+ const salt = randomBytes(16);
191
+ const iv = randomBytes(12);
192
+ const key = deriveKey(passphrase, salt);
193
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
194
+ const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
195
+ return {
196
+ salt: salt.toString('hex'),
197
+ iv: iv.toString('hex'),
198
+ authTag: cipher.getAuthTag().toString('hex'),
199
+ ciphertext: ciphertext.toString('hex'),
200
+ };
201
+ }
202
+ /** Decrypt an EncFile under a passphrase. Throws on wrong key or tampered
203
+ * ciphertext (auth-tag mismatch). Exported for tests. */
204
+ export function decryptForFallback(enc, passphrase) {
205
+ const salt = Buffer.from(enc.salt, 'hex');
206
+ const iv = Buffer.from(enc.iv, 'hex');
207
+ const authTag = Buffer.from(enc.authTag, 'hex');
208
+ const ciphertext = Buffer.from(enc.ciphertext, 'hex');
209
+ const key = deriveKey(passphrase, salt);
210
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
211
+ decipher.setAuthTag(authTag);
212
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
213
+ return plaintext.toString('utf8');
214
+ }
215
+ // ---------- file backend ----------
216
+ function fileFor(item) {
217
+ return path.join(fileDir(), `${item}.enc`);
218
+ }
219
+ function fileHas(item) {
220
+ return fs.existsSync(fileFor(item));
221
+ }
222
+ function fileGet(item, opts = {}) {
223
+ const fp = fileFor(item);
224
+ if (!fs.existsSync(fp)) {
225
+ throw new Error(`Secret '${item}' not found in encrypted store.`);
226
+ }
227
+ const raw = fs.readFileSync(fp, 'utf8');
228
+ let parsed;
229
+ try {
230
+ parsed = JSON.parse(raw);
231
+ }
232
+ catch {
233
+ throw new Error(`Encrypted secret file ${fp} is corrupt (not valid JSON).`);
234
+ }
235
+ try {
236
+ return decryptForFallback(parsed, getPassphrase(opts));
237
+ }
238
+ catch {
239
+ throw new Error(`Failed to decrypt '${item}'. Wrong AGENTS_SECRETS_PASSPHRASE or tampered file.`);
240
+ }
241
+ }
242
+ function fileSet(item, value, opts = {}) {
243
+ ensureFileDir();
244
+ const enc = encryptForFallback(value, getPassphrase(opts));
245
+ fs.writeFileSync(fileFor(item), JSON.stringify(enc), { mode: 0o600 });
246
+ }
247
+ function fileDelete(item) {
248
+ const fp = fileFor(item);
249
+ if (!fs.existsSync(fp))
250
+ return true; // idempotent, matches secret-tool clear
251
+ fs.unlinkSync(fp);
252
+ return true;
253
+ }
254
+ function fileList(prefix) {
255
+ const dir = fileDir();
256
+ if (!fs.existsSync(dir))
257
+ return [];
258
+ return fs.readdirSync(dir)
259
+ .filter((f) => f.endsWith('.enc'))
260
+ .map((f) => f.slice(0, -'.enc'.length))
261
+ .filter((name) => name.startsWith(prefix));
262
+ }
263
+ /** True if the fallback dir has any committed encrypted items. */
264
+ export function fileStoreHasItems() {
265
+ try {
266
+ return fs.readdirSync(fileDir()).some((e) => e.endsWith('.enc'));
267
+ }
268
+ catch {
269
+ return false;
270
+ }
271
+ }
272
+ /** Low-level file-store ops, exported so callers (linux fallback, macOS
273
+ * file-backed bundles) can opt into or out of passphrase auto-provision. */
274
+ export const fileStore = {
275
+ has: fileHas,
276
+ get: fileGet,
277
+ set: fileSet,
278
+ delete: fileDelete,
279
+ list: fileList,
280
+ };
281
+ /** File-only KeychainBackend (exported for tests; the Linux backend uses these
282
+ * ops with auto-provision allowed). */
283
+ export const fileBackend = {
284
+ has: fileHas,
285
+ get: (item) => fileGet(item),
286
+ set: (item, value) => fileSet(item, value),
287
+ delete: fileDelete,
288
+ list: fileList,
289
+ };
290
+ /** Test-only: reset module state (file dir + cached passphrase). */
291
+ export function _resetFileStoreForTest(opts = {}) {
292
+ fileDirOverride = opts.fileDir ?? null;
293
+ cachedPassphrase = opts.passphrase ?? null;
294
+ warnedAutoPassphrase = false;
295
+ }
@@ -7,36 +7,17 @@
7
7
  * (common on server-class Linux — no graphical login means the keyring
8
8
  * passphrase never enters the daemon, so `secret-tool store` fails with
9
9
  * "Cannot create an item in a locked collection"), we transparently switch
10
- * to a file-based AES-256-GCM encrypted store under
11
- * `~/.agents/.cache/secrets/`. The encryption key is scrypt-derived from a
12
- * passphrase read from `AGENTS_SECRETS_PASSPHRASE` (preferred) or a TTY
13
- * prompt. The decision is cached per process; one stderr line is emitted
14
- * the first time the fallback activates.
10
+ * to the AES-256-GCM encrypted-file store in ./filestore.ts. The decision is
11
+ * cached per process; one stderr line is emitted the first time the fallback
12
+ * activates.
15
13
  *
16
14
  * Secrets stored via secret-tool use:
17
15
  * service = "agents-cli"
18
16
  * account = username
19
17
  * item = the secret identifier
20
- *
21
- * File-fallback layout: one `<item>.enc` JSON file per item, mode 0600.
22
18
  */
23
19
  import type { KeychainBackend } from './index.js';
24
- /** Encrypted-file on-disk shape. Exported for tests. */
25
- export interface EncFile {
26
- salt: string;
27
- iv: string;
28
- authTag: string;
29
- ciphertext: string;
30
- }
31
- /** Encrypt plaintext under a passphrase using AES-256-GCM with a random
32
- * scrypt salt and a random 96-bit IV. Exported for tests. */
33
- export declare function encryptForFallback(plaintext: string, passphrase: string): EncFile;
34
- /** Decrypt an EncFile under a passphrase. Throws on wrong key or tampered
35
- * ciphertext (auth-tag mismatch). Exported for tests. */
36
- export declare function decryptForFallback(enc: EncFile, passphrase: string): string;
37
- /** File-only KeychainBackend (exported for tests; the public surface uses
38
- * the secret-tool-with-fallback `linuxBackend` below). */
39
- export declare const fileBackend: KeychainBackend;
20
+ export { encryptForFallback, decryptForFallback, fileBackend, type EncFile, } from './filestore.js';
40
21
  /** secret-tool lookup attributes:
41
22
  * service=agents-cli account=<user> item=<itemName> */
42
23
  export declare function hasSecretToolToken(item: string): boolean;
@@ -66,7 +47,8 @@ export declare function listSecretToolItems(prefix: string): string[];
66
47
  * AGENTS_SECRETS_PASSPHRASE is set). */
67
48
  export declare const linuxBackend: KeychainBackend;
68
49
  /** Test-only: reset module state so independent test cases don't bleed
69
- * passphrase / fallback decisions across each other. */
50
+ * passphrase / fallback decisions across each other. File-store state (file
51
+ * dir + cached passphrase) lives in ./filestore.ts and is reset there. */
70
52
  export declare function _resetForTest(opts?: {
71
53
  fileDir?: string | null;
72
54
  forceFileFallback?: boolean;