@phnx-labs/agents-cli 1.14.2 → 1.14.4
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/README.md +17 -7
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +7 -0
- package/dist/commands/browser.d.ts +3 -0
- package/dist/commands/browser.js +392 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +198 -11
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +184 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +47 -14
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +123 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources/commands.d.ts +46 -0
- package/dist/lib/resources/commands.js +208 -0
- package/dist/lib/resources/hooks.d.ts +12 -0
- package/dist/lib/resources/hooks.js +136 -0
- package/dist/lib/resources/index.d.ts +36 -0
- package/dist/lib/resources/index.js +69 -0
- package/dist/lib/resources/mcp.d.ts +34 -0
- package/dist/lib/resources/mcp.js +483 -0
- package/dist/lib/resources/permissions.d.ts +13 -0
- package/dist/lib/resources/permissions.js +184 -0
- package/dist/lib/resources/rules.d.ts +43 -0
- package/dist/lib/resources/rules.js +146 -0
- package/dist/lib/resources/skills.d.ts +37 -0
- package/dist/lib/resources/skills.js +238 -0
- package/dist/lib/resources/subagents.d.ts +46 -0
- package/dist/lib/resources/subagents.js +198 -0
- package/dist/lib/resources/types.d.ts +82 -0
- package/dist/lib/resources/types.js +8 -0
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +8 -1
- package/dist/lib/rotate.js +17 -4
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -78
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +12 -1
- package/dist/lib/teams/registry.js +12 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +12 -6
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +32 -3
- package/dist/lib/versions.js +147 -119
- package/package.json +3 -2
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- package/dist/lib/memory-compile.d.ts +0 -66
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Secret bundles -- named sets of keychain-backed environment variables.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Bundle metadata (name, description, vars map) is stored in the macOS
|
|
5
|
+
* Keychain as a JSON blob under `agents-cli.bundles.<name>`. Bundles created
|
|
6
|
+
* with `--icloud-sync` write the metadata to the iCloud-synced keychain so
|
|
7
|
+
* the full bundle definition (not just secret values) propagates across
|
|
8
|
+
* the user's Macs. Nothing about secrets ever lives in plaintext on disk.
|
|
9
|
+
*
|
|
10
|
+
* Secret values keep their old layout: one keychain item per key under
|
|
11
|
+
* `agents-cli.secrets.<bundle>.<key>`, sync-state matching the bundle's
|
|
12
|
+
* `icloud_sync` flag.
|
|
7
13
|
*/
|
|
8
14
|
import { type BundleValue, type SecretRef } from './index.js';
|
|
15
|
+
/** Allowed values for a secret's `type` metadata field. */
|
|
16
|
+
export declare const SECRET_TYPES: readonly ["api-key", "token", "password", "url", "database-url", "ssh-key", "certificate", "webhook", "note"];
|
|
17
|
+
export type SecretType = typeof SECRET_TYPES[number];
|
|
18
|
+
/** Per-secret metadata. All fields optional; absent ones omitted at write time. */
|
|
19
|
+
export interface VarMeta {
|
|
20
|
+
type?: SecretType;
|
|
21
|
+
/** ISO date 'YYYY-MM-DD'. Always future-dated at write time. */
|
|
22
|
+
expires?: string;
|
|
23
|
+
/** Singular freeform note. */
|
|
24
|
+
note?: string;
|
|
25
|
+
}
|
|
9
26
|
/** A named set of environment variable definitions backed by various secret providers. */
|
|
10
27
|
export interface SecretsBundle {
|
|
11
28
|
name: string;
|
|
12
29
|
description?: string;
|
|
13
30
|
allow_exec?: boolean;
|
|
14
|
-
/** When true, keychain-backed values
|
|
31
|
+
/** When true, keychain-backed values and bundle metadata sync via iCloud Keychain. */
|
|
15
32
|
icloud_sync?: boolean;
|
|
16
33
|
vars: Record<string, BundleValue>;
|
|
34
|
+
/** Optional per-var metadata, keyed by var name (parallel to `vars`). */
|
|
35
|
+
meta?: Record<string, VarMeta>;
|
|
17
36
|
}
|
|
37
|
+
export declare const RESERVED_ENV_NAMES: Set<string>;
|
|
38
|
+
export declare function bundleToEnvPrefix(name: string): string;
|
|
39
|
+
export declare function isReservedEnvName(key: string): boolean;
|
|
18
40
|
/** Validate a bundle name against the allowed pattern. Throws on invalid input. */
|
|
19
41
|
export declare function validateBundleName(name: string): void;
|
|
20
42
|
export declare function validateEnvKey(key: string): void;
|
|
43
|
+
/** Assert that `t` is one of the known SECRET_TYPES. Throws with the allowed list otherwise. */
|
|
44
|
+
export declare function validateSecretType(t: string): asserts t is SecretType;
|
|
45
|
+
/**
|
|
46
|
+
* Validate an `expires` value. Accepts strict 'YYYY-MM-DD' only and rejects
|
|
47
|
+
* any date <= now. We compare against end-of-day UTC for the chosen date so
|
|
48
|
+
* "today" is treated as past (per spec).
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateExpiresFutureDated(iso: string): void;
|
|
21
51
|
export declare function bundleExists(name: string): boolean;
|
|
22
52
|
export declare function readBundle(name: string): SecretsBundle;
|
|
23
53
|
export declare function writeBundle(bundle: SecretsBundle): void;
|
|
@@ -31,9 +61,36 @@ export interface BundleEntryInfo {
|
|
|
31
61
|
export declare function describeBundle(bundle: SecretsBundle): BundleEntryInfo[];
|
|
32
62
|
export declare function resolveBundleEnv(bundle: SecretsBundle): Record<string, string>;
|
|
33
63
|
export declare function keychainRef(key: string): string;
|
|
64
|
+
/** Options for rotateBundleSecret. */
|
|
65
|
+
export interface RotateOptions {
|
|
66
|
+
/** New plaintext value to write into keychain (replaces the old one). */
|
|
67
|
+
newValue: string;
|
|
68
|
+
/** When true, drop existing meta for this key. Mutually exclusive with `meta`. */
|
|
69
|
+
clearMeta?: boolean;
|
|
70
|
+
/** Patch to merge into existing meta. Undefined fields preserve current values. */
|
|
71
|
+
meta?: Partial<VarMeta>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rotate a keychain-backed secret in `bundle`. Errors if `key` is not present
|
|
75
|
+
* in the bundle (use `add` to introduce a new key). Preserves existing meta
|
|
76
|
+
* unless `clearMeta` or a `meta` patch is supplied.
|
|
77
|
+
*/
|
|
78
|
+
export declare function rotateBundleSecret(bundle: SecretsBundle, key: string, opts: RotateOptions): void;
|
|
34
79
|
export declare function keychainItemsForBundle(bundle: SecretsBundle): Array<{
|
|
35
80
|
key: string;
|
|
36
81
|
item: string;
|
|
37
82
|
}>;
|
|
38
83
|
export declare function parseDotenv(content: string): Record<string, string>;
|
|
84
|
+
/**
|
|
85
|
+
* One-shot migration: move legacy YAML bundles into the keychain. Scans both
|
|
86
|
+
* `~/.agents/secrets/` and `~/.agents-system/secrets/` — past versions of the
|
|
87
|
+
* CLI sometimes wrote bundles into the system repo even though that's never
|
|
88
|
+
* been a legitimate location. After migration the directories are removed so
|
|
89
|
+
* the system repo never carries a `secrets/` subdir again.
|
|
90
|
+
*
|
|
91
|
+
* Idempotent: re-runs after the dirs are gone are no-ops. Called eagerly at
|
|
92
|
+
* the top of every `agents secrets` subcommand. Skipped on the latency-
|
|
93
|
+
* sensitive `agents run` path.
|
|
94
|
+
*/
|
|
95
|
+
export declare function migrateLegacyBundles(): void;
|
|
39
96
|
export type { SecretRef };
|
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Secret bundles -- named sets of keychain-backed environment variables.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Bundle metadata (name, description, vars map) is stored in the macOS
|
|
5
|
+
* Keychain as a JSON blob under `agents-cli.bundles.<name>`. Bundles created
|
|
6
|
+
* with `--icloud-sync` write the metadata to the iCloud-synced keychain so
|
|
7
|
+
* the full bundle definition (not just secret values) propagates across
|
|
8
|
+
* the user's Macs. Nothing about secrets ever lives in plaintext on disk.
|
|
9
|
+
*
|
|
10
|
+
* Secret values keep their old layout: one keychain item per key under
|
|
11
|
+
* `agents-cli.secrets.<bundle>.<key>`, sync-state matching the bundle's
|
|
12
|
+
* `icloud_sync` flag.
|
|
7
13
|
*/
|
|
8
14
|
import * as fs from 'fs';
|
|
15
|
+
import * as os from 'os';
|
|
9
16
|
import * as path from 'path';
|
|
10
17
|
import * as yaml from 'yaml';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
const
|
|
18
|
+
import { deleteKeychainToken, getKeychainToken, hasKeychainToken, listKeychainItems, parseBundleValue, resolveRef, secretsKeychainItem, setKeychainToken, } from './index.js';
|
|
19
|
+
/** Allowed values for a secret's `type` metadata field. */
|
|
20
|
+
export const SECRET_TYPES = [
|
|
21
|
+
'api-key',
|
|
22
|
+
'token',
|
|
23
|
+
'password',
|
|
24
|
+
'url',
|
|
25
|
+
'database-url',
|
|
26
|
+
'ssh-key',
|
|
27
|
+
'certificate',
|
|
28
|
+
'webhook',
|
|
29
|
+
'note',
|
|
30
|
+
];
|
|
31
|
+
const BUNDLE_NAME_PATTERN = /^[a-z0-9][a-z0-9\-_.]{0,48}$/i;
|
|
14
32
|
const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
33
|
+
const BUNDLE_META_PREFIX = 'agents-cli.bundles.';
|
|
34
|
+
export const RESERVED_ENV_NAMES = new Set([
|
|
35
|
+
'PATH', 'HOME', 'USER', 'USERNAME', 'SHELL', 'PWD', 'OLDPWD',
|
|
36
|
+
'TERM', 'LANG', 'LC_ALL', 'DISPLAY', 'EDITOR', 'VISUAL',
|
|
37
|
+
'TMPDIR', 'TMP', 'TEMP', 'LOGNAME', 'UID', 'EUID', 'HOSTNAME',
|
|
38
|
+
]);
|
|
39
|
+
export function bundleToEnvPrefix(name) {
|
|
40
|
+
return name.replace(/[-\.]/g, '_').toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
export function isReservedEnvName(key) {
|
|
43
|
+
return RESERVED_ENV_NAMES.has(key);
|
|
44
|
+
}
|
|
15
45
|
/** Validate a bundle name against the allowed pattern. Throws on invalid input. */
|
|
16
46
|
export function validateBundleName(name) {
|
|
17
47
|
if (!BUNDLE_NAME_PATTERN.test(name)) {
|
|
18
|
-
throw new Error(`Invalid bundle name '${name}'. Use letters, digits, dash, underscore (max 48 chars).`);
|
|
48
|
+
throw new Error(`Invalid bundle name '${name}'. Use letters, digits, dash, underscore, dot (max 48 chars).`);
|
|
19
49
|
}
|
|
20
50
|
}
|
|
21
51
|
export function validateEnvKey(key) {
|
|
@@ -23,37 +53,64 @@ export function validateEnvKey(key) {
|
|
|
23
53
|
throw new Error(`Invalid environment variable name '${key}'. Must match [A-Za-z_][A-Za-z0-9_]*.`);
|
|
24
54
|
}
|
|
25
55
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
/** Assert that `t` is one of the known SECRET_TYPES. Throws with the allowed list otherwise. */
|
|
57
|
+
export function validateSecretType(t) {
|
|
58
|
+
if (!SECRET_TYPES.includes(t)) {
|
|
59
|
+
throw new Error(`Invalid type '${t}'. One of: ${SECRET_TYPES.join(', ')}.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate an `expires` value. Accepts strict 'YYYY-MM-DD' only and rejects
|
|
64
|
+
* any date <= now. We compare against end-of-day UTC for the chosen date so
|
|
65
|
+
* "today" is treated as past (per spec).
|
|
66
|
+
*/
|
|
67
|
+
export function validateExpiresFutureDated(iso) {
|
|
68
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) {
|
|
69
|
+
throw new Error(`Invalid --expires '${iso}'. Use YYYY-MM-DD.`);
|
|
70
|
+
}
|
|
71
|
+
const target = new Date(iso + 'T23:59:59Z');
|
|
72
|
+
if (Number.isNaN(target.getTime()))
|
|
73
|
+
throw new Error(`Invalid --expires date '${iso}'.`);
|
|
74
|
+
if (target.getTime() <= Date.now()) {
|
|
75
|
+
throw new Error(`--expires must be future-dated. Got '${iso}'.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function bundleMetaItem(name) {
|
|
79
|
+
return BUNDLE_META_PREFIX + name;
|
|
35
80
|
}
|
|
36
81
|
export function bundleExists(name) {
|
|
37
|
-
|
|
82
|
+
validateBundleName(name);
|
|
83
|
+
return hasKeychainToken(bundleMetaItem(name));
|
|
38
84
|
}
|
|
39
85
|
export function readBundle(name) {
|
|
40
86
|
validateBundleName(name);
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
let json;
|
|
88
|
+
try {
|
|
89
|
+
json = getKeychainToken(bundleMetaItem(name));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
43
92
|
throw new Error(`Secrets bundle '${name}' not found.`);
|
|
44
93
|
}
|
|
45
|
-
|
|
46
|
-
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(json);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error(`Bundle '${name}' is malformed.`);
|
|
100
|
+
}
|
|
47
101
|
if (!parsed || typeof parsed !== 'object') {
|
|
48
102
|
throw new Error(`Bundle '${name}' is malformed.`);
|
|
49
103
|
}
|
|
50
104
|
const bundle = {
|
|
51
|
-
name
|
|
105
|
+
name,
|
|
52
106
|
description: parsed.description,
|
|
53
107
|
allow_exec: Boolean(parsed.allow_exec),
|
|
54
108
|
icloud_sync: Boolean(parsed.icloud_sync),
|
|
55
109
|
vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
|
|
56
110
|
};
|
|
111
|
+
if (parsed.meta && typeof parsed.meta === 'object') {
|
|
112
|
+
bundle.meta = parsed.meta;
|
|
113
|
+
}
|
|
57
114
|
for (const key of Object.keys(bundle.vars)) {
|
|
58
115
|
validateEnvKey(key);
|
|
59
116
|
}
|
|
@@ -64,48 +121,59 @@ export function writeBundle(bundle) {
|
|
|
64
121
|
for (const key of Object.keys(bundle.vars)) {
|
|
65
122
|
validateEnvKey(key);
|
|
66
123
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
124
|
+
// Strip empty/all-undefined meta entries so the JSON stays tidy.
|
|
125
|
+
let meta;
|
|
126
|
+
if (bundle.meta) {
|
|
127
|
+
for (const [key, m] of Object.entries(bundle.meta)) {
|
|
128
|
+
const cleaned = {};
|
|
129
|
+
if (m.type)
|
|
130
|
+
cleaned.type = m.type;
|
|
131
|
+
if (m.expires)
|
|
132
|
+
cleaned.expires = m.expires;
|
|
133
|
+
if (m.note)
|
|
134
|
+
cleaned.note = m.note;
|
|
135
|
+
if (Object.keys(cleaned).length > 0) {
|
|
136
|
+
if (!meta)
|
|
137
|
+
meta = {};
|
|
138
|
+
meta[key] = cleaned;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const payload = {
|
|
71
143
|
description: bundle.description,
|
|
72
144
|
allow_exec: bundle.allow_exec ? true : undefined,
|
|
73
145
|
icloud_sync: bundle.icloud_sync ? true : undefined,
|
|
74
146
|
vars: bundle.vars,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
fs.renameSync(tmp, file);
|
|
147
|
+
meta,
|
|
148
|
+
};
|
|
149
|
+
const json = JSON.stringify(payload);
|
|
150
|
+
setKeychainToken(bundleMetaItem(bundle.name), json, Boolean(bundle.icloud_sync));
|
|
80
151
|
}
|
|
81
152
|
export function deleteBundle(name) {
|
|
82
153
|
validateBundleName(name);
|
|
83
|
-
|
|
84
|
-
if (!fs.existsSync(file))
|
|
85
|
-
return false;
|
|
86
|
-
fs.unlinkSync(file);
|
|
87
|
-
return true;
|
|
154
|
+
return deleteKeychainToken(bundleMetaItem(name));
|
|
88
155
|
}
|
|
89
156
|
export function listBundles() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
157
|
+
let services;
|
|
158
|
+
try {
|
|
159
|
+
services = listKeychainItems(BUNDLE_META_PREFIX);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const names = services
|
|
165
|
+
.map((s) => s.slice(BUNDLE_META_PREFIX.length))
|
|
166
|
+
.filter((n) => BUNDLE_NAME_PATTERN.test(n));
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const name of names) {
|
|
169
|
+
try {
|
|
170
|
+
out.push(readBundle(name));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Skip malformed bundles; surfaced via `agents secrets view <name>`.
|
|
106
174
|
}
|
|
107
175
|
}
|
|
108
|
-
return
|
|
176
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
109
177
|
}
|
|
110
178
|
export function describeBundle(bundle) {
|
|
111
179
|
const out = [];
|
|
@@ -149,10 +217,49 @@ export function resolveBundleEnv(bundle) {
|
|
|
149
217
|
}
|
|
150
218
|
return env;
|
|
151
219
|
}
|
|
152
|
-
// Build a keychain ref expression from a bundle+key pair, for storage in
|
|
220
|
+
// Build a keychain ref expression from a bundle+key pair, for storage in the bundle metadata.
|
|
153
221
|
export function keychainRef(key) {
|
|
154
222
|
return `keychain:${key}`;
|
|
155
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Rotate a keychain-backed secret in `bundle`. Errors if `key` is not present
|
|
226
|
+
* in the bundle (use `add` to introduce a new key). Preserves existing meta
|
|
227
|
+
* unless `clearMeta` or a `meta` patch is supplied.
|
|
228
|
+
*/
|
|
229
|
+
export function rotateBundleSecret(bundle, key, opts) {
|
|
230
|
+
validateBundleName(bundle.name);
|
|
231
|
+
validateEnvKey(key);
|
|
232
|
+
if (!(key in bundle.vars)) {
|
|
233
|
+
throw new Error(`Key '${key}' not in bundle '${bundle.name}'. Use 'agents secrets add' to add a new key.`);
|
|
234
|
+
}
|
|
235
|
+
const raw = bundle.vars[key];
|
|
236
|
+
// We only rotate keychain-backed values. Literals/refs aren't "secrets" in
|
|
237
|
+
// the same sense — pivot the user back to add/remove.
|
|
238
|
+
if (typeof raw !== 'string' || !raw.startsWith('keychain:')) {
|
|
239
|
+
throw new Error(`Key '${key}' in bundle '${bundle.name}' is not keychain-backed; cannot rotate.`);
|
|
240
|
+
}
|
|
241
|
+
const shortId = raw.slice('keychain:'.length);
|
|
242
|
+
const item = secretsKeychainItem(bundle.name, shortId);
|
|
243
|
+
setKeychainToken(item, opts.newValue, bundle.icloud_sync);
|
|
244
|
+
if (opts.clearMeta) {
|
|
245
|
+
if (bundle.meta)
|
|
246
|
+
delete bundle.meta[key];
|
|
247
|
+
}
|
|
248
|
+
else if (opts.meta && Object.keys(opts.meta).length > 0) {
|
|
249
|
+
if (!bundle.meta)
|
|
250
|
+
bundle.meta = {};
|
|
251
|
+
const current = bundle.meta[key] ?? {};
|
|
252
|
+
const patched = { ...current };
|
|
253
|
+
if (opts.meta.type !== undefined)
|
|
254
|
+
patched.type = opts.meta.type;
|
|
255
|
+
if (opts.meta.expires !== undefined)
|
|
256
|
+
patched.expires = opts.meta.expires;
|
|
257
|
+
if (opts.meta.note !== undefined)
|
|
258
|
+
patched.note = opts.meta.note;
|
|
259
|
+
bundle.meta[key] = patched;
|
|
260
|
+
}
|
|
261
|
+
writeBundle(bundle);
|
|
262
|
+
}
|
|
156
263
|
// Iterate all keychain-backed keys in a bundle for cleanup on rm/unset.
|
|
157
264
|
export function keychainItemsForBundle(bundle) {
|
|
158
265
|
const items = [];
|
|
@@ -187,3 +294,64 @@ export function parseDotenv(content) {
|
|
|
187
294
|
}
|
|
188
295
|
return out;
|
|
189
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* One-shot migration: move legacy YAML bundles into the keychain. Scans both
|
|
299
|
+
* `~/.agents/secrets/` and `~/.agents-system/secrets/` — past versions of the
|
|
300
|
+
* CLI sometimes wrote bundles into the system repo even though that's never
|
|
301
|
+
* been a legitimate location. After migration the directories are removed so
|
|
302
|
+
* the system repo never carries a `secrets/` subdir again.
|
|
303
|
+
*
|
|
304
|
+
* Idempotent: re-runs after the dirs are gone are no-ops. Called eagerly at
|
|
305
|
+
* the top of every `agents secrets` subcommand. Skipped on the latency-
|
|
306
|
+
* sensitive `agents run` path.
|
|
307
|
+
*/
|
|
308
|
+
export function migrateLegacyBundles() {
|
|
309
|
+
const home = os.homedir();
|
|
310
|
+
const dirs = [
|
|
311
|
+
path.join(home, '.agents', 'secrets'),
|
|
312
|
+
path.join(home, '.agents-system', 'secrets'),
|
|
313
|
+
];
|
|
314
|
+
let migrated = 0;
|
|
315
|
+
for (const dir of dirs) {
|
|
316
|
+
let entries;
|
|
317
|
+
try {
|
|
318
|
+
entries = fs.readdirSync(dir);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const ymls = entries.filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
324
|
+
for (const entry of ymls) {
|
|
325
|
+
const file = path.join(dir, entry);
|
|
326
|
+
const name = entry.replace(/\.(yml|yaml)$/, '');
|
|
327
|
+
try {
|
|
328
|
+
validateBundleName(name);
|
|
329
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
330
|
+
const parsed = yaml.parse(raw);
|
|
331
|
+
if (!parsed || typeof parsed !== 'object')
|
|
332
|
+
continue;
|
|
333
|
+
const bundle = {
|
|
334
|
+
name,
|
|
335
|
+
description: parsed.description,
|
|
336
|
+
allow_exec: Boolean(parsed.allow_exec),
|
|
337
|
+
icloud_sync: Boolean(parsed.icloud_sync),
|
|
338
|
+
vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
|
|
339
|
+
};
|
|
340
|
+
writeBundle(bundle);
|
|
341
|
+
fs.unlinkSync(file);
|
|
342
|
+
migrated++;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Leave malformed YAMLs in place so the user can inspect them.
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
if (fs.readdirSync(dir).length === 0)
|
|
350
|
+
fs.rmdirSync(dir);
|
|
351
|
+
}
|
|
352
|
+
catch { /* not empty or already gone */ }
|
|
353
|
+
}
|
|
354
|
+
if (migrated > 0) {
|
|
355
|
+
console.log(`Migrated ${migrated} legacy bundle${migrated === 1 ? '' : 's'} into keychain.`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* macOS Keychain integration for secure credential storage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* All reads/writes go through a signed Swift helper (keychain-helper.swift)
|
|
5
|
+
* compiled into AgentsKeychain.app. The .app embeds a provisioning profile
|
|
6
|
+
* that grants the application-identifier + keychain-access-groups entitlement
|
|
7
|
+
* macOS requires for kSecAttrSynchronizable writes (iCloud Keychain).
|
|
8
|
+
* For device-local writes the helper is invoked with the `nosync` arg.
|
|
7
9
|
*/
|
|
8
10
|
/** Supported secret resolution backends. */
|
|
9
11
|
export type SecretProvider = 'keychain' | 'env' | 'file' | 'exec';
|
|
@@ -13,14 +15,14 @@ export interface SecretRef {
|
|
|
13
15
|
value: string;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
|
-
* A bundle
|
|
18
|
+
* A bundle value: either a string (literal or provider-prefixed ref) or
|
|
17
19
|
* an object `{value: string}` used to escape a literal that would otherwise
|
|
18
20
|
* be parsed as a ref (e.g. a URL that happens to start with 'env:').
|
|
19
21
|
*/
|
|
20
22
|
export type BundleValue = string | {
|
|
21
23
|
value: string;
|
|
22
24
|
};
|
|
23
|
-
/** Parse a bundle
|
|
25
|
+
/** Parse a bundle value into either a literal string or a typed secret ref. */
|
|
24
26
|
export declare function parseBundleValue(raw: BundleValue): {
|
|
25
27
|
literal: string;
|
|
26
28
|
} | {
|
|
@@ -32,6 +34,21 @@ export declare function serializeRef(ref: SecretRef): string;
|
|
|
32
34
|
export declare function profileKeychainItem(provider: string): string;
|
|
33
35
|
/** Build the keychain item name for a secrets-bundle key. */
|
|
34
36
|
export declare function secretsKeychainItem(bundle: string, key: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Test seam: lets bundle storage tests swap the keychain backend for an
|
|
39
|
+
* in-memory map without touching the user's real keychain. Mocking is
|
|
40
|
+
* justified here because the alternative (touching real keychain in unit
|
|
41
|
+
* tests) is destructive and would require an interactive Keychain unlock.
|
|
42
|
+
*/
|
|
43
|
+
export interface KeychainBackend {
|
|
44
|
+
has(item: string, sync: boolean): boolean;
|
|
45
|
+
get(item: string, sync: boolean): string;
|
|
46
|
+
set(item: string, value: string, sync: boolean): void;
|
|
47
|
+
delete(item: string, sync: boolean): boolean;
|
|
48
|
+
list(prefix: string): string[];
|
|
49
|
+
}
|
|
50
|
+
/** Install a custom keychain backend (test only). Returns the previous backend so callers can restore. */
|
|
51
|
+
export declare function setKeychainBackendForTest(b: KeychainBackend | null): KeychainBackend | null;
|
|
35
52
|
/** Check if a keychain item exists (macOS only). */
|
|
36
53
|
export declare function hasKeychainToken(item: string, sync?: boolean): boolean;
|
|
37
54
|
/** Retrieve a secret value from the macOS Keychain. Throws if not found. */
|
|
@@ -40,6 +57,8 @@ export declare function getKeychainToken(item: string, sync?: boolean): string;
|
|
|
40
57
|
export declare function setKeychainToken(item: string, value: string, sync?: boolean): void;
|
|
41
58
|
/** Delete a keychain item. Returns true if it existed. */
|
|
42
59
|
export declare function deleteKeychainToken(item: string, sync?: boolean): boolean;
|
|
60
|
+
/** Enumerate keychain item service names whose name starts with the given prefix. */
|
|
61
|
+
export declare function listKeychainItems(prefix: string): string[];
|
|
43
62
|
/** Options controlling how secret refs are resolved. */
|
|
44
63
|
export interface ResolveOptions {
|
|
45
64
|
/** Translate a short keychain ID to a fully namespaced item name. */
|