@phnx-labs/agents-cli 1.20.87 → 1.20.89
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 +323 -0
- package/README.md +12 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/commands.js +7 -7
- package/dist/commands/doctor.d.ts +0 -19
- package/dist/commands/doctor.js +219 -305
- package/dist/commands/exec.js +7 -19
- package/dist/commands/factory.js +26 -2
- package/dist/commands/funnel.js +16 -1
- package/dist/commands/inspect.js +3 -5
- package/dist/commands/menubar.js +117 -34
- package/dist/commands/routines.js +25 -3
- package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
- package/dist/commands/secrets-rotate-passphrase.js +96 -0
- package/dist/commands/secrets.js +2 -0
- package/dist/commands/sessions.d.ts +7 -1
- package/dist/commands/sessions.js +40 -12
- package/dist/commands/ssh.js +3 -3
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/commands/webhook.js +7 -2
- package/dist/lib/agents.d.ts +31 -1
- package/dist/lib/agents.js +55 -0
- package/dist/lib/command-skills.d.ts +10 -0
- package/dist/lib/command-skills.js +14 -0
- package/dist/lib/commands.js +28 -2
- package/dist/lib/daemon.d.ts +29 -0
- package/dist/lib/daemon.js +75 -6
- package/dist/lib/devices/doctor-findings.d.ts +167 -0
- package/dist/lib/devices/doctor-findings.js +893 -0
- package/dist/lib/devices/fleet-divergence.d.ts +22 -0
- package/dist/lib/devices/fleet-divergence.js +34 -10
- package/dist/lib/devices/fleet-inventory.d.ts +17 -6
- package/dist/lib/devices/fleet-inventory.js +56 -8
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +14 -3
- package/dist/lib/exec.js +41 -8
- package/dist/lib/factory/snapshot.d.ts +78 -0
- package/dist/lib/factory/snapshot.js +209 -0
- package/dist/lib/fs-atomic.d.ts +14 -1
- package/dist/lib/fs-atomic.js +35 -3
- package/dist/lib/funnel.d.ts +1 -0
- package/dist/lib/funnel.js +8 -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 +2 -2
- package/dist/lib/menubar/install-menubar.d.ts +53 -2
- package/dist/lib/menubar/install-menubar.js +183 -28
- package/dist/lib/platform/process.d.ts +2 -0
- package/dist/lib/platform/process.js +5 -3
- package/dist/lib/project-resources.js +34 -20
- package/dist/lib/resources.d.ts +8 -0
- package/dist/lib/resources.js +34 -1
- package/dist/lib/routines-placement.d.ts +2 -1
- package/dist/lib/routines-placement.js +8 -4
- package/dist/lib/routines.d.ts +57 -1
- package/dist/lib/routines.js +74 -1
- package/dist/lib/runner.d.ts +16 -1
- package/dist/lib/runner.js +58 -16
- package/dist/lib/sandbox.d.ts +2 -0
- package/dist/lib/sandbox.js +38 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +9 -34
- package/dist/lib/secrets/filestore.d.ts +152 -34
- package/dist/lib/secrets/filestore.js +676 -123
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -6
- package/dist/lib/secrets/rc-hygiene.js +0 -24
- package/dist/lib/session/active.d.ts +6 -6
- package/dist/lib/session/active.js +6 -6
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +137 -1
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +76 -37
- package/dist/lib/session/remote-active.d.ts +4 -1
- package/dist/lib/session/remote-active.js +8 -2
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/viewing-in.d.ts +31 -0
- package/dist/lib/session/viewing-in.js +47 -0
- package/dist/lib/staleness/detectors/commands.js +14 -5
- package/dist/lib/staleness/types.d.ts +2 -0
- package/dist/lib/staleness/writers/commands.js +13 -7
- package/dist/lib/state.d.ts +17 -0
- package/dist/lib/state.js +30 -2
- package/dist/lib/triggers/handlers.d.ts +95 -0
- package/dist/lib/triggers/handlers.js +384 -0
- package/dist/lib/triggers/webhook.d.ts +10 -2
- package/dist/lib/triggers/webhook.js +65 -11
- package/dist/lib/usage.d.ts +72 -1
- package/dist/lib/usage.js +21 -27
- package/dist/lib/versions.js +30 -13
- package/package.json +1 -1
package/dist/lib/routines.js
CHANGED
|
@@ -569,6 +569,12 @@ export function validateTrigger(trigger) {
|
|
|
569
569
|
if (linear.label !== undefined && typeof linear.label !== 'string') {
|
|
570
570
|
errors.push('trigger.label must be a string');
|
|
571
571
|
}
|
|
572
|
+
if (linear.stateTo !== undefined && typeof linear.stateTo !== 'string') {
|
|
573
|
+
errors.push('trigger.stateTo must be a string');
|
|
574
|
+
}
|
|
575
|
+
if (linear.stateFrom !== undefined && typeof linear.stateFrom !== 'string') {
|
|
576
|
+
errors.push('trigger.stateFrom must be a string');
|
|
577
|
+
}
|
|
572
578
|
return errors;
|
|
573
579
|
}
|
|
574
580
|
function isParseableDate(value) {
|
|
@@ -702,8 +708,71 @@ export function hasCompletedOneShotRun(config, now = new Date()) {
|
|
|
702
708
|
export function shouldPurgeCompletedOneShotRoutine(config, now = new Date()) {
|
|
703
709
|
return hasCompletedOneShotRun(config, now);
|
|
704
710
|
}
|
|
711
|
+
function getPath(obj, path) {
|
|
712
|
+
const parts = path.split('.');
|
|
713
|
+
let current = obj;
|
|
714
|
+
for (const part of parts) {
|
|
715
|
+
if (current === null || current === undefined)
|
|
716
|
+
return undefined;
|
|
717
|
+
current = current[part];
|
|
718
|
+
}
|
|
719
|
+
return current;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Substitute `{{dotted.path}}` placeholders in a string using a webhook context.
|
|
723
|
+
* Missing values are replaced with an empty string.
|
|
724
|
+
*/
|
|
725
|
+
export function substituteWebhookPrompt(prompt, context) {
|
|
726
|
+
return prompt.replace(/\{\{([^{}]+)\}\}/g, (_, rawPath) => {
|
|
727
|
+
const value = getPath(context, rawPath.trim());
|
|
728
|
+
if (value === undefined || value === null)
|
|
729
|
+
return '';
|
|
730
|
+
return String(value);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Substitute `{{dotted.path}}` placeholders in a string destined for a SHELL,
|
|
735
|
+
* quoting every substituted value so payload content cannot break out of it.
|
|
736
|
+
*
|
|
737
|
+
* `run.command` is executed through a shell, and its context is built from an
|
|
738
|
+
* external webhook payload — `issue.title`, `issue.description`, and the GitHub
|
|
739
|
+
* `pull_request` fields are free text any outside contributor can set. Pasting
|
|
740
|
+
* those in raw (as {@link substituteWebhookPrompt} does, correctly, for prompts)
|
|
741
|
+
* turns an operator's `echo {{issue.title}}` into a command-injection sink.
|
|
742
|
+
*
|
|
743
|
+
* The template itself is operator-authored and stays unquoted, so pipes,
|
|
744
|
+
* redirects, and `&&` in the configured command keep working. Only the
|
|
745
|
+
* interpolated values are quoted.
|
|
746
|
+
*
|
|
747
|
+
* POSIX `sh` quoting: wrap in single quotes and close/escape/reopen for any
|
|
748
|
+
* embedded single quote. `exec` uses `cmd.exe` on Windows, which does not
|
|
749
|
+
* honour these rules — see `assertShellSubstitutionSupported`.
|
|
750
|
+
*/
|
|
751
|
+
export function substituteWebhookCommand(command, context) {
|
|
752
|
+
return command.replace(/\{\{([^{}]+)\}\}/g, (_, rawPath) => {
|
|
753
|
+
const value = getPath(context, rawPath.trim());
|
|
754
|
+
if (value === undefined || value === null)
|
|
755
|
+
return "''";
|
|
756
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Refuse a `run.command` carrying placeholders on a platform whose shell we
|
|
761
|
+
* cannot safely quote for. `child_process.exec` runs through `cmd.exe` on
|
|
762
|
+
* Windows, where POSIX single-quoting is not a quoting mechanism at all, so
|
|
763
|
+
* {@link substituteWebhookCommand} would not contain a hostile value.
|
|
764
|
+
*
|
|
765
|
+
* Fail loud rather than execute something we cannot prove is safe.
|
|
766
|
+
*/
|
|
767
|
+
export function assertShellSubstitutionSupported(command, platform = process.platform) {
|
|
768
|
+
if (platform === 'win32' && /\{\{[^{}]+\}\}/.test(command)) {
|
|
769
|
+
throw new Error('run.command with {{…}} placeholders is not supported on Windows: the values come from an ' +
|
|
770
|
+
'untrusted webhook payload and cmd.exe cannot be quoted safely. Use run.prompt, or a ' +
|
|
771
|
+
'command with no placeholders.');
|
|
772
|
+
}
|
|
773
|
+
}
|
|
705
774
|
/** Expand built-in and user-defined template variables in a job's prompt string. */
|
|
706
|
-
export function resolveJobPrompt(config) {
|
|
775
|
+
export function resolveJobPrompt(config, context) {
|
|
707
776
|
const now = new Date();
|
|
708
777
|
const tz = config.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
709
778
|
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
@@ -725,6 +794,10 @@ export function resolveJobPrompt(config) {
|
|
|
725
794
|
prompt = prompt.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
|
|
726
795
|
}
|
|
727
796
|
}
|
|
797
|
+
// Webhook-driven variables ({{issue.identifier}}, {{updatedFrom.state.name}}, ...)
|
|
798
|
+
if (context) {
|
|
799
|
+
prompt = substituteWebhookPrompt(prompt, context);
|
|
800
|
+
}
|
|
728
801
|
// Last report (special handling). Only a COMPLETED run's report is injected —
|
|
729
802
|
// a failed run's report.md is the agent's error text (e.g. a login prompt on
|
|
730
803
|
// an auth failure), and feeding that into the next prompt poisons every
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface RunResult {
|
|
|
23
23
|
meta: RunMeta;
|
|
24
24
|
reportPath: string | null;
|
|
25
25
|
}
|
|
26
|
+
/** Agents the daemon can actually run, derived from the command table above
|
|
27
|
+
* so the `--agent` help and any validation can never drift from it. */
|
|
28
|
+
export declare const ROUTINE_AGENT_IDS: readonly string[];
|
|
29
|
+
/** Stable working directory for routine children, independent of the daemon's launch cwd. */
|
|
30
|
+
export declare function routineSpawnCwd(config: Pick<JobConfig, 'repo'>, configuredRoot?: string | undefined): string;
|
|
26
31
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
27
32
|
export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
|
|
28
33
|
export declare function archiveRoutineTranscripts(meta: Pick<RunMeta, 'jobName' | 'runId' | 'agent'>, runDir: string, overlayHome?: string): void;
|
|
@@ -63,7 +68,7 @@ export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow
|
|
|
63
68
|
* (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
|
|
64
69
|
* with `agents run`.
|
|
65
70
|
*/
|
|
66
|
-
export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string): Record<string, string>;
|
|
71
|
+
export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string, overlayHome?: string): Record<string, string>;
|
|
67
72
|
export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
|
|
68
73
|
/**
|
|
69
74
|
* Optional lifecycle callbacks for a detached routine run. The daemon passes an
|
|
@@ -80,5 +85,15 @@ export interface RoutineHooks {
|
|
|
80
85
|
export declare function executeJobDetached(config: JobConfig, hooks?: RoutineHooks): Promise<RunMeta>;
|
|
81
86
|
/** Extract the final assistant message from a stream-JSON log file as a markdown report. */
|
|
82
87
|
export declare function extractReport(stdoutPath: string, agentType: AgentId): string | null;
|
|
88
|
+
/** Derive the final status of a detached run by reading the agent's stream-json
|
|
89
|
+
* tail. Detached children fire-and-forget, so we never see their exit code
|
|
90
|
+
* directly — but Claude's stream-json terminates with a `type: result` line
|
|
91
|
+
* that carries `is_error`. If we find it, the run completed cleanly (modulo
|
|
92
|
+
* agent-reported error). If not, the process likely died mid-stream and the
|
|
93
|
+
* caller should treat the run as failed. */
|
|
94
|
+
export declare function inferFinalStatusFromLog(stdoutPath: string, agent: AgentId): {
|
|
95
|
+
status: 'completed' | 'failed';
|
|
96
|
+
exitCode: number;
|
|
97
|
+
} | null;
|
|
83
98
|
/** Scan all runs marked "running" and finalize any whose process has exited. */
|
|
84
99
|
export declare function monitorRunningJobs(): void;
|
package/dist/lib/runner.js
CHANGED
|
@@ -32,18 +32,37 @@ import { getBinaryPath, isVersionInstalled, resolveVersion } from './versions.js
|
|
|
32
32
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, } from './rotate.js';
|
|
33
33
|
import { readAuthHealth, isDeadVerdict } from './auth-health.js';
|
|
34
34
|
import { machineId } from './machine-id.js';
|
|
35
|
+
import { isSelfUpdatingAgent } from './agents.js';
|
|
36
|
+
import { expandLocalHome, getProjectRoot } from './project-root.js';
|
|
35
37
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
36
38
|
const AGENT_COMMANDS = {
|
|
37
39
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
38
40
|
codex: ['codex', 'exec', '--sandbox', 'workspace-write', '{prompt}', '--json'],
|
|
39
41
|
gemini: ['gemini', '{prompt}', '--output-format', 'stream-json'],
|
|
42
|
+
cursor: ['cursor-agent', '-p', '{prompt}', '--output-format', 'stream-json'],
|
|
40
43
|
kimi: ['kimi', '--prompt', '{prompt}', '--output-format', 'stream-json'],
|
|
41
44
|
droid: ['droid', 'exec', '{prompt}', '-o', 'stream-json'],
|
|
42
45
|
};
|
|
46
|
+
/** Agents the daemon can actually run, derived from the command table above
|
|
47
|
+
* so the `--agent` help and any validation can never drift from it. */
|
|
48
|
+
export const ROUTINE_AGENT_IDS = Object.freeze(Object.keys(AGENT_COMMANDS));
|
|
43
49
|
const ROUTINE_TRANSCRIPT_SPECS = {
|
|
44
50
|
claude: [{ root: ['.claude', 'projects'], ext: '.jsonl' }],
|
|
45
51
|
codex: [{ root: ['.codex', 'sessions'], ext: '.jsonl' }],
|
|
52
|
+
cursor: [{ root: ['.cursor', 'projects'], ext: '.jsonl' }],
|
|
46
53
|
};
|
|
54
|
+
/** Stable working directory for routine children, independent of the daemon's launch cwd. */
|
|
55
|
+
export function routineSpawnCwd(config, configuredRoot = getProjectRoot()) {
|
|
56
|
+
if (!config.repo)
|
|
57
|
+
return os.homedir();
|
|
58
|
+
const [owner, repo] = config.repo.split('/');
|
|
59
|
+
if (configuredRoot) {
|
|
60
|
+
const root = path.resolve(expandLocalHome(configuredRoot));
|
|
61
|
+
if (path.basename(root) === owner)
|
|
62
|
+
return path.join(root, repo);
|
|
63
|
+
}
|
|
64
|
+
return path.join(os.homedir(), 'src', 'github.com', owner, repo);
|
|
65
|
+
}
|
|
47
66
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
48
67
|
export function buildJobCommand(config, resolvedPrompt) {
|
|
49
68
|
// Workflow branch: delegate to `agents run <workflow>` which handles subagent
|
|
@@ -133,6 +152,20 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
133
152
|
}
|
|
134
153
|
appendModelAndReasoning(cmd, config);
|
|
135
154
|
}
|
|
155
|
+
if (config.agent === 'cursor') {
|
|
156
|
+
// cursor-agent supports --plan, but the capability registry has not been
|
|
157
|
+
// upgraded yet. RUSH-2101 tracks adding that read-only mode after PR #1721.
|
|
158
|
+
const cursorMode = resolveHeadlessMode('cursor', mode, false, `routine ${config.name}`);
|
|
159
|
+
if (cursorMode === 'skip') {
|
|
160
|
+
cmd.push('-f');
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// The configured cwd is the user's workspace trust decision. --trust is
|
|
164
|
+
// narrower than --yolo/-f because it does not bypass tool permissions.
|
|
165
|
+
cmd.push('--trust');
|
|
166
|
+
}
|
|
167
|
+
appendModelAndReasoning(cmd, config);
|
|
168
|
+
}
|
|
136
169
|
if (config.agent === 'kimi') {
|
|
137
170
|
// kimi daemon jobs always run headless via `--prompt`, which cannot be
|
|
138
171
|
// combined with any startup-mode flag (--plan/--auto/--yolo all abort with
|
|
@@ -385,7 +418,7 @@ export function dispatchesViaAgentsRun(config) {
|
|
|
385
418
|
* (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
|
|
386
419
|
* with `agents run`.
|
|
387
420
|
*/
|
|
388
|
-
export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
|
|
421
|
+
export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone, overlayHome) {
|
|
389
422
|
const execEnv = buildExecEnv({
|
|
390
423
|
agent,
|
|
391
424
|
version,
|
|
@@ -415,6 +448,11 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
|
|
|
415
448
|
// provisioned box does. Drop it here so a routine always uses the login of the
|
|
416
449
|
// machine it runs on.
|
|
417
450
|
delete out.CLAUDE_CODE_OAUTH_TOKEN;
|
|
451
|
+
if (agent === 'cursor' && overlayHome) {
|
|
452
|
+
// prepareJobHome links this host's Cursor auth file here. Pin XDG_CONFIG_HOME
|
|
453
|
+
// to the overlay so an ambient value cannot bypass the routine sandbox.
|
|
454
|
+
out.XDG_CONFIG_HOME = path.join(overlayHome, '.config');
|
|
455
|
+
}
|
|
418
456
|
if (timezone)
|
|
419
457
|
out.TZ = timezone;
|
|
420
458
|
return out;
|
|
@@ -424,14 +462,14 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
|
|
|
424
462
|
* Rate-limit scanning uses only this attempt's log (not prior failover output).
|
|
425
463
|
* The attempt log is also appended into `combinedLogPath` for a continuous trail.
|
|
426
464
|
*/
|
|
427
|
-
function spawnJobAttempt(cmd, env, attemptLogPath, timeoutMs, combinedLogPath) {
|
|
465
|
+
function spawnJobAttempt(cmd, env, attemptLogPath, timeoutMs, combinedLogPath, cwd = os.homedir()) {
|
|
428
466
|
// Isolate this attempt's output so detectRateLimit never sees prior attempts.
|
|
429
467
|
fs.writeFileSync(attemptLogPath, '', { mode: 0o600 });
|
|
430
468
|
const stdoutFd = fs.openSync(attemptLogPath, 'a', 0o600);
|
|
431
469
|
return new Promise((resolve) => {
|
|
432
470
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
433
471
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
434
|
-
...backgroundSpawnOptions({ fdStdio: true }),
|
|
472
|
+
...backgroundSpawnOptions({ cwd, fdStdio: true }),
|
|
435
473
|
env,
|
|
436
474
|
});
|
|
437
475
|
let settled = false;
|
|
@@ -579,7 +617,7 @@ export async function executeJob(config, deps) {
|
|
|
579
617
|
const runDir = getRunDir(config.name, runId);
|
|
580
618
|
fs.mkdirSync(runDir, { recursive: true });
|
|
581
619
|
const baseEnv = injectRoutineActor(useSandbox
|
|
582
|
-
? buildSpawnEnv(overlayHome)
|
|
620
|
+
? buildSpawnEnv(overlayHome, config.env)
|
|
583
621
|
: { ...process.env }, config);
|
|
584
622
|
// Workflows run via `agents run <workflow>` which delegates to claude under the hood.
|
|
585
623
|
// Use 'claude' as the effective agent for report extraction and metadata when workflow is set.
|
|
@@ -621,16 +659,20 @@ export async function executeJob(config, deps) {
|
|
|
621
659
|
const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
|
|
622
660
|
// Loop path: delegate to runLoop (same driver as `agents run --loop` / workflow loop:).
|
|
623
661
|
if (config.loop) {
|
|
624
|
-
const spawnEnv = buildRoutineSpawnEnv(baseEnv, effectiveAgent, primaryVersion, config.timezone);
|
|
662
|
+
const spawnEnv = buildRoutineSpawnEnv(baseEnv, effectiveAgent, primaryVersion, config.timezone, overlayHome);
|
|
625
663
|
const execOptions = {
|
|
626
664
|
agent: effectiveAgent,
|
|
627
|
-
|
|
665
|
+
// Routine-supported self-updating CLIs (Cursor/Droid) use one global
|
|
666
|
+
// binary; a versioned shim would point at a nonexistent isolated install.
|
|
667
|
+
version: isSelfUpdatingAgent(effectiveAgent) ? undefined : primaryVersion,
|
|
628
668
|
prompt: resolvedPrompt,
|
|
629
669
|
mode: normalizeMode(config.mode),
|
|
630
670
|
effort: config.effort,
|
|
631
671
|
env: spawnEnv,
|
|
632
672
|
json: true,
|
|
633
673
|
headless: true,
|
|
674
|
+
modeWarningContext: `routine ${config.name}`,
|
|
675
|
+
modeWarningState: {},
|
|
634
676
|
...(config.config?.model ? { model: config.config.model } : {}),
|
|
635
677
|
...(config.allow?.dirs ? {
|
|
636
678
|
addDirs: config.allow.dirs
|
|
@@ -681,12 +723,12 @@ export async function executeJob(config, deps) {
|
|
|
681
723
|
e.TZ = config.timezone;
|
|
682
724
|
return e;
|
|
683
725
|
})()
|
|
684
|
-
: buildRoutineSpawnEnv(baseEnv, attemptAgent, attemptVersion, config.timezone);
|
|
726
|
+
: buildRoutineSpawnEnv(baseEnv, attemptAgent, attemptVersion, config.timezone, overlayHome);
|
|
685
727
|
// Remaining timeout budget shared across failover attempts.
|
|
686
728
|
const elapsed = Date.now() - Date.parse(meta.startedAt);
|
|
687
729
|
const remaining = Math.max(1_000, timeoutMs - (Number.isFinite(elapsed) ? elapsed : 0));
|
|
688
730
|
const attemptLogPath = path.join(runDir, `stdout.attempt-${i}.log`);
|
|
689
|
-
const attempt = await spawnJobAttempt(cmd, spawnEnv, attemptLogPath, remaining, stdoutPath);
|
|
731
|
+
const attempt = await spawnJobAttempt(cmd, spawnEnv, attemptLogPath, remaining, stdoutPath, routineSpawnCwd(config));
|
|
690
732
|
meta.pid = attempt.pid;
|
|
691
733
|
writeRunMeta(meta);
|
|
692
734
|
if (attempt.status === 'timeout') {
|
|
@@ -941,7 +983,7 @@ async function executeCommandJobForeground(config) {
|
|
|
941
983
|
const result = await new Promise((resolve) => {
|
|
942
984
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
943
985
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
944
|
-
...backgroundSpawnOptions({ fdStdio: true }),
|
|
986
|
+
...backgroundSpawnOptions({ cwd: routineSpawnCwd(config), fdStdio: true }),
|
|
945
987
|
env,
|
|
946
988
|
});
|
|
947
989
|
meta.pid = child.pid || null;
|
|
@@ -1057,7 +1099,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1057
1099
|
const stdoutPath = path.join(runDir, 'stdout.log');
|
|
1058
1100
|
const stdoutFd = fs.openSync(stdoutPath, 'w', 0o600);
|
|
1059
1101
|
const baseEnv = injectRoutineActor(useSandbox
|
|
1060
|
-
? buildSpawnEnv(overlayHome)
|
|
1102
|
+
? buildSpawnEnv(overlayHome, config.env)
|
|
1061
1103
|
: { ...process.env }, config);
|
|
1062
1104
|
const spawnEnv = dispatchesViaAgentsRun(config)
|
|
1063
1105
|
? (() => {
|
|
@@ -1067,7 +1109,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1067
1109
|
return e;
|
|
1068
1110
|
})()
|
|
1069
1111
|
// Non-command path only: config.agent is always set here (command/workflow branch earlier).
|
|
1070
|
-
: buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone);
|
|
1112
|
+
: buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone, overlayHome);
|
|
1071
1113
|
const effectiveAgent = config.workflow ? 'claude' : config.agent;
|
|
1072
1114
|
const meta = {
|
|
1073
1115
|
jobName: config.name,
|
|
@@ -1106,7 +1148,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1106
1148
|
}
|
|
1107
1149
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
1108
1150
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
1109
|
-
...backgroundSpawnOptions({ fdStdio: true }),
|
|
1151
|
+
...backgroundSpawnOptions({ cwd: routineSpawnCwd(config), fdStdio: true }),
|
|
1110
1152
|
env: spawnEnv,
|
|
1111
1153
|
});
|
|
1112
1154
|
let settled = false;
|
|
@@ -1204,7 +1246,7 @@ function executeCommandJobDetached(config, hooks) {
|
|
|
1204
1246
|
};
|
|
1205
1247
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
1206
1248
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
1207
|
-
...backgroundSpawnOptions({ fdStdio: true }),
|
|
1249
|
+
...backgroundSpawnOptions({ cwd: routineSpawnCwd(config), fdStdio: true }),
|
|
1208
1250
|
env,
|
|
1209
1251
|
});
|
|
1210
1252
|
// Record the real terminal status ourselves — the daemon stays alive after this
|
|
@@ -1262,7 +1304,7 @@ export function extractReport(stdoutPath, agentType) {
|
|
|
1262
1304
|
for (const line of lines) {
|
|
1263
1305
|
try {
|
|
1264
1306
|
const parsed = JSON.parse(line);
|
|
1265
|
-
if (agentType === 'claude') {
|
|
1307
|
+
if (agentType === 'claude' || agentType === 'cursor') {
|
|
1266
1308
|
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
1267
1309
|
for (const block of parsed.message.content) {
|
|
1268
1310
|
if (block.type === 'text' && block.text) {
|
|
@@ -1298,7 +1340,7 @@ export function extractReport(stdoutPath, agentType) {
|
|
|
1298
1340
|
* that carries `is_error`. If we find it, the run completed cleanly (modulo
|
|
1299
1341
|
* agent-reported error). If not, the process likely died mid-stream and the
|
|
1300
1342
|
* caller should treat the run as failed. */
|
|
1301
|
-
function inferFinalStatusFromLog(stdoutPath, agent) {
|
|
1343
|
+
export function inferFinalStatusFromLog(stdoutPath, agent) {
|
|
1302
1344
|
if (!fs.existsSync(stdoutPath))
|
|
1303
1345
|
return null;
|
|
1304
1346
|
try {
|
|
@@ -1309,7 +1351,7 @@ function inferFinalStatusFromLog(stdoutPath, agent) {
|
|
|
1309
1351
|
for (let i = lines.length - 1, scanned = 0; i >= 0 && scanned < 20; i--, scanned++) {
|
|
1310
1352
|
try {
|
|
1311
1353
|
const parsed = JSON.parse(lines[i]);
|
|
1312
|
-
if (agent === 'claude' && parsed.type === 'result') {
|
|
1354
|
+
if ((agent === 'claude' || agent === 'cursor') && parsed.type === 'result') {
|
|
1313
1355
|
return parsed.is_error
|
|
1314
1356
|
? { status: 'failed', exitCode: 1 }
|
|
1315
1357
|
: { status: 'completed', exitCode: 0 };
|
package/dist/lib/sandbox.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export declare function buildSpawnEnv(overlayHome: string, extraEnv?: Record<str
|
|
|
21
21
|
export declare function getJobHomePath(name: string): string;
|
|
22
22
|
/** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
|
|
23
23
|
export declare function prepareJobHome(config: JobConfig): string;
|
|
24
|
+
/** Link this host's Cursor login and CLI config into the disposable overlay. */
|
|
25
|
+
export declare function generateCursorConfig(overlayHome: string): void;
|
|
24
26
|
/** Remove a job's overlay HOME directory entirely. */
|
|
25
27
|
export declare function cleanJobHome(name: string): void;
|
|
26
28
|
/** Symlink allowed directories into the overlay HOME, skipping paths outside the real HOME. */
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -100,11 +100,49 @@ export function prepareJobHome(config) {
|
|
|
100
100
|
else if (config.agent === 'gemini') {
|
|
101
101
|
generateGeminiConfig(overlayHome, config);
|
|
102
102
|
}
|
|
103
|
+
else if (config.agent === 'cursor') {
|
|
104
|
+
generateCursorConfig(overlayHome);
|
|
105
|
+
}
|
|
103
106
|
if (config.allow?.dirs) {
|
|
104
107
|
symlinkAllowedDirs(overlayHome, config.allow.dirs);
|
|
105
108
|
}
|
|
106
109
|
return overlayHome;
|
|
107
110
|
}
|
|
111
|
+
/** Link this host's Cursor login and CLI config into the disposable overlay. */
|
|
112
|
+
export function generateCursorConfig(overlayHome) {
|
|
113
|
+
const realConfigHome = process.env.XDG_CONFIG_HOME || path.join(resolveRealHome(), '.config');
|
|
114
|
+
const realAuth = path.join(realConfigHome, 'cursor', 'auth.json');
|
|
115
|
+
if (!fs.existsSync(realAuth))
|
|
116
|
+
return;
|
|
117
|
+
const overlayCursorDir = path.join(overlayHome, '.config', 'cursor');
|
|
118
|
+
fs.mkdirSync(overlayCursorDir, { recursive: true });
|
|
119
|
+
const overlayAuth = path.join(overlayCursorDir, 'auth.json');
|
|
120
|
+
if (process.platform === 'win32') {
|
|
121
|
+
// File symlinks need Developer Mode on Windows. A same-volume hard link
|
|
122
|
+
// shares the credential inode without copying its contents to another host.
|
|
123
|
+
try {
|
|
124
|
+
fs.linkSync(realAuth, overlayAuth);
|
|
125
|
+
}
|
|
126
|
+
catch { /* cross-volume or link creation refused: Cursor will fail auth loudly */ }
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
fs.symlinkSync(realAuth, overlayAuth);
|
|
130
|
+
}
|
|
131
|
+
// Setting XDG_CONFIG_HOME makes Cursor look for cli-config.json beside auth.json.
|
|
132
|
+
// This file carries account identity (authId, displayName, email, userId) as
|
|
133
|
+
// well as preferences, so it must remain linked rather than copied.
|
|
134
|
+
const realCliConfig = path.join(resolveRealHome(), '.cursor', 'cli-config.json');
|
|
135
|
+
if (fs.existsSync(realCliConfig)) {
|
|
136
|
+
const overlayCliConfig = path.join(overlayCursorDir, 'cli-config.json');
|
|
137
|
+
try {
|
|
138
|
+
if (process.platform === 'win32')
|
|
139
|
+
fs.linkSync(realCliConfig, overlayCliConfig);
|
|
140
|
+
else
|
|
141
|
+
fs.symlinkSync(realCliConfig, overlayCliConfig);
|
|
142
|
+
}
|
|
143
|
+
catch { /* cross-volume or link creation refused: Cursor will fail auth loudly */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
108
146
|
/** Remove a job's overlay HOME directory entirely. */
|
|
109
147
|
export function cleanJobHome(name) {
|
|
110
148
|
const overlayHome = getJobHomePath(name);
|
|
Binary file
|
|
Binary file
|
|
@@ -47,20 +47,19 @@ const keychainStore = {
|
|
|
47
47
|
delete: deleteKeychainToken,
|
|
48
48
|
list: listKeychainItems,
|
|
49
49
|
};
|
|
50
|
-
// The file store auto-provisions a machine-local passphrase on
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
const FILE_ALLOW_AUTO_PROVISION = process.platform !== 'darwin';
|
|
50
|
+
// The file store auto-provisions a stable machine-local passphrase on EVERY
|
|
51
|
+
// platform (macOS included) — a 0600 key file, encryption-at-rest with the same
|
|
52
|
+
// posture as an SSH key — so `agents secrets` "just works" with no passphrase to
|
|
53
|
+
// set, type, or remember, and no Touch ID. Set AGENTS_SECRETS_PASSPHRASE to opt
|
|
54
|
+
// into an off-disk key.
|
|
56
55
|
const fileItemStore = {
|
|
57
56
|
has: (item) => fileStore.has(item),
|
|
58
|
-
get: (item) => fileStore.get(item
|
|
57
|
+
get: (item) => fileStore.get(item),
|
|
59
58
|
getBatch: (items) => {
|
|
60
59
|
const out = new Map();
|
|
61
60
|
for (const item of items) {
|
|
62
61
|
try {
|
|
63
|
-
out.set(item, fileStore.get(item
|
|
62
|
+
out.set(item, fileStore.get(item));
|
|
64
63
|
}
|
|
65
64
|
catch {
|
|
66
65
|
// Missing/undecryptable item — absent from the map, mirroring
|
|
@@ -69,10 +68,10 @@ const fileItemStore = {
|
|
|
69
68
|
}
|
|
70
69
|
return out;
|
|
71
70
|
},
|
|
72
|
-
set: (item, value) => fileStore.set(item, value
|
|
71
|
+
set: (item, value) => fileStore.set(item, value),
|
|
73
72
|
setBatch: (items) => {
|
|
74
73
|
for (const [item, value] of items) {
|
|
75
|
-
fileStore.set(item, value
|
|
74
|
+
fileStore.set(item, value);
|
|
76
75
|
}
|
|
77
76
|
},
|
|
78
77
|
delete: (item) => fileStore.delete(item),
|
|
@@ -116,24 +115,6 @@ export function bundleBackend(name) {
|
|
|
116
115
|
}
|
|
117
116
|
return 'keychain';
|
|
118
117
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Guard a file-backed bundle operation. On macOS the file store must be
|
|
121
|
-
* unlocked with an explicit passphrase (env or interactive prompt) — we refuse
|
|
122
|
-
* to silently auto-provision a machine-local key there, so a remote/headless
|
|
123
|
-
* Mac cannot decrypt on its own. Linux keeps the existing auto-provision
|
|
124
|
-
* behavior, so this is a no-op there.
|
|
125
|
-
*/
|
|
126
|
-
function assertFileBackendUsable(name) {
|
|
127
|
-
if (process.platform !== 'darwin')
|
|
128
|
-
return;
|
|
129
|
-
if (process.env.AGENTS_SECRETS_PASSPHRASE && process.env.AGENTS_SECRETS_PASSPHRASE.length > 0)
|
|
130
|
-
return;
|
|
131
|
-
if (process.stdin.isTTY)
|
|
132
|
-
return;
|
|
133
|
-
throw new Error(`File-backed bundle '${name}' needs AGENTS_SECRETS_PASSPHRASE to be set on macOS ` +
|
|
134
|
-
`(no biometry prompt is available headlessly). Set it for this run, e.g.\n` +
|
|
135
|
-
` AGENTS_SECRETS_PASSPHRASE=… agents secrets exec ${name} -- <command>`);
|
|
136
|
-
}
|
|
137
118
|
function assertVaultBackendUsable(name) {
|
|
138
119
|
if (getVaultSession().loggedIn)
|
|
139
120
|
return;
|
|
@@ -286,8 +267,6 @@ export function readBundleIfDecryptable(name) {
|
|
|
286
267
|
export function readBundle(name) {
|
|
287
268
|
validateBundleName(name);
|
|
288
269
|
const backend = bundleBackend(name);
|
|
289
|
-
if (backend === 'file')
|
|
290
|
-
assertFileBackendUsable(name);
|
|
291
270
|
if (backend === 'vault')
|
|
292
271
|
assertVaultBackendUsable(name);
|
|
293
272
|
let json;
|
|
@@ -395,8 +374,6 @@ export function shouldEvictAfterBundleWrite(skipRequested, noAgentEnv, backendOv
|
|
|
395
374
|
function prepareBundleWrite(bundle) {
|
|
396
375
|
validateBundleName(bundle.name);
|
|
397
376
|
const backend = bundle.backend ?? 'keychain';
|
|
398
|
-
if (backend === 'file')
|
|
399
|
-
assertFileBackendUsable(bundle.name);
|
|
400
377
|
if (backend === 'vault')
|
|
401
378
|
assertVaultBackendUsable(bundle.name);
|
|
402
379
|
for (const key of Object.keys(bundle.vars)) {
|
|
@@ -1177,8 +1154,6 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1177
1154
|
`never raises a Touch ID sheet on its own.`);
|
|
1178
1155
|
}
|
|
1179
1156
|
}
|
|
1180
|
-
if (backend === 'file')
|
|
1181
|
-
assertFileBackendUsable(name);
|
|
1182
1157
|
if (backend === 'vault')
|
|
1183
1158
|
assertVaultBackendUsable(name);
|
|
1184
1159
|
const store = itemStore(backend);
|