@phnx-labs/agents-cli 1.20.0 → 1.20.3
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.
- package/CHANGELOG.md +73 -0
- package/README.md +4 -4
- package/dist/commands/cli.js +3 -3
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +24 -7
- package/dist/commands/exec.js +36 -16
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +86 -7
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +117 -4
- package/dist/commands/pull.js +4 -4
- package/dist/commands/routines.js +6 -6
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +74 -39
- package/dist/commands/skills.js +22 -5
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +48 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.js +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +164 -8
- package/dist/commands/workflows.js +29 -6
- package/dist/index.js +4 -0
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +18 -14
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/chrome.js +4 -0
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/profiles.d.ts +3 -3
- package/dist/lib/browser/profiles.js +3 -3
- package/dist/lib/browser/service.js +19 -0
- package/dist/lib/browser/types.d.ts +4 -4
- package/dist/lib/cli-resources.d.ts +36 -8
- package/dist/lib/cli-resources.js +268 -46
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +39 -11
- package/dist/lib/exec.js +90 -31
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +68 -15
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +40 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +51 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +187 -8
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/routines-format.d.ts +17 -5
- package/dist/lib/routines-format.js +37 -16
- package/dist/lib/routines.d.ts +1 -1
- package/dist/lib/routines.js +2 -2
- package/dist/lib/runner.js +64 -10
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +18 -22
- package/dist/lib/secrets/bundles.js +75 -99
- package/dist/lib/secrets/index.d.ts +51 -27
- package/dist/lib/secrets/index.js +147 -156
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/shims.d.ts +4 -1
- package/dist/lib/shims.js +5 -35
- package/dist/lib/state.d.ts +14 -1
- package/dist/lib/state.js +49 -5
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +47 -21
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/types.d.ts +57 -1
- package/dist/lib/types.js +2 -0
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +35 -1
- package/dist/lib/versions.js +267 -64
- package/package.json +9 -8
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
package/dist/lib/runner.js
CHANGED
|
@@ -14,7 +14,8 @@ import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, } from './rout
|
|
|
14
14
|
import { getRunsDir } from './state.js';
|
|
15
15
|
import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
|
|
16
16
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
17
|
-
import { createTimer, maybeRotate,
|
|
17
|
+
import { createTimer, maybeRotate, redactPrompt } from './events.js';
|
|
18
|
+
import { normalizeMode } from './exec.js';
|
|
18
19
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
19
20
|
const AGENT_COMMANDS = {
|
|
20
21
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
@@ -36,13 +37,20 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
36
37
|
throw new Error(`Unsupported agent for daemon jobs: ${config.agent}`);
|
|
37
38
|
}
|
|
38
39
|
let cmd = template.map((part) => part.replace('{prompt}', resolvedPrompt));
|
|
40
|
+
// Canonicalize mode (accepts legacy `full` as alias for `skip`).
|
|
41
|
+
const mode = normalizeMode(config.mode);
|
|
39
42
|
if (config.agent === 'claude') {
|
|
40
|
-
if (
|
|
43
|
+
if (mode === 'edit') {
|
|
41
44
|
const planIndex = cmd.indexOf('plan');
|
|
42
45
|
if (planIndex !== -1)
|
|
43
46
|
cmd[planIndex] = 'acceptEdits';
|
|
44
47
|
}
|
|
45
|
-
else if (
|
|
48
|
+
else if (mode === 'auto') {
|
|
49
|
+
const planIndex = cmd.indexOf('plan');
|
|
50
|
+
if (planIndex !== -1)
|
|
51
|
+
cmd[planIndex] = 'auto';
|
|
52
|
+
}
|
|
53
|
+
else if (mode === 'skip') {
|
|
46
54
|
// Replace --permission-mode plan with --dangerously-skip-permissions
|
|
47
55
|
const pmIndex = cmd.indexOf('--permission-mode');
|
|
48
56
|
if (pmIndex !== -1)
|
|
@@ -63,10 +71,10 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
63
71
|
appendModelAndReasoning(cmd, config);
|
|
64
72
|
}
|
|
65
73
|
if (config.agent === 'codex') {
|
|
66
|
-
if (
|
|
74
|
+
if (mode === 'edit') {
|
|
67
75
|
cmd.push('--full-auto');
|
|
68
76
|
}
|
|
69
|
-
else if (
|
|
77
|
+
else if (mode === 'skip') {
|
|
70
78
|
// Remove sandbox restriction, just --full-auto
|
|
71
79
|
const sbIndex = cmd.indexOf('--sandbox');
|
|
72
80
|
if (sbIndex !== -1)
|
|
@@ -76,7 +84,10 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
76
84
|
appendModelAndReasoning(cmd, config);
|
|
77
85
|
}
|
|
78
86
|
if (config.agent === 'gemini') {
|
|
79
|
-
if (
|
|
87
|
+
if (mode === 'edit') {
|
|
88
|
+
cmd.push('--approval-mode', 'auto_edit');
|
|
89
|
+
}
|
|
90
|
+
else if (mode === 'skip') {
|
|
80
91
|
cmd.push('--yolo');
|
|
81
92
|
}
|
|
82
93
|
appendModelAndReasoning(cmd, config);
|
|
@@ -122,7 +133,7 @@ export async function executeJob(config) {
|
|
|
122
133
|
version: config.version,
|
|
123
134
|
jobName: config.name,
|
|
124
135
|
mode: config.mode,
|
|
125
|
-
|
|
136
|
+
...redactPrompt(config.prompt),
|
|
126
137
|
schedule: config.schedule,
|
|
127
138
|
});
|
|
128
139
|
const resolvedPrompt = resolveJobPrompt(config);
|
|
@@ -321,6 +332,39 @@ export function extractReport(stdoutPath, agentType) {
|
|
|
321
332
|
return null;
|
|
322
333
|
}
|
|
323
334
|
}
|
|
335
|
+
/** Derive the final status of a detached run by reading the agent's stream-json
|
|
336
|
+
* tail. Detached children fire-and-forget, so we never see their exit code
|
|
337
|
+
* directly — but Claude's stream-json terminates with a `type: result` line
|
|
338
|
+
* that carries `is_error`. If we find it, the run completed cleanly (modulo
|
|
339
|
+
* agent-reported error). If not, the process likely died mid-stream and the
|
|
340
|
+
* caller should treat the run as failed. */
|
|
341
|
+
function inferFinalStatusFromLog(stdoutPath, agent) {
|
|
342
|
+
if (!fs.existsSync(stdoutPath))
|
|
343
|
+
return null;
|
|
344
|
+
try {
|
|
345
|
+
const content = fs.readFileSync(stdoutPath, 'utf-8');
|
|
346
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
347
|
+
// Walk backwards over the last few lines — the result marker is always
|
|
348
|
+
// at the tail. Cap the scan so a huge stdout doesn't iterate forever.
|
|
349
|
+
for (let i = lines.length - 1, scanned = 0; i >= 0 && scanned < 20; i--, scanned++) {
|
|
350
|
+
try {
|
|
351
|
+
const parsed = JSON.parse(lines[i]);
|
|
352
|
+
if (agent === 'claude' && parsed.type === 'result') {
|
|
353
|
+
return parsed.is_error
|
|
354
|
+
? { status: 'failed', exitCode: 1 }
|
|
355
|
+
: { status: 'completed', exitCode: 0 };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// malformed JSONL line — keep scanning
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
324
368
|
/** Scan all runs marked "running" and finalize any whose process has exited. */
|
|
325
369
|
export function monitorRunningJobs() {
|
|
326
370
|
const runsDir = getRunsDir();
|
|
@@ -346,11 +390,21 @@ export function monitorRunningJobs() {
|
|
|
346
390
|
process.kill(meta.pid, 0);
|
|
347
391
|
}
|
|
348
392
|
catch { /* process no longer running */
|
|
349
|
-
|
|
393
|
+
const runDirPath = path.join(jobRunsPath, runDirEntry.name);
|
|
394
|
+
const stdoutPath = path.join(runDirPath, 'stdout.log');
|
|
395
|
+
// Prefer the agent's own success/error marker; fall back to "failed"
|
|
396
|
+
// only when the stream ended without one (process killed mid-run).
|
|
397
|
+
const inferred = inferFinalStatusFromLog(stdoutPath, meta.agent);
|
|
398
|
+
if (inferred) {
|
|
399
|
+
meta.status = inferred.status;
|
|
400
|
+
meta.exitCode = inferred.exitCode;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
meta.status = 'failed';
|
|
404
|
+
}
|
|
350
405
|
meta.completedAt = new Date().toISOString();
|
|
351
406
|
writeRunMeta(meta);
|
|
352
|
-
|
|
353
|
-
extractAndSaveReport(stdoutPath, meta.agent, path.join(jobRunsPath, runDirEntry.name));
|
|
407
|
+
extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
|
|
354
408
|
}
|
|
355
409
|
}
|
|
356
410
|
catch { /* corrupt or unreadable meta.json */ }
|
|
Binary file
|
|
Binary file
|
|
@@ -5,15 +5,7 @@
|
|
|
5
5
|
<key>files</key>
|
|
6
6
|
<dict/>
|
|
7
7
|
<key>files2</key>
|
|
8
|
-
<dict
|
|
9
|
-
<key>embedded.provisionprofile</key>
|
|
10
|
-
<dict>
|
|
11
|
-
<key>hash2</key>
|
|
12
|
-
<data>
|
|
13
|
-
2vfA/eR3dTYgNc/fXhdADUPkp5tRIepPzE3FCLfDx4w=
|
|
14
|
-
</data>
|
|
15
|
-
</dict>
|
|
16
|
-
</dict>
|
|
8
|
+
<dict/>
|
|
17
9
|
<key>rules</key>
|
|
18
10
|
<dict>
|
|
19
11
|
<key>^Resources/</key>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secret bundles
|
|
2
|
+
* Secret bundles — named sets of keychain-backed environment variables.
|
|
3
3
|
*
|
|
4
4
|
* Bundle metadata (name, description, vars map) is stored in the macOS
|
|
5
|
-
* Keychain as a JSON blob under `agents-cli.bundles.<name>`.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
5
|
+
* Keychain as a JSON blob under `agents-cli.bundles.<name>`. Secret values
|
|
6
|
+
* live one per keychain item under `agents-cli.secrets.<bundle>.<key>`.
|
|
7
|
+
* Every item is device-local and gated by Touch ID / device passcode — see
|
|
8
|
+
* src/lib/secrets/index.ts for the access-control story. Nothing about
|
|
9
|
+
* secrets ever lives in plaintext on disk.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `icloud_sync` flag.
|
|
11
|
+
* Cross-machine sync is handled by src/lib/secrets/sync.ts via an explicit
|
|
12
|
+
* encrypted export/import flow; the bundle layer is sync-agnostic.
|
|
13
13
|
*/
|
|
14
14
|
import { type BundleValue, type SecretRef } from './index.js';
|
|
15
15
|
/** Allowed values for a secret's `type` metadata field. */
|
|
@@ -28,8 +28,6 @@ export interface SecretsBundle {
|
|
|
28
28
|
name: string;
|
|
29
29
|
description?: string;
|
|
30
30
|
allow_exec?: boolean;
|
|
31
|
-
/** When true, keychain-backed values and bundle metadata sync via iCloud Keychain. */
|
|
32
|
-
icloud_sync?: boolean;
|
|
33
31
|
/** ISO 8601 UTC timestamp. Set once on the first writeBundle() for a bundle. */
|
|
34
32
|
created_at?: string;
|
|
35
33
|
/** ISO 8601 UTC timestamp. Refreshed on every writeBundle(). */
|
|
@@ -49,6 +47,7 @@ export declare const RESERVED_ENV_NAMES: Set<string>;
|
|
|
49
47
|
export declare function bundleToEnvPrefix(name: string): string;
|
|
50
48
|
export declare function isReservedEnvName(key: string): boolean;
|
|
51
49
|
export declare function isLoaderOrInterpreterEnv(name: string): boolean;
|
|
50
|
+
export declare function sanitizeProcessEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
52
51
|
/** Validate a bundle name against the allowed pattern. Throws on invalid input. */
|
|
53
52
|
export declare function validateBundleName(name: string): void;
|
|
54
53
|
export declare function validateEnvKey(key: string): void;
|
|
@@ -62,11 +61,7 @@ export declare function validateSecretType(t: string): asserts t is SecretType;
|
|
|
62
61
|
export declare function validateExpiresFutureDated(iso: string): void;
|
|
63
62
|
export declare function bundleExists(name: string): boolean;
|
|
64
63
|
export declare function readBundle(name: string): SecretsBundle;
|
|
65
|
-
export
|
|
66
|
-
/** Emit a secrets.set audit event. Internal bookkeeping writes turn this off. */
|
|
67
|
-
emitEvent?: boolean;
|
|
68
|
-
}
|
|
69
|
-
export declare function writeBundle(bundle: SecretsBundle, opts?: WriteBundleOptions): void;
|
|
64
|
+
export declare function writeBundle(bundle: SecretsBundle): void;
|
|
70
65
|
export declare function deleteBundle(name: string): boolean;
|
|
71
66
|
export declare function listBundles(): SecretsBundle[];
|
|
72
67
|
export interface BundleEntryInfo {
|
|
@@ -78,14 +73,15 @@ export declare function describeBundle(bundle: SecretsBundle): BundleEntryInfo[]
|
|
|
78
73
|
/** Options for resolveBundleEnv. */
|
|
79
74
|
export interface ResolveBundleOptions {
|
|
80
75
|
/**
|
|
81
|
-
* Human-readable label for who is requesting the secrets.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
76
|
+
* Human-readable label for who is requesting the secrets. Currently
|
|
77
|
+
* informational only — the helper's Touch ID prompt is set by the OS and
|
|
78
|
+
* cannot be reliably customized once we drop the per-batch reason path,
|
|
79
|
+
* but we keep this in the API so call sites stay explicit about who's
|
|
80
|
+
* about to read the bundle.
|
|
85
81
|
*/
|
|
86
82
|
caller?: string;
|
|
87
83
|
}
|
|
88
|
-
export declare function resolveBundleEnv(bundle: SecretsBundle,
|
|
84
|
+
export declare function resolveBundleEnv(bundle: SecretsBundle, _opts?: ResolveBundleOptions): Record<string, string>;
|
|
89
85
|
/**
|
|
90
86
|
* Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
|
|
91
87
|
*
|
|
@@ -135,8 +131,8 @@ export interface RenameOptions {
|
|
|
135
131
|
* 4) write new bundle metadata
|
|
136
132
|
* 5) delete the old per-key keychain items + old metadata
|
|
137
133
|
*
|
|
138
|
-
* Steps 1-4 are reversible. If 5 partially fails
|
|
139
|
-
*
|
|
134
|
+
* Steps 1-4 are reversible. If 5 partially fails, running `rename` again is
|
|
135
|
+
* a safe no-op for the source items.
|
|
140
136
|
*/
|
|
141
137
|
export declare function renameBundle(oldName: string, newName: string, opts?: RenameOptions): void;
|
|
142
138
|
export declare function keychainItemsForBundle(bundle: SecretsBundle): Array<{
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secret bundles
|
|
2
|
+
* Secret bundles — named sets of keychain-backed environment variables.
|
|
3
3
|
*
|
|
4
4
|
* Bundle metadata (name, description, vars map) is stored in the macOS
|
|
5
|
-
* Keychain as a JSON blob under `agents-cli.bundles.<name>`.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
5
|
+
* Keychain as a JSON blob under `agents-cli.bundles.<name>`. Secret values
|
|
6
|
+
* live one per keychain item under `agents-cli.secrets.<bundle>.<key>`.
|
|
7
|
+
* Every item is device-local and gated by Touch ID / device passcode — see
|
|
8
|
+
* src/lib/secrets/index.ts for the access-control story. Nothing about
|
|
9
|
+
* secrets ever lives in plaintext on disk.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `icloud_sync` flag.
|
|
11
|
+
* Cross-machine sync is handled by src/lib/secrets/sync.ts via an explicit
|
|
12
|
+
* encrypted export/import flow; the bundle layer is sync-agnostic.
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as os from 'os';
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import * as yaml from 'yaml';
|
|
18
|
-
import { deleteKeychainToken, getKeychainToken,
|
|
18
|
+
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, parseBundleValue, resolveRef, secretsKeychainItem, setKeychainToken, } from './index.js';
|
|
19
19
|
import { emit } from '../events.js';
|
|
20
20
|
/** Allowed values for a secret's `type` metadata field. */
|
|
21
21
|
export const SECRET_TYPES = [
|
|
@@ -63,6 +63,17 @@ export function isLoaderOrInterpreterEnv(name) {
|
|
|
63
63
|
'CDPATH',
|
|
64
64
|
].includes(upper);
|
|
65
65
|
}
|
|
66
|
+
export function sanitizeProcessEnv(env = process.env) {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [k, v] of Object.entries(env)) {
|
|
69
|
+
if (v === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
if (isLoaderOrInterpreterEnv(k))
|
|
72
|
+
continue;
|
|
73
|
+
out[k] = v;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
66
77
|
/** Validate a bundle name against the allowed pattern. Throws on invalid input. */
|
|
67
78
|
export function validateBundleName(name) {
|
|
68
79
|
if (!BUNDLE_NAME_PATTERN.test(name)) {
|
|
@@ -125,11 +136,12 @@ export function readBundle(name) {
|
|
|
125
136
|
if (!parsed || typeof parsed !== 'object') {
|
|
126
137
|
throw new Error(`Bundle '${name}' is malformed.`);
|
|
127
138
|
}
|
|
139
|
+
// Unknown fields on the JSON (e.g. legacy sync flags) are silently dropped
|
|
140
|
+
// here; the SecretsBundle shape is the only source of truth.
|
|
128
141
|
const bundle = {
|
|
129
142
|
name,
|
|
130
143
|
description: parsed.description,
|
|
131
144
|
allow_exec: Boolean(parsed.allow_exec),
|
|
132
|
-
icloud_sync: Boolean(parsed.icloud_sync),
|
|
133
145
|
vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
|
|
134
146
|
};
|
|
135
147
|
if (typeof parsed.created_at === 'string')
|
|
@@ -146,7 +158,7 @@ export function readBundle(name) {
|
|
|
146
158
|
}
|
|
147
159
|
return bundle;
|
|
148
160
|
}
|
|
149
|
-
export function writeBundle(bundle
|
|
161
|
+
export function writeBundle(bundle) {
|
|
150
162
|
validateBundleName(bundle.name);
|
|
151
163
|
for (const key of Object.keys(bundle.vars)) {
|
|
152
164
|
validateEnvKey(key);
|
|
@@ -179,7 +191,6 @@ export function writeBundle(bundle, opts = {}) {
|
|
|
179
191
|
const payload = {
|
|
180
192
|
description: bundle.description,
|
|
181
193
|
allow_exec: bundle.allow_exec ? true : undefined,
|
|
182
|
-
icloud_sync: bundle.icloud_sync ? true : undefined,
|
|
183
194
|
created_at: bundle.created_at,
|
|
184
195
|
updated_at: bundle.updated_at,
|
|
185
196
|
last_used: bundle.last_used,
|
|
@@ -187,10 +198,8 @@ export function writeBundle(bundle, opts = {}) {
|
|
|
187
198
|
meta,
|
|
188
199
|
};
|
|
189
200
|
const json = JSON.stringify(payload);
|
|
190
|
-
setKeychainToken(bundleMetaItem(bundle.name), json
|
|
191
|
-
|
|
192
|
-
emit('secrets.set', { bundle: bundle.name });
|
|
193
|
-
}
|
|
201
|
+
setKeychainToken(bundleMetaItem(bundle.name), json);
|
|
202
|
+
emit('secrets.set', { bundle: bundle.name });
|
|
194
203
|
}
|
|
195
204
|
export function deleteBundle(name) {
|
|
196
205
|
validateBundleName(name);
|
|
@@ -220,7 +229,7 @@ export function listBundles() {
|
|
|
220
229
|
// SecItemCopyMatching calls collapses the prompt to one. Mirrors the pattern
|
|
221
230
|
// already used by resolveBundleEnv for runtime secret injection.
|
|
222
231
|
const itemsToFetch = names.map(bundleMetaItem);
|
|
223
|
-
const fetched =
|
|
232
|
+
const fetched = getKeychainTokens(itemsToFetch);
|
|
224
233
|
const out = [];
|
|
225
234
|
for (const name of names) {
|
|
226
235
|
const json = fetched.get(bundleMetaItem(name));
|
|
@@ -240,7 +249,6 @@ export function listBundles() {
|
|
|
240
249
|
name,
|
|
241
250
|
description: parsed.description,
|
|
242
251
|
allow_exec: Boolean(parsed.allow_exec),
|
|
243
|
-
icloud_sync: Boolean(parsed.icloud_sync),
|
|
244
252
|
vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
|
|
245
253
|
};
|
|
246
254
|
if (typeof parsed.created_at === 'string')
|
|
@@ -294,90 +302,63 @@ function stampLastUsed(bundle) {
|
|
|
294
302
|
}
|
|
295
303
|
try {
|
|
296
304
|
bundle.last_used = new Date(nowMs).toISOString();
|
|
297
|
-
writeBundle(bundle
|
|
305
|
+
writeBundle(bundle);
|
|
298
306
|
}
|
|
299
307
|
catch {
|
|
300
308
|
// Swallow — telemetry must never block secret resolution.
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
|
-
|
|
311
|
+
// Walk the bundle and produce a flat env map. Every keychain: ref is gathered
|
|
312
|
+
// into a single batch read so macOS shows ONE Touch ID prompt for the whole
|
|
313
|
+
// bundle — including the metadata fetch that already happened in readBundle
|
|
314
|
+
// (the helper's auth context survives across separate invocations only via
|
|
315
|
+
// the per-process LAContext, so we still get one prompt for the batch even
|
|
316
|
+
// if metadata triggered an earlier one). Literals/env/file/exec refs are
|
|
317
|
+
// resolved inline and never reach the keychain.
|
|
318
|
+
export function resolveBundleEnv(bundle, _opts = {}) {
|
|
304
319
|
stampLastUsed(bundle);
|
|
305
320
|
const parsedByKey = new Map();
|
|
306
321
|
const keychainItemsToFetch = [];
|
|
307
|
-
const keychainKeys = [];
|
|
308
|
-
const kindCounts = {};
|
|
309
322
|
for (const [key, raw] of Object.entries(bundle.vars)) {
|
|
310
323
|
const parsed = parseBundleValue(raw);
|
|
311
324
|
parsedByKey.set(key, parsed);
|
|
312
|
-
const kind = 'literal' in parsed ? 'literal' : parsed.ref.provider;
|
|
313
|
-
kindCounts[kind] = (kindCounts[kind] ?? 0) + 1;
|
|
314
325
|
if ('ref' in parsed && parsed.ref.provider === 'keychain') {
|
|
315
|
-
|
|
316
|
-
keychainItemsToFetch.push(item);
|
|
317
|
-
keychainKeys.push(key);
|
|
326
|
+
keychainItemsToFetch.push(secretsKeychainItem(bundle.name, parsed.ref.value));
|
|
318
327
|
}
|
|
319
328
|
}
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const reason = opts.caller
|
|
337
|
-
? `read ${bundle.name} secrets (for ${opts.caller})`
|
|
338
|
-
: `read ${bundle.name} secrets`;
|
|
339
|
-
try {
|
|
340
|
-
// Single helper invocation, one biometric prompt.
|
|
341
|
-
const fetched = keychainItemsToFetch.length > 0
|
|
342
|
-
? getKeychainTokensBatch(keychainItemsToFetch, bundle.icloud_sync, reason)
|
|
343
|
-
: new Map();
|
|
344
|
-
// Second pass: assemble env. Keychain values come from the batch; everything
|
|
345
|
-
// else is resolved inline (literals and env/file/exec refs don't prompt).
|
|
346
|
-
const env = {};
|
|
347
|
-
for (const [key] of Object.entries(bundle.vars)) {
|
|
348
|
-
const parsed = parsedByKey.get(key);
|
|
349
|
-
if ('literal' in parsed) {
|
|
350
|
-
env[key] = parsed.literal;
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
if (parsed.ref.provider === 'keychain') {
|
|
354
|
-
const item = secretsKeychainItem(bundle.name, parsed.ref.value);
|
|
355
|
-
const value = fetched.get(item);
|
|
356
|
-
if (value === undefined) {
|
|
357
|
-
throw new Error(`Bundle '${bundle.name}' key '${key}': keychain item '${item}' not found. ` +
|
|
358
|
-
`Run: agents secrets add ${bundle.name} ${key}`);
|
|
359
|
-
}
|
|
360
|
-
env[key] = value;
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
try {
|
|
364
|
-
env[key] = resolveRef(parsed.ref, {
|
|
365
|
-
allowExec: bundle.allow_exec,
|
|
366
|
-
iCloudSync: bundle.icloud_sync,
|
|
367
|
-
keychainItemFor: (shortId) => secretsKeychainItem(bundle.name, shortId),
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
catch (err) {
|
|
371
|
-
throw new Error(`Bundle '${bundle.name}' key '${key}': ${err.message}`);
|
|
329
|
+
const fetched = keychainItemsToFetch.length > 0
|
|
330
|
+
? getKeychainTokens(keychainItemsToFetch)
|
|
331
|
+
: new Map();
|
|
332
|
+
const env = {};
|
|
333
|
+
for (const [key, raw] of Object.entries(bundle.vars)) {
|
|
334
|
+
const parsed = parsedByKey.get(key);
|
|
335
|
+
if ('literal' in parsed) {
|
|
336
|
+
env[key] = parsed.literal;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (parsed.ref.provider === 'keychain') {
|
|
340
|
+
const item = secretsKeychainItem(bundle.name, parsed.ref.value);
|
|
341
|
+
const value = fetched.get(item);
|
|
342
|
+
if (value === undefined) {
|
|
343
|
+
throw new Error(`Bundle '${bundle.name}' key '${key}': keychain item '${item}' not found. ` +
|
|
344
|
+
`Run: agents secrets add ${bundle.name} ${key}`);
|
|
372
345
|
}
|
|
346
|
+
env[key] = value;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
env[key] = resolveRef(parsed.ref, {
|
|
351
|
+
allowExec: bundle.allow_exec,
|
|
352
|
+
keychainItemFor: (shortId) => secretsKeychainItem(bundle.name, shortId),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
throw new Error(`Bundle '${bundle.name}' key '${key}': ${err.message}`);
|
|
373
357
|
}
|
|
374
|
-
emitReadAudit('success');
|
|
375
|
-
return env;
|
|
376
|
-
}
|
|
377
|
-
catch (err) {
|
|
378
|
-
emitReadAudit('error', err);
|
|
379
|
-
throw err;
|
|
380
358
|
}
|
|
359
|
+
// `caller` is intentionally unused; see ResolveBundleOptions.
|
|
360
|
+
void _opts.caller;
|
|
361
|
+
return env;
|
|
381
362
|
}
|
|
382
363
|
/**
|
|
383
364
|
* Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
|
|
@@ -406,10 +387,8 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
406
387
|
const reason = opts.caller
|
|
407
388
|
? `read ${name} secrets (for ${opts.caller})`
|
|
408
389
|
: `read ${name} secrets`;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// device-local items in one shot.
|
|
412
|
-
const fetched = getKeychainTokensBatch([metaItem, ...secretItems], false, reason);
|
|
390
|
+
void reason;
|
|
391
|
+
const fetched = getKeychainTokens([metaItem, ...secretItems]);
|
|
413
392
|
const json = fetched.get(metaItem);
|
|
414
393
|
if (json === undefined) {
|
|
415
394
|
throw new Error(`Secrets bundle '${name}' not found.`);
|
|
@@ -428,7 +407,6 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
428
407
|
name,
|
|
429
408
|
description: parsed.description,
|
|
430
409
|
allow_exec: Boolean(parsed.allow_exec),
|
|
431
|
-
icloud_sync: Boolean(parsed.icloud_sync),
|
|
432
410
|
vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
|
|
433
411
|
};
|
|
434
412
|
if (typeof parsed.created_at === 'string')
|
|
@@ -490,7 +468,6 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
490
468
|
try {
|
|
491
469
|
env[key] = resolveRef(p.ref, {
|
|
492
470
|
allowExec: bundle.allow_exec,
|
|
493
|
-
iCloudSync: bundle.icloud_sync,
|
|
494
471
|
keychainItemFor: (shortId) => secretsKeychainItem(bundle.name, shortId),
|
|
495
472
|
});
|
|
496
473
|
}
|
|
@@ -529,7 +506,7 @@ export function rotateBundleSecret(bundle, key, opts) {
|
|
|
529
506
|
}
|
|
530
507
|
const shortId = raw.slice('keychain:'.length);
|
|
531
508
|
const item = secretsKeychainItem(bundle.name, shortId);
|
|
532
|
-
setKeychainToken(item, opts.newValue
|
|
509
|
+
setKeychainToken(item, opts.newValue);
|
|
533
510
|
if (opts.clearMeta) {
|
|
534
511
|
if (bundle.meta)
|
|
535
512
|
delete bundle.meta[key];
|
|
@@ -560,8 +537,8 @@ export function rotateBundleSecret(bundle, key, opts) {
|
|
|
560
537
|
* 4) write new bundle metadata
|
|
561
538
|
* 5) delete the old per-key keychain items + old metadata
|
|
562
539
|
*
|
|
563
|
-
* Steps 1-4 are reversible. If 5 partially fails
|
|
564
|
-
*
|
|
540
|
+
* Steps 1-4 are reversible. If 5 partially fails, running `rename` again is
|
|
541
|
+
* a safe no-op for the source items.
|
|
565
542
|
*/
|
|
566
543
|
export function renameBundle(oldName, newName, opts = {}) {
|
|
567
544
|
validateBundleName(oldName);
|
|
@@ -579,7 +556,7 @@ export function renameBundle(oldName, newName, opts = {}) {
|
|
|
579
556
|
}
|
|
580
557
|
const dest = readBundle(newName);
|
|
581
558
|
for (const { item } of keychainItemsForBundle(dest)) {
|
|
582
|
-
deleteKeychainToken(item
|
|
559
|
+
deleteKeychainToken(item);
|
|
583
560
|
}
|
|
584
561
|
deleteBundle(newName);
|
|
585
562
|
}
|
|
@@ -592,15 +569,15 @@ export function renameBundle(oldName, newName, opts = {}) {
|
|
|
592
569
|
continue;
|
|
593
570
|
const shortId = raw.slice('keychain:'.length);
|
|
594
571
|
const newItem = secretsKeychainItem(newName, shortId);
|
|
595
|
-
const value = getKeychainToken(oldItem
|
|
596
|
-
setKeychainToken(newItem, value
|
|
572
|
+
const value = getKeychainToken(oldItem);
|
|
573
|
+
setKeychainToken(newItem, value);
|
|
597
574
|
}
|
|
598
575
|
// writeBundle preserves source.created_at and refreshes updated_at.
|
|
599
576
|
const renamed = { ...source, name: newName };
|
|
600
577
|
writeBundle(renamed);
|
|
601
578
|
// Cleanup: delete the old per-key keychain items, then the old metadata.
|
|
602
579
|
for (const { item: oldItem } of sourceItems) {
|
|
603
|
-
deleteKeychainToken(oldItem
|
|
580
|
+
deleteKeychainToken(oldItem);
|
|
604
581
|
}
|
|
605
582
|
deleteBundle(oldName);
|
|
606
583
|
emit('secrets.rename', { from: oldName, to: newName });
|
|
@@ -674,7 +651,6 @@ export async function migrateLegacyBundles(confirmBundle) {
|
|
|
674
651
|
name,
|
|
675
652
|
description: parsed.description,
|
|
676
653
|
allow_exec: Boolean(parsed.allow_exec),
|
|
677
|
-
icloud_sync: Boolean(parsed.icloud_sync),
|
|
678
654
|
vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
|
|
679
655
|
};
|
|
680
656
|
const keys = Object.keys(bundle.vars);
|