@phnx-labs/agents-cli 1.20.63 → 1.20.65
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 +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
package/dist/lib/runner.js
CHANGED
|
@@ -23,6 +23,8 @@ import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
|
|
|
23
23
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
24
24
|
import { createTimer, maybeRotate, redactPrompt } from './events.js';
|
|
25
25
|
import { normalizeMode, buildExecEnv, detectRateLimit, } from './exec.js';
|
|
26
|
+
import { loadTask as loadHostTask } from './hosts/tasks.js';
|
|
27
|
+
import { reconcileTask as reconcileHostTask } from './hosts/reconcile.js';
|
|
26
28
|
import { backgroundSpawnOptions } from './platform/process.js';
|
|
27
29
|
import { getBinaryPath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
28
30
|
import { getConfiguredRunStrategy, resolveRunVersion, rotationFailoverChain, readinessFromCandidate, } from './rotate.js';
|
|
@@ -44,17 +46,20 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
44
46
|
const cmd = ['agents', 'run', config.workflow, resolvedPrompt, '--mode', config.mode];
|
|
45
47
|
return cmd;
|
|
46
48
|
}
|
|
49
|
+
// Past the workflow branch this is an agent (or resume) job — command jobs never
|
|
50
|
+
// reach buildJobCommand (execute*Job branches out first), and validateJob guarantees agent.
|
|
51
|
+
const agent = config.agent;
|
|
47
52
|
// Resume branch: reopen an EXISTING session via `agents run <agent> --resume <id>`
|
|
48
53
|
// instead of starting fresh. The real session resumes with its full prior context
|
|
49
54
|
// (index-based lookup, cwd-independent) and `resolvedPrompt` becomes its next turn —
|
|
50
55
|
// so a self-scheduled wake (e.g. /hibernate) is handled by the session that scheduled
|
|
51
56
|
// it, not a fresh, context-less agent that would refuse an "opaque" instruction.
|
|
52
57
|
if (config.resume) {
|
|
53
|
-
return ['agents', 'run',
|
|
58
|
+
return ['agents', 'run', agent, '--resume', config.resume, resolvedPrompt, '--mode', config.mode];
|
|
54
59
|
}
|
|
55
|
-
const template = AGENT_COMMANDS[
|
|
60
|
+
const template = AGENT_COMMANDS[agent];
|
|
56
61
|
if (!template) {
|
|
57
|
-
throw new Error(`Unsupported agent for daemon jobs: ${
|
|
62
|
+
throw new Error(`Unsupported agent for daemon jobs: ${agent}`);
|
|
58
63
|
}
|
|
59
64
|
let cmd = template.map((part) => part.replace('{prompt}', resolvedPrompt));
|
|
60
65
|
// Canonicalize mode (accepts legacy `full` as alias for `skip`).
|
|
@@ -155,10 +160,12 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
155
160
|
* Reasoning level (config.config.reasoning) maps to per-agent flags via models.ts.
|
|
156
161
|
*/
|
|
157
162
|
function appendModelAndReasoning(cmd, config) {
|
|
163
|
+
// Only called from buildJobCommand's agent path — config.agent is set.
|
|
164
|
+
const agent = config.agent;
|
|
158
165
|
const model = config.config?.model;
|
|
159
166
|
if (model) {
|
|
160
167
|
if (config.version) {
|
|
161
|
-
const resolved = resolveModel(
|
|
168
|
+
const resolved = resolveModel(agent, config.version, model);
|
|
162
169
|
if (resolved.warning) {
|
|
163
170
|
process.stderr.write(`[agents] ${resolved.warning}\n`);
|
|
164
171
|
}
|
|
@@ -170,7 +177,7 @@ function appendModelAndReasoning(cmd, config) {
|
|
|
170
177
|
}
|
|
171
178
|
const reasoning = config.config?.reasoning;
|
|
172
179
|
if (reasoning) {
|
|
173
|
-
const flags = buildReasoningFlags(
|
|
180
|
+
const flags = buildReasoningFlags(agent, reasoning);
|
|
174
181
|
if (flags.length > 0)
|
|
175
182
|
cmd.push(...flags);
|
|
176
183
|
}
|
|
@@ -178,6 +185,48 @@ function appendModelAndReasoning(cmd, config) {
|
|
|
178
185
|
function generateRunId() {
|
|
179
186
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
180
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Build the argv for a command-mode routine: run the shell string directly
|
|
190
|
+
* through the platform shell. No agent binary, no rotation, no sandbox.
|
|
191
|
+
*/
|
|
192
|
+
function buildShellCommand(command) {
|
|
193
|
+
return process.platform === 'win32'
|
|
194
|
+
? ['cmd', '/c', command]
|
|
195
|
+
: ['/bin/sh', '-c', command];
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Real (un-sandboxed) environment for a command routine. Command routines do
|
|
199
|
+
* `npm i -g` / `git pull` and need the actual $HOME / $PATH, not the sandbox
|
|
200
|
+
* overlay. Only TZ is injected when the routine pins a timezone.
|
|
201
|
+
*/
|
|
202
|
+
function commandSpawnEnv(config) {
|
|
203
|
+
const env = { ...process.env };
|
|
204
|
+
if (config.timezone)
|
|
205
|
+
env.TZ = config.timezone;
|
|
206
|
+
return env;
|
|
207
|
+
}
|
|
208
|
+
/** POSIX single-quote a string so it is safe to embed in a `/bin/sh -c` script. */
|
|
209
|
+
function shSingleQuote(s) {
|
|
210
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Detached command routines write their own exit code to `<runDir>/exit-code`
|
|
214
|
+
* (see the wrapper in executeCommandJobDetached). `monitorRunningJobs` reads it
|
|
215
|
+
* to recover the true terminal status when the daemon restarted between spawn and
|
|
216
|
+
* exit and so missed the in-process `child.on('exit')`. Returns null when the
|
|
217
|
+
* file is absent/unparseable (child killed or crashed before writing it).
|
|
218
|
+
*/
|
|
219
|
+
function readCommandExitCode(runDir) {
|
|
220
|
+
try {
|
|
221
|
+
const raw = fs.readFileSync(path.join(runDir, 'exit-code'), 'utf-8').trim();
|
|
222
|
+
if (!/^-?\d+$/.test(raw))
|
|
223
|
+
return null;
|
|
224
|
+
return parseInt(raw, 10);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
181
230
|
/**
|
|
182
231
|
* Resolve the version/account chain for a routine the same way `agents run`
|
|
183
232
|
* does: honor an explicit `version:` pin; otherwise use the configured run
|
|
@@ -191,6 +240,8 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd()) {
|
|
|
191
240
|
if (config.workflow) {
|
|
192
241
|
return { chain: [], rotation: null, pinned: false };
|
|
193
242
|
}
|
|
243
|
+
// resolveRoutineLaunch is only called for agent jobs (workflow returns above;
|
|
244
|
+
// command jobs branch out of execute*Job before reaching this).
|
|
194
245
|
const agent = config.agent;
|
|
195
246
|
if (config.version) {
|
|
196
247
|
const version = config.version;
|
|
@@ -399,6 +450,18 @@ export async function executeJob(config, deps) {
|
|
|
399
450
|
if (eligibility) {
|
|
400
451
|
throw new Error(eligibility.message);
|
|
401
452
|
}
|
|
453
|
+
// `host:` placement — the job body runs on another machine over SSH; local
|
|
454
|
+
// version selection / sandbox / spawn do not apply. Sync callers (manual
|
|
455
|
+
// `routines run`, catchup) follow the remote run to completion.
|
|
456
|
+
if (config.host) {
|
|
457
|
+
return executeJobOnHost(config, { detached: false });
|
|
458
|
+
}
|
|
459
|
+
// Command-mode: run a plain shell command directly (no agent, no rotation,
|
|
460
|
+
// no pinning, no sandbox overlay). Reuses the run-record machinery so
|
|
461
|
+
// list/runs/overdue keep working.
|
|
462
|
+
if (config.command) {
|
|
463
|
+
return executeCommandJobForeground(config);
|
|
464
|
+
}
|
|
402
465
|
maybeRotate();
|
|
403
466
|
const launch = await resolveRoutineLaunch(config);
|
|
404
467
|
const primaryVersion = launch.chain[0]?.version ?? config.version;
|
|
@@ -425,6 +488,7 @@ export async function executeJob(config, deps) {
|
|
|
425
488
|
: { ...process.env };
|
|
426
489
|
// Workflows run via `agents run <workflow>` which delegates to claude under the hood.
|
|
427
490
|
// Use 'claude' as the effective agent for report extraction and metadata when workflow is set.
|
|
491
|
+
// (command jobs branched out earlier, so config.agent is set on the non-workflow path.)
|
|
428
492
|
const effectiveAgent = config.workflow ? 'claude' : config.agent;
|
|
429
493
|
const meta = {
|
|
430
494
|
jobName: config.name,
|
|
@@ -561,6 +625,153 @@ export async function executeJob(config, deps) {
|
|
|
561
625
|
timer.end({ status: 'failed', exitCode: 1, runId });
|
|
562
626
|
return { meta, reportPath: null };
|
|
563
627
|
}
|
|
628
|
+
async function executeJobOnHost(config, opts) {
|
|
629
|
+
if (config.workflow) {
|
|
630
|
+
throw new Error(`Routine '${config.name}' runs a workflow bundle, which can't execute on a host yet — remove 'host:' or 'workflow:'.`);
|
|
631
|
+
}
|
|
632
|
+
if (config.loop) {
|
|
633
|
+
throw new Error(`Routine '${config.name}' uses 'loop:', which can't execute on a host yet — remove 'host:' or 'loop:'.`);
|
|
634
|
+
}
|
|
635
|
+
if (config.command) {
|
|
636
|
+
throw new Error(`Routine '${config.name}' uses 'command:', which can't execute on a host yet — remove 'host:' or 'command:'.`);
|
|
637
|
+
}
|
|
638
|
+
const { resolveHostRunTarget, dispatchPromptToHost } = await import('./hosts/run-target.js');
|
|
639
|
+
const host = await resolveHostRunTarget(config.host);
|
|
640
|
+
const timer = createTimer('agent.run', {
|
|
641
|
+
agent: config.agent,
|
|
642
|
+
jobName: config.name,
|
|
643
|
+
mode: config.mode,
|
|
644
|
+
host: host.name,
|
|
645
|
+
...redactPrompt(config.prompt),
|
|
646
|
+
schedule: config.schedule,
|
|
647
|
+
});
|
|
648
|
+
const runId = generateRunId();
|
|
649
|
+
const runDir = getRunDir(config.name, runId);
|
|
650
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
651
|
+
const meta = {
|
|
652
|
+
jobName: config.name,
|
|
653
|
+
runId,
|
|
654
|
+
agent: config.agent,
|
|
655
|
+
pid: null, // no local process — the run lives on the host
|
|
656
|
+
spawnedAt: Date.now(),
|
|
657
|
+
status: 'running',
|
|
658
|
+
startedAt: new Date().toISOString(),
|
|
659
|
+
completedAt: null,
|
|
660
|
+
exitCode: null,
|
|
661
|
+
host: host.name,
|
|
662
|
+
};
|
|
663
|
+
writeRunMeta(meta);
|
|
664
|
+
const { task, exitCode } = await dispatchPromptToHost(host, {
|
|
665
|
+
agent: config.agent,
|
|
666
|
+
prompt: resolveJobPrompt(config),
|
|
667
|
+
mode: normalizeMode(config.mode),
|
|
668
|
+
effort: config.effort,
|
|
669
|
+
model: config.config?.model,
|
|
670
|
+
timeout: config.timeout, // enforced by the REMOTE agents run
|
|
671
|
+
remoteCwd: config.remoteCwd,
|
|
672
|
+
name: config.name,
|
|
673
|
+
cwd: runDir,
|
|
674
|
+
follow: !opts.detached,
|
|
675
|
+
});
|
|
676
|
+
meta.hostTaskId = task.id;
|
|
677
|
+
// Sync path: a real exit code finalizes now. -1 (follow window closed) and
|
|
678
|
+
// the detached path leave the meta `running` for the monitor to reconcile.
|
|
679
|
+
if (!opts.detached && exitCode !== null && exitCode !== undefined && exitCode !== -1) {
|
|
680
|
+
meta.status = exitCode === 0 ? 'completed' : 'failed';
|
|
681
|
+
meta.exitCode = exitCode;
|
|
682
|
+
meta.completedAt = new Date().toISOString();
|
|
683
|
+
}
|
|
684
|
+
writeRunMeta(meta);
|
|
685
|
+
timer.end({ status: meta.status, exitCode: meta.exitCode ?? undefined, runId });
|
|
686
|
+
return { meta, reportPath: null };
|
|
687
|
+
}
|
|
688
|
+
/** Spawn a job as a detached process and return immediately with run metadata. */
|
|
689
|
+
async function executeCommandJobForeground(config) {
|
|
690
|
+
const timer = createTimer('agent.run', {
|
|
691
|
+
jobName: config.name,
|
|
692
|
+
mode: config.mode,
|
|
693
|
+
schedule: config.schedule,
|
|
694
|
+
});
|
|
695
|
+
const runId = generateRunId();
|
|
696
|
+
const runDir = getRunDir(config.name, runId);
|
|
697
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
698
|
+
const stdoutPath = path.join(runDir, 'stdout.log');
|
|
699
|
+
const stdoutFd = fs.openSync(stdoutPath, 'w', 0o600);
|
|
700
|
+
const meta = {
|
|
701
|
+
jobName: config.name,
|
|
702
|
+
runId,
|
|
703
|
+
command: config.command,
|
|
704
|
+
pid: null,
|
|
705
|
+
spawnedAt: Date.now(),
|
|
706
|
+
status: 'running',
|
|
707
|
+
startedAt: new Date().toISOString(),
|
|
708
|
+
completedAt: null,
|
|
709
|
+
exitCode: null,
|
|
710
|
+
};
|
|
711
|
+
writeRunMeta(meta);
|
|
712
|
+
const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
|
|
713
|
+
const cmd = buildShellCommand(config.command);
|
|
714
|
+
const env = commandSpawnEnv(config);
|
|
715
|
+
process.stderr.write(`[agents] routine ${config.name}: running command\n`);
|
|
716
|
+
const result = await new Promise((resolve) => {
|
|
717
|
+
const child = spawn(cmd[0], cmd.slice(1), {
|
|
718
|
+
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
719
|
+
...backgroundSpawnOptions({ fdStdio: true }),
|
|
720
|
+
env,
|
|
721
|
+
});
|
|
722
|
+
meta.pid = child.pid || null;
|
|
723
|
+
writeRunMeta(meta);
|
|
724
|
+
let settled = false;
|
|
725
|
+
const finish = (r) => {
|
|
726
|
+
if (settled)
|
|
727
|
+
return;
|
|
728
|
+
settled = true;
|
|
729
|
+
try {
|
|
730
|
+
fs.closeSync(stdoutFd);
|
|
731
|
+
}
|
|
732
|
+
catch { /* fd already closed */ }
|
|
733
|
+
resolve(r);
|
|
734
|
+
};
|
|
735
|
+
const timeoutTimer = setTimeout(() => {
|
|
736
|
+
try {
|
|
737
|
+
if (child.pid)
|
|
738
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
739
|
+
}
|
|
740
|
+
catch { /* process already exited */ }
|
|
741
|
+
setTimeout(() => {
|
|
742
|
+
try {
|
|
743
|
+
if (child.pid)
|
|
744
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
745
|
+
}
|
|
746
|
+
catch { /* process already exited */ }
|
|
747
|
+
}, 5000);
|
|
748
|
+
finish({ exitCode: null, status: 'timeout' });
|
|
749
|
+
}, timeoutMs);
|
|
750
|
+
child.on('exit', (code) => {
|
|
751
|
+
clearTimeout(timeoutTimer);
|
|
752
|
+
finish({ exitCode: code, status: code === 0 ? 'completed' : 'failed' });
|
|
753
|
+
});
|
|
754
|
+
child.on('error', (err) => {
|
|
755
|
+
clearTimeout(timeoutTimer);
|
|
756
|
+
finish({ exitCode: 1, status: 'failed', error: err.message });
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
meta.status = result.status;
|
|
760
|
+
meta.exitCode = result.exitCode ?? (result.status === 'completed' ? 0 : 1);
|
|
761
|
+
meta.completedAt = new Date().toISOString();
|
|
762
|
+
writeRunMeta(meta);
|
|
763
|
+
if (result.error) {
|
|
764
|
+
process.stderr.write(`[agents] routine ${config.name}: command spawn failed: ${result.error}\n`);
|
|
765
|
+
}
|
|
766
|
+
timer.end({
|
|
767
|
+
status: meta.status,
|
|
768
|
+
exitCode: meta.exitCode ?? undefined,
|
|
769
|
+
runId,
|
|
770
|
+
...(result.error ? { error: result.error } : {}),
|
|
771
|
+
});
|
|
772
|
+
return { meta, reportPath: null };
|
|
773
|
+
}
|
|
774
|
+
/** Spawn a job as a detached process and return immediately with run metadata. */
|
|
564
775
|
/** Spawn a job as a detached process and return immediately with run metadata. */
|
|
565
776
|
export async function executeJobDetached(config) {
|
|
566
777
|
const eligibility = checkJobDeviceEligibility(config);
|
|
@@ -568,6 +779,17 @@ export async function executeJobDetached(config) {
|
|
|
568
779
|
process.stderr.write(`[agents] daemon: skipping '${config.name}' — ${eligibility.message}\n`);
|
|
569
780
|
throw new Error(eligibility.message);
|
|
570
781
|
}
|
|
782
|
+
// `host:` placement — dispatch over SSH and return; the monitor finalizes.
|
|
783
|
+
if (config.host) {
|
|
784
|
+
const { meta } = await executeJobOnHost(config, { detached: true });
|
|
785
|
+
return meta;
|
|
786
|
+
}
|
|
787
|
+
// Command-mode: fire a plain shell command detached (no agent, no rotation,
|
|
788
|
+
// no pinning, no sandbox overlay). Still writes a run record so the daemon,
|
|
789
|
+
// list/runs, and overdue tracking keep working.
|
|
790
|
+
if (config.command) {
|
|
791
|
+
return executeCommandJobDetached(config);
|
|
792
|
+
}
|
|
571
793
|
// Pre-flight: pick a healthy version/account so the daemon does not launch
|
|
572
794
|
// into a credit-exhausted install. Detached cannot mid-run failover (no exit
|
|
573
795
|
// wait); the next schedule tick re-selects if this attempt still fails.
|
|
@@ -577,7 +799,7 @@ export async function executeJobDetached(config) {
|
|
|
577
799
|
let cmd = buildJobCommand(config, resolvedPrompt);
|
|
578
800
|
// workflow AND resume dispatch through `agents run` — never binary-pin them (pinning
|
|
579
801
|
// rewrites cmd[0] to the agent binary → broken `<binary> run …`).
|
|
580
|
-
if (!dispatchesViaAgentsRun(config) && version) {
|
|
802
|
+
if (!dispatchesViaAgentsRun(config) && version && config.agent) {
|
|
581
803
|
cmd = pinJobBinary(cmd, config.agent, version);
|
|
582
804
|
}
|
|
583
805
|
// Resume must run against the REAL home: `--resume <id>` resolves the session from
|
|
@@ -601,6 +823,7 @@ export async function executeJobDetached(config) {
|
|
|
601
823
|
e.TZ = config.timezone;
|
|
602
824
|
return e;
|
|
603
825
|
})()
|
|
826
|
+
// Non-command path only: config.agent is always set here (command/workflow branch earlier).
|
|
604
827
|
: buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone);
|
|
605
828
|
const effectiveAgent = config.workflow ? 'claude' : config.agent;
|
|
606
829
|
const meta = {
|
|
@@ -640,6 +863,74 @@ export async function executeJobDetached(config) {
|
|
|
640
863
|
writeRunMeta(meta);
|
|
641
864
|
return meta;
|
|
642
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* Detached (fire-and-forget) execution for a command-mode routine. Mirrors the
|
|
868
|
+
* agent detached flow: write an initial running record, spawn the shell command
|
|
869
|
+
* un-sandboxed, unref, then record the pid. The daemon does not wait for exit;
|
|
870
|
+
* `monitorRunningJobs` reaps the record on the next tick.
|
|
871
|
+
*/
|
|
872
|
+
function executeCommandJobDetached(config) {
|
|
873
|
+
const runId = generateRunId();
|
|
874
|
+
const runDir = getRunDir(config.name, runId);
|
|
875
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
876
|
+
const stdoutPath = path.join(runDir, 'stdout.log');
|
|
877
|
+
const stdoutFd = fs.openSync(stdoutPath, 'w', 0o600);
|
|
878
|
+
// Wrap the shell so the child records its own exit code to <runDir>/exit-code.
|
|
879
|
+
// The in-process `child.on('exit')` below writes the terminal record while the
|
|
880
|
+
// daemon is alive (the common case); the file lets monitorRunningJobs recover
|
|
881
|
+
// the real status if the daemon restarted between spawn and exit. (win32 relies
|
|
882
|
+
// on the exit event only.)
|
|
883
|
+
const exitCodePath = path.join(runDir, 'exit-code');
|
|
884
|
+
// Run the command in a SUBSHELL `( … )` so that if it calls `exit`, only the
|
|
885
|
+
// subshell exits — the outer shell still captures `$?` and writes the file.
|
|
886
|
+
const cmd = process.platform === 'win32'
|
|
887
|
+
? buildShellCommand(config.command)
|
|
888
|
+
: ['/bin/sh', '-c',
|
|
889
|
+
`(\n${config.command}\n)\n__ac_rc=$?; printf '%s' "$__ac_rc" > ${shSingleQuote(exitCodePath)} 2>/dev/null; exit $__ac_rc`];
|
|
890
|
+
const env = commandSpawnEnv(config);
|
|
891
|
+
const meta = {
|
|
892
|
+
jobName: config.name,
|
|
893
|
+
runId,
|
|
894
|
+
command: config.command,
|
|
895
|
+
pid: null,
|
|
896
|
+
spawnedAt: Date.now(),
|
|
897
|
+
status: 'running',
|
|
898
|
+
startedAt: new Date().toISOString(),
|
|
899
|
+
completedAt: null,
|
|
900
|
+
exitCode: null,
|
|
901
|
+
};
|
|
902
|
+
const child = spawn(cmd[0], cmd.slice(1), {
|
|
903
|
+
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
904
|
+
...backgroundSpawnOptions({ fdStdio: true }),
|
|
905
|
+
env,
|
|
906
|
+
});
|
|
907
|
+
// Record the real terminal status ourselves — the daemon stays alive after this
|
|
908
|
+
// fire-and-forget call, so the exit event fires here. (monitorRunningJobs no
|
|
909
|
+
// longer force-fails command jobs; it reads exit-code only on the restart edge.)
|
|
910
|
+
let settled = false;
|
|
911
|
+
const settle = (status, exitCode) => {
|
|
912
|
+
if (settled)
|
|
913
|
+
return;
|
|
914
|
+
settled = true;
|
|
915
|
+
meta.status = status;
|
|
916
|
+
meta.exitCode = exitCode;
|
|
917
|
+
meta.completedAt = new Date().toISOString();
|
|
918
|
+
writeRunMeta(meta);
|
|
919
|
+
};
|
|
920
|
+
child.on('exit', (code) => settle(code === 0 ? 'completed' : 'failed', code ?? 1));
|
|
921
|
+
child.on('error', (err) => {
|
|
922
|
+
settle('failed', 1);
|
|
923
|
+
process.stderr.write(`[agents] daemon: command spawn failed for job "${config.name}": ${err.message}\n`);
|
|
924
|
+
});
|
|
925
|
+
child.unref();
|
|
926
|
+
try {
|
|
927
|
+
fs.closeSync(stdoutFd);
|
|
928
|
+
}
|
|
929
|
+
catch { /* fd already closed */ }
|
|
930
|
+
meta.pid = child.pid || null;
|
|
931
|
+
writeRunMeta(meta);
|
|
932
|
+
return meta;
|
|
933
|
+
}
|
|
643
934
|
function extractAndSaveReport(stdoutPath, agentType, runDir) {
|
|
644
935
|
try {
|
|
645
936
|
const report = extractReport(stdoutPath, agentType);
|
|
@@ -769,6 +1060,26 @@ function isPidOurs(pid, spawnedAt) {
|
|
|
769
1060
|
return true;
|
|
770
1061
|
}
|
|
771
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Finalize one `host:`-placed run by healing its host-task sidecar against the
|
|
1065
|
+
* remote `.exit` (lib/hosts/reconcile.ts). Mutates + persists the meta only
|
|
1066
|
+
* when the sidecar reached a terminal state.
|
|
1067
|
+
*/
|
|
1068
|
+
function finalizeHostRun(meta) {
|
|
1069
|
+
try {
|
|
1070
|
+
const task = loadHostTask(meta.hostTaskId);
|
|
1071
|
+
if (!task)
|
|
1072
|
+
return;
|
|
1073
|
+
const healed = reconcileHostTask(task);
|
|
1074
|
+
if (healed.status !== 'completed' && healed.status !== 'failed')
|
|
1075
|
+
return;
|
|
1076
|
+
meta.status = healed.status;
|
|
1077
|
+
meta.exitCode = healed.exitCode ?? (healed.status === 'completed' ? 0 : 1);
|
|
1078
|
+
meta.completedAt = healed.finishedAt ?? new Date().toISOString();
|
|
1079
|
+
writeRunMeta(meta);
|
|
1080
|
+
}
|
|
1081
|
+
catch { /* unreachable host or unreadable sidecar — retry next sweep */ }
|
|
1082
|
+
}
|
|
772
1083
|
/** Scan all runs marked "running" and finalize any whose process has exited. */
|
|
773
1084
|
export function monitorRunningJobs() {
|
|
774
1085
|
const runsDir = getRunsDir();
|
|
@@ -788,30 +1099,54 @@ export function monitorRunningJobs() {
|
|
|
788
1099
|
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
789
1100
|
if (meta.status !== 'running')
|
|
790
1101
|
continue;
|
|
1102
|
+
// `host:`-placed run — no local pid to watch. Reconcile against the
|
|
1103
|
+
// remote `.exit` (completion is confirmed, never guessed: an
|
|
1104
|
+
// unreachable host leaves the run `running` for the next sweep).
|
|
1105
|
+
if (meta.hostTaskId) {
|
|
1106
|
+
finalizeHostRun(meta);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
791
1109
|
if (!meta.pid)
|
|
792
1110
|
continue;
|
|
793
1111
|
const runDirPath = path.join(jobRunsPath, runDirEntry.name);
|
|
794
1112
|
const stdoutPath = path.join(runDirPath, 'stdout.log');
|
|
1113
|
+
// Command-mode records carry no agent; there is no stream-json report to
|
|
1114
|
+
// parse or extract. Reap them on pid liveness alone.
|
|
1115
|
+
const isCommandRun = Boolean(meta.command) || !meta.agent;
|
|
795
1116
|
const wallClockMs = Date.now() - Date.parse(meta.startedAt);
|
|
796
1117
|
if (Number.isFinite(wallClockMs) && wallClockMs > MAX_WALL_CLOCK_MS) {
|
|
797
1118
|
meta.status = 'timeout';
|
|
798
1119
|
meta.completedAt = new Date().toISOString();
|
|
799
1120
|
writeRunMeta(meta);
|
|
800
|
-
|
|
1121
|
+
if (!isCommandRun)
|
|
1122
|
+
extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
|
|
801
1123
|
continue;
|
|
802
1124
|
}
|
|
803
1125
|
if (!isPidOurs(meta.pid, meta.spawnedAt)) {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1126
|
+
if (isCommandRun) {
|
|
1127
|
+
// Command routines normally record their own terminal status via
|
|
1128
|
+
// child.on('exit') (so this record would already be non-'running' and
|
|
1129
|
+
// skipped above). Reaching here means the daemon restarted mid-run and
|
|
1130
|
+
// missed the exit event — recover the true code from the exit-code file
|
|
1131
|
+
// the child wrote; its absence means the child was killed/crashed.
|
|
1132
|
+
const ec = readCommandExitCode(runDirPath);
|
|
1133
|
+
meta.status = ec === 0 ? 'completed' : 'failed';
|
|
1134
|
+
meta.exitCode = ec;
|
|
808
1135
|
}
|
|
809
1136
|
else {
|
|
810
|
-
|
|
1137
|
+
const inferred = inferFinalStatusFromLog(stdoutPath, meta.agent);
|
|
1138
|
+
if (inferred) {
|
|
1139
|
+
meta.status = inferred.status;
|
|
1140
|
+
meta.exitCode = inferred.exitCode;
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
meta.status = 'failed';
|
|
1144
|
+
}
|
|
811
1145
|
}
|
|
812
1146
|
meta.completedAt = new Date().toISOString();
|
|
813
1147
|
writeRunMeta(meta);
|
|
814
|
-
|
|
1148
|
+
if (!isCommandRun)
|
|
1149
|
+
extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
|
|
815
1150
|
}
|
|
816
1151
|
}
|
|
817
1152
|
catch { /* corrupt or unreadable meta.json */ }
|
package/dist/lib/sandbox.d.ts
CHANGED
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
import type { JobConfig } from './routines.js';
|
|
10
10
|
/** Build a restricted environment for a sandboxed process, setting HOME to the overlay. */
|
|
11
11
|
export declare function buildSpawnEnv(overlayHome: string, extraEnv?: Record<string, string>): Record<string, string>;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Get the overlay HOME directory path for a named job.
|
|
14
|
+
*
|
|
15
|
+
* The job name originates from routine YAML (`name:` field or file basename),
|
|
16
|
+
* which can arrive from a synced user/system config repo. `safeJoin` contains
|
|
17
|
+
* it to a single segment beneath the routines dir so a crafted name such as
|
|
18
|
+
* `../../../..` cannot steer `prepareJobHome`/`cleanJobHome` (which does a
|
|
19
|
+
* recursive `rmSync`) at a path outside `~/.agents/routines`.
|
|
20
|
+
*/
|
|
13
21
|
export declare function getJobHomePath(name: string): string;
|
|
14
22
|
/** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
|
|
15
23
|
export declare function prepareJobHome(config: JobConfig): string;
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as path from 'path';
|
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
13
13
|
import { getRoutinesDir, getUserAgentsDir } from './state.js';
|
|
14
|
+
import { safeJoin } from './paths.js';
|
|
14
15
|
import { createLink } from './platform/index.js';
|
|
15
16
|
function resolveRealHome() {
|
|
16
17
|
const home = os.homedir();
|
|
@@ -79,9 +80,17 @@ export function buildSpawnEnv(overlayHome, extraEnv) {
|
|
|
79
80
|
}
|
|
80
81
|
return env;
|
|
81
82
|
}
|
|
82
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Get the overlay HOME directory path for a named job.
|
|
85
|
+
*
|
|
86
|
+
* The job name originates from routine YAML (`name:` field or file basename),
|
|
87
|
+
* which can arrive from a synced user/system config repo. `safeJoin` contains
|
|
88
|
+
* it to a single segment beneath the routines dir so a crafted name such as
|
|
89
|
+
* `../../../..` cannot steer `prepareJobHome`/`cleanJobHome` (which does a
|
|
90
|
+
* recursive `rmSync`) at a path outside `~/.agents/routines`.
|
|
91
|
+
*/
|
|
83
92
|
export function getJobHomePath(name) {
|
|
84
|
-
return path.join(getRoutinesDir(), name, 'home');
|
|
93
|
+
return path.join(safeJoin(getRoutinesDir(), name), 'home');
|
|
85
94
|
}
|
|
86
95
|
/** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
|
|
87
96
|
export function prepareJobHome(config) {
|
|
@@ -207,6 +207,15 @@ export declare function agentGetSync(name: string): {
|
|
|
207
207
|
bundle: SecretsBundle;
|
|
208
208
|
env: Record<string, string>;
|
|
209
209
|
} | null;
|
|
210
|
+
/**
|
|
211
|
+
* Synchronous liveness check: is a broker actually LISTENING and answering (not
|
|
212
|
+
* just a lingering socket file)? Used to decide whether the auto-cache may take
|
|
213
|
+
* the synchronous warm path — a dead broker whose socket outlived it (crash,
|
|
214
|
+
* OOM, version-skew teardown) must NOT drag a foreground read through the
|
|
215
|
+
* worker's 20s cold-start budget. A stale socket refuses instantly, so this is
|
|
216
|
+
* fast in both the alive and dead cases. macOS only.
|
|
217
|
+
*/
|
|
218
|
+
export declare function agentReachableSync(): boolean;
|
|
210
219
|
/**
|
|
211
220
|
* Synchronously evict one bundle from the broker. Called after a mutating
|
|
212
221
|
* keychain write (add / rotate / remove / rename / delete) so the broker never
|
|
@@ -237,25 +246,54 @@ export declare function agentAutoLoadMetaSync(nameSetHash: string, bundles: Secr
|
|
|
237
246
|
* per ~7d), so auto-caching is ON by default; opt out with
|
|
238
247
|
* `secrets.agent.auto: false`. Best-effort; an unreadable meta reads as on. */
|
|
239
248
|
export declare function secretsAgentAutoEnabled(): boolean;
|
|
249
|
+
/** Minimum / maximum bounds for the configurable hold window. A too-small value
|
|
250
|
+
* would defeat the broker (constant re-prompts); a too-large one pins secrets in
|
|
251
|
+
* memory far longer than intended. */
|
|
252
|
+
export declare const MIN_HOLD_MS: number;
|
|
253
|
+
export declare const MAX_HOLD_MS: number;
|
|
254
|
+
/**
|
|
255
|
+
* How long an unlocked / auto-cached bundle is held before the next read
|
|
256
|
+
* re-prompts. Defaults to DEFAULT_TTL_MS (7d); override with
|
|
257
|
+
* `secrets.agent.holdMs` (milliseconds) in agents.yaml — e.g. 86400000 for a 24h
|
|
258
|
+
* cap. Clamped to [MIN_HOLD_MS, MAX_HOLD_MS] so a typo can neither disable the
|
|
259
|
+
* hold nor pin a secret in memory indefinitely. Best-effort: an unreadable or
|
|
260
|
+
* non-numeric value falls back to the 7d default. Pure except for the meta read.
|
|
261
|
+
*/
|
|
262
|
+
export declare function secretsHoldMs(): number;
|
|
263
|
+
/** Pure clamp for a configured `holdMs`: a positive finite number is bounded to
|
|
264
|
+
* [MIN_HOLD_MS, MAX_HOLD_MS]; anything else (absent, 0, negative, NaN, non-number)
|
|
265
|
+
* falls back to the 7d default. Exported for direct unit testing. */
|
|
266
|
+
export declare function clampHoldMs(v: unknown): number;
|
|
240
267
|
/**
|
|
241
268
|
* Fire-and-forget: populate the broker with a freshly-resolved bundle so the
|
|
242
269
|
* NEXT process reads it without a prompt. Used by the auto-cache path after a
|
|
243
|
-
* real keychain read of a `daily`-policy bundle
|
|
244
|
-
*
|
|
245
|
-
*
|
|
270
|
+
* real keychain read of a `daily`-policy bundle, so the NEXT concurrent read is
|
|
271
|
+
* silent. Env travels over stdin, never argv.
|
|
272
|
+
*
|
|
273
|
+
* Reliability (this is what makes `daily` actually "stick"): when a broker is
|
|
274
|
+
* ALREADY listening, warm it SYNCHRONOUSLY with a bounded wait so the bundle is
|
|
275
|
+
* held by the time this process exits. The old detached-only path lost the race
|
|
276
|
+
* under load — a short-lived reader (`agents secrets export`, a release-script
|
|
277
|
+
* loop) exited before the unref'd worker connected, so the cache silently never
|
|
278
|
+
* populated and every read re-prompted despite the `daily` policy. Only when the
|
|
279
|
+
* broker must COLD-START (no socket yet) do we fall back to the detached worker,
|
|
280
|
+
* so a first-ever read never blocks on a multi-second broker boot.
|
|
246
281
|
*
|
|
247
|
-
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping
|
|
248
|
-
*
|
|
249
|
-
* broker is itself a cold-starting full CLI and can take several seconds to bind
|
|
250
|
-
* the socket, so a short fixed budget would give up before it's ready and the
|
|
251
|
-
* cache would silently never populate. Best-effort; never throws. macOS only.
|
|
282
|
+
* The worker reuses the robust `ensureAgentRunning` path (spawn-then-ping) rather
|
|
283
|
+
* than a tight inline retry loop. Best-effort; never throws. macOS only.
|
|
252
284
|
*/
|
|
253
285
|
export declare function agentAutoLoadSync(name: string, bundle: SecretsBundle, env: Record<string, string>, ttlMs: number): void;
|
|
254
286
|
/**
|
|
255
287
|
* Body of the hidden `secrets _agent-load` worker. Reads one `{name, bundle,
|
|
256
288
|
* env, ttlMs}` payload from stdin, ensures the broker is up (robust, generous
|
|
257
|
-
* budget), and loads the bundle into it.
|
|
258
|
-
*
|
|
289
|
+
* budget), and loads the bundle into it.
|
|
290
|
+
*
|
|
291
|
+
* Exit code is load-truthful: 0 ONLY when the bundle was actually loaded into a
|
|
292
|
+
* reachable broker; non-zero on any failure (malformed payload, broker couldn't
|
|
293
|
+
* be brought up, or the load transport failed). The synchronous caller
|
|
294
|
+
* (agentAutoLoadSync) relies on this to decide whether to skip the detached
|
|
295
|
+
* fallback — a bare "process exited 0" would otherwise be a false-positive
|
|
296
|
+
* success that silently reintroduces the very re-prompt storm this path fixes.
|
|
259
297
|
*/
|
|
260
298
|
export declare function runAgentLoadFromStdin(): Promise<void>;
|
|
261
299
|
/** Store a resolved bundle in the broker. Returns false on transport failure. */
|