@evomap/evolver-core 2.0.0-beta.17 → 2.0.0-beta.19
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/dist/algo/candidateAssembly.js +21 -2
- package/dist/algo/cycleEngine.d.ts +12 -0
- package/dist/algo/cycleEngine.js +36 -4
- package/dist/algo/geneHealth.d.ts +2 -2
- package/dist/algo/geneHealth.js +5 -4
- package/dist/algo/geneSelection.d.ts +1 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/assetstore/assetSidecarRecords.js +4 -0
- package/dist/assetstore/assetStoreHealth.js +41 -24
- package/dist/assetstore/assetStoreStorage.d.ts +1 -1
- package/dist/assetstore/assetStoreStorage.js +16 -7
- package/dist/assetstore/localJsonl.d.ts +2 -1
- package/dist/assetstore/localJsonl.js +54 -10
- package/dist/assetstore/provenance.d.ts +24 -0
- package/dist/assetstore/provenance.js +219 -12
- package/dist/assetstore/provider.d.ts +20 -1
- package/dist/assetstore/provider.js +34 -1
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/v1EnvCompat.d.ts +110 -0
- package/dist/bootstrap/v1EnvCompat.js +256 -0
- package/dist/events/public.d.ts +1 -1
- package/dist/events/public.js +1 -1
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +18 -1
- package/dist/exec/autoExec.js +24 -9
- package/dist/exec/autonomousCycle.d.ts +19 -4
- package/dist/exec/autonomousCycle.js +63 -13
- package/dist/exec/claudeBridge.d.ts +25 -7
- package/dist/exec/claudeBridge.js +264 -29
- package/dist/exec/prompt.js +5 -1
- package/dist/exec/runnerRegistry.d.ts +68 -26
- package/dist/exec/runnerRegistry.js +307 -72
- package/dist/exec/selfPr.js +1 -7
- package/dist/feedback/envelope.d.ts +61 -0
- package/dist/feedback/envelope.js +168 -0
- package/dist/feedback/index.d.ts +1 -0
- package/dist/feedback/index.js +1 -0
- package/dist/hub/assetCallLog.d.ts +35 -1
- package/dist/hub/assetCallLog.js +124 -1
- package/dist/hub/bindings.d.ts +8 -1
- package/dist/hub/bindings.js +17 -6
- package/dist/hub/capability.d.ts +11 -1
- package/dist/hub/fake.d.ts +2 -2
- package/dist/hub/fake.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/mailbox/dispatch.d.ts +1 -1
- package/dist/mailbox/dispatch.js +22 -6
- package/dist/mailbox/envelope.d.ts +7 -1
- package/dist/mailbox/envelope.js +9 -2
- package/dist/mailbox/ipcServer.d.ts +10 -2
- package/dist/mailbox/ipcServer.js +163 -13
- package/dist/mailbox/store.d.ts +38 -2
- package/dist/mailbox/store.js +416 -27
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +202 -0
- package/dist/signals/expand.js +17 -6
- package/dist/signals/index.d.ts +2 -1
- package/dist/signals/index.js +2 -1
- package/dist/strategy/constraintAblation.js +115 -369
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +339 -0
- package/dist/trace/index.d.ts +2 -1
- package/dist/trace/index.js +2 -1
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/verify/validation.d.ts +11 -1
- package/dist/verify/validation.js +31 -0
- package/package.json +4 -1
|
@@ -5,11 +5,35 @@
|
|
|
5
5
|
// nothing here spawns a real agent in tests except through spawnCapture, which the bridge injects fakes around.
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { join as joinPath, delimiter as pathDelimiter } from 'node:path';
|
|
8
|
-
import {
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { chmodSync, closeSync, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
9
10
|
export const DEFAULT_TIMEOUT_MS = 600_000;
|
|
11
|
+
export const MAX_AGENT_SESSION_ID_CHARS = 128;
|
|
12
|
+
const NATIVE_SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10
13
|
/** Per-stream stdout/stderr capture ceiling. A child can emit indefinitely without growing the parent heap. */
|
|
11
14
|
export const DEFAULT_MAX_CAPTURE_BYTES = 1_048_576;
|
|
12
15
|
const MIN_MAX_CAPTURE_BYTES = 256;
|
|
16
|
+
export class AgentSessionResumeError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.name = 'AgentSessionResumeError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Validate before spawn so malformed or cross-harness session targets always fail closed. */
|
|
25
|
+
export function validateAgentSessionResume(resume, expectedRunner) {
|
|
26
|
+
if (resume.runner !== expectedRunner) {
|
|
27
|
+
throw new AgentSessionResumeError('runner_mismatch', `session resume runner '${resume.runner}' does not match selected runner '${expectedRunner}'`);
|
|
28
|
+
}
|
|
29
|
+
if (expectedRunner !== 'claude' && expectedRunner !== 'cursor') {
|
|
30
|
+
throw new AgentSessionResumeError('unsupported_runner', `runner '${expectedRunner}' does not support native session resume`);
|
|
31
|
+
}
|
|
32
|
+
if (!NATIVE_SESSION_ID_PATTERN.test(resume.sessionId)) {
|
|
33
|
+
throw new AgentSessionResumeError('invalid_session_id', `session resume identifier must be ${MAX_AGENT_SESSION_ID_CHARS} characters or fewer and use only letters, numbers, '.', '_', or '-'`);
|
|
34
|
+
}
|
|
35
|
+
return resume;
|
|
36
|
+
}
|
|
13
37
|
/** Thrown when permission bypass is requested without bounding the agent's tools (would be an unbounded autonomous agent). */
|
|
14
38
|
export class UnboundedSkipPermissionsError extends Error {
|
|
15
39
|
constructor() {
|
|
@@ -17,6 +41,13 @@ export class UnboundedSkipPermissionsError extends Error {
|
|
|
17
41
|
this.name = 'UnboundedSkipPermissionsError';
|
|
18
42
|
}
|
|
19
43
|
}
|
|
44
|
+
/** Thrown when Codex permission options cannot be enforced by its CLI. */
|
|
45
|
+
export class UnsupportedCodexPermissionOptionsError extends Error {
|
|
46
|
+
constructor() {
|
|
47
|
+
super('codex runner does not support skipPermissions or allowedTools: the bypass is danger-full-access and Codex has no per-tool allowlist');
|
|
48
|
+
this.name = 'UnsupportedCodexPermissionOptionsError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
20
51
|
/** Thrown when Cursor skipPermissions is requested before the runner can enforce per-run permissions. */
|
|
21
52
|
export class UnsupportedCursorSkipPermissionsError extends Error {
|
|
22
53
|
constructor() {
|
|
@@ -24,6 +55,13 @@ export class UnsupportedCursorSkipPermissionsError extends Error {
|
|
|
24
55
|
this.name = 'UnsupportedCursorSkipPermissionsError';
|
|
25
56
|
}
|
|
26
57
|
}
|
|
58
|
+
/** Thrown when Cursor workspace trust is requested without verified host containment. */
|
|
59
|
+
export class UnsupportedCursorWorkspaceTrustError extends Error {
|
|
60
|
+
constructor() {
|
|
61
|
+
super('cursor runner does not support workspaceTrust: --trust grants host filesystem and network access that a Git worktree cannot contain');
|
|
62
|
+
this.name = 'UnsupportedCursorWorkspaceTrustError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
27
65
|
/** Thrown when Gemini permission options cannot be mapped to a verified bounded CLI contract. */
|
|
28
66
|
export class UnsupportedGeminiPermissionOptionsError extends Error {
|
|
29
67
|
constructor() {
|
|
@@ -164,6 +202,13 @@ export function killWindowsProcessTree(pid, spawnCommand = spawn, timeoutMs = WI
|
|
|
164
202
|
}
|
|
165
203
|
});
|
|
166
204
|
}
|
|
205
|
+
/** Thrown when Cursor allowedTools are requested without a verified per-tool CLI allowlist. */
|
|
206
|
+
export class UnsupportedCursorAllowedToolsError extends Error {
|
|
207
|
+
constructor() {
|
|
208
|
+
super('cursor runner does not support allowedTools: cursor-agent has no verified per-run tool allowlist');
|
|
209
|
+
this.name = 'UnsupportedCursorAllowedToolsError';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
167
212
|
/** A redirected stdout artifact could not be finalized; the subprocess outcome remains available for classification. */
|
|
168
213
|
export class SpawnCaptureFinalizeError extends Error {
|
|
169
214
|
result;
|
|
@@ -353,7 +398,7 @@ export function spawnCapture(cmd, args, opts) {
|
|
|
353
398
|
}
|
|
354
399
|
let child;
|
|
355
400
|
try {
|
|
356
|
-
child = spawn(r.cmd, r.args, {
|
|
401
|
+
child = (opts.spawnCommand ?? spawn)(r.cmd, r.args, {
|
|
357
402
|
cwd: opts.cwd,
|
|
358
403
|
shell: false,
|
|
359
404
|
detached,
|
|
@@ -474,6 +519,43 @@ export function spawnCapture(cmd, args, opts) {
|
|
|
474
519
|
}
|
|
475
520
|
child.stdout?.on('data', (d) => { stdoutCapture.append(d); });
|
|
476
521
|
child.stderr?.on('data', (d) => { stderrCapture.append(d); });
|
|
522
|
+
const stdinCompletion = opts.input === undefined
|
|
523
|
+
? Promise.resolve(undefined)
|
|
524
|
+
: new Promise((resolveInput) => {
|
|
525
|
+
const stdin = child.stdin;
|
|
526
|
+
if (!stdin) {
|
|
527
|
+
resolveInput(new Error('runner stdin is unavailable'));
|
|
528
|
+
void killTree();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
let completed = false;
|
|
532
|
+
const complete = (error) => {
|
|
533
|
+
if (completed)
|
|
534
|
+
return false;
|
|
535
|
+
completed = true;
|
|
536
|
+
resolveInput(error);
|
|
537
|
+
return true;
|
|
538
|
+
};
|
|
539
|
+
stdin.on('error', (error) => {
|
|
540
|
+
if (complete(error instanceof Error ? error : new Error(String(error)))) {
|
|
541
|
+
void killTree();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
stdin.on('finish', () => complete());
|
|
545
|
+
stdin.on('close', () => {
|
|
546
|
+
if (complete(new Error('runner stdin closed before prompt delivery'))) {
|
|
547
|
+
void killTree();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
try {
|
|
551
|
+
stdin.end(opts.input);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
if (complete(error instanceof Error ? error : new Error(String(error)))) {
|
|
555
|
+
void killTree();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
477
559
|
child.on('error', (e) => {
|
|
478
560
|
void settle(() => {
|
|
479
561
|
cleanupOwnedStdoutFile();
|
|
@@ -481,40 +563,43 @@ export function spawnCapture(cmd, args, opts) {
|
|
|
481
563
|
});
|
|
482
564
|
});
|
|
483
565
|
child.on('close', (code) => {
|
|
484
|
-
void
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
566
|
+
void stdinCompletion.then((inputError) => {
|
|
567
|
+
void settle((stdoutFileError) => {
|
|
568
|
+
const stdout = stdoutCapture.result();
|
|
569
|
+
const stderr = stderrCapture.result();
|
|
570
|
+
const result = {
|
|
571
|
+
code,
|
|
572
|
+
stdout: stdout.text,
|
|
573
|
+
stderr: stderr.text,
|
|
574
|
+
termination,
|
|
575
|
+
stdoutBytes: redirectedStdoutBytes ?? stdout.bytes,
|
|
576
|
+
stderrBytes: stderr.bytes,
|
|
577
|
+
stdoutTruncated: stdout.truncated,
|
|
578
|
+
stderrTruncated: stderr.truncated,
|
|
579
|
+
...(opts.stdoutFile ? { stdoutRedirected: true } : {}),
|
|
580
|
+
};
|
|
581
|
+
if (stdoutFileError !== undefined) {
|
|
582
|
+
cleanupOwnedStdoutFile();
|
|
583
|
+
reject(new SpawnCaptureFinalizeError(result, stdoutFileError));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (inputError && termination === 'exit' && code === 0) {
|
|
587
|
+
cleanupOwnedStdoutFile();
|
|
588
|
+
reject(inputError);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
resolve(result);
|
|
592
|
+
});
|
|
504
593
|
});
|
|
505
594
|
});
|
|
506
|
-
if (opts.input !== undefined) {
|
|
507
|
-
child.stdin?.write(opts.input);
|
|
508
|
-
child.stdin?.end();
|
|
509
|
-
}
|
|
510
595
|
});
|
|
511
596
|
}
|
|
512
597
|
/** Map the shared process result into the failure taxonomy used by plain-text runners. */
|
|
513
|
-
export function classifyBasicRunnerResult(runner, result, timeoutMs) {
|
|
598
|
+
export function classifyBasicRunnerResult(runner, result, timeoutMs, resume) {
|
|
514
599
|
if (result.termination === 'timeout') {
|
|
515
600
|
return {
|
|
516
601
|
ok: false,
|
|
517
|
-
output: result.stdout,
|
|
602
|
+
output: resume ? '' : result.stdout,
|
|
518
603
|
error: `${runner} timed out after ${timeoutMs}ms`,
|
|
519
604
|
failureKind: 'timeout',
|
|
520
605
|
exitCode: result.code,
|
|
@@ -523,17 +608,45 @@ export function classifyBasicRunnerResult(runner, result, timeoutMs) {
|
|
|
523
608
|
if (result.termination === 'cancelled') {
|
|
524
609
|
return {
|
|
525
610
|
ok: false,
|
|
526
|
-
output: result.stdout,
|
|
611
|
+
output: resume ? '' : result.stdout,
|
|
527
612
|
error: `${runner} execution cancelled`,
|
|
528
613
|
failureKind: 'cancelled',
|
|
529
614
|
exitCode: result.code,
|
|
530
615
|
};
|
|
531
616
|
}
|
|
617
|
+
// Runner stdout is agent content and may legitimately discuss these errors, including on a later failure.
|
|
618
|
+
const diagnostic = result.stderr;
|
|
619
|
+
if (result.code !== 0 && /permission denied|access denied|not authorized|unauthorized|authentication required|please (?:log|sign) in|login required/i.test(diagnostic)) {
|
|
620
|
+
return {
|
|
621
|
+
ok: false,
|
|
622
|
+
output: resume ? '' : result.stdout,
|
|
623
|
+
error: `${runner} permission denied while executing${resume ? ' resumed session' : ''}`,
|
|
624
|
+
failureKind: 'permission_denied',
|
|
625
|
+
exitCode: result.code,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const escapedResumeId = resume?.sessionId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
629
|
+
const contextualMissingSession = escapedResumeId
|
|
630
|
+
? new RegExp(`(?:session|conversation|chat)\\s+(?:(?:with\\s+)?id[:=]?\\s+)?["']?${escapedResumeId}["']?\\s+(?:was\\s+)?(?:not found|does not exist)`, 'i').test(diagnostic)
|
|
631
|
+
: false;
|
|
632
|
+
if (resume && (contextualMissingSession || /(?:session|conversation|chat) (?:was )?not found|no (?:conversation|session|chat) found|invalid session(?: id)?|unable to resume|cannot resume|(?:session|conversation|chat) does not exist|expired session/i.test(diagnostic))) {
|
|
633
|
+
return {
|
|
634
|
+
ok: false,
|
|
635
|
+
output: '',
|
|
636
|
+
error: `${runner} resume session is missing, stale, or unavailable`,
|
|
637
|
+
failureKind: 'runtime_error',
|
|
638
|
+
exitCode: result.code,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
532
641
|
if (result.code !== 0) {
|
|
642
|
+
const error = result.stderr || `${runner} exited with code ${String(result.code)}`;
|
|
643
|
+
const safeError = resume
|
|
644
|
+
? error.replaceAll(resume.sessionId, '[session-id]')
|
|
645
|
+
: error;
|
|
533
646
|
return {
|
|
534
647
|
ok: false,
|
|
535
648
|
output: result.stdout,
|
|
536
|
-
error:
|
|
649
|
+
error: safeError,
|
|
537
650
|
failureKind: 'non_zero_exit',
|
|
538
651
|
exitCode: result.code,
|
|
539
652
|
};
|
|
@@ -549,12 +662,22 @@ function spawnFailureResult(error) {
|
|
|
549
662
|
exitCode: null,
|
|
550
663
|
};
|
|
551
664
|
}
|
|
665
|
+
export const CLAUDE_SAFE_AUTONOMOUS_TOOLS = ['Read', 'Edit', 'Write', 'Glob', 'Grep'];
|
|
666
|
+
const CLAUDE_SAFE_AUTONOMOUS_TOOL_SET = new Set(CLAUDE_SAFE_AUTONOMOUS_TOOLS);
|
|
667
|
+
export function hasBoundedClaudeFileAccess(opts) {
|
|
668
|
+
return opts?.permissionMode === 'acceptEdits'
|
|
669
|
+
&& opts.skipPermissions !== true
|
|
670
|
+
&& (opts.allowedTools?.length ?? 0) === 0
|
|
671
|
+
&& Array.isArray(opts.tools)
|
|
672
|
+
&& opts.tools.length > 0
|
|
673
|
+
&& opts.tools.every((tool) => CLAUDE_SAFE_AUTONOMOUS_TOOL_SET.has(tool));
|
|
674
|
+
}
|
|
552
675
|
/**
|
|
553
|
-
* Build the `claude -p` argv for the given options (pure
|
|
676
|
+
* Build the `claude -p` argv for the given options (pure and testable without spawning).
|
|
554
677
|
* Safety invariant: skipPermissions (bypassing prompts) is only allowed together with a non-empty
|
|
555
|
-
* allowedTools
|
|
678
|
+
* allowedTools; otherwise it would be an unattended agent with full tools and no gate; refuse loudly.
|
|
556
679
|
*/
|
|
557
|
-
export function claudeRunnerArgs(opts = {}) {
|
|
680
|
+
export function claudeRunnerArgs(opts = {}, resume) {
|
|
558
681
|
const bounded = !!(opts.allowedTools && opts.allowedTools.length > 0);
|
|
559
682
|
if (opts.skipPermissions && !bounded)
|
|
560
683
|
throw new UnboundedSkipPermissionsError();
|
|
@@ -563,22 +686,33 @@ export function claudeRunnerArgs(opts = {}) {
|
|
|
563
686
|
args.push('--dangerously-skip-permissions');
|
|
564
687
|
if (opts.allowedTools && opts.allowedTools.length > 0)
|
|
565
688
|
args.push('--allowedTools', ...opts.allowedTools);
|
|
689
|
+
if (opts.permissionMode)
|
|
690
|
+
args.push('--permission-mode', opts.permissionMode);
|
|
691
|
+
if (opts.tools && opts.tools.length > 0)
|
|
692
|
+
args.push('--tools', opts.tools.join(','));
|
|
693
|
+
if (opts.permissionMode === 'acceptEdits') {
|
|
694
|
+
args.push('--strict-mcp-config', '--disable-slash-commands', '--setting-sources', '');
|
|
695
|
+
}
|
|
566
696
|
if (opts.model)
|
|
567
697
|
args.push('--model', opts.model);
|
|
698
|
+
if (resume) {
|
|
699
|
+
validateAgentSessionResume(resume, 'claude');
|
|
700
|
+
args.push('--resume', resume.sessionId);
|
|
701
|
+
}
|
|
568
702
|
return args;
|
|
569
703
|
}
|
|
570
704
|
/**
|
|
571
|
-
* Build a headless `claude -p` agent runner. Prompt fed via stdin (no shell, no argv length limit).
|
|
572
|
-
* unattended
|
|
573
|
-
* permission prompts but bound the agent to file edits. Validated end to end against a real agent.
|
|
705
|
+
* Build a headless `claude -p` agent runner. Prompt is fed via stdin (no shell, no argv length limit).
|
|
706
|
+
* For unattended edits, prefer permissionMode: 'acceptEdits' with the bounded file/search tool list.
|
|
574
707
|
*/
|
|
575
708
|
export function makeClaudeHeadlessRunner(opts = {}) {
|
|
576
709
|
const args = claudeRunnerArgs(opts);
|
|
577
710
|
return async (prompt, ctx) => {
|
|
578
711
|
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
712
|
+
const runArgs = ctx.resume ? claudeRunnerArgs(opts, ctx.resume) : args;
|
|
579
713
|
try {
|
|
580
|
-
const result = await spawnCapture('claude',
|
|
581
|
-
return classifyBasicRunnerResult('claude', result, timeoutMs);
|
|
714
|
+
const result = await spawnCapture('claude', runArgs, { cwd: ctx.cwd, timeoutMs, input: prompt, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
|
|
715
|
+
return classifyBasicRunnerResult('claude', result, timeoutMs, ctx.resume);
|
|
582
716
|
}
|
|
583
717
|
catch (e) {
|
|
584
718
|
return spawnFailureResult(e);
|
|
@@ -589,34 +723,30 @@ export function makeClaudeHeadlessRunner(opts = {}) {
|
|
|
589
723
|
export const claudeHeadlessRunner = makeClaudeHeadlessRunner();
|
|
590
724
|
// --- Codex runner (#66 multi-harness) ---
|
|
591
725
|
/**
|
|
592
|
-
* Build the `codex exec` argv (pure). Verified live against codex-cli 0.
|
|
593
|
-
* - sandboxed default →
|
|
594
|
-
*
|
|
595
|
-
* -
|
|
596
|
-
*
|
|
597
|
-
* acknowledgement guard, same shape as claude.
|
|
726
|
+
* Build the `codex exec` argv (pure). Verified live against codex-cli 0.144.6:
|
|
727
|
+
* - sandboxed default → `--ask-for-approval never exec --sandbox workspace-write`: edits the workspace
|
|
728
|
+
* without waiting for interactive approval. The wrapper's worktree + allowedRoots are the outer containment.
|
|
729
|
+
* - permission overrides fail closed: Codex has no per-tool allowlist, and a Git worktree does not contain
|
|
730
|
+
* danger-full-access host filesystem or network access.
|
|
598
731
|
*/
|
|
599
732
|
export function codexRunnerArgs(opts = {}) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const args = ['exec'];
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
args.push('--sandbox', opts.skipPermissions ? 'danger-full-access' : 'workspace-write');
|
|
607
|
-
if (opts.skipPermissions)
|
|
608
|
-
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
733
|
+
if (opts.skipPermissions || opts.allowedTools !== undefined) {
|
|
734
|
+
throw new UnsupportedCodexPermissionOptionsError();
|
|
735
|
+
}
|
|
736
|
+
const args = ['--ask-for-approval', 'never', 'exec'];
|
|
737
|
+
args.push('--sandbox', 'workspace-write');
|
|
738
|
+
args.push('--ephemeral');
|
|
609
739
|
if (opts.model)
|
|
610
740
|
args.push('--model', opts.model);
|
|
611
741
|
return args;
|
|
612
742
|
}
|
|
613
|
-
/** Headless `codex exec` runner. Working root pinned with `--cd`; prompt is
|
|
614
|
-
export function makeCodexHeadlessRunner(opts = {}) {
|
|
743
|
+
/** Headless `codex exec` runner. Working root pinned with `--cd`; prompt is sent over stdin. */
|
|
744
|
+
export function makeCodexHeadlessRunner(opts = {}, spawnCaptureFn = spawnCapture) {
|
|
615
745
|
const args = codexRunnerArgs(opts);
|
|
616
746
|
return async (prompt, ctx) => {
|
|
617
747
|
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
618
748
|
try {
|
|
619
|
-
const result = await
|
|
749
|
+
const result = await spawnCaptureFn('codex', [...args, '--cd', ctx.cwd, '-'], { cwd: ctx.cwd, timeoutMs, input: prompt, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
|
|
620
750
|
return classifyBasicRunnerResult('codex', result, timeoutMs);
|
|
621
751
|
}
|
|
622
752
|
catch (e) {
|
|
@@ -696,21 +826,96 @@ export function classifyGeminiRunnerResult(result, timeoutMs) {
|
|
|
696
826
|
export function geminiRunnerArgs(opts = {}) {
|
|
697
827
|
if (opts.skipPermissions || (opts.allowedTools?.length ?? 0) > 0)
|
|
698
828
|
throw new UnsupportedGeminiPermissionOptionsError();
|
|
699
|
-
const args = [
|
|
829
|
+
const args = [
|
|
830
|
+
'--output-format', 'json',
|
|
831
|
+
'--approval-mode', 'auto_edit',
|
|
832
|
+
'--skip-trust',
|
|
833
|
+
'--extensions', 'none',
|
|
834
|
+
'--allowed-mcp-server-names', '__evolver_no_mcp__',
|
|
835
|
+
];
|
|
700
836
|
if (opts.model)
|
|
701
837
|
args.push('--model', opts.model);
|
|
702
838
|
return args;
|
|
703
839
|
}
|
|
840
|
+
const GEMINI_AUTH_FILE_MAX_BYTES = 1_048_576;
|
|
841
|
+
function copyGeminiAuthFile(sourceDir, targetDir, name) {
|
|
842
|
+
const source = joinPath(sourceDir, name);
|
|
843
|
+
try {
|
|
844
|
+
const stat = lstatSync(source);
|
|
845
|
+
if (!stat.isFile() || stat.size > GEMINI_AUTH_FILE_MAX_BYTES)
|
|
846
|
+
return;
|
|
847
|
+
const target = joinPath(targetDir, name);
|
|
848
|
+
copyFileSync(source, target);
|
|
849
|
+
chmodSync(target, 0o600);
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
// Missing or unreadable optional auth state must fail closed in Gemini itself.
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function sanitizedGeminiAuthSettings(sourceDir) {
|
|
856
|
+
try {
|
|
857
|
+
const parsed = JSON.parse(readFileSync(joinPath(sourceDir, 'settings.json'), 'utf8'));
|
|
858
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
859
|
+
return {};
|
|
860
|
+
const security = parsed['security'];
|
|
861
|
+
if (!security || typeof security !== 'object' || Array.isArray(security))
|
|
862
|
+
return {};
|
|
863
|
+
const auth = security['auth'];
|
|
864
|
+
if (!auth || typeof auth !== 'object' || Array.isArray(auth))
|
|
865
|
+
return {};
|
|
866
|
+
const selectedType = auth['selectedType'];
|
|
867
|
+
return typeof selectedType === 'string' && selectedType.length <= 128
|
|
868
|
+
? { security: { auth: { selectedType } } }
|
|
869
|
+
: {};
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
return {};
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function isolatedGeminiEnv(env, removeTempDir) {
|
|
876
|
+
const root = mkdtempSync(joinPath(tmpdir(), 'evolver-gemini-'));
|
|
877
|
+
const globalDir = joinPath(root, '.gemini');
|
|
878
|
+
mkdirSync(globalDir, { mode: 0o700 });
|
|
879
|
+
const sourceHome = env['GEMINI_CLI_HOME'] || env['HOME'];
|
|
880
|
+
const sourceDir = sourceHome ? joinPath(sourceHome, '.gemini') : undefined;
|
|
881
|
+
if (sourceDir) {
|
|
882
|
+
copyGeminiAuthFile(sourceDir, globalDir, 'oauth_creds.json');
|
|
883
|
+
copyGeminiAuthFile(sourceDir, globalDir, 'google_accounts.json');
|
|
884
|
+
}
|
|
885
|
+
const authSettings = sourceDir ? sanitizedGeminiAuthSettings(sourceDir) : {};
|
|
886
|
+
writeFileSync(joinPath(globalDir, 'settings.json'), `${JSON.stringify(authSettings)}\n`, { mode: 0o600 });
|
|
887
|
+
const systemSettings = joinPath(root, 'system-settings.json');
|
|
888
|
+
const systemDefaults = joinPath(root, 'system-defaults.json');
|
|
889
|
+
writeFileSync(systemDefaults, '{}\n', { mode: 0o600 });
|
|
890
|
+
writeFileSync(systemSettings, '{"hooksConfig":{"enabled":false},"admin":{"mcp":{"enabled":false}}}\n', { mode: 0o600 });
|
|
891
|
+
const isolatedEnv = { ...env };
|
|
892
|
+
for (const name of Object.keys(isolatedEnv)) {
|
|
893
|
+
if (name.startsWith('GEMINI_CLI_') || name.startsWith('XDG_'))
|
|
894
|
+
delete isolatedEnv[name];
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
env: {
|
|
898
|
+
...isolatedEnv,
|
|
899
|
+
GEMINI_CLI_HOME: root,
|
|
900
|
+
GEMINI_CLI_SYSTEM_DEFAULTS_PATH: systemDefaults,
|
|
901
|
+
GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettings,
|
|
902
|
+
},
|
|
903
|
+
cleanup: () => removeTempDir(root),
|
|
904
|
+
};
|
|
905
|
+
}
|
|
704
906
|
/** Headless Gemini runner with structured failure classification; stdout text alone never proves execution success. */
|
|
705
|
-
export function makeGeminiHeadlessRunner(opts = {}) {
|
|
907
|
+
export function makeGeminiHeadlessRunner(opts = {}, removeTempDir = (path) => rmSync(path, { recursive: true, force: true })) {
|
|
706
908
|
const args = geminiRunnerArgs(opts);
|
|
707
909
|
return async (prompt, ctx) => {
|
|
910
|
+
let cleanup = () => { };
|
|
708
911
|
try {
|
|
709
912
|
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
913
|
+
const isolated = isolatedGeminiEnv({ ...(ctx.env ?? process.env) }, removeTempDir);
|
|
914
|
+
cleanup = isolated.cleanup;
|
|
710
915
|
const result = await spawnCapture('gemini', [...args, '--prompt', prompt], {
|
|
711
916
|
cwd: ctx.cwd,
|
|
712
917
|
timeoutMs,
|
|
713
|
-
|
|
918
|
+
env: isolated.env,
|
|
714
919
|
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
715
920
|
});
|
|
716
921
|
return classifyGeminiRunnerResult(result, timeoutMs);
|
|
@@ -718,6 +923,14 @@ export function makeGeminiHeadlessRunner(opts = {}) {
|
|
|
718
923
|
catch (error) {
|
|
719
924
|
return { ok: false, output: '', error: error instanceof Error ? error.message : String(error), failureKind: 'spawn_failed', exitCode: null };
|
|
720
925
|
}
|
|
926
|
+
finally {
|
|
927
|
+
try {
|
|
928
|
+
cleanup();
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
// Cleanup is best-effort and must not replace the subprocess result.
|
|
932
|
+
}
|
|
933
|
+
}
|
|
721
934
|
};
|
|
722
935
|
}
|
|
723
936
|
// --- Cursor runner (#66 multi-harness) ---
|
|
@@ -731,47 +944,69 @@ export function makeGeminiHeadlessRunner(opts = {}) {
|
|
|
731
944
|
// - `--model <model>` exists (e.g. gpt-5, sonnet-4, sonnet-4-thinking); `--list-models` enumerates.
|
|
732
945
|
// - auth: `CURSOR_API_KEY` or `--api-key` (the spec's envAllow prefix is CURSOR_).
|
|
733
946
|
// - cursor has its OWN `--sandbox enabled|disabled` and `-w/--worktree`; we still wrap with our git worktree.
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
947
|
+
// Bundle and auth preflight verified on Windows with Cursor Agent 2026.06.15. The Windows launcher is a
|
|
948
|
+
// PowerShell shim and its own version regex rejects the current timestamped version-dir shape. resolveSpawnCommand
|
|
949
|
+
// therefore bypasses both scripts and runs the newest verified node.exe + index.js bundle directly, shell-free.
|
|
950
|
+
// If that known bundle layout cannot be found, the runner still fail-fasts.
|
|
738
951
|
//
|
|
739
|
-
// SAFETY:
|
|
740
|
-
//
|
|
741
|
-
// wrapper worktree still contains default cursor runs, but it is not a permissions/sandbox substitute for skip.
|
|
742
|
-
// Safe default keeps skip OFF (CURSOR_DEFAULT_AGENT_OPTIONS), so the wiring is exercised by tests with fakes.
|
|
952
|
+
// SAFETY: `--trust`, `--force`, permission bypass, and per-tool allowlists remain fail-closed. The worktree is a
|
|
953
|
+
// measurement/patch-containment boundary, not an OS/network sandbox.
|
|
743
954
|
/**
|
|
744
955
|
* Build the `cursor-agent` argv (pure). Ground-truth from `cursor-agent --help` (#66): base `-p --output-format
|
|
745
956
|
* text` (headless, write+shell access). `--model` is a real flag. skipPermissions is rejected until Cursor has a
|
|
746
957
|
* verified per-run allowlist/sandbox mapping; allowedTools is not emitted because cursor has no per-tool allowlist.
|
|
747
958
|
*/
|
|
748
|
-
export function cursorRunnerArgs(opts = {}) {
|
|
959
|
+
export function cursorRunnerArgs(opts = {}, resume, managedWorktreeName) {
|
|
749
960
|
if (opts.skipPermissions)
|
|
750
961
|
throw new UnsupportedCursorSkipPermissionsError();
|
|
962
|
+
if (opts.workspaceTrust !== undefined)
|
|
963
|
+
throw new UnsupportedCursorWorkspaceTrustError();
|
|
964
|
+
if (opts.allowedTools !== undefined) {
|
|
965
|
+
throw new UnsupportedCursorAllowedToolsError();
|
|
966
|
+
}
|
|
751
967
|
const args = ['-p', '--output-format', 'text'];
|
|
752
968
|
if (opts.model)
|
|
753
969
|
args.push('--model', opts.model);
|
|
970
|
+
if (resume) {
|
|
971
|
+
validateAgentSessionResume(resume, 'cursor');
|
|
972
|
+
args.push('--resume', resume.sessionId);
|
|
973
|
+
}
|
|
974
|
+
if (managedWorktreeName) {
|
|
975
|
+
if (!NATIVE_SESSION_ID_PATTERN.test(managedWorktreeName)) {
|
|
976
|
+
throw new AgentSessionResumeError('invalid_session_id', 'managed worktree name is invalid');
|
|
977
|
+
}
|
|
978
|
+
args.push('--worktree', managedWorktreeName, '--skip-worktree-setup');
|
|
979
|
+
}
|
|
754
980
|
return args;
|
|
755
981
|
}
|
|
982
|
+
function cursorManagedWorktreePath(stdout) {
|
|
983
|
+
const match = /^Using worktree: (.+)$/m.exec(stdout);
|
|
984
|
+
return match?.[1]?.trim();
|
|
985
|
+
}
|
|
756
986
|
/**
|
|
757
987
|
* Headless `cursor-agent` runner. Prompt passed as the trailing positional arg (shell:false, no injection risk;
|
|
758
|
-
* docs show `cursor-agent -p "<prompt>"`). cwd is set via spawn.
|
|
759
|
-
*
|
|
988
|
+
* docs show `cursor-agent -p "<prompt>"`). cwd is set via spawn. Workspace trust must be certified by the
|
|
989
|
+
* bridge refuses built-in autonomous Cursor until host containment is verified.
|
|
760
990
|
*/
|
|
761
991
|
export function makeCursorHeadlessRunner(opts = {}, platform = process.platform) {
|
|
762
992
|
const args = cursorRunnerArgs(opts);
|
|
763
993
|
return async (prompt, ctx) => {
|
|
994
|
+
const runArgs = ctx.resume || ctx.managedWorktreeName
|
|
995
|
+
? cursorRunnerArgs(opts, ctx.resume, ctx.managedWorktreeName)
|
|
996
|
+
: args;
|
|
764
997
|
assertCursorRunnerPlatformSupported(platform, ctx.env ?? process.env);
|
|
765
998
|
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
766
999
|
try {
|
|
767
|
-
const result = await spawnCapture('cursor-agent', [...
|
|
1000
|
+
const result = await spawnCapture('cursor-agent', [...runArgs, prompt], {
|
|
768
1001
|
cwd: ctx.cwd,
|
|
769
1002
|
timeoutMs,
|
|
770
1003
|
resolvePlatform: platform,
|
|
771
1004
|
...(ctx.env ? { env: ctx.env } : {}),
|
|
772
1005
|
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
773
1006
|
});
|
|
774
|
-
|
|
1007
|
+
const classified = classifyBasicRunnerResult('cursor', result, timeoutMs, ctx.resume);
|
|
1008
|
+
const managedWorktreePath = cursorManagedWorktreePath(`${result.stdout}\n${result.stderr}`);
|
|
1009
|
+
return managedWorktreePath ? { ...classified, managedWorktreePath } : classified;
|
|
775
1010
|
}
|
|
776
1011
|
catch (e) {
|
|
777
1012
|
return spawnFailureResult(e);
|
package/dist/exec/selfPr.js
CHANGED
|
@@ -6,15 +6,9 @@
|
|
|
6
6
|
// 3. leak scan of the diff before any push 6. cooldown + diff dedup
|
|
7
7
|
// evaluateSelfPr is PURE (decision only); the `gh` call is an injected GhRunner seam so a test never creates a
|
|
8
8
|
// real PR. Nothing here runs unless the caller has wired enabled:true AND a non-empty allowedRoots.
|
|
9
|
-
import {
|
|
9
|
+
import { isWithinRoot } from './claudeBridge.js';
|
|
10
10
|
import { defaultReadManifest, isObfuscatedFile } from './selfPrObfuscation.js';
|
|
11
11
|
export const DEFAULT_SELF_PR_GATES = { minScore: 0.9, minStreak: 2, maxFiles: 5, maxLines: 200, cooldownMs: 24 * 60 * 60 * 1000 };
|
|
12
|
-
/** Whether `child` is the same as or nested under `root` (both resolved absolute). */
|
|
13
|
-
function isWithinRoot(child, root) {
|
|
14
|
-
const c = resolvePath(child);
|
|
15
|
-
const r = resolvePath(root);
|
|
16
|
-
return c === r || c.startsWith(r.endsWith(sep) ? r : r + sep);
|
|
17
|
-
}
|
|
18
12
|
/**
|
|
19
13
|
* Decide whether a self-PR may be created. Pure — combines every gate and returns the FIRST failing reason, so
|
|
20
14
|
* the default (disabled, empty allowlist) is always a clean refusal that runs nothing.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const PRIORITY_AXES: readonly ["task_success", "user_preference", "quality", "safety", "cost", "latency", "other"];
|
|
2
|
+
export declare const LABELS: readonly ["positive", "negative", "mixed", "neutral"];
|
|
3
|
+
export declare const ATTENTION_LEVELS: readonly ["full", "limited", "skimmed", "unknown"];
|
|
4
|
+
export declare const EVIDENCE_KINDS: readonly ["evolution_event", "evolution_outcome", "user_override", "review", "turn", "external"];
|
|
5
|
+
export type FeedbackPriorityAxis = typeof PRIORITY_AXES[number];
|
|
6
|
+
export type FeedbackLabel = typeof LABELS[number];
|
|
7
|
+
export type EvaluatorAttentionLevel = typeof ATTENTION_LEVELS[number];
|
|
8
|
+
export type FeedbackEvidenceKind = typeof EVIDENCE_KINDS[number];
|
|
9
|
+
export interface EvaluatorAttention {
|
|
10
|
+
level: EvaluatorAttentionLevel;
|
|
11
|
+
observed_items?: number;
|
|
12
|
+
elapsed_ms?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface FeedbackEvidenceRef {
|
|
15
|
+
kind: FeedbackEvidenceKind;
|
|
16
|
+
id: string;
|
|
17
|
+
summary?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface FeedbackEnvelope {
|
|
20
|
+
priority_axis: FeedbackPriorityAxis;
|
|
21
|
+
label: FeedbackLabel;
|
|
22
|
+
scalar: number;
|
|
23
|
+
indecision: boolean;
|
|
24
|
+
conflict: boolean;
|
|
25
|
+
evaluator_attention: EvaluatorAttention;
|
|
26
|
+
evidence_ref: FeedbackEvidenceRef;
|
|
27
|
+
uncertainty: number;
|
|
28
|
+
}
|
|
29
|
+
export interface FeedbackAggregate {
|
|
30
|
+
dominant_label: 'positive' | 'negative' | null;
|
|
31
|
+
uncertainty: number;
|
|
32
|
+
sample_count: number;
|
|
33
|
+
}
|
|
34
|
+
export interface FeedbackEnvelopeInput {
|
|
35
|
+
priority_axis?: unknown;
|
|
36
|
+
priorityAxis?: unknown;
|
|
37
|
+
scalar?: unknown;
|
|
38
|
+
indecision?: unknown;
|
|
39
|
+
conflict?: unknown;
|
|
40
|
+
evaluator_attention?: unknown;
|
|
41
|
+
evaluatorAttention?: unknown;
|
|
42
|
+
evidence_ref?: unknown;
|
|
43
|
+
evidenceRef?: unknown;
|
|
44
|
+
}
|
|
45
|
+
export interface FeedbackOutcomeLike {
|
|
46
|
+
score?: unknown;
|
|
47
|
+
user_override?: unknown;
|
|
48
|
+
}
|
|
49
|
+
export declare function clamp01(value: unknown): number;
|
|
50
|
+
export declare function labelFromScalar(value: unknown): FeedbackLabel;
|
|
51
|
+
export declare function normalizeAttention(input: unknown): EvaluatorAttention;
|
|
52
|
+
export declare function evidenceRef(kind: unknown, id: unknown, options?: {
|
|
53
|
+
summary?: unknown;
|
|
54
|
+
}): FeedbackEvidenceRef;
|
|
55
|
+
export declare function normalizeEvidenceRef(input: unknown): FeedbackEvidenceRef;
|
|
56
|
+
export declare function envelopeUncertainty(scalar: unknown, attentionLevel: EvaluatorAttentionLevel, indecision: boolean, conflict: boolean): number;
|
|
57
|
+
export declare function fromScalarFeedback(options?: FeedbackEnvelopeInput | null): FeedbackEnvelope;
|
|
58
|
+
export declare function fromOutcomeScalar(outcome: unknown, options?: Omit<FeedbackEnvelopeInput, 'scalar'>): FeedbackEnvelope | null;
|
|
59
|
+
export declare function withConflict(envelope: FeedbackEnvelope): FeedbackEnvelope;
|
|
60
|
+
export declare function withIndecision(envelope: FeedbackEnvelope): FeedbackEnvelope;
|
|
61
|
+
export declare function aggregateFeedbackEnvelopes(envelopes: readonly FeedbackEnvelope[] | null | undefined): FeedbackAggregate;
|