@phnx-labs/agents-cli 1.20.50 → 1.20.52
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 +40 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.js +7 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +66 -8
- package/dist/commands/factory.js +9 -4
- package/dist/commands/feed.d.ts +9 -0
- package/dist/commands/feed.js +69 -0
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +9 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +23 -0
- package/dist/commands/secrets.js +174 -70
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +5 -27
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/index.js +6 -2
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +15 -24
- package/dist/lib/cloud/rush.js +7 -32
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.d.ts +6 -0
- package/dist/lib/crabbox/lease.js +40 -10
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +73 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +288 -101
- package/dist/lib/exec.js +32 -14
- package/dist/lib/feed.d.ts +56 -0
- package/dist/lib/feed.js +251 -0
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +24 -10
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/runner.js +10 -2
- 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/agent.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +38 -8
- package/dist/lib/secrets/icloud-import.d.ts +70 -0
- package/dist/lib/secrets/icloud-import.js +173 -0
- package/dist/lib/secrets/index.d.ts +36 -0
- package/dist/lib/secrets/index.js +99 -9
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +60 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +100 -11
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +9 -3
- package/dist/lib/shims.js +44 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/types.d.ts +24 -1
- package/dist/lib/versions.d.ts +0 -26
- package/dist/lib/versions.js +2 -145
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
|
@@ -25,7 +25,7 @@ import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainTo
|
|
|
25
25
|
import { fileStore } from './filestore.js';
|
|
26
26
|
import { emit } from '../events.js';
|
|
27
27
|
import { readMeta } from '../state.js';
|
|
28
|
-
import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, secretsAgentAutoEnabled, DEFAULT_TTL_MS } from './agent.js';
|
|
28
|
+
import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, agentEvictSync, secretsAgentAutoEnabled, DEFAULT_TTL_MS } from './agent.js';
|
|
29
29
|
import { createHash } from 'node:crypto';
|
|
30
30
|
const keychainStore = {
|
|
31
31
|
has: hasKeychainToken,
|
|
@@ -105,9 +105,9 @@ export const SECRET_TYPES = [
|
|
|
105
105
|
];
|
|
106
106
|
/** Minimum gap between last_used updates so the keychain isn't written on every secrets injection. */
|
|
107
107
|
const LAST_USED_THROTTLE_MS = 60_000;
|
|
108
|
-
const BUNDLE_NAME_PATTERN = /^[a-z0-9][a-z0-9\-_.]{0,48}$/i;
|
|
109
|
-
const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
110
|
-
const BUNDLE_META_PREFIX = 'agents-cli.bundles.';
|
|
108
|
+
export const BUNDLE_NAME_PATTERN = /^[a-z0-9][a-z0-9\-_.]{0,48}$/i;
|
|
109
|
+
export const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
110
|
+
export const BUNDLE_META_PREFIX = 'agents-cli.bundles.';
|
|
111
111
|
const SECRETS_ITEM_PREFIX = 'agents-cli.secrets.';
|
|
112
112
|
export const RESERVED_ENV_NAMES = new Set([
|
|
113
113
|
'PATH', 'HOME', 'USER', 'USERNAME', 'SHELL', 'PWD', 'OLDPWD',
|
|
@@ -277,7 +277,24 @@ export function secretsDefaultPolicy() {
|
|
|
277
277
|
export function bundlePolicy(bundle) {
|
|
278
278
|
return bundle.policy ?? secretsDefaultPolicy();
|
|
279
279
|
}
|
|
280
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Whether a bundle write should evict the broker-held copy. Pure + exported
|
|
282
|
+
* for regression coverage. Skips when the writer opted out (stampLastUsed),
|
|
283
|
+
* when the broker integration is disabled (AGENTS_SECRETS_NO_AGENT — the same
|
|
284
|
+
* kill-switch the read fast-path honors), or when a test keychain backend is
|
|
285
|
+
* installed (an in-memory backend has no real keychain behind it, and a test
|
|
286
|
+
* writing bundle 'prod' must never evict the user's real 'prod' unlock).
|
|
287
|
+
*/
|
|
288
|
+
export function shouldEvictAfterBundleWrite(skipRequested, noAgentEnv, backendOverridden) {
|
|
289
|
+
if (skipRequested)
|
|
290
|
+
return false;
|
|
291
|
+
if (noAgentEnv === '1')
|
|
292
|
+
return false;
|
|
293
|
+
if (backendOverridden)
|
|
294
|
+
return false;
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
export function writeBundle(bundle, opts = {}) {
|
|
281
298
|
validateBundleName(bundle.name);
|
|
282
299
|
const backend = bundle.backend ?? 'keychain';
|
|
283
300
|
if (backend === 'file')
|
|
@@ -337,12 +354,20 @@ export function writeBundle(bundle) {
|
|
|
337
354
|
// no-ACL command is missing) rather than silently landing an ACL'd item.
|
|
338
355
|
itemStore(backend).set(bundleMetaItem(bundle.name), json, { noAcl: bundle.policy === 'never' });
|
|
339
356
|
emit('secrets.set', { module: 'secrets', bundle: bundle.name });
|
|
357
|
+
// A broker-held snapshot predates this write; evict it so the next read
|
|
358
|
+
// re-resolves from the keychain instead of serving stale values.
|
|
359
|
+
if (shouldEvictAfterBundleWrite(Boolean(opts.skipBrokerEviction), process.env.AGENTS_SECRETS_NO_AGENT, isKeychainBackendOverridden())) {
|
|
360
|
+
agentEvictSync(bundle.name);
|
|
361
|
+
}
|
|
340
362
|
}
|
|
341
363
|
export function deleteBundle(name) {
|
|
342
364
|
validateBundleName(name);
|
|
343
365
|
const deleted = itemStore(bundleBackend(name)).delete(bundleMetaItem(name));
|
|
344
366
|
if (deleted) {
|
|
345
367
|
emit('secrets.delete', { module: 'secrets', bundle: name });
|
|
368
|
+
if (shouldEvictAfterBundleWrite(false, process.env.AGENTS_SECRETS_NO_AGENT, isKeychainBackendOverridden())) {
|
|
369
|
+
agentEvictSync(name);
|
|
370
|
+
}
|
|
346
371
|
}
|
|
347
372
|
return deleted;
|
|
348
373
|
}
|
|
@@ -513,7 +538,9 @@ function stampLastUsed(bundle) {
|
|
|
513
538
|
}
|
|
514
539
|
try {
|
|
515
540
|
bundle.last_used = new Date(nowMs).toISOString();
|
|
516
|
-
|
|
541
|
+
// skipBrokerEviction: this stamp fires on every broker HIT; letting it
|
|
542
|
+
// evict would make the cache destroy itself on first use.
|
|
543
|
+
writeBundle(bundle, { skipBrokerEviction: true });
|
|
517
544
|
}
|
|
518
545
|
catch {
|
|
519
546
|
// Swallow — telemetry must never block secret resolution.
|
|
@@ -694,7 +721,7 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
694
721
|
emit('secrets.get', {
|
|
695
722
|
module: 'secrets',
|
|
696
723
|
bundle: name,
|
|
697
|
-
|
|
724
|
+
operation: opts.caller,
|
|
698
725
|
status: 'success',
|
|
699
726
|
source: 'agent',
|
|
700
727
|
keyCount: Object.keys(filtered.env).length,
|
|
@@ -702,6 +729,9 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
702
729
|
return filtered;
|
|
703
730
|
}
|
|
704
731
|
}
|
|
732
|
+
if (opts.agentOnly) {
|
|
733
|
+
throw new Error(`Secrets bundle '${name}' is not unlocked in the secrets agent.`);
|
|
734
|
+
}
|
|
705
735
|
if (backend === 'file')
|
|
706
736
|
assertFileBackendUsable(name);
|
|
707
737
|
const store = itemStore(backend);
|
|
@@ -784,7 +814,7 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
784
814
|
emit('secrets.get', {
|
|
785
815
|
module: 'secrets',
|
|
786
816
|
bundle: bundle.name,
|
|
787
|
-
|
|
817
|
+
operation: opts.caller,
|
|
788
818
|
status,
|
|
789
819
|
keyCount: keys.length,
|
|
790
820
|
keys,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery for LEGACY SYNCHRONIZABLE (iCloud Keychain) bundles.
|
|
3
|
+
*
|
|
4
|
+
* The pre-biometry helper era defaulted bundles to iCloud Keychain sync. The
|
|
5
|
+
* device-local cutover (biometry ACL + kSecAttrSynchronizable false on every
|
|
6
|
+
* query) orphaned those items: they still sync back via iCloud Keychain, but
|
|
7
|
+
* neither `secrets list` nor `migrate-acl` can see them. This module powers
|
|
8
|
+
* `agents secrets import --from icloud` — discover the orphaned bundles,
|
|
9
|
+
* re-import them as normal device-local bundles, and optionally purge the
|
|
10
|
+
* iCloud copies.
|
|
11
|
+
*
|
|
12
|
+
* The item-name scheme is the same one the modern store uses (see bundles.ts):
|
|
13
|
+
* metadata under `agents-cli.bundles.<name>`, one value per key under
|
|
14
|
+
* `agents-cli.secrets.<bundle>.<key>`. Env keys can never contain a dot
|
|
15
|
+
* (ENV_KEY_PATTERN), so splitting a secret service at its LAST dot recovers
|
|
16
|
+
* the bundle/key boundary even for dotted bundle names like `hetzner.com`.
|
|
17
|
+
*/
|
|
18
|
+
import { type SecretsBackend } from './bundles.js';
|
|
19
|
+
/** One orphaned iCloud bundle, as discovered from the synced item names. */
|
|
20
|
+
export interface SyncedBundleCandidate {
|
|
21
|
+
/** Bundle name derived from the iCloud service names. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Env keys that have a per-key secret item in the iCloud keychain. */
|
|
24
|
+
keys: string[];
|
|
25
|
+
/** True when an `agents-cli.bundles.<name>` metadata item exists in iCloud. */
|
|
26
|
+
hasMeta: boolean;
|
|
27
|
+
/** Every iCloud service name belonging to this candidate (the purge set). */
|
|
28
|
+
services: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Group raw synced service names into per-bundle candidates. Pure — separated
|
|
32
|
+
* from discovery so the parsing rules are unit-testable without a keychain.
|
|
33
|
+
*
|
|
34
|
+
* A bundle can surface as metadata only (`agents-cli.bundles.<name>`), as
|
|
35
|
+
* secret items only (`agents-cli.secrets.<bundle>.<KEY>` — metadata never
|
|
36
|
+
* synced), or both; all three shapes appear in real iCloud strays, so every
|
|
37
|
+
* one becomes a candidate.
|
|
38
|
+
*/
|
|
39
|
+
export declare function groupSyncedServices(services: string[]): SyncedBundleCandidate[];
|
|
40
|
+
/** Enumerate the iCloud keychain and return every orphaned bundle candidate. */
|
|
41
|
+
export declare function discoverSyncedBundles(): SyncedBundleCandidate[];
|
|
42
|
+
export interface ImportSyncedOptions {
|
|
43
|
+
/** Overwrite keys that already exist in the local bundle. */
|
|
44
|
+
force?: boolean;
|
|
45
|
+
/** Store imported values as literals in the bundle metadata (no keychain items). */
|
|
46
|
+
allPlaintext?: boolean;
|
|
47
|
+
/** Backend for a newly created bundle (existing bundles keep theirs). */
|
|
48
|
+
backend?: SecretsBackend;
|
|
49
|
+
/** Delete the iCloud copies of successfully-read items after import. */
|
|
50
|
+
purge?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface ImportSyncedResult {
|
|
53
|
+
name: string;
|
|
54
|
+
added: number;
|
|
55
|
+
skipped: number;
|
|
56
|
+
/** Keys whose iCloud value could not be read (left in place, never purged). */
|
|
57
|
+
missing: string[];
|
|
58
|
+
purged: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Import one discovered iCloud bundle into the local (device-local) store.
|
|
62
|
+
*
|
|
63
|
+
* Values come from the synced secret items; the synced metadata item, when
|
|
64
|
+
* present, contributes the description, literal vars, and non-keychain refs
|
|
65
|
+
* (env:/file:/exec: refs carry no stored secret, so copying the ref preserves
|
|
66
|
+
* them exactly). Existing local keys are skipped unless `force`. With `purge`,
|
|
67
|
+
* only services whose value was successfully read are deleted from iCloud —
|
|
68
|
+
* an unreadable item is never destroyed.
|
|
69
|
+
*/
|
|
70
|
+
export declare function importSyncedBundle(candidate: SyncedBundleCandidate, opts?: ImportSyncedOptions): ImportSyncedResult;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery for LEGACY SYNCHRONIZABLE (iCloud Keychain) bundles.
|
|
3
|
+
*
|
|
4
|
+
* The pre-biometry helper era defaulted bundles to iCloud Keychain sync. The
|
|
5
|
+
* device-local cutover (biometry ACL + kSecAttrSynchronizable false on every
|
|
6
|
+
* query) orphaned those items: they still sync back via iCloud Keychain, but
|
|
7
|
+
* neither `secrets list` nor `migrate-acl` can see them. This module powers
|
|
8
|
+
* `agents secrets import --from icloud` — discover the orphaned bundles,
|
|
9
|
+
* re-import them as normal device-local bundles, and optionally purge the
|
|
10
|
+
* iCloud copies.
|
|
11
|
+
*
|
|
12
|
+
* The item-name scheme is the same one the modern store uses (see bundles.ts):
|
|
13
|
+
* metadata under `agents-cli.bundles.<name>`, one value per key under
|
|
14
|
+
* `agents-cli.secrets.<bundle>.<key>`. Env keys can never contain a dot
|
|
15
|
+
* (ENV_KEY_PATTERN), so splitting a secret service at its LAST dot recovers
|
|
16
|
+
* the bundle/key boundary even for dotted bundle names like `hetzner.com`.
|
|
17
|
+
*/
|
|
18
|
+
import { deleteSyncedKeychainItem, getSyncedKeychainTokens, listSyncedKeychainItems, parseBundleValue, secretsKeychainItem, serializeRef, SECRETS_ITEM_PREFIX, } from './index.js';
|
|
19
|
+
import { BUNDLE_META_PREFIX, BUNDLE_NAME_PATTERN, ENV_KEY_PATTERN, bundleExists, bundleItemStore, bundlePolicy, keychainRef, readBundle, writeBundle, } from './bundles.js';
|
|
20
|
+
/**
|
|
21
|
+
* Group raw synced service names into per-bundle candidates. Pure — separated
|
|
22
|
+
* from discovery so the parsing rules are unit-testable without a keychain.
|
|
23
|
+
*
|
|
24
|
+
* A bundle can surface as metadata only (`agents-cli.bundles.<name>`), as
|
|
25
|
+
* secret items only (`agents-cli.secrets.<bundle>.<KEY>` — metadata never
|
|
26
|
+
* synced), or both; all three shapes appear in real iCloud strays, so every
|
|
27
|
+
* one becomes a candidate.
|
|
28
|
+
*/
|
|
29
|
+
export function groupSyncedServices(services) {
|
|
30
|
+
const byName = new Map();
|
|
31
|
+
const claim = (name) => {
|
|
32
|
+
let c = byName.get(name);
|
|
33
|
+
if (!c) {
|
|
34
|
+
c = { name, keys: [], hasMeta: false, services: [] };
|
|
35
|
+
byName.set(name, c);
|
|
36
|
+
}
|
|
37
|
+
return c;
|
|
38
|
+
};
|
|
39
|
+
for (const svc of services) {
|
|
40
|
+
if (svc.startsWith(BUNDLE_META_PREFIX)) {
|
|
41
|
+
const name = svc.slice(BUNDLE_META_PREFIX.length);
|
|
42
|
+
if (!BUNDLE_NAME_PATTERN.test(name))
|
|
43
|
+
continue;
|
|
44
|
+
const c = claim(name);
|
|
45
|
+
c.hasMeta = true;
|
|
46
|
+
c.services.push(svc);
|
|
47
|
+
}
|
|
48
|
+
else if (svc.startsWith(SECRETS_ITEM_PREFIX)) {
|
|
49
|
+
const rest = svc.slice(SECRETS_ITEM_PREFIX.length);
|
|
50
|
+
const cut = rest.lastIndexOf('.');
|
|
51
|
+
if (cut <= 0)
|
|
52
|
+
continue;
|
|
53
|
+
const name = rest.slice(0, cut);
|
|
54
|
+
const key = rest.slice(cut + 1);
|
|
55
|
+
if (!BUNDLE_NAME_PATTERN.test(name) || !ENV_KEY_PATTERN.test(key))
|
|
56
|
+
continue;
|
|
57
|
+
const c = claim(name);
|
|
58
|
+
if (!c.keys.includes(key))
|
|
59
|
+
c.keys.push(key);
|
|
60
|
+
c.services.push(svc);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
64
|
+
}
|
|
65
|
+
/** Enumerate the iCloud keychain and return every orphaned bundle candidate. */
|
|
66
|
+
export function discoverSyncedBundles() {
|
|
67
|
+
return groupSyncedServices(listSyncedKeychainItems('agents-cli.'));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Import one discovered iCloud bundle into the local (device-local) store.
|
|
71
|
+
*
|
|
72
|
+
* Values come from the synced secret items; the synced metadata item, when
|
|
73
|
+
* present, contributes the description, literal vars, and non-keychain refs
|
|
74
|
+
* (env:/file:/exec: refs carry no stored secret, so copying the ref preserves
|
|
75
|
+
* them exactly). Existing local keys are skipped unless `force`. With `purge`,
|
|
76
|
+
* only services whose value was successfully read are deleted from iCloud —
|
|
77
|
+
* an unreadable item is never destroyed.
|
|
78
|
+
*/
|
|
79
|
+
export function importSyncedBundle(candidate, opts = {}) {
|
|
80
|
+
const values = getSyncedKeychainTokens(candidate.services);
|
|
81
|
+
let bundle;
|
|
82
|
+
if (bundleExists(candidate.name)) {
|
|
83
|
+
bundle = readBundle(candidate.name);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
bundle = {
|
|
87
|
+
name: candidate.name,
|
|
88
|
+
backend: opts.backend === 'file' ? 'file' : undefined,
|
|
89
|
+
vars: {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const metaService = BUNDLE_META_PREFIX + candidate.name;
|
|
93
|
+
let metaVars = {};
|
|
94
|
+
const metaJson = values.get(metaService);
|
|
95
|
+
if (metaJson !== undefined) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(metaJson);
|
|
98
|
+
if (parsed && typeof parsed === 'object') {
|
|
99
|
+
if (typeof parsed.description === 'string' && !bundle.description) {
|
|
100
|
+
bundle.description = parsed.description;
|
|
101
|
+
}
|
|
102
|
+
if (parsed.vars && typeof parsed.vars === 'object') {
|
|
103
|
+
metaVars = parsed.vars;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Corrupt legacy metadata — the per-key secret items still import.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const store = bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' });
|
|
112
|
+
let added = 0;
|
|
113
|
+
let skipped = 0;
|
|
114
|
+
const missing = [];
|
|
115
|
+
// Keys with a synced secret item: re-store the value device-locally.
|
|
116
|
+
for (const key of candidate.keys) {
|
|
117
|
+
const value = values.get(secretsKeychainItem(candidate.name, key));
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
missing.push(key);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!opts.force && key in bundle.vars) {
|
|
123
|
+
skipped++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (opts.allPlaintext) {
|
|
127
|
+
bundle.vars[key] = { value };
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
store.set(secretsKeychainItem(candidate.name, key), value);
|
|
131
|
+
bundle.vars[key] = keychainRef(key);
|
|
132
|
+
}
|
|
133
|
+
added++;
|
|
134
|
+
}
|
|
135
|
+
// Vars declared only in the synced metadata: literals and non-keychain refs
|
|
136
|
+
// carry everything they need; a keychain ref without its synced item is
|
|
137
|
+
// unrecoverable.
|
|
138
|
+
for (const [key, raw] of Object.entries(metaVars)) {
|
|
139
|
+
if (!ENV_KEY_PATTERN.test(key))
|
|
140
|
+
continue;
|
|
141
|
+
if (candidate.keys.includes(key))
|
|
142
|
+
continue; // the secret item already covered it
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = parseBundleValue(raw);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue; // malformed legacy entry
|
|
149
|
+
}
|
|
150
|
+
if ('ref' in parsed && parsed.ref.provider === 'keychain') {
|
|
151
|
+
if (!missing.includes(key))
|
|
152
|
+
missing.push(key);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!opts.force && key in bundle.vars) {
|
|
156
|
+
skipped++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
bundle.vars[key] = 'literal' in parsed ? { value: parsed.literal } : serializeRef(parsed.ref);
|
|
160
|
+
added++;
|
|
161
|
+
}
|
|
162
|
+
writeBundle(bundle);
|
|
163
|
+
let purged = 0;
|
|
164
|
+
if (opts.purge) {
|
|
165
|
+
for (const svc of candidate.services) {
|
|
166
|
+
if (!values.has(svc))
|
|
167
|
+
continue; // never destroy an item we couldn't read
|
|
168
|
+
if (deleteSyncedKeychainItem(svc))
|
|
169
|
+
purged++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { name: candidate.name, added, skipped, missing, purged };
|
|
173
|
+
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import type { NativeImportReport } from './fallback.js';
|
|
26
26
|
export type { NativeImportReport, NativeImportResult, NativeImportStatus } from './fallback.js';
|
|
27
|
+
export declare const SECRETS_ITEM_PREFIX = "agents-cli.secrets.";
|
|
27
28
|
/** Supported secret resolution backends. */
|
|
28
29
|
export type SecretProvider = 'keychain' | 'env' | 'file' | 'exec';
|
|
29
30
|
/** A typed reference to a secret, consisting of a provider and a provider-specific value. */
|
|
@@ -141,6 +142,41 @@ export declare function listKeychainItems(prefix: string): string[];
|
|
|
141
142
|
* stragglers instead of every item (which would be a Touch ID storm).
|
|
142
143
|
*/
|
|
143
144
|
export declare function listLegacyKeychainItems(prefix: string): string[];
|
|
145
|
+
/**
|
|
146
|
+
* Test seam for the LEGACY SYNCHRONIZABLE (iCloud Keychain) recovery path.
|
|
147
|
+
* The main `KeychainBackend` seam models the live device-local store; this one
|
|
148
|
+
* models the orphaned iCloud items that `secrets import --from icloud` reads.
|
|
149
|
+
* Kept separate so a test can populate both sides independently.
|
|
150
|
+
*/
|
|
151
|
+
export interface SyncedKeychainBackend {
|
|
152
|
+
list(prefix: string): string[];
|
|
153
|
+
getBatch(items: string[]): Map<string, string>;
|
|
154
|
+
delete(item: string): boolean;
|
|
155
|
+
}
|
|
156
|
+
export declare function setSyncedKeychainBackendForTest(b: SyncedKeychainBackend | null): SyncedKeychainBackend | null;
|
|
157
|
+
/**
|
|
158
|
+
* Enumerate LEGACY SYNCHRONIZABLE (iCloud Keychain) item names with the given
|
|
159
|
+
* prefix — bundles written by the pre-biometry helper era, which defaulted
|
|
160
|
+
* secrets to iCloud Keychain sync. The device-local cutover orphaned them:
|
|
161
|
+
* every modern query pins synchronizable=false, so only the helper's
|
|
162
|
+
* `list-synced` verb can see them. Silent (attributes only, never decrypts).
|
|
163
|
+
* macOS only — Linux/Windows never had iCloud Keychain sync, so this returns [].
|
|
164
|
+
*/
|
|
165
|
+
export declare function listSyncedKeychainItems(prefix: string): string[];
|
|
166
|
+
/**
|
|
167
|
+
* Batch-read LEGACY SYNCHRONIZABLE (iCloud Keychain) items. Returns a map of
|
|
168
|
+
* item name → value; missing items are simply absent. Pre-biometry items carry
|
|
169
|
+
* no biometry ACL, so this does not normally prompt. macOS only — returns an
|
|
170
|
+
* empty map on Linux/Windows.
|
|
171
|
+
*/
|
|
172
|
+
export declare function getSyncedKeychainTokens(items: string[]): Map<string, string>;
|
|
173
|
+
/**
|
|
174
|
+
* Delete a LEGACY SYNCHRONIZABLE (iCloud Keychain) item after a successful
|
|
175
|
+
* import (`--purge`). Matches synchronizable items only — the device-local
|
|
176
|
+
* copy the import wrote is untouched. iCloud propagates the deletion to the
|
|
177
|
+
* user's other devices. Returns true if a copy was removed.
|
|
178
|
+
*/
|
|
179
|
+
export declare function deleteSyncedKeychainItem(item: string): boolean;
|
|
144
180
|
/**
|
|
145
181
|
* One-time upgrade for a keychain item that was written by a previous helper
|
|
146
182
|
* generation with a trusted-app ACL. The helper reads the legacy item
|
|
@@ -30,7 +30,7 @@ import { linuxBackend, usesFileFallback as linuxUsesFileFallback, importNativeSe
|
|
|
30
30
|
import { windowsBackend, usesFileFallback as windowsUsesFileFallback, importNativeCredManItems } from './windows.js';
|
|
31
31
|
import { getKeychainHelperPath } from './install-helper.js';
|
|
32
32
|
const SERVICE_PREFIX = 'agents-cli';
|
|
33
|
-
const SECRETS_ITEM_PREFIX = `${SERVICE_PREFIX}.secrets.`;
|
|
33
|
+
export const SECRETS_ITEM_PREFIX = `${SERVICE_PREFIX}.secrets.`;
|
|
34
34
|
const BUNDLES_ITEM_PREFIX = `${SERVICE_PREFIX}.bundles.`;
|
|
35
35
|
const REF_PATTERN = /^(keychain|env|file|exec):(.+)$/s;
|
|
36
36
|
/** Parse a bundle value into either a literal string or a typed secret ref. */
|
|
@@ -234,12 +234,20 @@ export function getKeychainTokens(items) {
|
|
|
234
234
|
throw new Error(msg || `Failed to batch-read ${items.length} keychain items.`);
|
|
235
235
|
}
|
|
236
236
|
const out = child.stdout?.toString() ?? '';
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
237
|
+
parseBatchRecords(out, result);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Parse the helper's batch-read output into `into`. The format is shared by
|
|
242
|
+
* `get-batch` and `get-batch-synced` — a sequence of records, one per service
|
|
243
|
+
* in input order:
|
|
244
|
+
* "V <service>\n<value>\n" (present)
|
|
245
|
+
* "M <service>\n" (missing)
|
|
246
|
+
* Service names are validated newline/'='-free by setKeychainToken below
|
|
247
|
+
* and values are rejected if they contain newlines — so splitting on '\n'
|
|
248
|
+
* and walking line-by-line is unambiguous.
|
|
249
|
+
*/
|
|
250
|
+
function parseBatchRecords(out, into) {
|
|
243
251
|
const lines = out.split('\n');
|
|
244
252
|
let i = 0;
|
|
245
253
|
while (i < lines.length) {
|
|
@@ -249,7 +257,7 @@ export function getKeychainTokens(items) {
|
|
|
249
257
|
if (line.startsWith('V ')) {
|
|
250
258
|
const service = line.slice(2);
|
|
251
259
|
const value = lines[i + 1] ?? '';
|
|
252
|
-
|
|
260
|
+
into.set(service, value);
|
|
253
261
|
i += 2;
|
|
254
262
|
}
|
|
255
263
|
else if (line.startsWith('M ')) {
|
|
@@ -262,7 +270,6 @@ export function getKeychainTokens(items) {
|
|
|
262
270
|
throw new Error(`Malformed get-batch output line: ${JSON.stringify(line)}`);
|
|
263
271
|
}
|
|
264
272
|
}
|
|
265
|
-
return result;
|
|
266
273
|
}
|
|
267
274
|
/** Store or update a secret value in the keychain/keyring. Device-local;
|
|
268
275
|
* biometry-gated on macOS. `opts.noAcl` (the `never` prompt-policy) writes our
|
|
@@ -408,6 +415,89 @@ export function listLegacyKeychainItems(prefix) {
|
|
|
408
415
|
const out = result.stdout?.toString() || '';
|
|
409
416
|
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
410
417
|
}
|
|
418
|
+
let syncedBackend = null;
|
|
419
|
+
export function setSyncedKeychainBackendForTest(b) {
|
|
420
|
+
const prev = syncedBackend;
|
|
421
|
+
syncedBackend = b;
|
|
422
|
+
return prev;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Enumerate LEGACY SYNCHRONIZABLE (iCloud Keychain) item names with the given
|
|
426
|
+
* prefix — bundles written by the pre-biometry helper era, which defaulted
|
|
427
|
+
* secrets to iCloud Keychain sync. The device-local cutover orphaned them:
|
|
428
|
+
* every modern query pins synchronizable=false, so only the helper's
|
|
429
|
+
* `list-synced` verb can see them. Silent (attributes only, never decrypts).
|
|
430
|
+
* macOS only — Linux/Windows never had iCloud Keychain sync, so this returns [].
|
|
431
|
+
*/
|
|
432
|
+
export function listSyncedKeychainItems(prefix) {
|
|
433
|
+
if (syncedBackend)
|
|
434
|
+
return syncedBackend.list(prefix);
|
|
435
|
+
if (backend)
|
|
436
|
+
return [];
|
|
437
|
+
assertSupportedPlatform();
|
|
438
|
+
if (isLinux() || isWindows())
|
|
439
|
+
return [];
|
|
440
|
+
const bin = getKeychainHelperPath();
|
|
441
|
+
const result = spawnSync(bin, ['list-synced', prefix], {
|
|
442
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
443
|
+
});
|
|
444
|
+
if (result.status !== 0) {
|
|
445
|
+
const msg = result.stderr?.toString().trim();
|
|
446
|
+
throw new Error(msg || `Failed to enumerate iCloud keychain items with prefix '${prefix}'.`);
|
|
447
|
+
}
|
|
448
|
+
const out = result.stdout?.toString() || '';
|
|
449
|
+
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Batch-read LEGACY SYNCHRONIZABLE (iCloud Keychain) items. Returns a map of
|
|
453
|
+
* item name → value; missing items are simply absent. Pre-biometry items carry
|
|
454
|
+
* no biometry ACL, so this does not normally prompt. macOS only — returns an
|
|
455
|
+
* empty map on Linux/Windows.
|
|
456
|
+
*/
|
|
457
|
+
export function getSyncedKeychainTokens(items) {
|
|
458
|
+
const result = new Map();
|
|
459
|
+
if (items.length === 0)
|
|
460
|
+
return result;
|
|
461
|
+
if (syncedBackend)
|
|
462
|
+
return syncedBackend.getBatch(items);
|
|
463
|
+
if (backend)
|
|
464
|
+
return result;
|
|
465
|
+
assertSupportedPlatform();
|
|
466
|
+
if (isLinux() || isWindows())
|
|
467
|
+
return result;
|
|
468
|
+
const bin = getKeychainHelperPath();
|
|
469
|
+
const child = spawnSync(bin, ['get-batch-synced', os.userInfo().username, ...items], {
|
|
470
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
471
|
+
});
|
|
472
|
+
if (child.status === 4) {
|
|
473
|
+
throw new Error(`Auth cancelled while reading ${items.length} iCloud keychain item(s).`);
|
|
474
|
+
}
|
|
475
|
+
if (child.status !== 0) {
|
|
476
|
+
const msg = child.stderr?.toString().trim();
|
|
477
|
+
throw new Error(msg || `Failed to batch-read ${items.length} iCloud keychain items.`);
|
|
478
|
+
}
|
|
479
|
+
parseBatchRecords(child.stdout?.toString() ?? '', result);
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Delete a LEGACY SYNCHRONIZABLE (iCloud Keychain) item after a successful
|
|
484
|
+
* import (`--purge`). Matches synchronizable items only — the device-local
|
|
485
|
+
* copy the import wrote is untouched. iCloud propagates the deletion to the
|
|
486
|
+
* user's other devices. Returns true if a copy was removed.
|
|
487
|
+
*/
|
|
488
|
+
export function deleteSyncedKeychainItem(item) {
|
|
489
|
+
if (syncedBackend)
|
|
490
|
+
return syncedBackend.delete(item);
|
|
491
|
+
if (backend)
|
|
492
|
+
return false;
|
|
493
|
+
assertSupportedPlatform();
|
|
494
|
+
if (isLinux() || isWindows())
|
|
495
|
+
return false;
|
|
496
|
+
const bin = getKeychainHelperPath();
|
|
497
|
+
return spawnSync(bin, ['delete-synced', item, os.userInfo().username], {
|
|
498
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
499
|
+
}).status === 0;
|
|
500
|
+
}
|
|
411
501
|
/**
|
|
412
502
|
* One-time upgrade for a keychain item that was written by a previous helper
|
|
413
503
|
* generation with a trusted-app ACL. The helper reads the legacy item
|
package/dist/lib/secrets/sync.js
CHANGED
|
@@ -193,7 +193,7 @@ export async function pushBundle(name, opts) {
|
|
|
193
193
|
emit('secrets.get', {
|
|
194
194
|
module: 'secrets',
|
|
195
195
|
bundle: name,
|
|
196
|
-
|
|
196
|
+
operation: 'sync push',
|
|
197
197
|
source: 'sync-push',
|
|
198
198
|
status: 'success',
|
|
199
199
|
keyCount: Object.keys(snap.secrets).length,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PidSessionEntry } from './pid-registry.js';
|
|
2
|
-
import { type SessionActivity, type AwaitingReason, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
2
|
+
import { type SessionActivity, type AwaitingReason, type StructuredQuestion, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
3
3
|
import { type SessionProvenance } from './provenance.js';
|
|
4
4
|
/**
|
|
5
5
|
* Per-PID `lsof` probes run bounded and staggered rather than as one parallel
|
|
@@ -30,6 +30,16 @@ export interface ActiveSession {
|
|
|
30
30
|
activity?: SessionActivity;
|
|
31
31
|
/** Why the agent is waiting, when activity is waiting_input. */
|
|
32
32
|
awaitingReason?: AwaitingReason;
|
|
33
|
+
/** The structured decision (question/plan/permission + options) the agent is waiting on. */
|
|
34
|
+
question?: StructuredQuestion;
|
|
35
|
+
/**
|
|
36
|
+
* Plan markdown from the last `ExitPlanMode` tool call. Present when the
|
|
37
|
+
* transcript ever entered plan-review; `awaitingReason === 'plan_review'`
|
|
38
|
+
* says whether it is still pending.
|
|
39
|
+
*/
|
|
40
|
+
plan?: string;
|
|
41
|
+
/** Last few assistant turns (most-recent last), for at-a-glance context in the UI. */
|
|
42
|
+
tail?: string[];
|
|
33
43
|
/** PR opened during the session. */
|
|
34
44
|
pr?: DetectedPr;
|
|
35
45
|
/** Worktree the session runs in. */
|
|
@@ -236,6 +236,9 @@ function applyState(base, state, fallbackFile) {
|
|
|
236
236
|
status: statusFromActivity(state.activity),
|
|
237
237
|
activity: state.activity,
|
|
238
238
|
awaitingReason: state.awaitingReason,
|
|
239
|
+
question: state.question,
|
|
240
|
+
plan: state.plan,
|
|
241
|
+
tail: state.tail,
|
|
239
242
|
// Prefer the live preview (latest turn); keep the first-prompt topic as a fallback.
|
|
240
243
|
preview: state.preview ?? base.preview,
|
|
241
244
|
pr: state.pr,
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface SessionRow {
|
|
|
35
35
|
pr_number: number | null;
|
|
36
36
|
worktree_slug: string | null;
|
|
37
37
|
ticket_id: string | null;
|
|
38
|
+
plan: string | null;
|
|
38
39
|
}
|
|
39
40
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
40
41
|
export interface ScanStamp {
|
|
@@ -199,10 +200,6 @@ export interface TopCostSession {
|
|
|
199
200
|
* vanished, mirroring querySessions' liveness filter.
|
|
200
201
|
*/
|
|
201
202
|
export declare function topSessionsByCost(n: number, options?: QueryOptions): TopCostSession[];
|
|
202
|
-
/** Return the set of all file paths currently tracked in the sessions table. */
|
|
203
|
-
export declare function getAllFilePaths(): Set<string>;
|
|
204
|
-
/** Look up sessions by their source file paths. */
|
|
205
|
-
export declare function getSessionsByFilePaths(paths: string[]): Map<string, SessionMeta>;
|
|
206
203
|
/** Look up a single session by its unique ID. */
|
|
207
204
|
export declare function getSessionById(id: string): SessionMeta | null;
|
|
208
205
|
/**
|