@phnx-labs/agents-cli 1.20.74 → 1.20.76
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 +893 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +31 -3
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +58 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +1 -32
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +37 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/lib/runner.js
CHANGED
|
@@ -22,13 +22,16 @@ import { getRunsDir } from './state.js';
|
|
|
22
22
|
import { prepareJobHome, buildSpawnEnv, getJobHomePath } from './sandbox.js';
|
|
23
23
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
24
24
|
import { createTimer, maybeRotate, redactPrompt } from './events.js';
|
|
25
|
-
import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, } from './exec.js';
|
|
25
|
+
import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, detectAuthFailure, isAuthFailureFromLog, authFailureReason, } from './exec.js';
|
|
26
26
|
import { loadTask as loadHostTask } from './hosts/tasks.js';
|
|
27
27
|
import { reconcileTask as reconcileHostTask } from './hosts/reconcile.js';
|
|
28
28
|
import { backgroundSpawnOptions } from './platform/process.js';
|
|
29
29
|
import { walkForFiles } from './fs-walk.js';
|
|
30
|
-
import { getBinaryPath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
31
|
-
import {
|
|
30
|
+
import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
31
|
+
import { claudeHomeHasOwnCredential } from './agents.js';
|
|
32
|
+
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, } from './rotate.js';
|
|
33
|
+
import { readAuthHealth, isDeadVerdict } from './auth-health.js';
|
|
34
|
+
import { machineId } from './machine-id.js';
|
|
32
35
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
33
36
|
const AGENT_COMMANDS = {
|
|
34
37
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
@@ -287,6 +290,19 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd()) {
|
|
|
287
290
|
pinned: true,
|
|
288
291
|
};
|
|
289
292
|
}
|
|
293
|
+
// Account pin: resolve the login identity to the version slot that holds it and
|
|
294
|
+
// run pinned — no rotation, no failover onto other accounts — so concurrent
|
|
295
|
+
// unattended routines never share (and mutually revoke) one single-use OAuth
|
|
296
|
+
// credential (RUSH-1957). Falls through to the strategy only when the account
|
|
297
|
+
// is not signed in on this box, with a loud warning rather than a silent stall.
|
|
298
|
+
if (config.account) {
|
|
299
|
+
const version = await resolveAccountVersion(agent, config.account);
|
|
300
|
+
if (version) {
|
|
301
|
+
return { chain: [{ agent, version }], rotation: null, pinned: true };
|
|
302
|
+
}
|
|
303
|
+
process.stderr.write(`[agents] routine ${config.name}: account '${config.account}' is not signed in for ${agent}; ` +
|
|
304
|
+
`falling back to strategy — pin a signed-in account to stop OAuth-rotation revocation\n`);
|
|
305
|
+
}
|
|
290
306
|
const strategy = getConfiguredRunStrategy(agent, cwd);
|
|
291
307
|
let version;
|
|
292
308
|
let rotation = null;
|
|
@@ -383,6 +399,19 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
|
|
|
383
399
|
if (v !== undefined)
|
|
384
400
|
out[k] = v;
|
|
385
401
|
}
|
|
402
|
+
// RUSH-1979: the daemon injects one ambient CLAUDE_CODE_OAUTH_TOKEN (RUSH-1759)
|
|
403
|
+
// so a token-less default account still authenticates. When balanced rotation
|
|
404
|
+
// pins THIS spawn to a specific account's CLAUDE_CONFIG_DIR that holds its own
|
|
405
|
+
// credential, that ambient token would shadow the account (Claude and the Linux
|
|
406
|
+
// shim both prefer the env var) — making the pool inert and 401ing on the one
|
|
407
|
+
// stale token. Drop it here so the pinned account authenticates itself; keep it
|
|
408
|
+
// when the account has no on-disk credential (the RUSH-1759 default).
|
|
409
|
+
if (agent === 'claude' &&
|
|
410
|
+
version &&
|
|
411
|
+
out.CLAUDE_CONFIG_DIR &&
|
|
412
|
+
claudeHomeHasOwnCredential(getVersionHomePath('claude', version))) {
|
|
413
|
+
delete out.CLAUDE_CODE_OAUTH_TOKEN;
|
|
414
|
+
}
|
|
386
415
|
if (timezone)
|
|
387
416
|
out.TZ = timezone;
|
|
388
417
|
return out;
|
|
@@ -536,6 +565,25 @@ export async function executeJob(config, deps) {
|
|
|
536
565
|
exitCode: null,
|
|
537
566
|
};
|
|
538
567
|
writeRunMeta(meta);
|
|
568
|
+
// Auth preflight: if the last live probe rejected this (agent, version)'s
|
|
569
|
+
// token (verdict `revoked`), the run is guaranteed to fail auth — fail fast
|
|
570
|
+
// before spawning instead of producing a doomed run + poisoned report. Cache-
|
|
571
|
+
// only (the daemon refreshes it periodically); fail OPEN on any non-revoked or
|
|
572
|
+
// missing verdict so a stale/absent probe or a network blip never blocks a
|
|
573
|
+
// run, and agents with no live probe (codex/gemini/grok) are never blocked.
|
|
574
|
+
const preflightVersion = launch.chain[0]?.version;
|
|
575
|
+
if (preflightVersion) {
|
|
576
|
+
const health = readAuthHealth(machineId(), effectiveAgent, preflightVersion);
|
|
577
|
+
if (health && isDeadVerdict(health.verdict)) {
|
|
578
|
+
const reason = `auth_preflight: ${health.verdict}`;
|
|
579
|
+
process.stderr.write(`[agents] routine ${config.name}: ${effectiveAgent}@${preflightVersion} token ${health.verdict} — skipping run (re-login required)\n`);
|
|
580
|
+
finalizeRunMeta(meta, 'failed', 1, { errorMessage: reason });
|
|
581
|
+
writeRunMeta(meta);
|
|
582
|
+
timer.end({ status: 'failed', exitCode: 1, runId, error: reason });
|
|
583
|
+
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
584
|
+
return { meta, reportPath: null };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
539
587
|
const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
|
|
540
588
|
// Loop path: delegate to runLoop (same driver as `agents run --loop` / workflow loop:).
|
|
541
589
|
if (config.loop) {
|
|
@@ -616,6 +664,21 @@ export async function executeJob(config, deps) {
|
|
|
616
664
|
return { meta, reportPath };
|
|
617
665
|
}
|
|
618
666
|
if (attempt.status === 'completed') {
|
|
667
|
+
// Exit code alone is unreliable for auth: a logged-out Claude can exit 0
|
|
668
|
+
// with a `result` event carrying is_error:true (terminal_reason
|
|
669
|
+
// "completed"). Consult the SAME structural signal the detached path uses
|
|
670
|
+
// so both paths agree — raw text is deliberately NOT used here, so a
|
|
671
|
+
// genuinely-completed run that merely mentions an auth phrase stays a
|
|
672
|
+
// success (processFailed:false).
|
|
673
|
+
if (isAuthFailureFromLog(attempt.logText, effectiveAgent, { processFailed: false })) {
|
|
674
|
+
const reason = authFailureReason(attempt.logText) ?? 'authentication_failed';
|
|
675
|
+
finalizeRunMeta(meta, 'failed', attempt.exitCode ?? 1, { errorMessage: `auth_failed: ${reason}` });
|
|
676
|
+
writeRunMeta(meta);
|
|
677
|
+
timer.end({ status: 'failed', exitCode: meta.exitCode ?? undefined, runId, error: `auth_failed: ${reason}` });
|
|
678
|
+
// Never persist the login-error text as the report.
|
|
679
|
+
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
680
|
+
return { meta, reportPath: null };
|
|
681
|
+
}
|
|
619
682
|
finalizeRunMeta(meta, 'completed', 0);
|
|
620
683
|
writeRunMeta(meta);
|
|
621
684
|
timer.end({ status: 'completed', exitCode: 0, runId });
|
|
@@ -633,18 +696,36 @@ export async function executeJob(config, deps) {
|
|
|
633
696
|
fs.appendFileSync(stdoutPath, `\n[agents] ${label} hit rate/usage limit — failover → ${nextLabel}\n`);
|
|
634
697
|
continue;
|
|
635
698
|
}
|
|
699
|
+
// Auth failure — the agent is logged out / token revoked. Unlike a rate
|
|
700
|
+
// limit it is not self-healing by failover (every chain entry on the same
|
|
701
|
+
// account fails identically), so rate-limit is classified first (above) and
|
|
702
|
+
// auth only when NOT rate-limited. Classified so the failure is visible and,
|
|
703
|
+
// critically, so the login-error text is never persisted as the report.
|
|
704
|
+
const authFailed = !rateLimited && (isAuthFailureFromLog(attempt.logText, effectiveAgent, { processFailed: true }) ||
|
|
705
|
+
(attempt.error ? detectAuthFailure(attempt.error) : false));
|
|
636
706
|
if (attempt.error) {
|
|
637
707
|
process.stderr.write(`[agents] routine ${config.name}: spawn failed for ${label}: ${attempt.error}\n`);
|
|
638
708
|
}
|
|
639
|
-
|
|
709
|
+
const authReason = authFailed
|
|
710
|
+
? (authFailureReason(attempt.logText)
|
|
711
|
+
?? (attempt.error ? authFailureReason(attempt.error) : null)
|
|
712
|
+
?? 'authentication_failed')
|
|
713
|
+
: null;
|
|
714
|
+
const failureErrorMessage = authReason
|
|
715
|
+
? `auth_failed: ${authReason}`
|
|
716
|
+
: (attempt.error ?? undefined);
|
|
717
|
+
finalizeRunMeta(meta, 'failed', attempt.exitCode ?? 1, failureErrorMessage ? { errorMessage: failureErrorMessage } : undefined);
|
|
640
718
|
writeRunMeta(meta);
|
|
641
719
|
timer.end({
|
|
642
720
|
status: 'failed',
|
|
643
721
|
exitCode: meta.exitCode ?? undefined,
|
|
644
722
|
runId,
|
|
645
|
-
...(
|
|
723
|
+
...(failureErrorMessage ? { error: failureErrorMessage } : {}),
|
|
646
724
|
});
|
|
647
|
-
|
|
725
|
+
// On auth failure the last assistant text IS the login error — never persist
|
|
726
|
+
// it as report.md; it would otherwise be injected into the next run's prompt
|
|
727
|
+
// via {last_report}.
|
|
728
|
+
const reportPath = authFailed ? null : extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
648
729
|
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
649
730
|
return { meta, reportPath };
|
|
650
731
|
}
|
|
@@ -876,10 +957,26 @@ export async function executeJobDetached(config) {
|
|
|
876
957
|
finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
|
|
877
958
|
writeRunMeta(meta);
|
|
878
959
|
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
879
|
-
|
|
960
|
+
// Never persist the report on an auth failure — the last assistant text is
|
|
961
|
+
// the login error, which would poison the next run's {last_report} prompt.
|
|
962
|
+
const isAuthFailure = !!errorMessage && errorMessage.startsWith('auth_failed:');
|
|
963
|
+
if (status !== 'timeout' && !isAuthFailure)
|
|
880
964
|
extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
881
965
|
};
|
|
882
966
|
child.on('exit', (code) => {
|
|
967
|
+
let logText = '';
|
|
968
|
+
try {
|
|
969
|
+
logText = fs.readFileSync(stdoutPath, 'utf-8');
|
|
970
|
+
}
|
|
971
|
+
catch { /* log unreadable */ }
|
|
972
|
+
// processFailed gates the raw-text fallback so a COMPLETED run (exit 0) that
|
|
973
|
+
// merely mentions an auth phrase is never misclassified; the structural
|
|
974
|
+
// marker still catches an exit-0 auth failure on its own.
|
|
975
|
+
if (isAuthFailureFromLog(logText, effectiveAgent, { processFailed: (code ?? 1) !== 0 })) {
|
|
976
|
+
const reason = authFailureReason(logText) ?? 'authentication_failed';
|
|
977
|
+
settle('failed', code ?? 1, `auth_failed: ${reason}`);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
883
980
|
const inferred = inferFinalStatusFromLog(stdoutPath, effectiveAgent);
|
|
884
981
|
if (inferred) {
|
|
885
982
|
settle(inferred.status, inferred.exitCode);
|
|
@@ -40,6 +40,7 @@ export declare const DEFAULT_TTL_MS: number;
|
|
|
40
40
|
* spawnSync argv (unlike a NUL byte); `status` hides these entries.
|
|
41
41
|
*/
|
|
42
42
|
export declare const META_CACHE_PREFIX = "!meta:";
|
|
43
|
+
export { SYNC_GET_CMD, SYNC_PING_CMD, SYNC_LOCK_CMD } from './sync-commands.js';
|
|
43
44
|
/**
|
|
44
45
|
* Decide whether a persistent broker should self-heal onto freshly-installed
|
|
45
46
|
* code (exit so launchd relaunches it). Only when the store is EMPTY: exiting
|
|
@@ -232,6 +233,38 @@ export declare function startHostedBroker(): Promise<{
|
|
|
232
233
|
/** True if a broker socket exists at all. Cheap; gates the sync read so the
|
|
233
234
|
* never-unlocked path stays a single stat. */
|
|
234
235
|
export declare function agentSocketExists(): boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Spawn one of the `__secrets-*` sync clients and return its result.
|
|
238
|
+
*
|
|
239
|
+
* These three call sites used to hand-roll `spawnSync(process.execPath, ['-e',
|
|
240
|
+
* <inline node program>, …])`. That is correct only for a JS install, where
|
|
241
|
+
* `process.execPath` is `node`. Since 1.20.53 the shipped macOS `agents` is a
|
|
242
|
+
* **bun-compiled Mach-O**, where `process.execPath` is the CLI binary itself —
|
|
243
|
+
* so the spawn became `agents -e <program> …`, which commander rejects with
|
|
244
|
+
* `error: unknown option '-e'` and a non-zero exit. Every sync client then took
|
|
245
|
+
* its own failure path (`agentGetSync` → null, `agentReachableSync` → false,
|
|
246
|
+
* `agentEvictSync` → no-op), which reads as "broker down" and falls through to
|
|
247
|
+
* a real keychain read. Net effect on the standalone binary: the broker cache
|
|
248
|
+
* was never hit, so the `daily` policy's one-prompt-per-7d never applied and
|
|
249
|
+
* every bundle read re-popped Touch ID.
|
|
250
|
+
*
|
|
251
|
+
* Same defect class as the broker-launch bug fixed in 1.20.56 (see cliSpawn's
|
|
252
|
+
* doc comment); these three sites were simply never converted. Routing them
|
|
253
|
+
* through `getCliLaunch` fixes both install shapes at once and keeps a single
|
|
254
|
+
* code path.
|
|
255
|
+
*
|
|
256
|
+
* The subcommands are top-level `__secrets-*` tokens intercepted in index.ts
|
|
257
|
+
* BEFORE commander and before the CLI's startup work, exactly like
|
|
258
|
+
* `__daemon-run` and `__vault-age-helper`. That is load-bearing, not cosmetic:
|
|
259
|
+
* a normal command path runs `checkForUpdates()` and `spawnDetachedSync()` on
|
|
260
|
+
* every invocation (index.ts), so registering these as ordinary hidden
|
|
261
|
+
* subcommands would fire an update check and spawn a detached background sync
|
|
262
|
+
* on every cache hit — turning the hot read path into a process fork storm.
|
|
263
|
+
*/
|
|
264
|
+
export declare function syncClientLaunch(sub: string[], agentsBin?: string): {
|
|
265
|
+
command: string;
|
|
266
|
+
args: string[];
|
|
267
|
+
};
|
|
235
268
|
/**
|
|
236
269
|
* Synchronous read for the hot path. Returns the cached resolved bundle, or
|
|
237
270
|
* null if the agent isn't running / doesn't hold this bundle / anything fails
|
|
@@ -241,6 +274,20 @@ export declare function agentGetSync(name: string): {
|
|
|
241
274
|
bundle: SecretsBundle;
|
|
242
275
|
env: Record<string, string>;
|
|
243
276
|
} | null;
|
|
277
|
+
/**
|
|
278
|
+
* Last non-empty line of a child's stdout — the payload line.
|
|
279
|
+
*
|
|
280
|
+
* The inline `node -e` client this replaced was a bare node process that could
|
|
281
|
+
* only ever emit its own JSON. The replacement boots the real CLI, so anything
|
|
282
|
+
* the CLI prints on the way up shares that stream. Today the known chatter (the
|
|
283
|
+
* `~/.agents/ is N commits behind` notice) correctly goes to stderr, but a
|
|
284
|
+
* single future stdout write anywhere in startup would make `JSON.parse` throw
|
|
285
|
+
* and turn every cache hit back into a silent miss — i.e. quietly reintroduce
|
|
286
|
+
* the Touch-ID-on-every-read bug this fix closes, with no failing test to catch
|
|
287
|
+
* it. Anchoring on the last line makes the payload the terminator of the
|
|
288
|
+
* stream rather than the whole of it, so preceding chatter is inert.
|
|
289
|
+
*/
|
|
290
|
+
export declare function lastLine(stdout: string): string;
|
|
244
291
|
/**
|
|
245
292
|
* Synchronous liveness check: is a broker actually LISTENING and answering (not
|
|
246
293
|
* just a lingering socket file)? Used to decide whether the auto-cache may take
|
|
@@ -259,6 +306,25 @@ export declare function agentReachableSync(): boolean;
|
|
|
259
306
|
* macOS only.
|
|
260
307
|
*/
|
|
261
308
|
export declare function agentEvictSync(name: string): void;
|
|
309
|
+
/** Body of `__secrets-get <name>`. Prints `{bundle, env}` as JSON on a
|
|
310
|
+
* cache hit. Exit 0 = hit, 3 = miss or broker down. */
|
|
311
|
+
export declare function runAgentGetSync(name: string): Promise<number>;
|
|
312
|
+
/** Body of `__secrets-ping`. Exit 0 = a broker is listening and speaking
|
|
313
|
+
* our protocol, 3 = nothing there. Deliberately does NOT gate on
|
|
314
|
+
* PROTOCOL_VERSION: this only decides whether the auto-cache may take the
|
|
315
|
+
* synchronous warm path, and a version-skewed broker still answers reads. That
|
|
316
|
+
* matches the inline program this replaced. */
|
|
317
|
+
export declare function runAgentPingSync(): Promise<number>;
|
|
318
|
+
/** Body of `__secrets-lock <name>`. Best-effort evict; exit 0 even when no
|
|
319
|
+
* broker answers, so a missing broker never fails the mutating write that
|
|
320
|
+
* triggered it (agentEvictSync discards this either way).
|
|
321
|
+
*
|
|
322
|
+
* An empty name is refused rather than forwarded: the broker treats a nameless
|
|
323
|
+
* lock as lock-ALL (handleAgentRequest), so a bare `__secrets-lock` would wipe
|
|
324
|
+
* every held bundle and re-prompt Touch ID for each. That was unreachable while
|
|
325
|
+
* this was an inline `node -e` string; as a top-level argv token it is one typo
|
|
326
|
+
* away, and agentEvictSync only ever locks by name. */
|
|
327
|
+
export declare function runAgentLockSync(name: string): Promise<number>;
|
|
262
328
|
/**
|
|
263
329
|
* Read the cached `secrets list` metadata snapshot for the given keychain
|
|
264
330
|
* name-set hash, or null on miss / no broker / off-darwin. Reuses the value
|
|
@@ -364,4 +430,3 @@ export declare function agentPing(): Promise<{
|
|
|
364
430
|
* gets starved under heavy load, so it's last.
|
|
365
431
|
*/
|
|
366
432
|
export declare function ensureAgentRunning(timeoutMs?: number): Promise<boolean>;
|
|
367
|
-
export {};
|
|
@@ -35,6 +35,7 @@ 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 { rehydrateSessions, pruneSessionsOnSleep } from './session-store.js';
|
|
38
|
+
import { SYNC_GET_CMD, SYNC_PING_CMD, SYNC_LOCK_CMD } from './sync-commands.js';
|
|
38
39
|
/** Bumped when the wire protocol changes; a client that pings a mismatched
|
|
39
40
|
* server kills and respawns it rather than talking a stale dialect. */
|
|
40
41
|
const PROTOCOL_VERSION = 1;
|
|
@@ -58,6 +59,34 @@ export const META_CACHE_PREFIX = '!meta:';
|
|
|
58
59
|
const IDLE_EXIT_MS = 5 * 60 * 1000; // 5m
|
|
59
60
|
/** How often the broker sweeps expired entries. */
|
|
60
61
|
const SWEEP_INTERVAL_MS = 30 * 1000;
|
|
62
|
+
/**
|
|
63
|
+
* Timeouts for the three synchronous broker clients, split across the process
|
|
64
|
+
* boundary they now straddle.
|
|
65
|
+
*
|
|
66
|
+
* `SOCKET_*` bound the socket round-trip inside the spawned `__secrets-*`
|
|
67
|
+
* child, and carry over unchanged from the inline `node -e` programs these
|
|
68
|
+
* replaced. `SYNC_*` bound the parent's `spawnSync` and must additionally cover
|
|
69
|
+
* the child's boot, so each is its socket budget plus ~2s of headroom. A parent
|
|
70
|
+
* timeout that fired before the child's own timer would report "broker down"
|
|
71
|
+
* for a broker that is merely slow, which costs a Touch ID prompt — so the
|
|
72
|
+
* parent budget is deliberately the looser of the two.
|
|
73
|
+
*
|
|
74
|
+
* Boot cost, measured on the bun-compiled binary on an M-series Mac (median of
|
|
75
|
+
* 15, against a live broker): ~96ms via the index.ts intercept, ~165ms if the
|
|
76
|
+
* intercept is moved below the startup statements, ~44ms for the `node -e` this
|
|
77
|
+
* replaces. The intercept is what keeps the regression modest; see index.ts.
|
|
78
|
+
*/
|
|
79
|
+
const SOCKET_GET_TIMEOUT_MS = 2000;
|
|
80
|
+
const SOCKET_PING_TIMEOUT_MS = 700;
|
|
81
|
+
const SOCKET_LOCK_TIMEOUT_MS = 2000;
|
|
82
|
+
const SYNC_GET_TIMEOUT_MS = 4000;
|
|
83
|
+
const SYNC_PING_TIMEOUT_MS = 2500;
|
|
84
|
+
const SYNC_LOCK_TIMEOUT_MS = 4000;
|
|
85
|
+
// The argv tokens live in a leaf module so index.ts can bind the SAME values
|
|
86
|
+
// without importing this one. Re-exported here for callers already reaching for
|
|
87
|
+
// agent.js. See sync-commands.ts for why a shared binding rather than matching
|
|
88
|
+
// literals: the drift it prevents is silent and costs a Touch ID prompt per read.
|
|
89
|
+
export { SYNC_GET_CMD, SYNC_PING_CMD, SYNC_LOCK_CMD } from './sync-commands.js';
|
|
61
90
|
/**
|
|
62
91
|
* Decide whether a persistent broker should self-heal onto freshly-installed
|
|
63
92
|
* code (exit so launchd relaunches it). Only when the store is EMPTY: exiting
|
|
@@ -744,35 +773,52 @@ export function agentSocketExists() {
|
|
|
744
773
|
return onDarwin() && fs.existsSync(socketPath());
|
|
745
774
|
}
|
|
746
775
|
/**
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
* -
|
|
776
|
+
* Spawn one of the `__secrets-*` sync clients and return its result.
|
|
777
|
+
*
|
|
778
|
+
* These three call sites used to hand-roll `spawnSync(process.execPath, ['-e',
|
|
779
|
+
* <inline node program>, …])`. That is correct only for a JS install, where
|
|
780
|
+
* `process.execPath` is `node`. Since 1.20.53 the shipped macOS `agents` is a
|
|
781
|
+
* **bun-compiled Mach-O**, where `process.execPath` is the CLI binary itself —
|
|
782
|
+
* so the spawn became `agents -e <program> …`, which commander rejects with
|
|
783
|
+
* `error: unknown option '-e'` and a non-zero exit. Every sync client then took
|
|
784
|
+
* its own failure path (`agentGetSync` → null, `agentReachableSync` → false,
|
|
785
|
+
* `agentEvictSync` → no-op), which reads as "broker down" and falls through to
|
|
786
|
+
* a real keychain read. Net effect on the standalone binary: the broker cache
|
|
787
|
+
* was never hit, so the `daily` policy's one-prompt-per-7d never applied and
|
|
788
|
+
* every bundle read re-popped Touch ID.
|
|
789
|
+
*
|
|
790
|
+
* Same defect class as the broker-launch bug fixed in 1.20.56 (see cliSpawn's
|
|
791
|
+
* doc comment); these three sites were simply never converted. Routing them
|
|
792
|
+
* through `getCliLaunch` fixes both install shapes at once and keeps a single
|
|
793
|
+
* code path.
|
|
794
|
+
*
|
|
795
|
+
* The subcommands are top-level `__secrets-*` tokens intercepted in index.ts
|
|
796
|
+
* BEFORE commander and before the CLI's startup work, exactly like
|
|
797
|
+
* `__daemon-run` and `__vault-age-helper`. That is load-bearing, not cosmetic:
|
|
798
|
+
* a normal command path runs `checkForUpdates()` and `spawnDetachedSync()` on
|
|
799
|
+
* every invocation (index.ts), so registering these as ordinary hidden
|
|
800
|
+
* subcommands would fire an update check and spawn a detached background sync
|
|
801
|
+
* on every cache hit — turning the hot read path into a process fork storm.
|
|
753
802
|
*/
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
process.exit(3);
|
|
774
|
-
});
|
|
775
|
-
`;
|
|
803
|
+
export function syncClientLaunch(sub, agentsBin) {
|
|
804
|
+
return agentsBin ? getCliLaunch(sub, agentsBin) : getCliLaunch(sub);
|
|
805
|
+
}
|
|
806
|
+
function syncClient(sub, timeout) {
|
|
807
|
+
// Soft on every failure: bundles.ts's fast path promises "any failure falls
|
|
808
|
+
// through to the real keychain read below". spawnSync reports most failures
|
|
809
|
+
// via r.error rather than throwing, but getCliLaunch/getAgentsBinPath DO
|
|
810
|
+
// throw on a broken install — a bunfs entry whose execPath is gone, or a
|
|
811
|
+
// browser/computer shim with no sibling index.js. Without this catch those
|
|
812
|
+
// turn a graceful fallback into a crash, and only on already-degraded
|
|
813
|
+
// installs, which is the worst time for it.
|
|
814
|
+
try {
|
|
815
|
+
const { command, args } = syncClientLaunch(sub);
|
|
816
|
+
return spawnSync(command, args, { encoding: 'utf-8', timeout });
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
776
822
|
/**
|
|
777
823
|
* Synchronous read for the hot path. Returns the cached resolved bundle, or
|
|
778
824
|
* null if the agent isn't running / doesn't hold this bundle / anything fails
|
|
@@ -781,14 +827,11 @@ c.on('data', (d) => {
|
|
|
781
827
|
export function agentGetSync(name) {
|
|
782
828
|
if (!agentSocketExists())
|
|
783
829
|
return null;
|
|
784
|
-
const r =
|
|
785
|
-
|
|
786
|
-
timeout: 3000,
|
|
787
|
-
});
|
|
788
|
-
if (r.status !== 0 || !r.stdout)
|
|
830
|
+
const r = syncClient([SYNC_GET_CMD, name], SYNC_GET_TIMEOUT_MS);
|
|
831
|
+
if (!r || r.status !== 0 || !r.stdout)
|
|
789
832
|
return null;
|
|
790
833
|
try {
|
|
791
|
-
const o = JSON.parse(r.stdout);
|
|
834
|
+
const o = JSON.parse(lastLine(r.stdout));
|
|
792
835
|
if (!o || typeof o !== 'object' || !o.env)
|
|
793
836
|
return null;
|
|
794
837
|
return { bundle: o.bundle, env: o.env };
|
|
@@ -797,30 +840,28 @@ export function agentGetSync(name) {
|
|
|
797
840
|
return null;
|
|
798
841
|
}
|
|
799
842
|
}
|
|
800
|
-
/**
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
});
|
|
823
|
-
`;
|
|
843
|
+
/**
|
|
844
|
+
* Last non-empty line of a child's stdout — the payload line.
|
|
845
|
+
*
|
|
846
|
+
* The inline `node -e` client this replaced was a bare node process that could
|
|
847
|
+
* only ever emit its own JSON. The replacement boots the real CLI, so anything
|
|
848
|
+
* the CLI prints on the way up shares that stream. Today the known chatter (the
|
|
849
|
+
* `~/.agents/ is N commits behind` notice) correctly goes to stderr, but a
|
|
850
|
+
* single future stdout write anywhere in startup would make `JSON.parse` throw
|
|
851
|
+
* and turn every cache hit back into a silent miss — i.e. quietly reintroduce
|
|
852
|
+
* the Touch-ID-on-every-read bug this fix closes, with no failing test to catch
|
|
853
|
+
* it. Anchoring on the last line makes the payload the terminator of the
|
|
854
|
+
* stream rather than the whole of it, so preceding chatter is inert.
|
|
855
|
+
*/
|
|
856
|
+
export function lastLine(stdout) {
|
|
857
|
+
const lines = stdout.split('\n');
|
|
858
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
859
|
+
const t = lines[i].trim();
|
|
860
|
+
if (t)
|
|
861
|
+
return t;
|
|
862
|
+
}
|
|
863
|
+
return '';
|
|
864
|
+
}
|
|
824
865
|
/**
|
|
825
866
|
* Synchronous liveness check: is a broker actually LISTENING and answering (not
|
|
826
867
|
* just a lingering socket file)? Used to decide whether the auto-cache may take
|
|
@@ -834,36 +875,9 @@ export function agentReachableSync() {
|
|
|
834
875
|
return false;
|
|
835
876
|
if (!agentSocketExists())
|
|
836
877
|
return false;
|
|
837
|
-
const r =
|
|
838
|
-
return r.status === 0 && !r.error;
|
|
878
|
+
const r = syncClient([SYNC_PING_CMD], SYNC_PING_TIMEOUT_MS);
|
|
879
|
+
return r !== null && r.status === 0 && !r.error;
|
|
839
880
|
}
|
|
840
|
-
/**
|
|
841
|
-
* Inline node program for the synchronous evict path. Mirrors SYNC_GET_PROGRAM:
|
|
842
|
-
* writeBundle is synchronous and called synchronously everywhere, so a stale
|
|
843
|
-
* broker entry must be evicted without awaiting a socket round-trip. Sends one
|
|
844
|
-
* {cmd:'lock', name} and exits 0 (evicted or nothing held) / 3 (agent down).
|
|
845
|
-
* argv after -e: [execPath, <socket>, <name>].
|
|
846
|
-
*/
|
|
847
|
-
const SYNC_LOCK_PROGRAM = `
|
|
848
|
-
const net = require('net'), fs = require('fs');
|
|
849
|
-
const sock = process.argv[1], name = process.argv[2], tokenPath = process.argv[3];
|
|
850
|
-
let token; try { token = fs.readFileSync(tokenPath, 'utf-8').trim() || undefined; } catch (e) {}
|
|
851
|
-
const c = net.createConnection(sock);
|
|
852
|
-
let buf = '';
|
|
853
|
-
const down = () => { try { c.destroy(); } catch (e) {} process.exit(3); };
|
|
854
|
-
const timer = setTimeout(down, 2000);
|
|
855
|
-
c.on('error', down);
|
|
856
|
-
c.on('connect', () => c.write(JSON.stringify({ cmd: 'lock', name, token }) + '\\n'));
|
|
857
|
-
c.setEncoding('utf-8');
|
|
858
|
-
c.on('data', (d) => {
|
|
859
|
-
buf += d;
|
|
860
|
-
const nl = buf.indexOf('\\n');
|
|
861
|
-
if (nl < 0) return;
|
|
862
|
-
clearTimeout(timer);
|
|
863
|
-
try { c.destroy(); } catch (e) {}
|
|
864
|
-
process.exit(0);
|
|
865
|
-
});
|
|
866
|
-
`;
|
|
867
881
|
/**
|
|
868
882
|
* Synchronously evict one bundle from the broker. Called after a mutating
|
|
869
883
|
* keychain write (add / rotate / remove / rename / delete) so the broker never
|
|
@@ -877,10 +891,60 @@ export function agentEvictSync(name) {
|
|
|
877
891
|
return;
|
|
878
892
|
if (!agentSocketExists())
|
|
879
893
|
return;
|
|
880
|
-
try
|
|
881
|
-
|
|
894
|
+
// No try/catch: syncClient swallows its own failures and returns null.
|
|
895
|
+
syncClient([SYNC_LOCK_CMD, name], SYNC_LOCK_TIMEOUT_MS);
|
|
896
|
+
}
|
|
897
|
+
// ─── Top-level `__secrets-*` sync-client entrypoints ────────────────────────
|
|
898
|
+
// The bodies behind the three spawns above. Each does one socket round-trip and
|
|
899
|
+
// signals the parent through its exit code, mirroring the inline `node -e`
|
|
900
|
+
// programs these replaced: 0 = hit/alive, 3 = miss/down. Lock deviates twice:
|
|
901
|
+
// it reports 0 even when no broker answers, and 3 for an empty name, which it
|
|
902
|
+
// refuses rather than forwarding as a lock-ALL (see runAgentLockSync).
|
|
903
|
+
// Dispatched from index.ts BEFORE commander and before the startup statements,
|
|
904
|
+
// so a cache hit never runs checkForUpdates() or forks a detached sync.
|
|
905
|
+
/** Body of `__secrets-get <name>`. Prints `{bundle, env}` as JSON on a
|
|
906
|
+
* cache hit. Exit 0 = hit, 3 = miss or broker down. */
|
|
907
|
+
export async function runAgentGetSync(name) {
|
|
908
|
+
const r = await request({ cmd: 'get', name }, SOCKET_GET_TIMEOUT_MS);
|
|
909
|
+
if (r?.ok === true && r.cmd === 'get' && r.hit) {
|
|
910
|
+
// Trailing newline: the parent reads the LAST line (see lastLine), so the
|
|
911
|
+
// payload must terminate the stream even if anything ever precedes it.
|
|
912
|
+
//
|
|
913
|
+
// Awaited on the write callback, not fire-and-forget: stdout is a pipe here
|
|
914
|
+
// (the parent captures it), pipe writes are asynchronous, and the caller
|
|
915
|
+
// exits the process the moment this resolves. An unflushed write would be
|
|
916
|
+
// truncated on exit — the parent would see a partial or empty payload, treat
|
|
917
|
+
// it as a miss, and fall through to a keychain read, silently costing the
|
|
918
|
+
// Touch ID prompt this whole path exists to avoid.
|
|
919
|
+
const payload = JSON.stringify({ bundle: r.bundle, env: r.env }) + '\n';
|
|
920
|
+
await new Promise((resolve) => { process.stdout.write(payload, () => resolve()); });
|
|
921
|
+
return 0;
|
|
882
922
|
}
|
|
883
|
-
|
|
923
|
+
return 3;
|
|
924
|
+
}
|
|
925
|
+
/** Body of `__secrets-ping`. Exit 0 = a broker is listening and speaking
|
|
926
|
+
* our protocol, 3 = nothing there. Deliberately does NOT gate on
|
|
927
|
+
* PROTOCOL_VERSION: this only decides whether the auto-cache may take the
|
|
928
|
+
* synchronous warm path, and a version-skewed broker still answers reads. That
|
|
929
|
+
* matches the inline program this replaced. */
|
|
930
|
+
export async function runAgentPingSync() {
|
|
931
|
+
const r = await request({ cmd: 'ping' }, SOCKET_PING_TIMEOUT_MS);
|
|
932
|
+
return r?.ok === true && r.cmd === 'ping' ? 0 : 3;
|
|
933
|
+
}
|
|
934
|
+
/** Body of `__secrets-lock <name>`. Best-effort evict; exit 0 even when no
|
|
935
|
+
* broker answers, so a missing broker never fails the mutating write that
|
|
936
|
+
* triggered it (agentEvictSync discards this either way).
|
|
937
|
+
*
|
|
938
|
+
* An empty name is refused rather than forwarded: the broker treats a nameless
|
|
939
|
+
* lock as lock-ALL (handleAgentRequest), so a bare `__secrets-lock` would wipe
|
|
940
|
+
* every held bundle and re-prompt Touch ID for each. That was unreachable while
|
|
941
|
+
* this was an inline `node -e` string; as a top-level argv token it is one typo
|
|
942
|
+
* away, and agentEvictSync only ever locks by name. */
|
|
943
|
+
export async function runAgentLockSync(name) {
|
|
944
|
+
if (!name)
|
|
945
|
+
return 3;
|
|
946
|
+
await request({ cmd: 'lock', name }, SOCKET_LOCK_TIMEOUT_MS);
|
|
947
|
+
return 0;
|
|
884
948
|
}
|
|
885
949
|
// Key inside the cached entry's env that holds the JSON metadata snapshot.
|
|
886
950
|
const META_SNAPSHOT_KEY = '__snapshot__';
|
|
@@ -114,6 +114,32 @@ export declare function validateSecretType(t: string): asserts t is SecretType;
|
|
|
114
114
|
*/
|
|
115
115
|
export declare function validateExpiresFutureDated(iso: string): void;
|
|
116
116
|
export declare function bundleExists(name: string): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Thrown by `readBundle` for the one state `readBundleIfDecryptable` may treat
|
|
119
|
+
* as "present but permanently unreadable": file-store ciphertext on disk that
|
|
120
|
+
* will not decrypt with the passphrase in effect (lost/rotated key or a tampered
|
|
121
|
+
* store). It is deliberately narrow — a bundle that is merely *locked for this
|
|
122
|
+
* run* (headless macOS without `AGENTS_SECRETS_PASSPHRASE`, or a vault that is
|
|
123
|
+
* not logged in) is recoverable and must not be collapsed into this.
|
|
124
|
+
*/
|
|
125
|
+
export declare class BundleUndecryptableError extends Error {
|
|
126
|
+
constructor(message: string);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Read a bundle, or return null when its metadata is present but genuinely
|
|
130
|
+
* cannot be decrypted — a lost or rotated file-store passphrase, or a tampered
|
|
131
|
+
* file store (signalled by `BundleUndecryptableError`).
|
|
132
|
+
*
|
|
133
|
+
* Deleting such a bundle is the only way out of that state, and deletion needs
|
|
134
|
+
* no plaintext, so it must not be gated behind a successful decrypt. Every other
|
|
135
|
+
* failure rethrows: a genuinely missing bundle ("not found"), and — critically —
|
|
136
|
+
* a bundle that is only *temporarily locked* for this run (headless macOS with no
|
|
137
|
+
* `AGENTS_SECRETS_PASSPHRASE`, or a not-logged-in vault). Collapsing that
|
|
138
|
+
* recoverable "set the env / log in" state into "unreadable, safe to delete"
|
|
139
|
+
* would let `secrets delete <name> --yes` silently destroy a perfectly healthy
|
|
140
|
+
* bundle from a cron/launchd run that merely forgot to export the passphrase.
|
|
141
|
+
*/
|
|
142
|
+
export declare function readBundleIfDecryptable(name: string): SecretsBundle | null;
|
|
117
143
|
export declare function readBundle(name: string): SecretsBundle;
|
|
118
144
|
/** The default prompt policy applied to bundles without an explicit per-bundle
|
|
119
145
|
* policy. Configurable via `secrets.policy` in agents.yaml; `daily` (one Touch
|
|
@@ -147,6 +173,14 @@ export declare function shouldEvictAfterBundleWrite(skipRequested: boolean, noAg
|
|
|
147
173
|
export declare function writeBundle(bundle: SecretsBundle, opts?: WriteBundleOptions): void;
|
|
148
174
|
export declare function writeBundleWithItems(bundle: SecretsBundle, items: Map<string, string>, opts?: WriteBundleOptions): void;
|
|
149
175
|
export declare function deleteBundle(name: string): boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Re-write already-read keychain bundle metadata items WITHOUT the biometry ACL.
|
|
178
|
+
* `metaJsonByName` maps bundle name → the exact metadata JSON listBundles just
|
|
179
|
+
* batch-read, so writing it back only flips the ACL (via the helper's
|
|
180
|
+
* delete-then-add `set-no-acl`) — contents and updated_at are preserved and no
|
|
181
|
+
* extra keychain read is issued. Exported for tests. Returns the count healed.
|
|
182
|
+
*/
|
|
183
|
+
export declare function healKeychainBundleMetadata(metaJsonByName: Map<string, string>): number;
|
|
150
184
|
export declare function listBundles(): SecretsBundle[];
|
|
151
185
|
export interface BundleEntryInfo {
|
|
152
186
|
key: string;
|