@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- 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 +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import * as net from 'net';
|
|
27
27
|
import type { SecretsBundle } from './bundles.js';
|
|
28
28
|
import { GLOBAL_HARNESS, bundleScopeChain } from './scope.js';
|
|
29
|
+
import { type SecretLease } from './lease.js';
|
|
29
30
|
export { GLOBAL_HARNESS, bundleScopeChain };
|
|
30
31
|
/** Default lifetime of an unlocked bundle when `--ttl` is not given. */
|
|
31
32
|
export declare const DEFAULT_TTL_MS: number;
|
|
@@ -72,6 +73,7 @@ export interface StoredBundle {
|
|
|
72
73
|
/** epoch ms; the entry is gone once Date.now() passes this. */
|
|
73
74
|
expiresAt: number;
|
|
74
75
|
harness: string;
|
|
76
|
+
lease?: SecretLease;
|
|
75
77
|
}
|
|
76
78
|
/** One unlocked bundle as reported by `status`. */
|
|
77
79
|
export interface AgentStatusEntry {
|
|
@@ -79,6 +81,8 @@ export interface AgentStatusEntry {
|
|
|
79
81
|
expiresAt: number;
|
|
80
82
|
keyCount: number;
|
|
81
83
|
harness: string;
|
|
84
|
+
leaseId?: string;
|
|
85
|
+
keys?: string[];
|
|
82
86
|
}
|
|
83
87
|
/**
|
|
84
88
|
* Read the current broker capability token, or null if none is present. Clients
|
|
@@ -119,11 +123,16 @@ export type Request = {
|
|
|
119
123
|
bundle: SecretsBundle;
|
|
120
124
|
env: Record<string, string>;
|
|
121
125
|
ttlMs: number;
|
|
126
|
+
lease?: SecretLease;
|
|
122
127
|
token?: string;
|
|
123
128
|
} | {
|
|
124
129
|
cmd: 'lock';
|
|
125
130
|
name?: string;
|
|
126
131
|
token?: string;
|
|
132
|
+
} | {
|
|
133
|
+
cmd: 'revoke';
|
|
134
|
+
leaseId: string;
|
|
135
|
+
token?: string;
|
|
127
136
|
} | {
|
|
128
137
|
cmd: 'status';
|
|
129
138
|
token?: string;
|
|
@@ -143,6 +152,7 @@ export type Response = {
|
|
|
143
152
|
hit: true;
|
|
144
153
|
bundle: SecretsBundle;
|
|
145
154
|
env: Record<string, string>;
|
|
155
|
+
lease?: SecretLease;
|
|
146
156
|
} | {
|
|
147
157
|
ok: true;
|
|
148
158
|
cmd: 'load';
|
|
@@ -150,6 +160,10 @@ export type Response = {
|
|
|
150
160
|
ok: true;
|
|
151
161
|
cmd: 'lock';
|
|
152
162
|
wiped: number;
|
|
163
|
+
} | {
|
|
164
|
+
ok: true;
|
|
165
|
+
cmd: 'revoke';
|
|
166
|
+
wiped: number;
|
|
153
167
|
} | {
|
|
154
168
|
ok: true;
|
|
155
169
|
cmd: 'status';
|
|
@@ -237,7 +251,7 @@ export declare function runSecretsAgent(opts?: {
|
|
|
237
251
|
*
|
|
238
252
|
* Serves the SAME socket and wire protocol as the standalone `runSecretsAgent`
|
|
239
253
|
* — so every existing client (`agentGetSync`, `agentPing`, `agentAutoLoadSync`)
|
|
240
|
-
* keeps working
|
|
254
|
+
* keeps working through the versioned protocol — but it is daemon-safe:
|
|
241
255
|
*
|
|
242
256
|
* - no pid-file single-instance guard (the daemon owns the instance);
|
|
243
257
|
* - no `process.exit`, no SIGTERM/SIGINT handlers, no self-heal/idle-exit
|
|
@@ -296,6 +310,7 @@ export declare function syncClientLaunch(sub: string[], agentsBin?: string): {
|
|
|
296
310
|
export declare function agentGetSync(name: string, harness?: string): {
|
|
297
311
|
bundle: SecretsBundle;
|
|
298
312
|
env: Record<string, string>;
|
|
313
|
+
lease?: SecretLease;
|
|
299
314
|
} | null;
|
|
300
315
|
/**
|
|
301
316
|
* Last non-empty line of a child's stdout — the payload line.
|
|
@@ -410,7 +425,7 @@ export declare function clampHoldMs(v: unknown): number;
|
|
|
410
425
|
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping) rather
|
|
411
426
|
* than a tight inline retry loop. Best-effort; never throws. macOS only.
|
|
412
427
|
*/
|
|
413
|
-
export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number, harness?: string): void;
|
|
428
|
+
export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number, harness?: string, lease?: SecretLease): void;
|
|
414
429
|
/**
|
|
415
430
|
* Body of the hidden `secrets _agent-load` worker. Reads one `{name, bundle,
|
|
416
431
|
* env, ttlMs}` payload from stdin, ensures the broker is up (robust, generous
|
|
@@ -425,10 +440,11 @@ export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, e
|
|
|
425
440
|
*/
|
|
426
441
|
export declare function runAgentLoadFromStdin(): Promise<void>;
|
|
427
442
|
/** Store a resolved bundle in the broker. Returns false on transport failure. */
|
|
428
|
-
export declare function agentLoad(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number, harness?: string): Promise<boolean>;
|
|
443
|
+
export declare function agentLoad(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number, harness?: string, lease?: SecretLease): Promise<boolean>;
|
|
429
444
|
/** Wipe one bundle (or all if name omitted) from the broker. Returns the count
|
|
430
445
|
* wiped, or 0 when no broker is running. */
|
|
431
446
|
export declare function agentLock(name?: string): Promise<number>;
|
|
447
|
+
export declare function agentRevoke(leaseId: string): Promise<number>;
|
|
432
448
|
/** List currently-unlocked bundles, or [] when no broker is running. The
|
|
433
449
|
* internal `secrets list` metadata-cache entry is filtered out here as well as
|
|
434
450
|
* server-side: during a rollout a NEW client can talk to an OLD broker that
|
|
@@ -35,8 +35,11 @@ import { getKeychainHelperPath } from './install-helper.js';
|
|
|
35
35
|
import { getCliVersion, getCliVersionFresh } from '../version.js';
|
|
36
36
|
import { getCliLaunch } from '../cli-entry.js';
|
|
37
37
|
import { GLOBAL_HARNESS, bundleScopeChain } from './scope.js';
|
|
38
|
-
import { rehydrateSessions, pruneSessionsOnSleep } from './session-store.js';
|
|
38
|
+
import { deleteLeaseSession, rehydrateSessions, pruneSessionsOnSleep } from './session-store.js';
|
|
39
39
|
import { SYNC_GET_CMD, SYNC_PING_CMD, SYNC_LOCK_CMD } from './sync-commands.js';
|
|
40
|
+
import { MAX_LEASE_MS, MIN_LEASE_MS } from './lease.js';
|
|
41
|
+
import { selectLeasedEnv } from './lease.js';
|
|
42
|
+
import { emitSecretAudit } from './audit.js';
|
|
40
43
|
// Re-exported so callers already reaching for agent.js keep one obvious home for
|
|
41
44
|
// the scope vocabulary; the definitions live in the leaf module scope.ts because
|
|
42
45
|
// agent.ts and session-store.ts import each other and a cyclic `const` read can
|
|
@@ -44,7 +47,7 @@ import { SYNC_GET_CMD, SYNC_PING_CMD, SYNC_LOCK_CMD } from './sync-commands.js';
|
|
|
44
47
|
export { GLOBAL_HARNESS, bundleScopeChain };
|
|
45
48
|
/** Bumped when the wire protocol changes; a client that pings a mismatched
|
|
46
49
|
* server kills and respawns it rather than talking a stale dialect. */
|
|
47
|
-
const PROTOCOL_VERSION =
|
|
50
|
+
const PROTOCOL_VERSION = 3;
|
|
48
51
|
/** Default lifetime of an unlocked bundle when `--ttl` is not given. */
|
|
49
52
|
export const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7d
|
|
50
53
|
/**
|
|
@@ -153,7 +156,7 @@ function rehydrateStore(now = Date.now()) {
|
|
|
153
156
|
const store = new Map();
|
|
154
157
|
for (const { name, entry } of rehydrateSessions(now)) {
|
|
155
158
|
const harness = entry.harness || GLOBAL_HARNESS;
|
|
156
|
-
store.set(scopedBundleKey(name, harness), { bundle: entry.bundle, env: entry.env, expiresAt: entry.expiresAt, harness });
|
|
159
|
+
store.set(scopedBundleKey(name, harness), { bundle: entry.bundle, env: entry.env, expiresAt: entry.expiresAt, harness, lease: entry.lease });
|
|
157
160
|
}
|
|
158
161
|
return store;
|
|
159
162
|
}
|
|
@@ -351,16 +354,30 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
351
354
|
continue;
|
|
352
355
|
if (now >= e.expiresAt) {
|
|
353
356
|
store.delete(key);
|
|
357
|
+
if (e.lease) {
|
|
358
|
+
deleteLeaseSession(e.lease.id);
|
|
359
|
+
emitSecretAudit({ event: 'secrets.lease-expire', bundle: e.bundle.name, operation: 'lease-expire', source: 'broker', status: 'success', keys: e.lease.keys, keyCount: e.lease.keys.length, agent: e.lease.harness });
|
|
360
|
+
}
|
|
354
361
|
continue;
|
|
355
|
-
}
|
|
356
|
-
return { ok: true, cmd: 'get', hit: true, bundle: e.bundle, env: e.env };
|
|
362
|
+
}
|
|
363
|
+
return { ok: true, cmd: 'get', hit: true, bundle: e.bundle, env: e.env, lease: e.lease };
|
|
357
364
|
}
|
|
358
365
|
return { ok: true, cmd: 'get', hit: false };
|
|
359
366
|
}
|
|
360
|
-
case 'load':
|
|
367
|
+
case 'load': {
|
|
361
368
|
const harness = req.harness || GLOBAL_HARNESS;
|
|
362
|
-
|
|
369
|
+
let env = req.env;
|
|
370
|
+
if (req.lease) {
|
|
371
|
+
try {
|
|
372
|
+
env = selectLeasedEnv(req.lease, req.env, now);
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
return { ok: false, error: err.message };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
store.set(scopedBundleKey(req.name, harness), { bundle: req.bundle, env, expiresAt: req.lease?.expiresAt ?? now + req.ttlMs, harness, lease: req.lease });
|
|
363
379
|
return { ok: true, cmd: 'load' };
|
|
380
|
+
}
|
|
364
381
|
case 'lock': {
|
|
365
382
|
if (req.name) {
|
|
366
383
|
let wiped = 0;
|
|
@@ -373,6 +390,13 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
373
390
|
store.clear();
|
|
374
391
|
return { ok: true, cmd: 'lock', wiped };
|
|
375
392
|
}
|
|
393
|
+
case 'revoke': {
|
|
394
|
+
let wiped = 0;
|
|
395
|
+
for (const [key, entry] of store)
|
|
396
|
+
if (entry.lease?.id === req.leaseId && store.delete(key))
|
|
397
|
+
wiped++;
|
|
398
|
+
return { ok: true, cmd: 'revoke', wiped };
|
|
399
|
+
}
|
|
376
400
|
case 'status': {
|
|
377
401
|
const entries = [];
|
|
378
402
|
for (const [name, e] of store) {
|
|
@@ -380,7 +404,7 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
380
404
|
continue;
|
|
381
405
|
if (e.bundle.name.startsWith(META_CACHE_PREFIX))
|
|
382
406
|
continue; // internal list cache
|
|
383
|
-
entries.push({ name: e.bundle.name, expiresAt: e.expiresAt, keyCount: Object.keys(e.env).length, harness: e.harness });
|
|
407
|
+
entries.push({ name: e.bundle.name, expiresAt: e.expiresAt, keyCount: Object.keys(e.env).length, harness: e.harness, leaseId: e.lease?.id, keys: e.lease?.keys });
|
|
384
408
|
}
|
|
385
409
|
return { ok: true, cmd: 'status', entries };
|
|
386
410
|
}
|
|
@@ -648,8 +672,13 @@ export async function runSecretsAgent(opts = {}) {
|
|
|
648
672
|
const sweep = () => {
|
|
649
673
|
const now = Date.now();
|
|
650
674
|
for (const [name, e] of store)
|
|
651
|
-
if (now >= e.expiresAt)
|
|
675
|
+
if (now >= e.expiresAt) {
|
|
652
676
|
store.delete(name);
|
|
677
|
+
if (e.lease) {
|
|
678
|
+
deleteLeaseSession(e.lease.id);
|
|
679
|
+
emitSecretAudit({ event: 'secrets.lease-expire', bundle: e.bundle.name, operation: 'lease-expire', source: 'broker', status: 'success', keys: e.lease.keys, keyCount: e.lease.keys.length, agent: e.lease.harness });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
653
682
|
const live = realBundleCount(store);
|
|
654
683
|
// Self-heal onto a newer in-place install — but ONLY while no real unlocks
|
|
655
684
|
// are held, so we never wipe live unlocks and force a re-prompt (#435). A
|
|
@@ -766,7 +795,7 @@ export async function runSecretsAgent(opts = {}) {
|
|
|
766
795
|
*
|
|
767
796
|
* Serves the SAME socket and wire protocol as the standalone `runSecretsAgent`
|
|
768
797
|
* — so every existing client (`agentGetSync`, `agentPing`, `agentAutoLoadSync`)
|
|
769
|
-
* keeps working
|
|
798
|
+
* keeps working through the versioned protocol — but it is daemon-safe:
|
|
770
799
|
*
|
|
771
800
|
* - no pid-file single-instance guard (the daemon owns the instance);
|
|
772
801
|
* - no `process.exit`, no SIGTERM/SIGINT handlers, no self-heal/idle-exit
|
|
@@ -795,9 +824,15 @@ export async function startHostedBroker() {
|
|
|
795
824
|
// whole daemon down with it.
|
|
796
825
|
const sweepTimer = setInterval(() => {
|
|
797
826
|
const now = Date.now();
|
|
798
|
-
for (const [name, e] of store)
|
|
799
|
-
if (now
|
|
800
|
-
|
|
827
|
+
for (const [name, e] of store) {
|
|
828
|
+
if (now < e.expiresAt)
|
|
829
|
+
continue;
|
|
830
|
+
store.delete(name);
|
|
831
|
+
if (e.lease) {
|
|
832
|
+
deleteLeaseSession(e.lease.id);
|
|
833
|
+
emitSecretAudit({ event: 'secrets.lease-expire', bundle: e.bundle.name, operation: 'lease-expire', source: 'broker', status: 'success', keys: e.lease.keys, keyCount: e.lease.keys.length, agent: e.lease.harness });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
801
836
|
}, SWEEP_INTERVAL_MS);
|
|
802
837
|
// Auto-lock on sleep, same as the standalone broker: the signed helper emits
|
|
803
838
|
// LOCK/SLEEP lines; wipe the in-memory store on a wipe-worthy event.
|
|
@@ -943,7 +978,7 @@ export function agentGetSync(name, harness = GLOBAL_HARNESS) {
|
|
|
943
978
|
const o = JSON.parse(lastLine(r.stdout));
|
|
944
979
|
if (!o || typeof o !== 'object' || !o.env)
|
|
945
980
|
return null;
|
|
946
|
-
return { bundle: o.bundle, env: o.env };
|
|
981
|
+
return { bundle: o.bundle, env: o.env, lease: o.lease };
|
|
947
982
|
}
|
|
948
983
|
catch {
|
|
949
984
|
return null;
|
|
@@ -1025,7 +1060,7 @@ export async function runAgentGetSync(name, harness = GLOBAL_HARNESS) {
|
|
|
1025
1060
|
// truncated on exit — the parent would see a partial or empty payload, treat
|
|
1026
1061
|
// it as a miss, and fall through to a keychain read, silently costing the
|
|
1027
1062
|
// Touch ID prompt this whole path exists to avoid.
|
|
1028
|
-
const payload = JSON.stringify({ bundle: r.bundle, env: r.env }) + '\n';
|
|
1063
|
+
const payload = JSON.stringify({ bundle: r.bundle, env: r.env, lease: r.lease }) + '\n';
|
|
1029
1064
|
await new Promise((resolve) => { process.stdout.write(payload, () => resolve()); });
|
|
1030
1065
|
return 0;
|
|
1031
1066
|
}
|
|
@@ -1120,8 +1155,8 @@ export function secretsAgentDurable() {
|
|
|
1120
1155
|
/** Minimum / maximum bounds for the configurable hold window. A too-small value
|
|
1121
1156
|
* would defeat the broker (constant re-prompts); a too-large one pins secrets in
|
|
1122
1157
|
* memory far longer than intended. */
|
|
1123
|
-
export const MIN_HOLD_MS =
|
|
1124
|
-
export const MAX_HOLD_MS =
|
|
1158
|
+
export const MIN_HOLD_MS = MIN_LEASE_MS;
|
|
1159
|
+
export const MAX_HOLD_MS = MAX_LEASE_MS;
|
|
1125
1160
|
/**
|
|
1126
1161
|
* How long an unlocked / auto-cached bundle is held before the next read
|
|
1127
1162
|
* re-prompts. Defaults to DEFAULT_TTL_MS (7d); override with
|
|
@@ -1165,10 +1200,10 @@ export function clampHoldMs(v) {
|
|
|
1165
1200
|
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping) rather
|
|
1166
1201
|
* than a tight inline retry loop. Best-effort; never throws. macOS only.
|
|
1167
1202
|
*/
|
|
1168
|
-
export function agentAutoLoadSync(name, bundle, env, ttlMs, harness = GLOBAL_HARNESS) {
|
|
1203
|
+
export function agentAutoLoadSync(name, bundle, env, ttlMs, harness = GLOBAL_HARNESS, lease) {
|
|
1169
1204
|
if (!onDarwin())
|
|
1170
1205
|
return;
|
|
1171
|
-
const payload = JSON.stringify({ name, bundle, env, ttlMs, harness });
|
|
1206
|
+
const payload = JSON.stringify({ name, bundle, env, ttlMs, harness, lease });
|
|
1172
1207
|
// Broker actually LISTENING → deterministic synchronous warm (bounded; the read
|
|
1173
1208
|
// already paid a Touch ID, so <1s here is invisible). We gate on a real liveness
|
|
1174
1209
|
// ping, NOT mere socket-file existence: a broker that died leaving its socket
|
|
@@ -1235,13 +1270,13 @@ export async function runAgentLoadFromStdin() {
|
|
|
1235
1270
|
process.exitCode = 1; // broker couldn't be brought up — did NOT load
|
|
1236
1271
|
return;
|
|
1237
1272
|
}
|
|
1238
|
-
const loaded = await agentLoad(payload.name, payload.bundle, payload.env, payload.ttlMs ?? DEFAULT_TTL_MS, payload.harness ?? GLOBAL_HARNESS);
|
|
1273
|
+
const loaded = await agentLoad(payload.name, payload.bundle, payload.env, payload.ttlMs ?? DEFAULT_TTL_MS, payload.harness ?? GLOBAL_HARNESS, payload.lease);
|
|
1239
1274
|
if (!loaded)
|
|
1240
1275
|
process.exitCode = 1; // transport failed — did NOT load
|
|
1241
1276
|
}
|
|
1242
1277
|
/** Store a resolved bundle in the broker. Returns false on transport failure. */
|
|
1243
|
-
export async function agentLoad(name, bundle, env, ttlMs, harness = GLOBAL_HARNESS) {
|
|
1244
|
-
const r = await request({ cmd: 'load', name, bundle, env, ttlMs, harness });
|
|
1278
|
+
export async function agentLoad(name, bundle, env, ttlMs, harness = GLOBAL_HARNESS, lease) {
|
|
1279
|
+
const r = await request({ cmd: 'load', name, bundle, env, ttlMs, harness, lease });
|
|
1245
1280
|
return r?.ok === true && r.cmd === 'load';
|
|
1246
1281
|
}
|
|
1247
1282
|
/** Wipe one bundle (or all if name omitted) from the broker. Returns the count
|
|
@@ -1250,6 +1285,10 @@ export async function agentLock(name) {
|
|
|
1250
1285
|
const r = await request({ cmd: 'lock', name });
|
|
1251
1286
|
return r?.ok === true && r.cmd === 'lock' ? r.wiped : 0;
|
|
1252
1287
|
}
|
|
1288
|
+
export async function agentRevoke(leaseId) {
|
|
1289
|
+
const r = await request({ cmd: 'revoke', leaseId });
|
|
1290
|
+
return r?.ok === true && r.cmd === 'revoke' ? r.wiped : 0;
|
|
1291
|
+
}
|
|
1253
1292
|
/** List currently-unlocked bundles, or [] when no broker is running. The
|
|
1254
1293
|
* internal `secrets list` metadata-cache entry is filtered out here as well as
|
|
1255
1294
|
* server-side: during a rollout a NEW client can talk to an OLD broker that
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SecretAuditEvent = 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view';
|
|
1
|
+
export type SecretAuditEvent = 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.lease-denied' | 'secrets.lease-expire';
|
|
2
2
|
export interface SecretAuditParams {
|
|
3
3
|
/** Which audit event this is — a read (`secrets.get`) or an unlock grant. */
|
|
4
4
|
event: SecretAuditEvent;
|
|
@@ -46,6 +46,8 @@ const USAGE_KIND = {
|
|
|
46
46
|
'secrets.import': 'import',
|
|
47
47
|
'secrets.export': 'export',
|
|
48
48
|
'secrets.view': 'view',
|
|
49
|
+
'secrets.lease-denied': 'access',
|
|
50
|
+
'secrets.lease-expire': 'access',
|
|
49
51
|
};
|
|
50
52
|
/**
|
|
51
53
|
* The agent/harness identity to attribute a secret access to. Explicit callers
|
|
@@ -264,9 +264,9 @@ export interface ResolveBundleOptions {
|
|
|
264
264
|
export declare function canCacheResolvedEnv(bundle: SecretsBundle, selectedKeys: Set<string>, keyMode: ResolveBundleOptions['keyMode']): boolean;
|
|
265
265
|
/**
|
|
266
266
|
* Apply the --keys subset + expiry gate to an already-resolved snapshot from
|
|
267
|
-
* the secrets-agent fast-path. The agent stores
|
|
268
|
-
* naive fast-path return
|
|
269
|
-
* values. Mirrors the slow-path pre-checks in `resolveBundleEnv` /
|
|
267
|
+
* the secrets-agent fast-path. The agent stores either a full unlock or a
|
|
268
|
+
* scoped lease env, so a naive fast-path return could silently defeat --keys
|
|
269
|
+
* and inject expired values. Mirrors the slow-path pre-checks in `resolveBundleEnv` /
|
|
270
270
|
* `readAndResolveBundleEnv` and returns a new env whose keys match the subset.
|
|
271
271
|
*
|
|
272
272
|
* Exported for tests; production callers reach it via the fast-path branch in
|
|
@@ -1033,9 +1033,9 @@ export function canCacheResolvedEnv(bundle, selectedKeys, keyMode) {
|
|
|
1033
1033
|
}
|
|
1034
1034
|
/**
|
|
1035
1035
|
* Apply the --keys subset + expiry gate to an already-resolved snapshot from
|
|
1036
|
-
* the secrets-agent fast-path. The agent stores
|
|
1037
|
-
* naive fast-path return
|
|
1038
|
-
* values. Mirrors the slow-path pre-checks in `resolveBundleEnv` /
|
|
1036
|
+
* the secrets-agent fast-path. The agent stores either a full unlock or a
|
|
1037
|
+
* scoped lease env, so a naive fast-path return could silently defeat --keys
|
|
1038
|
+
* and inject expired values. Mirrors the slow-path pre-checks in `resolveBundleEnv` /
|
|
1039
1039
|
* `readAndResolveBundleEnv` and returns a new env whose keys match the subset.
|
|
1040
1040
|
*
|
|
1041
1041
|
* Exported for tests; production callers reach it via the fast-path branch in
|
|
@@ -1230,7 +1230,12 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1230
1230
|
const harness = opts.agent || process.env.AGENTS_AGENT_NAME || GLOBAL_HARNESS;
|
|
1231
1231
|
const hit = agentGetSync(name, harness);
|
|
1232
1232
|
if (hit) {
|
|
1233
|
-
|
|
1233
|
+
const denied = (opts.keys ?? []).filter((key) => hit.lease && !hit.lease.keys.includes(key));
|
|
1234
|
+
if (denied.length > 0) {
|
|
1235
|
+
emitSecretAudit({ event: 'secrets.lease-denied', bundle: name, operation: opts.caller, source: 'agent', status: 'error', keys: denied, keyCount: denied.length, agent: harness, error: 'key outside lease scope' });
|
|
1236
|
+
throw new Error(`Secret lease '${hit.lease?.id}' does not grant key(s): ${denied.join(', ')}`);
|
|
1237
|
+
}
|
|
1238
|
+
// The agent stores a full unlock or a scoped lease env. Apply the same subset filter and
|
|
1234
1239
|
// expiry gate as the slow path — without this, `--secrets-keys X` would
|
|
1235
1240
|
// silently inject every key and an expired key would flow through after
|
|
1236
1241
|
// the first cache-populating run.
|
|
@@ -1256,13 +1261,18 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1256
1261
|
const resolved = resolveSession(name, Date.now(), harness);
|
|
1257
1262
|
if (resolved) {
|
|
1258
1263
|
const session = resolved.entry;
|
|
1264
|
+
const denied = (opts.keys ?? []).filter((key) => session.lease && !session.lease.keys.includes(key));
|
|
1265
|
+
if (denied.length > 0) {
|
|
1266
|
+
emitSecretAudit({ event: 'secrets.lease-denied', bundle: name, operation: opts.caller, source: 'session', status: 'error', keys: denied, keyCount: denied.length, agent: harness, error: 'key outside lease scope' });
|
|
1267
|
+
throw new Error(`Secret lease '${session.lease?.id}' does not grant key(s): ${denied.join(', ')}`);
|
|
1268
|
+
}
|
|
1259
1269
|
const filtered = filterAgentHitBySubsetAndExpiry({ bundle: session.bundle, env: session.env }, opts);
|
|
1260
1270
|
stampLastUsed(filtered.bundle);
|
|
1261
1271
|
// Re-warm the broker with the remaining TTL so later reads hit RAM and
|
|
1262
1272
|
// `agents secrets status` is honest. Re-warm under the scope the grant was
|
|
1263
1273
|
// MADE in (resolved.harness), never the asking scope — re-warming a global
|
|
1264
1274
|
// grant as `claude` would silently narrow it for every other harness.
|
|
1265
|
-
agentAutoLoadSync(name, session.bundle, session.env, Math.max(1, session.expiresAt - Date.now()), resolved.harness);
|
|
1275
|
+
agentAutoLoadSync(name, session.bundle, session.env, Math.max(1, session.expiresAt - Date.now()), resolved.harness, session.lease);
|
|
1266
1276
|
emitSecretAudit({
|
|
1267
1277
|
event: 'secrets.get',
|
|
1268
1278
|
bundle: name,
|
|
@@ -51,6 +51,8 @@ export declare function encryptForFallback(plaintext: string, passphrase: string
|
|
|
51
51
|
/** Decrypt an EncFile under a passphrase. Throws on wrong key or tampered
|
|
52
52
|
* ciphertext (auth-tag mismatch). Exported for tests. */
|
|
53
53
|
export declare function decryptForFallback(enc: EncFile, passphrase: string): string;
|
|
54
|
+
/** Absolute path of one encrypted file-store item. No read or decrypt occurs. */
|
|
55
|
+
export declare function fileStoreItemPath(item: string): string;
|
|
54
56
|
declare function fileHas(item: string): boolean;
|
|
55
57
|
declare function fileGet(item: string): string;
|
|
56
58
|
declare function fileGetBatch(items: string[]): Map<string, string>;
|
|
@@ -214,6 +214,10 @@ export function decryptForFallback(enc, passphrase) {
|
|
|
214
214
|
function fileFor(item) {
|
|
215
215
|
return path.join(fileDir(), `${item}.enc`);
|
|
216
216
|
}
|
|
217
|
+
/** Absolute path of one encrypted file-store item. No read or decrypt occurs. */
|
|
218
|
+
export function fileStoreItemPath(item) {
|
|
219
|
+
return fileFor(item);
|
|
220
|
+
}
|
|
217
221
|
function fileHas(item) {
|
|
218
222
|
return fs.existsSync(fileFor(item));
|
|
219
223
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const MIN_LEASE_MS: number;
|
|
2
|
+
export declare const MAX_LEASE_MS: number;
|
|
3
|
+
export interface SecretLease {
|
|
4
|
+
id: string;
|
|
5
|
+
bundle: string;
|
|
6
|
+
keys: string[];
|
|
7
|
+
createdAt: number;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
harness: string;
|
|
10
|
+
sleepPersist: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function clampLeaseTtlMs(ttlMs: number): number;
|
|
13
|
+
export declare function normalizeLeaseKeys(keys: readonly string[], availableKeys: readonly string[]): string[];
|
|
14
|
+
export declare function createSecretLease(input: {
|
|
15
|
+
bundle: string;
|
|
16
|
+
keys: readonly string[];
|
|
17
|
+
availableKeys: readonly string[];
|
|
18
|
+
ttlMs: number;
|
|
19
|
+
now?: number;
|
|
20
|
+
id?: string;
|
|
21
|
+
harness?: string;
|
|
22
|
+
sleepPersist?: boolean;
|
|
23
|
+
}): SecretLease;
|
|
24
|
+
export declare function leaseIsActive(lease: SecretLease, now?: number): boolean;
|
|
25
|
+
export declare function selectLeasedEnv(lease: SecretLease, env: Record<string, string>, now?: number): Record<string, string>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import { GLOBAL_HARNESS } from './scope.js';
|
|
3
|
+
export const MIN_LEASE_MS = 60 * 1000;
|
|
4
|
+
export const MAX_LEASE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
5
|
+
export function clampLeaseTtlMs(ttlMs) {
|
|
6
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
7
|
+
throw new Error('Lease duration must be a positive finite number.');
|
|
8
|
+
}
|
|
9
|
+
return Math.min(Math.max(Math.floor(ttlMs), MIN_LEASE_MS), MAX_LEASE_MS);
|
|
10
|
+
}
|
|
11
|
+
export function normalizeLeaseKeys(keys, availableKeys) {
|
|
12
|
+
const requested = [...new Set(keys.map((key) => key.trim()).filter(Boolean))].sort();
|
|
13
|
+
if (requested.length === 0)
|
|
14
|
+
throw new Error('A secret lease requires at least one key.');
|
|
15
|
+
const available = new Set(availableKeys);
|
|
16
|
+
const missing = requested.filter((key) => !available.has(key));
|
|
17
|
+
if (missing.length > 0)
|
|
18
|
+
throw new Error(`Unknown secret lease key(s): ${missing.join(', ')}`);
|
|
19
|
+
return requested;
|
|
20
|
+
}
|
|
21
|
+
export function createSecretLease(input) {
|
|
22
|
+
const createdAt = input.now ?? Date.now();
|
|
23
|
+
const ttlMs = clampLeaseTtlMs(input.ttlMs);
|
|
24
|
+
return {
|
|
25
|
+
id: input.id ?? randomBytes(12).toString('hex'),
|
|
26
|
+
bundle: input.bundle,
|
|
27
|
+
keys: normalizeLeaseKeys(input.keys, input.availableKeys),
|
|
28
|
+
createdAt,
|
|
29
|
+
expiresAt: createdAt + ttlMs,
|
|
30
|
+
harness: input.harness ?? GLOBAL_HARNESS,
|
|
31
|
+
sleepPersist: input.sleepPersist ?? false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function leaseIsActive(lease, now = Date.now()) {
|
|
35
|
+
return now < lease.expiresAt;
|
|
36
|
+
}
|
|
37
|
+
export function selectLeasedEnv(lease, env, now = Date.now()) {
|
|
38
|
+
if (!leaseIsActive(lease, now))
|
|
39
|
+
throw new Error(`Secret lease '${lease.id}' has expired.`);
|
|
40
|
+
const missing = lease.keys.filter((key) => typeof env[key] !== 'string');
|
|
41
|
+
if (missing.length > 0)
|
|
42
|
+
throw new Error(`Secret lease '${lease.id}' is missing value(s): ${missing.join(', ')}`);
|
|
43
|
+
return Object.fromEntries(lease.keys.map((key) => [key, env[key]]));
|
|
44
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* persistence is an optimization, not a correctness dependency.
|
|
26
26
|
*/
|
|
27
27
|
import type { SecretsBundle } from './bundles.js';
|
|
28
|
+
import type { SecretLease } from './lease.js';
|
|
28
29
|
/** Prefix for all durable session items (device-local, no-ACL). */
|
|
29
30
|
export declare const SESSION_ITEM_PREFIX = "agents-cli.session.";
|
|
30
31
|
/** Fixed-name index item — the ONLY thing we ever need to find without a known
|
|
@@ -39,6 +40,7 @@ export interface SessionEntry {
|
|
|
39
40
|
/** true only for `--durable` unlocks — survives SLEEP. */
|
|
40
41
|
sleepPersist: boolean;
|
|
41
42
|
harness?: string;
|
|
43
|
+
lease?: SecretLease;
|
|
42
44
|
}
|
|
43
45
|
/** Metadata for one held bundle, kept in the index so we can rehydrate / prune
|
|
44
46
|
* without reading every blob. */
|
|
@@ -90,6 +92,11 @@ export declare function loadSession(name: string, now?: number, harness?: string
|
|
|
90
92
|
export declare function deleteBundleSessions(name: string): void;
|
|
91
93
|
/** Delete one bundle's session blob and prune it from the index. */
|
|
92
94
|
export declare function deleteSession(name: string, harness?: string): void;
|
|
95
|
+
export declare function deleteLeaseSession(leaseId: string): number;
|
|
96
|
+
export declare function activeLeaseSessions(now?: number): Array<{
|
|
97
|
+
name: string;
|
|
98
|
+
lease: SecretLease;
|
|
99
|
+
}>;
|
|
93
100
|
/** Delete every session blob + the index (for `secrets lock --all`). */
|
|
94
101
|
export declare function deleteAllSessions(): void;
|
|
95
102
|
/** Rehydrate every unexpired session into `[name, entry]` pairs for the broker to
|
|
@@ -192,6 +192,27 @@ export function deleteSession(name, harness = GLOBAL_HARNESS) {
|
|
|
192
192
|
/* best-effort */
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
+
export function deleteLeaseSession(leaseId) {
|
|
196
|
+
if (!shouldPersist())
|
|
197
|
+
return 0;
|
|
198
|
+
const index = readIndex();
|
|
199
|
+
let deleted = 0;
|
|
200
|
+
for (const [key, meta] of Object.entries(index.bundles)) {
|
|
201
|
+
const bundleName = key.split(':').slice(1).join(':');
|
|
202
|
+
const harness = meta.harness || GLOBAL_HARNESS;
|
|
203
|
+
const entry = loadSession(bundleName, Date.now(), harness);
|
|
204
|
+
if (entry?.lease?.id !== leaseId)
|
|
205
|
+
continue;
|
|
206
|
+
deleteSession(bundleName, harness);
|
|
207
|
+
deleted++;
|
|
208
|
+
}
|
|
209
|
+
return deleted;
|
|
210
|
+
}
|
|
211
|
+
export function activeLeaseSessions(now = Date.now()) {
|
|
212
|
+
return rehydrateSessions(now)
|
|
213
|
+
.filter((item) => Boolean(item.entry.lease))
|
|
214
|
+
.map(({ name, entry }) => ({ name, lease: entry.lease }));
|
|
215
|
+
}
|
|
195
216
|
/** Delete every session blob + the index (for `secrets lock --all`). */
|
|
196
217
|
export function deleteAllSessions() {
|
|
197
218
|
if (!shouldPersist())
|
|
@@ -136,15 +136,30 @@ export declare function verifyInstalledVersion(packageRoot: string, expectedVers
|
|
|
136
136
|
*/
|
|
137
137
|
export declare function refreshAliasShims(packageRoot: string): void;
|
|
138
138
|
export interface AgentsCliInstall {
|
|
139
|
-
/** The PATH entry (`<dir>/agents`) that resolves to this install. */
|
|
140
|
-
binPath
|
|
139
|
+
/** The PATH entry (`<dir>/agents`) that resolves to this install, when found through PATH. */
|
|
140
|
+
binPath?: string;
|
|
141
141
|
/** Package root containing package.json and dist/. */
|
|
142
142
|
packageRoot: string;
|
|
143
143
|
version: string;
|
|
144
|
+
/** Whether this copy uses the serialized, atomic helper-bundle installer. */
|
|
145
|
+
atomicHelperInstall: boolean;
|
|
144
146
|
}
|
|
147
|
+
export interface FindAgentsCliInstallsOptions {
|
|
148
|
+
homeDir?: string;
|
|
149
|
+
fnmDir?: string;
|
|
150
|
+
npmCacheDir?: string;
|
|
151
|
+
globalNodeModulesDirs?: string[];
|
|
152
|
+
}
|
|
153
|
+
export interface MultiInstallInventoryEntry {
|
|
154
|
+
packageRoot: string;
|
|
155
|
+
version: string;
|
|
156
|
+
note: string;
|
|
157
|
+
}
|
|
158
|
+
export declare function buildMultiInstallInventory(runningRoot: string, runningVersion: string, installs: AgentsCliInstall[]): MultiInstallInventoryEntry[];
|
|
145
159
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
160
|
+
* Resolve every `agents` entrypoint on PATH, then inspect the bounded global
|
|
161
|
+
* install layouts used by NVM, fnm, Volta, Bun, npm, and npx. More than one
|
|
162
|
+
* distinct package root means upgrades,
|
|
148
163
|
* shims, and the command the user types can act on different copies — the
|
|
149
164
|
* divergence behind silently-failing self-updates.
|
|
150
165
|
*
|
|
@@ -158,4 +173,4 @@ export interface AgentsCliInstall {
|
|
|
158
173
|
* @phnx-labs/agents-cli is some other tool and is skipped.
|
|
159
174
|
* POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
|
|
160
175
|
*/
|
|
161
|
-
export declare function findAgentsCliInstalls(pathEnv: string): AgentsCliInstall[];
|
|
176
|
+
export declare function findAgentsCliInstalls(pathEnv: string, opts?: FindAgentsCliInstallsOptions): AgentsCliInstall[];
|