@phnx-labs/agents-cli 1.20.76 → 1.20.78
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 +264 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +143 -9
- package/dist/commands/exec.js +87 -19
- package/dist/commands/repo.js +8 -0
- package/dist/commands/routines.js +275 -15
- package/dist/commands/secrets.js +65 -17
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +128 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-picker.d.ts +28 -1
- package/dist/commands/sessions-picker.js +222 -15
- package/dist/commands/sessions.d.ts +117 -4
- package/dist/commands/sessions.js +350 -62
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +48 -3
- package/dist/index.js +5 -6
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/agents.d.ts +0 -21
- package/dist/lib/agents.js +0 -37
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +9 -32
- package/dist/lib/daemon.js +70 -84
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +27 -4
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/git.d.ts +13 -0
- package/dist/lib/git.js +102 -4
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hosts/dispatch.d.ts +9 -0
- package/dist/lib/hosts/dispatch.js +24 -4
- package/dist/lib/hosts/passthrough.js +7 -2
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +33 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +4 -0
- package/dist/lib/hosts/run-target.js +5 -1
- package/dist/lib/hosts/session-index.js +3 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +9 -0
- package/dist/lib/menubar/install-menubar.js +14 -0
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/overdue.d.ts +4 -5
- package/dist/lib/overdue.js +8 -41
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +68 -0
- package/dist/lib/routines.js +74 -7
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +156 -28
- package/dist/lib/sandbox.js +0 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +11 -4
- package/dist/lib/secrets/agent.js +46 -24
- package/dist/lib/secrets/bundles.d.ts +21 -7
- package/dist/lib/secrets/bundles.js +82 -38
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- package/dist/lib/secrets/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +16 -2
- package/dist/lib/secrets/session-store.js +118 -15
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/session/active.d.ts +17 -0
- package/dist/lib/session/active.js +105 -7
- package/dist/lib/session/db.d.ts +43 -0
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +28 -3
- package/dist/lib/session/discover.js +110 -14
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/parse.js +11 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.js +18 -16
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +94 -12
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/state.d.ts +7 -3
- package/dist/lib/state.js +20 -5
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/lib/runner.js
CHANGED
|
@@ -27,8 +27,7 @@ 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,
|
|
31
|
-
import { claudeHomeHasOwnCredential } from './agents.js';
|
|
30
|
+
import { getBinaryPath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
32
31
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, } from './rotate.js';
|
|
33
32
|
import { readAuthHealth, isDeadVerdict } from './auth-health.js';
|
|
34
33
|
import { machineId } from './machine-id.js';
|
|
@@ -399,19 +398,12 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
|
|
|
399
398
|
if (v !== undefined)
|
|
400
399
|
out[k] = v;
|
|
401
400
|
}
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
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
|
-
}
|
|
401
|
+
// No Claude token is injected here. A routine authenticates through the pinned
|
|
402
|
+
// account's own CLAUDE_CONFIG_DIR login (buildExecEnv points it at the
|
|
403
|
+
// per-account version home), identical to interactive `agents run`. Claude
|
|
404
|
+
// Code's interactive session refreshes itself per-device; keeping the daemon
|
|
405
|
+
// out of the credential entirely is what avoids the fleet-wide rotation logout
|
|
406
|
+
// — a shared/rotating token was the cause, not the fix.
|
|
415
407
|
if (timezone)
|
|
416
408
|
out.TZ = timezone;
|
|
417
409
|
return out;
|
|
@@ -512,11 +504,19 @@ export async function executeJob(config, deps) {
|
|
|
512
504
|
if (eligibility) {
|
|
513
505
|
throw new Error(eligibility.message);
|
|
514
506
|
}
|
|
515
|
-
//
|
|
516
|
-
// version selection / sandbox / spawn
|
|
517
|
-
// `routines run`, catchup) follow the
|
|
518
|
-
|
|
519
|
-
|
|
507
|
+
// Placement (hostStrategy / bare host:) — body may run on another machine
|
|
508
|
+
// over SSH or in the cloud; local version selection / sandbox / spawn then
|
|
509
|
+
// do not apply. Sync callers (manual `routines run`, catchup) follow the
|
|
510
|
+
// remote run to completion when possible.
|
|
511
|
+
{
|
|
512
|
+
const { resolvePlacementTarget } = await import('./routines-placement.js');
|
|
513
|
+
const target = resolvePlacementTarget(config);
|
|
514
|
+
if (target.mode === 'host') {
|
|
515
|
+
return executeJobOnHost({ ...config, host: target.host }, { detached: false });
|
|
516
|
+
}
|
|
517
|
+
if (target.mode === 'cloud') {
|
|
518
|
+
return executeJobOnCloud(config, { detached: false });
|
|
519
|
+
}
|
|
520
520
|
}
|
|
521
521
|
// Command-mode: run a plain shell command directly (no agent, no rotation,
|
|
522
522
|
// no pinning, no sandbox overlay). Reuses the run-record machinery so
|
|
@@ -735,6 +735,86 @@ export async function executeJob(config, deps) {
|
|
|
735
735
|
timer.end({ status: 'failed', exitCode: 1, runId });
|
|
736
736
|
return { meta, reportPath: null };
|
|
737
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Dispatch a routine to the agent's native cloud provider (or the configured
|
|
740
|
+
* default). Writes a local run record with `cloudTaskId` so list/runs still
|
|
741
|
+
* work; does not wait for cloud completion on the detached path.
|
|
742
|
+
*/
|
|
743
|
+
async function executeJobOnCloud(config, opts) {
|
|
744
|
+
if (config.workflow) {
|
|
745
|
+
throw new Error(`Routine '${config.name}' runs a workflow bundle, which can't execute in the cloud yet — remove 'hostStrategy: cloud' or 'workflow:'.`);
|
|
746
|
+
}
|
|
747
|
+
if (config.loop) {
|
|
748
|
+
throw new Error(`Routine '${config.name}' uses 'loop:', which can't execute in the cloud yet — remove 'hostStrategy: cloud' or 'loop:'.`);
|
|
749
|
+
}
|
|
750
|
+
if (config.command) {
|
|
751
|
+
throw new Error(`Routine '${config.name}' uses 'command:', which can't execute in the cloud yet — remove 'hostStrategy: cloud' or 'command:'.`);
|
|
752
|
+
}
|
|
753
|
+
if (!config.agent) {
|
|
754
|
+
throw new Error(`Routine '${config.name}' hostStrategy: cloud requires an agent`);
|
|
755
|
+
}
|
|
756
|
+
const { resolveProvider } = await import('./cloud/registry.js');
|
|
757
|
+
const { insertTask } = await import('./cloud/store.js');
|
|
758
|
+
const provider = resolveProvider(undefined, config.agent);
|
|
759
|
+
const timer = createTimer('agent.run', {
|
|
760
|
+
agent: config.agent,
|
|
761
|
+
jobName: config.name,
|
|
762
|
+
mode: config.mode,
|
|
763
|
+
placement: 'cloud',
|
|
764
|
+
...redactPrompt(config.prompt),
|
|
765
|
+
schedule: config.schedule,
|
|
766
|
+
});
|
|
767
|
+
const runId = generateRunId();
|
|
768
|
+
const runDir = getRunDir(config.name, runId);
|
|
769
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
770
|
+
const meta = {
|
|
771
|
+
jobName: config.name,
|
|
772
|
+
runId,
|
|
773
|
+
agent: config.agent,
|
|
774
|
+
pid: null,
|
|
775
|
+
spawnedAt: Date.now(),
|
|
776
|
+
status: 'running',
|
|
777
|
+
startedAt: new Date().toISOString(),
|
|
778
|
+
completedAt: null,
|
|
779
|
+
exitCode: null,
|
|
780
|
+
};
|
|
781
|
+
writeRunMeta(meta);
|
|
782
|
+
try {
|
|
783
|
+
const task = await provider.dispatch({
|
|
784
|
+
prompt: resolveJobPrompt(config),
|
|
785
|
+
agent: config.agent,
|
|
786
|
+
repo: config.repo,
|
|
787
|
+
timeout: config.timeout,
|
|
788
|
+
model: config.config?.model,
|
|
789
|
+
});
|
|
790
|
+
try {
|
|
791
|
+
insertTask(task);
|
|
792
|
+
}
|
|
793
|
+
catch { /* store is best-effort */ }
|
|
794
|
+
meta.cloudTaskId = task.id;
|
|
795
|
+
meta.cloudProvider = task.provider;
|
|
796
|
+
// Terminal cloud responses (e.g. antigravity) finalize immediately.
|
|
797
|
+
// Async providers leave the run `running` with the cloud task id for
|
|
798
|
+
// the user to follow via `agents cloud status`.
|
|
799
|
+
if (task.status === 'completed') {
|
|
800
|
+
finalizeRunMeta(meta, 'completed', 0);
|
|
801
|
+
}
|
|
802
|
+
else if (task.status === 'failed' || task.status === 'cancelled') {
|
|
803
|
+
finalizeRunMeta(meta, 'failed', 1, { errorMessage: task.summary ?? `cloud ${task.status}` });
|
|
804
|
+
}
|
|
805
|
+
// Non-terminal statuses stay `running` for both detached and sync paths;
|
|
806
|
+
// the user follows via `agents cloud status <id>`.
|
|
807
|
+
writeRunMeta(meta);
|
|
808
|
+
timer.end({ status: meta.status, exitCode: meta.exitCode ?? undefined, runId });
|
|
809
|
+
return { meta, reportPath: null };
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
finalizeRunMeta(meta, 'failed', 1, { errorMessage: err.message });
|
|
813
|
+
writeRunMeta(meta);
|
|
814
|
+
timer.end({ status: 'failed', exitCode: 1, runId, error: err.message });
|
|
815
|
+
throw err;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
738
818
|
async function executeJobOnHost(config, opts) {
|
|
739
819
|
if (config.workflow) {
|
|
740
820
|
throw new Error(`Routine '${config.name}' runs a workflow bundle, which can't execute on a host yet — remove 'host:' or 'workflow:'.`);
|
|
@@ -877,24 +957,44 @@ async function executeCommandJobForeground(config) {
|
|
|
877
957
|
});
|
|
878
958
|
return { meta, reportPath: null };
|
|
879
959
|
}
|
|
960
|
+
/** Invoke a lifecycle hook without letting a caller error break run finalization. */
|
|
961
|
+
function safeHook(fn) {
|
|
962
|
+
if (!fn)
|
|
963
|
+
return;
|
|
964
|
+
try {
|
|
965
|
+
fn();
|
|
966
|
+
}
|
|
967
|
+
catch { /* hooks are best-effort */ }
|
|
968
|
+
}
|
|
880
969
|
/** Spawn a job as a detached process and return immediately with run metadata. */
|
|
881
|
-
|
|
882
|
-
export async function executeJobDetached(config) {
|
|
970
|
+
export async function executeJobDetached(config, hooks) {
|
|
883
971
|
const eligibility = checkJobDeviceEligibility(config);
|
|
884
972
|
if (eligibility) {
|
|
885
973
|
process.stderr.write(`[agents] daemon: skipping '${config.name}' — ${eligibility.message}\n`);
|
|
886
974
|
throw new Error(eligibility.message);
|
|
887
975
|
}
|
|
888
|
-
//
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
976
|
+
// Placement (hostStrategy / bare host:) — dispatch off-box and return; the
|
|
977
|
+
// monitor finalizes host: runs, cloud runs stay terminal when dispatch ends.
|
|
978
|
+
// Either way the in-process onFinish hook does not fire for off-box routines
|
|
979
|
+
// (the monitor tick observes an already-finalized record), so notify-desktop
|
|
980
|
+
// sends the finish notification only for local detached runs below (RUSH-2030).
|
|
981
|
+
{
|
|
982
|
+
const { resolvePlacementTarget } = await import('./routines-placement.js');
|
|
983
|
+
const target = resolvePlacementTarget(config);
|
|
984
|
+
if (target.mode === 'host') {
|
|
985
|
+
const { meta } = await executeJobOnHost({ ...config, host: target.host }, { detached: true });
|
|
986
|
+
return meta;
|
|
987
|
+
}
|
|
988
|
+
if (target.mode === 'cloud') {
|
|
989
|
+
const { meta } = await executeJobOnCloud(config, { detached: true });
|
|
990
|
+
return meta;
|
|
991
|
+
}
|
|
892
992
|
}
|
|
893
993
|
// Command-mode: fire a plain shell command detached (no agent, no rotation,
|
|
894
994
|
// no pinning, no sandbox overlay). Still writes a run record so the daemon,
|
|
895
995
|
// list/runs, and overdue tracking keep working.
|
|
896
996
|
if (config.command) {
|
|
897
|
-
return executeCommandJobDetached(config);
|
|
997
|
+
return executeCommandJobDetached(config, hooks);
|
|
898
998
|
}
|
|
899
999
|
// Pre-flight: pick a healthy version/account so the daemon does not launch
|
|
900
1000
|
// into a credit-exhausted install. Detached cannot mid-run failover (no exit
|
|
@@ -944,6 +1044,28 @@ export async function executeJobDetached(config) {
|
|
|
944
1044
|
completedAt: null,
|
|
945
1045
|
exitCode: null,
|
|
946
1046
|
};
|
|
1047
|
+
// Auth preflight (mirrors executeJob): with no injected token, a daemon-fired
|
|
1048
|
+
// Claude routine authenticates via the pinned account's own CLAUDE_CONFIG_DIR
|
|
1049
|
+
// login. If the last live probe rejected that (agent, version)'s token, the run
|
|
1050
|
+
// is guaranteed to 401 — fail fast with a re-login hint instead of spawning a
|
|
1051
|
+
// doomed run + poisoned report. Cache-only; fails OPEN on any non-dead/missing
|
|
1052
|
+
// verdict, and agents with no live probe (codex/gemini/grok) are never blocked.
|
|
1053
|
+
const preflightVersion = launch.chain[0]?.version;
|
|
1054
|
+
if (preflightVersion) {
|
|
1055
|
+
const health = readAuthHealth(machineId(), effectiveAgent, preflightVersion);
|
|
1056
|
+
if (health && isDeadVerdict(health.verdict)) {
|
|
1057
|
+
const reason = `auth_preflight: ${health.verdict}`;
|
|
1058
|
+
process.stderr.write(`[agents] routine ${config.name}: ${effectiveAgent}@${preflightVersion} token ${health.verdict} — skipping run (re-login required)\n`);
|
|
1059
|
+
try {
|
|
1060
|
+
fs.closeSync(stdoutFd);
|
|
1061
|
+
}
|
|
1062
|
+
catch { /* already closed */ }
|
|
1063
|
+
finalizeRunMeta(meta, 'failed', 1, { errorMessage: reason });
|
|
1064
|
+
writeRunMeta(meta);
|
|
1065
|
+
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1066
|
+
return meta;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
947
1069
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
948
1070
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
949
1071
|
...backgroundSpawnOptions({ fdStdio: true }),
|
|
@@ -962,6 +1084,9 @@ export async function executeJobDetached(config) {
|
|
|
962
1084
|
const isAuthFailure = !!errorMessage && errorMessage.startsWith('auth_failed:');
|
|
963
1085
|
if (status !== 'timeout' && !isAuthFailure)
|
|
964
1086
|
extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
1087
|
+
// Fire the finish/output notification AFTER the report is written so the hook
|
|
1088
|
+
// can read report.md (RUSH-2030). Best-effort; never breaks finalization.
|
|
1089
|
+
safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
|
|
965
1090
|
};
|
|
966
1091
|
child.on('exit', (code) => {
|
|
967
1092
|
let logText = '';
|
|
@@ -1008,7 +1133,7 @@ export async function executeJobDetached(config) {
|
|
|
1008
1133
|
* un-sandboxed, unref, then record the pid. The daemon does not wait for exit;
|
|
1009
1134
|
* `monitorRunningJobs` reaps the record on the next tick.
|
|
1010
1135
|
*/
|
|
1011
|
-
function executeCommandJobDetached(config) {
|
|
1136
|
+
function executeCommandJobDetached(config, hooks) {
|
|
1012
1137
|
const runId = generateRunId();
|
|
1013
1138
|
const runDir = getRunDir(config.name, runId);
|
|
1014
1139
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -1053,6 +1178,9 @@ function executeCommandJobDetached(config) {
|
|
|
1053
1178
|
settled = true;
|
|
1054
1179
|
finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
|
|
1055
1180
|
writeRunMeta(meta);
|
|
1181
|
+
// Finish notification (RUSH-2030). For command routines the threshold only
|
|
1182
|
+
// surfaces failures, decided in routine-notify.ts. Best-effort.
|
|
1183
|
+
safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
|
|
1056
1184
|
};
|
|
1057
1185
|
child.on('exit', (code) => settle(code === 0 ? 'completed' : 'failed', code ?? 1));
|
|
1058
1186
|
child.on('error', (err) => {
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -44,12 +44,6 @@ const ENV_ALLOWLIST = [
|
|
|
44
44
|
'VISUAL',
|
|
45
45
|
'NO_COLOR',
|
|
46
46
|
'FORCE_COLOR',
|
|
47
|
-
// Headless Claude auth: the routines daemon injects CLAUDE_CODE_OAUTH_TOKEN
|
|
48
|
-
// from the `claude` secrets bundle (see daemon.ts). Without this allowlist
|
|
49
|
-
// entry, sandboxed routine spawns drop the token and look "unconfigured".
|
|
50
|
-
// Intentionally NOT ANTHROPIC_API_KEY / other provider secrets — those stay
|
|
51
|
-
// stripped (see tests/sandbox.test.ts "does not include sensitive env vars").
|
|
52
|
-
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
53
47
|
];
|
|
54
48
|
/** Tools safe to grant as wildcards (no filesystem access). */
|
|
55
49
|
const SAFE_TOOLS = {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -3,9 +3,21 @@
|
|
|
3
3
|
<plist version="1.0">
|
|
4
4
|
<dict>
|
|
5
5
|
<key>files</key>
|
|
6
|
-
<dict
|
|
6
|
+
<dict>
|
|
7
|
+
<key>Resources/AppIcon.icns</key>
|
|
8
|
+
<data>
|
|
9
|
+
Vd5nfogg74zSRNspluv4uvDecUA=
|
|
10
|
+
</data>
|
|
11
|
+
</dict>
|
|
7
12
|
<key>files2</key>
|
|
8
13
|
<dict>
|
|
14
|
+
<key>Resources/AppIcon.icns</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>hash2</key>
|
|
17
|
+
<data>
|
|
18
|
+
DznVe0VgYOux7+B/aiHNgGYLCvSJKzgXDTPJn2jNkNg=
|
|
19
|
+
</data>
|
|
20
|
+
</dict>
|
|
9
21
|
<key>embedded.provisionprofile</key>
|
|
10
22
|
<dict>
|
|
11
23
|
<key>hash2</key>
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import * as net from 'net';
|
|
27
27
|
import type { SecretsBundle } from './bundles.js';
|
|
28
|
+
import { GLOBAL_HARNESS, bundleScopeChain } from './scope.js';
|
|
29
|
+
export { GLOBAL_HARNESS, bundleScopeChain };
|
|
28
30
|
/** Default lifetime of an unlocked bundle when `--ttl` is not given. */
|
|
29
31
|
export declare const DEFAULT_TTL_MS: number;
|
|
30
32
|
/**
|
|
@@ -69,12 +71,14 @@ export interface StoredBundle {
|
|
|
69
71
|
env: Record<string, string>;
|
|
70
72
|
/** epoch ms; the entry is gone once Date.now() passes this. */
|
|
71
73
|
expiresAt: number;
|
|
74
|
+
harness: string;
|
|
72
75
|
}
|
|
73
76
|
/** One unlocked bundle as reported by `status`. */
|
|
74
77
|
export interface AgentStatusEntry {
|
|
75
78
|
name: string;
|
|
76
79
|
expiresAt: number;
|
|
77
80
|
keyCount: number;
|
|
81
|
+
harness: string;
|
|
78
82
|
}
|
|
79
83
|
/**
|
|
80
84
|
* Read the current broker capability token, or null if none is present. Clients
|
|
@@ -106,10 +110,12 @@ export type Request = {
|
|
|
106
110
|
} | {
|
|
107
111
|
cmd: 'get';
|
|
108
112
|
name: string;
|
|
113
|
+
harness?: string;
|
|
109
114
|
token?: string;
|
|
110
115
|
} | {
|
|
111
116
|
cmd: 'load';
|
|
112
117
|
name: string;
|
|
118
|
+
harness?: string;
|
|
113
119
|
bundle: SecretsBundle;
|
|
114
120
|
env: Record<string, string>;
|
|
115
121
|
ttlMs: number;
|
|
@@ -166,6 +172,7 @@ export type Response = {
|
|
|
166
172
|
* exiting. Pure + exported for unit testing.
|
|
167
173
|
*/
|
|
168
174
|
export declare function realBundleCount(store: Map<string, StoredBundle>): number;
|
|
175
|
+
export declare function scopedBundleKey(name: string, harness: string): string;
|
|
169
176
|
export declare function handleAgentRequest(store: Map<string, StoredBundle>, req: Request, now?: number): Response;
|
|
170
177
|
/**
|
|
171
178
|
* Decide whether a `watch-lock` helper line should wipe the in-memory store.
|
|
@@ -270,7 +277,7 @@ export declare function syncClientLaunch(sub: string[], agentsBin?: string): {
|
|
|
270
277
|
* null if the agent isn't running / doesn't hold this bundle / anything fails
|
|
271
278
|
* (soft — caller falls through to the real keychain). macOS only.
|
|
272
279
|
*/
|
|
273
|
-
export declare function agentGetSync(name: string): {
|
|
280
|
+
export declare function agentGetSync(name: string, harness?: string): {
|
|
274
281
|
bundle: SecretsBundle;
|
|
275
282
|
env: Record<string, string>;
|
|
276
283
|
} | null;
|
|
@@ -308,7 +315,7 @@ export declare function agentReachableSync(): boolean;
|
|
|
308
315
|
export declare function agentEvictSync(name: string): void;
|
|
309
316
|
/** Body of `__secrets-get <name>`. Prints `{bundle, env}` as JSON on a
|
|
310
317
|
* cache hit. Exit 0 = hit, 3 = miss or broker down. */
|
|
311
|
-
export declare function runAgentGetSync(name: string): Promise<number>;
|
|
318
|
+
export declare function runAgentGetSync(name: string, harness?: string): Promise<number>;
|
|
312
319
|
/** Body of `__secrets-ping`. Exit 0 = a broker is listening and speaking
|
|
313
320
|
* our protocol, 3 = nothing there. Deliberately does NOT gate on
|
|
314
321
|
* PROTOCOL_VERSION: this only decides whether the auto-cache may take the
|
|
@@ -387,7 +394,7 @@ export declare function clampHoldMs(v: unknown): number;
|
|
|
387
394
|
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping) rather
|
|
388
395
|
* than a tight inline retry loop. Best-effort; never throws. macOS only.
|
|
389
396
|
*/
|
|
390
|
-
export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number): void;
|
|
397
|
+
export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number, harness?: string): void;
|
|
391
398
|
/**
|
|
392
399
|
* Body of the hidden `secrets _agent-load` worker. Reads one `{name, bundle,
|
|
393
400
|
* env, ttlMs}` payload from stdin, ensures the broker is up (robust, generous
|
|
@@ -402,7 +409,7 @@ export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, e
|
|
|
402
409
|
*/
|
|
403
410
|
export declare function runAgentLoadFromStdin(): Promise<void>;
|
|
404
411
|
/** Store a resolved bundle in the broker. Returns false on transport failure. */
|
|
405
|
-
export declare function agentLoad(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number): Promise<boolean>;
|
|
412
|
+
export declare function agentLoad(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number, harness?: string): Promise<boolean>;
|
|
406
413
|
/** Wipe one bundle (or all if name omitted) from the broker. Returns the count
|
|
407
414
|
* wiped, or 0 when no broker is running. */
|
|
408
415
|
export declare function agentLock(name?: string): Promise<number>;
|
|
@@ -34,11 +34,17 @@ import { isAlive } from '../platform/process.js';
|
|
|
34
34
|
import { getKeychainHelperPath } from './install-helper.js';
|
|
35
35
|
import { getCliVersion, getCliVersionFresh } from '../version.js';
|
|
36
36
|
import { getCliLaunch } from '../cli-entry.js';
|
|
37
|
+
import { GLOBAL_HARNESS, bundleScopeChain } from './scope.js';
|
|
37
38
|
import { rehydrateSessions, pruneSessionsOnSleep } from './session-store.js';
|
|
38
39
|
import { SYNC_GET_CMD, SYNC_PING_CMD, SYNC_LOCK_CMD } from './sync-commands.js';
|
|
40
|
+
// Re-exported so callers already reaching for agent.js keep one obvious home for
|
|
41
|
+
// the scope vocabulary; the definitions live in the leaf module scope.ts because
|
|
42
|
+
// agent.ts and session-store.ts import each other and a cyclic `const` read can
|
|
43
|
+
// hit the ESM temporal dead zone at runtime.
|
|
44
|
+
export { GLOBAL_HARNESS, bundleScopeChain };
|
|
39
45
|
/** Bumped when the wire protocol changes; a client that pings a mismatched
|
|
40
46
|
* server kills and respawns it rather than talking a stale dialect. */
|
|
41
|
-
const PROTOCOL_VERSION =
|
|
47
|
+
const PROTOCOL_VERSION = 2;
|
|
42
48
|
/** Default lifetime of an unlocked bundle when `--ttl` is not given. */
|
|
43
49
|
export const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7d
|
|
44
50
|
/**
|
|
@@ -133,7 +139,8 @@ function onDarwin() {
|
|
|
133
139
|
function rehydrateStore(now = Date.now()) {
|
|
134
140
|
const store = new Map();
|
|
135
141
|
for (const { name, entry } of rehydrateSessions(now)) {
|
|
136
|
-
|
|
142
|
+
const harness = entry.harness || GLOBAL_HARNESS;
|
|
143
|
+
store.set(scopedBundleKey(name, harness), { bundle: entry.bundle, env: entry.env, expiresAt: entry.expiresAt, harness });
|
|
137
144
|
}
|
|
138
145
|
return store;
|
|
139
146
|
}
|
|
@@ -291,11 +298,14 @@ export async function uninstallSecretsAgentService() {
|
|
|
291
298
|
*/
|
|
292
299
|
export function realBundleCount(store) {
|
|
293
300
|
let n = 0;
|
|
294
|
-
for (const
|
|
295
|
-
if (!name.startsWith(META_CACHE_PREFIX))
|
|
301
|
+
for (const e of store.values())
|
|
302
|
+
if (!e.bundle.name.startsWith(META_CACHE_PREFIX))
|
|
296
303
|
n++;
|
|
297
304
|
return n;
|
|
298
305
|
}
|
|
306
|
+
export function scopedBundleKey(name, harness) {
|
|
307
|
+
return `${harness}:${name}`;
|
|
308
|
+
}
|
|
299
309
|
export function handleAgentRequest(store, req, now = Date.now()) {
|
|
300
310
|
switch (req.cmd) {
|
|
301
311
|
case 'ping':
|
|
@@ -305,20 +315,32 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
305
315
|
// the broker is running pre-upgrade code and should be restarted.
|
|
306
316
|
return { ok: true, cmd: 'ping', version: PROTOCOL_VERSION, cliVersion: getCliVersion() };
|
|
307
317
|
case 'get': {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
318
|
+
// Walk own-harness → global so a `--for` grant wins over a global one and
|
|
319
|
+
// an unscoped unlock serves every harness (bundleScopeChain).
|
|
320
|
+
for (const scope of bundleScopeChain(req.harness)) {
|
|
321
|
+
const key = scopedBundleKey(req.name, scope);
|
|
322
|
+
const e = store.get(key);
|
|
323
|
+
if (!e)
|
|
324
|
+
continue;
|
|
325
|
+
if (now >= e.expiresAt) {
|
|
326
|
+
store.delete(key);
|
|
327
|
+
continue;
|
|
328
|
+
} // drop expired on read
|
|
329
|
+
return { ok: true, cmd: 'get', hit: true, bundle: e.bundle, env: e.env };
|
|
313
330
|
}
|
|
314
|
-
return { ok: true, cmd: 'get', hit:
|
|
331
|
+
return { ok: true, cmd: 'get', hit: false };
|
|
315
332
|
}
|
|
316
333
|
case 'load':
|
|
317
|
-
|
|
334
|
+
const harness = req.harness || GLOBAL_HARNESS;
|
|
335
|
+
store.set(scopedBundleKey(req.name, harness), { bundle: req.bundle, env: req.env, expiresAt: now + req.ttlMs, harness });
|
|
318
336
|
return { ok: true, cmd: 'load' };
|
|
319
337
|
case 'lock': {
|
|
320
338
|
if (req.name) {
|
|
321
|
-
|
|
339
|
+
let wiped = 0;
|
|
340
|
+
for (const [key, entry] of store)
|
|
341
|
+
if (entry.bundle.name === req.name && store.delete(key))
|
|
342
|
+
wiped++;
|
|
343
|
+
return { ok: true, cmd: 'lock', wiped };
|
|
322
344
|
}
|
|
323
345
|
const wiped = store.size;
|
|
324
346
|
store.clear();
|
|
@@ -329,9 +351,9 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
329
351
|
for (const [name, e] of store) {
|
|
330
352
|
if (now >= e.expiresAt)
|
|
331
353
|
continue;
|
|
332
|
-
if (name.startsWith(META_CACHE_PREFIX))
|
|
333
|
-
continue; // internal list cache
|
|
334
|
-
entries.push({ name, expiresAt: e.expiresAt, keyCount: Object.keys(e.env).length });
|
|
354
|
+
if (e.bundle.name.startsWith(META_CACHE_PREFIX))
|
|
355
|
+
continue; // internal list cache
|
|
356
|
+
entries.push({ name: e.bundle.name, expiresAt: e.expiresAt, keyCount: Object.keys(e.env).length, harness: e.harness });
|
|
335
357
|
}
|
|
336
358
|
return { ok: true, cmd: 'status', entries };
|
|
337
359
|
}
|
|
@@ -824,10 +846,10 @@ function syncClient(sub, timeout) {
|
|
|
824
846
|
* null if the agent isn't running / doesn't hold this bundle / anything fails
|
|
825
847
|
* (soft — caller falls through to the real keychain). macOS only.
|
|
826
848
|
*/
|
|
827
|
-
export function agentGetSync(name) {
|
|
849
|
+
export function agentGetSync(name, harness = GLOBAL_HARNESS) {
|
|
828
850
|
if (!agentSocketExists())
|
|
829
851
|
return null;
|
|
830
|
-
const r = syncClient([SYNC_GET_CMD, name], SYNC_GET_TIMEOUT_MS);
|
|
852
|
+
const r = syncClient([SYNC_GET_CMD, name, harness], SYNC_GET_TIMEOUT_MS);
|
|
831
853
|
if (!r || r.status !== 0 || !r.stdout)
|
|
832
854
|
return null;
|
|
833
855
|
try {
|
|
@@ -904,8 +926,8 @@ export function agentEvictSync(name) {
|
|
|
904
926
|
// so a cache hit never runs checkForUpdates() or forks a detached sync.
|
|
905
927
|
/** Body of `__secrets-get <name>`. Prints `{bundle, env}` as JSON on a
|
|
906
928
|
* 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);
|
|
929
|
+
export async function runAgentGetSync(name, harness = GLOBAL_HARNESS) {
|
|
930
|
+
const r = await request({ cmd: 'get', name, harness }, SOCKET_GET_TIMEOUT_MS);
|
|
909
931
|
if (r?.ok === true && r.cmd === 'get' && r.hit) {
|
|
910
932
|
// Trailing newline: the parent reads the LAST line (see lastLine), so the
|
|
911
933
|
// payload must terminate the stream even if anything ever precedes it.
|
|
@@ -1056,10 +1078,10 @@ export function clampHoldMs(v) {
|
|
|
1056
1078
|
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping) rather
|
|
1057
1079
|
* than a tight inline retry loop. Best-effort; never throws. macOS only.
|
|
1058
1080
|
*/
|
|
1059
|
-
export function agentAutoLoadSync(name, bundle, env, ttlMs) {
|
|
1081
|
+
export function agentAutoLoadSync(name, bundle, env, ttlMs, harness = GLOBAL_HARNESS) {
|
|
1060
1082
|
if (!onDarwin())
|
|
1061
1083
|
return;
|
|
1062
|
-
const payload = JSON.stringify({ name, bundle, env, ttlMs });
|
|
1084
|
+
const payload = JSON.stringify({ name, bundle, env, ttlMs, harness });
|
|
1063
1085
|
// Broker actually LISTENING → deterministic synchronous warm (bounded; the read
|
|
1064
1086
|
// already paid a Touch ID, so <1s here is invisible). We gate on a real liveness
|
|
1065
1087
|
// ping, NOT mere socket-file existence: a broker that died leaving its socket
|
|
@@ -1126,13 +1148,13 @@ export async function runAgentLoadFromStdin() {
|
|
|
1126
1148
|
process.exitCode = 1; // broker couldn't be brought up — did NOT load
|
|
1127
1149
|
return;
|
|
1128
1150
|
}
|
|
1129
|
-
const loaded = await agentLoad(payload.name, payload.bundle, payload.env, payload.ttlMs ?? DEFAULT_TTL_MS);
|
|
1151
|
+
const loaded = await agentLoad(payload.name, payload.bundle, payload.env, payload.ttlMs ?? DEFAULT_TTL_MS, payload.harness ?? GLOBAL_HARNESS);
|
|
1130
1152
|
if (!loaded)
|
|
1131
1153
|
process.exitCode = 1; // transport failed — did NOT load
|
|
1132
1154
|
}
|
|
1133
1155
|
/** Store a resolved bundle in the broker. Returns false on transport failure. */
|
|
1134
|
-
export async function agentLoad(name, bundle, env, ttlMs) {
|
|
1135
|
-
const r = await request({ cmd: 'load', name, bundle, env, ttlMs });
|
|
1156
|
+
export async function agentLoad(name, bundle, env, ttlMs, harness = GLOBAL_HARNESS) {
|
|
1157
|
+
const r = await request({ cmd: 'load', name, bundle, env, ttlMs, harness });
|
|
1136
1158
|
return r?.ok === true && r.cmd === 'load';
|
|
1137
1159
|
}
|
|
1138
1160
|
/** Wipe one bundle (or all if name omitted) from the broker. Returns the count
|
|
@@ -198,6 +198,12 @@ export interface ResolveBundleOptions {
|
|
|
198
198
|
* about to read the bundle.
|
|
199
199
|
*/
|
|
200
200
|
caller?: string;
|
|
201
|
+
/** Harness type whose unlock may be reused (claude, codex, kimi, ...). */
|
|
202
|
+
agent?: string;
|
|
203
|
+
/** Human duration rendered in the Touch ID prompt. */
|
|
204
|
+
duration?: string;
|
|
205
|
+
/** Explicitly permit this agent request to raise interactive authentication. */
|
|
206
|
+
interactiveUnlock?: boolean;
|
|
201
207
|
/**
|
|
202
208
|
* Skip the secrets-agent fast-path and read straight from the keychain
|
|
203
209
|
* (popping Touch ID). Set by callers that must NOT serve a cached snapshot —
|
|
@@ -272,15 +278,19 @@ export declare function resolveBundleEnv(bundle: SecretsBundle, _opts?: ResolveB
|
|
|
272
278
|
* True when the current process is a background / non-interactive context that
|
|
273
279
|
* must NEVER raise a Keychain biometry prompt on the interactive user's screen —
|
|
274
280
|
* a prompt nobody is watching. Two signals, either sufficient:
|
|
275
|
-
* - `AGENTS_RUNTIME` is `headless` or `
|
|
276
|
-
*
|
|
277
|
-
*
|
|
281
|
+
* - `AGENTS_RUNTIME` is `headless`, `teams`, or `terminal` — i.e. ANY agent
|
|
282
|
+
* launch, interactive included, and inherited by everything spawned beneath
|
|
283
|
+
* one (set on the child env by `agents run --headless`, scheduled routines,
|
|
284
|
+
* teammates, and interactive runs — see exec.ts:430, runner.ts,
|
|
285
|
+
* teams/agents.ts).
|
|
278
286
|
* - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
|
|
279
287
|
* stdio is redirected to a log — e.g. a release script run in the
|
|
280
288
|
* background as `( ... ) >log 2>&1 </dev/null`).
|
|
281
289
|
* `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
|
|
282
|
-
* even in a non-TTY context. An
|
|
283
|
-
*
|
|
290
|
+
* even in a non-TTY context. An `eval "$(agents secrets export X)"` typed in a
|
|
291
|
+
* PLAIN shell has no AGENTS_RUNTIME, so it is not classified headless and still
|
|
292
|
+
* prompts. Run beneath an agent it inherits AGENTS_RUNTIME and resolves
|
|
293
|
+
* broker-only — the agent, not the human, is the caller there.
|
|
284
294
|
*
|
|
285
295
|
* Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
|
|
286
296
|
* broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
|
|
@@ -291,9 +301,12 @@ export declare function resolveBundleEnv(bundle: SecretsBundle, _opts?: ResolveB
|
|
|
291
301
|
*
|
|
292
302
|
* A read in a macOS headless context resolves broker-only (agentOnly) and fails
|
|
293
303
|
* fast with an actionable error instead of hijacking Touch ID. This generalizes
|
|
294
|
-
* the per-caller pattern
|
|
304
|
+
* the per-caller broker-only pattern used across the headless secrets readers.
|
|
295
305
|
*/
|
|
296
|
-
export declare function isHeadlessSecretsContext(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform
|
|
306
|
+
export declare function isHeadlessSecretsContext(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, tty?: {
|
|
307
|
+
stdin?: boolean;
|
|
308
|
+
stdout?: boolean;
|
|
309
|
+
}): boolean;
|
|
297
310
|
/**
|
|
298
311
|
* Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
|
|
299
312
|
*
|
|
@@ -311,6 +324,7 @@ export declare function readAndResolveBundleEnv(name: string, opts?: ResolveBund
|
|
|
311
324
|
bundle: SecretsBundle;
|
|
312
325
|
env: Record<string, string>;
|
|
313
326
|
};
|
|
327
|
+
export declare function humanUnlockDuration(ms: number): string;
|
|
314
328
|
export declare function keychainRef(key: string): string;
|
|
315
329
|
/** Options for rotateBundleSecret. */
|
|
316
330
|
export interface RotateOptions {
|