@evomap/evolver-core 2.0.0-beta.2 → 2.0.0-beta.22
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/assets/gep/genes.jsonl +5 -5
- package/dist/algo/bans.d.ts +10 -1
- package/dist/algo/bans.js +56 -6
- package/dist/algo/candidateAssembly.d.ts +25 -2
- package/dist/algo/candidateAssembly.js +171 -26
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/conversationSniffer.js +25 -8
- package/dist/algo/cycleEngine.d.ts +63 -5
- package/dist/algo/cycleEngine.js +320 -42
- package/dist/algo/cycleFailureClassifier.d.ts +1 -1
- package/dist/algo/evolutionEvent.d.ts +26 -0
- package/dist/algo/evolutionEvent.js +32 -0
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +36 -3
- package/dist/algo/geneHealth.js +47 -4
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +126 -6
- package/dist/algo/geneSelection.js +479 -36
- package/dist/algo/index.d.ts +6 -1
- package/dist/algo/index.js +6 -1
- package/dist/algo/kautoProjection.d.ts +41 -0
- package/dist/algo/kautoProjection.js +95 -0
- package/dist/algo/kautoValidator.d.ts +68 -0
- package/dist/algo/kautoValidator.js +256 -0
- package/dist/algo/memoryGraph.d.ts +62 -0
- package/dist/algo/memoryGraph.js +86 -0
- package/dist/algo/orchestrator.d.ts +17 -1
- package/dist/algo/orchestrator.js +30 -4
- package/dist/algo/publishEligibility.d.ts +34 -0
- package/dist/algo/publishEligibility.js +52 -0
- package/dist/algo/solidify.d.ts +11 -2
- package/dist/algo/solidify.js +37 -7
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetrepair/hubRejection.d.ts +12 -0
- package/dist/assetrepair/hubRejection.js +109 -0
- package/dist/assetrepair/index.d.ts +2 -0
- package/dist/assetrepair/index.js +2 -0
- package/dist/assetrepair/repair.d.ts +33 -0
- package/dist/assetrepair/repair.js +155 -0
- package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
- package/dist/assetstore/assetSidecarRecords.js +384 -0
- package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
- package/dist/assetstore/assetSidecarRecovery.js +288 -0
- package/dist/assetstore/assetStoreHealth.d.ts +81 -0
- package/dist/assetstore/assetStoreHealth.js +319 -0
- package/dist/assetstore/assetStoreLayout.d.ts +2 -0
- package/dist/assetstore/assetStoreLayout.js +6 -0
- package/dist/assetstore/assetStoreStorage.d.ts +42 -0
- package/dist/assetstore/assetStoreStorage.js +336 -0
- package/dist/assetstore/assetSyncLedger.d.ts +91 -1
- package/dist/assetstore/assetSyncLedger.js +718 -59
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +5 -0
- package/dist/assetstore/index.js +5 -0
- package/dist/assetstore/learningHistory.js +3 -3
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +12 -1
- package/dist/assetstore/localJsonl.js +251 -42
- package/dist/assetstore/provenance.d.ts +93 -4
- package/dist/assetstore/provenance.js +368 -84
- package/dist/assetstore/provider.d.ts +63 -0
- package/dist/assetstore/provider.js +97 -6
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +39 -1
- package/dist/assetstore/reviewLedger.d.ts +8 -2
- package/dist/assetstore/reviewLedger.js +71 -45
- package/dist/assetstore/unionReadStore.d.ts +25 -0
- package/dist/assetstore/unionReadStore.js +119 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +3 -1
- package/dist/benchmark/index.js +3 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/benchmark/triggerShift.d.ts +62 -0
- package/dist/benchmark/triggerShift.js +106 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +3 -1
- package/dist/bootstrap/index.js +3 -1
- package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
- package/dist/bootstrap/v1EnvCompat.js +303 -0
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +2 -1
- package/dist/events/ingest.js +15 -0
- package/dist/events/paths.d.ts +12 -10
- package/dist/events/paths.js +24 -20
- package/dist/events/public.d.ts +2 -2
- package/dist/events/public.js +2 -2
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +68 -4
- package/dist/exec/autoExec.js +392 -33
- package/dist/exec/autonomousCycle.d.ts +31 -4
- package/dist/exec/autonomousCycle.js +71 -13
- package/dist/exec/claudeBridge.d.ts +80 -15
- package/dist/exec/claudeBridge.js +845 -76
- package/dist/exec/executionBinding.d.ts +414 -0
- package/dist/exec/executionBinding.js +588 -0
- package/dist/exec/index.d.ts +1 -0
- package/dist/exec/index.js +1 -0
- package/dist/exec/openPrRegistry.d.ts +8 -2
- package/dist/exec/openPrRegistry.js +32 -22
- package/dist/exec/prompt.js +14 -1
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +153 -36
- package/dist/exec/runnerRegistry.js +848 -65
- 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/hooks/hooks.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 +29 -8
- package/dist/hub/capability.d.ts +130 -4
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +3 -2
- package/dist/hub/fake.js +2 -1
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/questionGenerator.d.ts +5 -1
- package/dist/hub/questionGenerator.js +8 -6
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +122 -7
- package/dist/index.d.ts +6 -1
- package/dist/index.js +7 -1
- package/dist/issueReporter/index.d.ts +156 -0
- package/dist/issueReporter/index.js +1688 -0
- 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 +12 -2
- package/dist/mailbox/ipcServer.js +183 -13
- package/dist/mailbox/store.d.ts +89 -3
- package/dist/mailbox/store.js +895 -41
- package/dist/modelCompatibility.d.ts +164 -0
- package/dist/modelCompatibility.js +309 -0
- package/dist/observers/valueDigestObserver.d.ts +9 -0
- package/dist/observers/valueDigestObserver.js +35 -2
- package/dist/ops/cleanup.js +1 -1
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/savingsCore.js +1 -2
- package/dist/ops/selfUpdate.d.ts +18 -1
- package/dist/ops/selfUpdate.js +88 -23
- package/dist/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +24 -24
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/proofOfWork.d.ts +125 -6
- package/dist/schema/proofOfWork.js +102 -4
- package/dist/schema/signal.d.ts +3 -3
- package/dist/schema/signal.js +1 -1
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +202 -0
- package/dist/signals/cycleHistoryFromEvents.js +17 -8
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +169 -1
- package/dist/signals/extractor.d.ts +2 -2
- package/dist/signals/extractor.js +31 -6
- package/dist/signals/index.d.ts +4 -1
- package/dist/signals/index.js +4 -1
- package/dist/signals/metaSignals.d.ts +4 -0
- package/dist/signals/metaSignals.js +42 -0
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/signalGate.js +1 -1
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/strategy/constraintAblation.d.ts +64 -0
- package/dist/strategy/constraintAblation.js +2820 -0
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +339 -0
- package/dist/strategy/index.d.ts +2 -1
- package/dist/strategy/index.js +2 -1
- package/dist/trace/index.d.ts +3 -1
- package/dist/trace/index.js +3 -1
- package/dist/trace/learningTrace.d.ts +216 -0
- package/dist/trace/learningTrace.js +298 -0
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fetchPort.d.ts +1 -0
- package/dist/util/fetchPort.js +11 -0
- package/dist/util/fileLock.d.ts +113 -7
- package/dist/util/fileLock.js +1035 -80
- package/dist/util/index.d.ts +3 -1
- package/dist/util/index.js +2 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +30 -0
- package/dist/verify/sandboxRunner.js +370 -27
- package/dist/verify/sandboxedValidation.d.ts +16 -2
- package/dist/verify/sandboxedValidation.js +200 -22
- package/dist/verify/validation.d.ts +35 -4
- package/dist/verify/validation.js +164 -16
- package/dist/wire/geneHints.d.ts +88 -16
- package/dist/wire/geneHints.js +124 -15
- package/dist/wire/index.d.ts +8 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/schemaGate.d.ts +21 -0
- package/dist/wire/schemaGate.js +119 -10
- package/dist/workflow/dsl.d.ts +24 -3
- package/dist/workflow/dsl.js +4 -0
- package/dist/workflow/engine.d.ts +5 -1
- package/dist/workflow/engine.js +3 -0
- package/dist/workflow/index.d.ts +3 -1
- package/dist/workflow/index.js +3 -1
- package/dist/workflow/runtime.d.ts +110 -0
- package/dist/workflow/runtime.js +1298 -0
- package/dist/workflow/stateStore.d.ts +172 -0
- package/dist/workflow/stateStore.js +1044 -0
- package/package.json +12 -4
|
@@ -11,19 +11,20 @@
|
|
|
11
11
|
// SAFETY: default-OFF. The factory throws ExecBridgeDisabledError unless explicitly enabled (opts.enabled)
|
|
12
12
|
// or EVOLVE_EXEC_BRIDGE === '1'. Wiring it in by accident must fail loudly rather than silently spawn an
|
|
13
13
|
// autonomous agent.
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { basename, dirname, resolve as resolvePath, sep, join as joinPath } from 'node:path';
|
|
16
|
+
import { homedir, tmpdir } from 'node:os';
|
|
17
|
+
import { closeSync, existsSync, lstatSync, mkdtempSync, openSync, readdirSync, realpathSync, rmdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
17
18
|
import { renderExecPrompt } from './prompt.js';
|
|
18
19
|
import { parseGitShortstat, gitDiffProof } from './proofOfWork.js';
|
|
19
20
|
// Policy enforcement core (#107): checkPolicy runs the always-on global guards (blast hard cap +
|
|
20
21
|
// protected paths + destructive deletes) on EVERY exec — gene or not — plus the per-gene constraints when a
|
|
21
22
|
// gene supplies them. It supersedes the old gene-gated `checkChangeConstraints` call (the no-gene-no-guard hole).
|
|
22
23
|
import { checkPolicy, summarizeViolations } from './policy/index.js';
|
|
23
|
-
import { spawnCapture, getRunnerSpec, DEFAULT_TIMEOUT_MS } from './runnerRegistry.js';
|
|
24
|
+
import { spawnCapture, SpawnCaptureFinalizeError, getRunnerSpec, validateAgentSessionResume, DEFAULT_TIMEOUT_MS } from './runnerRegistry.js';
|
|
24
25
|
// Re-export the runner layer so existing importers of ./claudeBridge.js (and the `exec` namespace) keep their
|
|
25
26
|
// surface after the #91-6 split — the registry simply has a clearer home now.
|
|
26
|
-
export { resolveSpawnCommand, spawnCapture, UnboundedSkipPermissionsError, UnsupportedCursorSkipPermissionsError, claudeRunnerArgs, makeClaudeHeadlessRunner, claudeHeadlessRunner, codexRunnerArgs, makeCodexHeadlessRunner, cursorRunnerArgs, makeCursorHeadlessRunner, getRunnerSpec, } from './runnerRegistry.js';
|
|
27
|
+
export { resolveSpawnCommand, spawnCapture, DEFAULT_MAX_CAPTURE_BYTES, MAX_AGENT_SESSION_ID_CHARS, AgentSessionResumeError, validateAgentSessionResume, UnboundedSkipPermissionsError, UnsupportedCodexPermissionOptionsError, UnsupportedCursorAllowedToolsError, UnsupportedCursorSkipPermissionsError, UnsupportedCursorWorkspaceTrustError, UnsupportedGeminiPermissionOptionsError, claudeRunnerArgs, makeClaudeHeadlessRunner, claudeHeadlessRunner, codexRunnerArgs, makeCodexHeadlessRunner, cursorRunnerArgs, makeCursorHeadlessRunner, getRunnerSpec, hasBoundedClaudeFileAccess, CLAUDE_SAFE_AUTONOMOUS_TOOLS, geminiRunnerArgs, makeGeminiHeadlessRunner, classifyGeminiRunnerResult, } from './runnerRegistry.js';
|
|
27
28
|
export class ExecBridgeDisabledError extends Error {
|
|
28
29
|
constructor() {
|
|
29
30
|
super('exec bridge is disabled — set EVOLVE_EXEC_BRIDGE=1 or pass { enabled: true } to enable agent execution');
|
|
@@ -41,12 +42,11 @@ export class ExecBridgeForbiddenError extends Error {
|
|
|
41
42
|
* Thrown when a FULL-ACCESS (or unverified) agent run is requested without worktree isolation. The throwaway
|
|
42
43
|
* worktree is the containment WE control — allowedRoots gates the cwd but cannot stop an auto-approved or
|
|
43
44
|
* unsandboxed process writing/running outside it. Gated runners:
|
|
44
|
-
* - codex
|
|
45
|
-
* workspace-write, so only the skip path is gated.
|
|
45
|
+
* - codex permission overrides are refused by the runner because it has no enforceable per-tool allowlist.
|
|
46
46
|
* - cursor default → gated UNCONDITIONALLY. cursor-agent base `-p` already documents write+shell access, cursor
|
|
47
47
|
* skipPermissions is rejected by the runner layer, and the scaffold remains unverified (#66/#181) — so default
|
|
48
48
|
* cursor still needs the wrapper worktree rather than risk mutating the real tree.
|
|
49
|
-
*
|
|
49
|
+
* Claude is fail-closed below because a tool-name allowlist does not constrain absolute filesystem paths.
|
|
50
50
|
*/
|
|
51
51
|
export class UnsandboxedFullAccessRequiresIsolationError extends Error {
|
|
52
52
|
constructor() {
|
|
@@ -54,11 +54,243 @@ export class UnsandboxedFullAccessRequiresIsolationError extends Error {
|
|
|
54
54
|
this.name = 'UnsandboxedFullAccessRequiresIsolationError';
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
export class WorkspaceTrustRequiresIsolationError extends Error {
|
|
58
|
+
code = 'WORKSPACE_TRUST_REQUIRES_ISOLATION';
|
|
59
|
+
constructor() {
|
|
60
|
+
super('workspaceTrust=isolated-worktree requires isolation=worktree');
|
|
61
|
+
this.name = 'WorkspaceTrustRequiresIsolationError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export class UnsupportedCursorBuiltInRunnerError extends Error {
|
|
65
|
+
constructor() {
|
|
66
|
+
super('built-in cursor autonomous execution is unsupported until host filesystem and network containment is verified');
|
|
67
|
+
this.name = 'UnsupportedCursorBuiltInRunnerError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class UnsupportedClaudeBuiltInRunnerError extends Error {
|
|
71
|
+
constructor() {
|
|
72
|
+
super('built-in claude autonomous execution is unsupported until host filesystem and network containment is verified');
|
|
73
|
+
this.name = 'UnsupportedClaudeBuiltInRunnerError';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export class UnsupportedCodexBuiltInRunnerError extends Error {
|
|
77
|
+
constructor() {
|
|
78
|
+
super('built-in codex autonomous execution is unsupported because workspace-write does not contain host filesystem reads');
|
|
79
|
+
this.name = 'UnsupportedCodexBuiltInRunnerError';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export class UnsafeWorktreePathError extends Error {
|
|
83
|
+
constructor(reason) {
|
|
84
|
+
super(`worktree isolation refused: ${reason}`);
|
|
85
|
+
this.name = 'UnsafeWorktreePathError';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function errorCode(error) {
|
|
89
|
+
return typeof error === 'object' && error !== null && 'code' in error
|
|
90
|
+
? String(error.code)
|
|
91
|
+
: undefined;
|
|
92
|
+
}
|
|
93
|
+
function assertPathAbsent(path) {
|
|
94
|
+
try {
|
|
95
|
+
lstatSync(path);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (errorCode(error) === 'ENOENT')
|
|
99
|
+
return;
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
throw new UnsafeWorktreePathError('reserved destination already exists');
|
|
103
|
+
}
|
|
104
|
+
function reserveWorktreePath() {
|
|
105
|
+
// The random 0700 container is atomically created by the OS. The git destination remains absent inside it,
|
|
106
|
+
// so `git worktree add` cannot adopt an attacker-precreated path from the shared temp directory.
|
|
107
|
+
const container = mkdtempSync(joinPath(tmpdir(), 'evolver-wt-'));
|
|
108
|
+
const containerStat = lstatSync(container);
|
|
109
|
+
if (containerStat.isSymbolicLink() || !containerStat.isDirectory()) {
|
|
110
|
+
throw new UnsafeWorktreePathError('temporary reservation is not a real directory');
|
|
111
|
+
}
|
|
112
|
+
const workDir = joinPath(container, 'worktree');
|
|
113
|
+
assertPathAbsent(workDir);
|
|
114
|
+
return {
|
|
115
|
+
container,
|
|
116
|
+
containerDev: containerStat.dev,
|
|
117
|
+
containerIno: containerStat.ino,
|
|
118
|
+
workDir,
|
|
119
|
+
expectedRealPath: joinPath(realpathSync(container), 'worktree'),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function verifyWorktreePath(reservation) {
|
|
123
|
+
const stat = lstatSync(reservation.workDir);
|
|
124
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
125
|
+
throw new UnsafeWorktreePathError('git worktree destination is not a real directory');
|
|
126
|
+
}
|
|
127
|
+
if (realpathSync(reservation.workDir) !== reservation.expectedRealPath) {
|
|
128
|
+
throw new UnsafeWorktreePathError('git worktree destination resolves outside its reservation');
|
|
129
|
+
}
|
|
130
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
131
|
+
}
|
|
132
|
+
function worktreePathStillOwned(reservation, identity) {
|
|
133
|
+
try {
|
|
134
|
+
const stat = lstatSync(reservation.workDir);
|
|
135
|
+
return !stat.isSymbolicLink()
|
|
136
|
+
&& stat.isDirectory()
|
|
137
|
+
&& stat.dev === identity.dev
|
|
138
|
+
&& stat.ino === identity.ino
|
|
139
|
+
&& realpathSync(reservation.workDir) === reservation.expectedRealPath;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function verifyManagedCursorWorktree(path, expectedName, repoCwd, git, managedRoot) {
|
|
146
|
+
const resolvedPath = resolvePath(path);
|
|
147
|
+
const cursorRoot = realpathSync(managedRoot);
|
|
148
|
+
if (resolvedPath !== path) {
|
|
149
|
+
throw new UnsafeWorktreePathError('Cursor reported a non-canonical worktree path');
|
|
150
|
+
}
|
|
151
|
+
const realPath = realpathSync(resolvedPath);
|
|
152
|
+
if (!isWithinRoot(realPath, cursorRoot)) {
|
|
153
|
+
throw new UnsafeWorktreePathError('Cursor reported a worktree outside its managed root');
|
|
154
|
+
}
|
|
155
|
+
if (basename(realPath) !== expectedName) {
|
|
156
|
+
throw new UnsafeWorktreePathError('Cursor reported an unexpected worktree name');
|
|
157
|
+
}
|
|
158
|
+
const stat = lstatSync(resolvedPath);
|
|
159
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
160
|
+
throw new UnsafeWorktreePathError('Cursor worktree is not a real directory');
|
|
161
|
+
}
|
|
162
|
+
const [repoCommonDir, worktreeCommonDir] = await Promise.all([
|
|
163
|
+
git(['rev-parse', '--path-format=absolute', '--git-common-dir'], repoCwd),
|
|
164
|
+
git(['rev-parse', '--path-format=absolute', '--git-common-dir'], realPath),
|
|
165
|
+
]);
|
|
166
|
+
if (realpathSync(repoCommonDir.trim()) !== realpathSync(worktreeCommonDir.trim())) {
|
|
167
|
+
throw new UnsafeWorktreePathError('Cursor worktree belongs to a different repository');
|
|
168
|
+
}
|
|
169
|
+
return { path: realPath, dev: stat.dev, ino: stat.ino };
|
|
170
|
+
}
|
|
171
|
+
async function cleanupManagedCursorWorktree(identity, git, repoCwd) {
|
|
172
|
+
const stat = lstatSync(identity.path);
|
|
173
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || stat.dev !== identity.dev || stat.ino !== identity.ino) {
|
|
174
|
+
throw new UnsafeWorktreePathError('verified Cursor worktree path changed before cleanup');
|
|
175
|
+
}
|
|
176
|
+
await git(['worktree', 'remove', '--force', identity.path], repoCwd, undefined, { processSignalMode: 'ignore' });
|
|
177
|
+
assertPathAbsent(identity.path);
|
|
178
|
+
}
|
|
179
|
+
async function discoverManagedCursorWorktree(expectedName, repoCwd, git, managedRoot) {
|
|
180
|
+
const cursorRoot = managedRoot;
|
|
181
|
+
if (!existsSync(cursorRoot))
|
|
182
|
+
return undefined;
|
|
183
|
+
const verified = [];
|
|
184
|
+
let validationError;
|
|
185
|
+
for (const entry of readdirSync(cursorRoot, { withFileTypes: true })) {
|
|
186
|
+
if (!entry.isDirectory())
|
|
187
|
+
continue;
|
|
188
|
+
const candidate = joinPath(cursorRoot, entry.name, expectedName);
|
|
189
|
+
if (!existsSync(candidate))
|
|
190
|
+
continue;
|
|
191
|
+
try {
|
|
192
|
+
verified.push(await verifyManagedCursorWorktree(candidate, expectedName, repoCwd, git, cursorRoot));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// Unverified same-name paths are never read or removed.
|
|
196
|
+
validationError ??= error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (verified.length > 1) {
|
|
200
|
+
throw new UnsafeWorktreePathError('multiple verified Cursor worktrees matched the generated name');
|
|
201
|
+
}
|
|
202
|
+
if (verified.length === 0 && validationError)
|
|
203
|
+
throw validationError;
|
|
204
|
+
return verified[0];
|
|
205
|
+
}
|
|
206
|
+
async function resolveManagedCursorWorktree(reportedPath, expectedName, repoCwd, git, managedRoot) {
|
|
207
|
+
if (!reportedPath)
|
|
208
|
+
return discoverManagedCursorWorktree(expectedName, repoCwd, git, managedRoot);
|
|
209
|
+
try {
|
|
210
|
+
return await verifyManagedCursorWorktree(reportedPath, expectedName, repoCwd, git, managedRoot);
|
|
211
|
+
}
|
|
212
|
+
catch (reportedPathError) {
|
|
213
|
+
const discovered = await discoverManagedCursorWorktree(expectedName, repoCwd, git, managedRoot);
|
|
214
|
+
if (discovered)
|
|
215
|
+
return discovered;
|
|
216
|
+
throw reportedPathError;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function removeEmptyReservation(reservation) {
|
|
220
|
+
try {
|
|
221
|
+
const stat = lstatSync(reservation.container);
|
|
222
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()
|
|
223
|
+
|| stat.dev !== reservation.containerDev || stat.ino !== reservation.containerIno)
|
|
224
|
+
return;
|
|
225
|
+
rmdirSync(reservation.container);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
// Never recurse through a path that may have been replaced. Empty, owned reservations are the only thing
|
|
229
|
+
// removed directly; non-empty or already-gone containers are intentionally left for safe diagnosis.
|
|
230
|
+
if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(errorCode(error) ?? ''))
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function cleanupWorktreeReservation(reservation, identity, git, repoCwd) {
|
|
235
|
+
let cleanupIdentity = identity;
|
|
236
|
+
if (!cleanupIdentity) {
|
|
237
|
+
try {
|
|
238
|
+
cleanupIdentity = verifyWorktreePath(reservation);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (errorCode(error) !== 'ENOENT')
|
|
242
|
+
throw error;
|
|
243
|
+
// `git worktree add` may register metadata before failing without creating the destination. Give Git a
|
|
244
|
+
// signal-shielded chance to prune that partial registration, then reclaim only the still-empty container.
|
|
245
|
+
try {
|
|
246
|
+
await git(['worktree', 'remove', '--force', reservation.workDir], repoCwd, undefined, { processSignalMode: 'ignore' });
|
|
247
|
+
}
|
|
248
|
+
catch { /* no child exists, so an unregistered cleanup failure is harmless */ }
|
|
249
|
+
removeEmptyReservation(reservation);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (cleanupIdentity) {
|
|
254
|
+
if (!worktreePathStillOwned(reservation, cleanupIdentity)) {
|
|
255
|
+
throw new UnsafeWorktreePathError('verified worktree path changed before cleanup');
|
|
256
|
+
}
|
|
257
|
+
await git(['worktree', 'remove', '--force', reservation.workDir], repoCwd, undefined, { processSignalMode: 'ignore' });
|
|
258
|
+
assertPathAbsent(reservation.workDir);
|
|
259
|
+
}
|
|
260
|
+
removeEmptyReservation(reservation);
|
|
261
|
+
}
|
|
262
|
+
/** Resolve existing path components so an allowlisted symlink cannot redirect execution outside its root. */
|
|
263
|
+
function canonicalPath(path) {
|
|
264
|
+
let current = resolvePath(path);
|
|
265
|
+
const missing = [];
|
|
266
|
+
while (true) {
|
|
267
|
+
try {
|
|
268
|
+
return resolvePath(realpathSync(current), ...missing.reverse());
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
const code = error.code;
|
|
272
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
273
|
+
return undefined;
|
|
274
|
+
const parent = dirname(current);
|
|
275
|
+
if (parent === current)
|
|
276
|
+
return undefined;
|
|
277
|
+
missing.push(basename(current));
|
|
278
|
+
current = parent;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Whether `child` is the same as, or nested under, `root`, including filesystem symlink resolution. */
|
|
283
|
+
export function isWithinRoot(child, root) {
|
|
284
|
+
return canonicalPathWithinRoots(child, [root]) !== undefined;
|
|
285
|
+
}
|
|
286
|
+
function canonicalPathWithinRoots(child, roots) {
|
|
287
|
+
const c = canonicalPath(child);
|
|
288
|
+
if (!c)
|
|
289
|
+
return undefined;
|
|
290
|
+
return roots.some((root) => {
|
|
291
|
+
const r = canonicalPath(root);
|
|
292
|
+
return Boolean(r && (c === r || c.startsWith(r.endsWith(sep) ? r : r + sep)));
|
|
293
|
+
}) ? c : undefined;
|
|
62
294
|
}
|
|
63
295
|
/**
|
|
64
296
|
* Sensitive env keys to strip before spawning an agent/tool (finding #39.2): evolver/hub/cloud secrets must
|
|
@@ -76,6 +308,11 @@ const ALLOW_ENV_KEYS = new Set([
|
|
|
76
308
|
'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LANGUAGE', 'TERM', 'TZ', 'PWD', 'HOSTNAME',
|
|
77
309
|
'TMPDIR', 'TMP', 'TEMP', 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
|
|
78
310
|
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
311
|
+
// Node's built-in fetch (undici) does NOT honor HTTP(S)_PROXY unless NODE_USE_ENV_PROXY=1 is set (Node 20+).
|
|
312
|
+
// An agent CLI that uses global fetch (e.g. the gemini CLI on system Node) is otherwise unreachable behind a
|
|
313
|
+
// proxy even though the proxy vars above are allowed. This is a neutral runtime knob, not a secret — pair it
|
|
314
|
+
// with the proxy vars so a proxied agent can actually reach its API. Absent (unset) → no-op.
|
|
315
|
+
'NODE_USE_ENV_PROXY',
|
|
79
316
|
// Windows OS essentials (absent on POSIX → no-op there): a process can't run, resolve PATHEXT shims, or find
|
|
80
317
|
// its own per-user config/auth dir without these. They are OS runtime vars, NOT app secrets — the secret
|
|
81
318
|
// env (NODE_SECRET/AWS_*/GH_TOKEN/vendor keys) is still denied by the whitelist. Their absence is why an
|
|
@@ -124,49 +361,284 @@ export function scrubAgentEnv(env, opts = {}) {
|
|
|
124
361
|
}
|
|
125
362
|
return out;
|
|
126
363
|
}
|
|
127
|
-
|
|
128
|
-
|
|
364
|
+
class ExecBridgeRunCancelledError extends Error {
|
|
365
|
+
constructor() {
|
|
366
|
+
super('exec bridge run cancelled');
|
|
367
|
+
this.name = 'ExecBridgeRunCancelledError';
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* 绑定执行不能接受无法收到权威截止时间取消信号的旧验证器或写入器。
|
|
372
|
+
* AbortSignal 本身是协作式的;先拒绝不暴露该信号的回调,避免旧 hook
|
|
373
|
+
* 被静默地当作无界工作运行。
|
|
374
|
+
*/
|
|
375
|
+
export class ExecBridgeCancellationContractError extends Error {
|
|
376
|
+
constructor(hook) {
|
|
377
|
+
super(`bound execution ${hook} must accept AbortSignal so deadline cancellation can be enforced`);
|
|
378
|
+
this.name = 'ExecBridgeCancellationContractError';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
class ExecBridgeRunTimedOutError extends Error {
|
|
382
|
+
constructor() {
|
|
383
|
+
super('exec bridge run timed out');
|
|
384
|
+
this.name = 'ExecBridgeRunTimedOutError';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* 将绑定运行时限制应用到完整 bridge 生命周期,而不仅是 agent 子进程。验证和补丁持久化等每个操作都会
|
|
389
|
+
* 收到同一个派生信号。无法接受该信号的回调会在绑定运行启动前被拒绝(见
|
|
390
|
+
* ExecBridgeCancellationContractError),因此下面的 Promise.race 仅作为不遵守协作式取消约定时的最后
|
|
391
|
+
* 响应边界,而不是隐式放行旧 hook。
|
|
392
|
+
*/
|
|
393
|
+
function createExecutionDeadline(parentSignal, maxRuntimeMs) {
|
|
394
|
+
const controller = new AbortController();
|
|
395
|
+
let expired = false;
|
|
396
|
+
let rejectInterrupt = () => { };
|
|
397
|
+
const interrupt = new Promise((_, reject) => { rejectInterrupt = reject; });
|
|
398
|
+
const onParentAbort = () => {
|
|
399
|
+
if (controller.signal.aborted)
|
|
400
|
+
return;
|
|
401
|
+
controller.abort(parentSignal?.reason ?? new ExecBridgeRunCancelledError());
|
|
402
|
+
rejectInterrupt(new ExecBridgeRunCancelledError());
|
|
403
|
+
};
|
|
404
|
+
const timer = maxRuntimeMs === undefined ? undefined : setTimeout(() => {
|
|
405
|
+
expired = true;
|
|
406
|
+
controller.abort(new ExecBridgeRunTimedOutError());
|
|
407
|
+
rejectInterrupt(new ExecBridgeRunTimedOutError());
|
|
408
|
+
}, Math.min(maxRuntimeMs, 2_147_483_647));
|
|
409
|
+
parentSignal?.addEventListener('abort', onParentAbort, { once: true });
|
|
410
|
+
if (parentSignal?.aborted)
|
|
411
|
+
onParentAbort();
|
|
412
|
+
return {
|
|
413
|
+
// 未配置本地运行时定时器时保留调用方显式传入的 parent signal,维持既有取消身份;绑定运行始终使用
|
|
414
|
+
// 带截止时间的派生信号。
|
|
415
|
+
signal: maxRuntimeMs === undefined && parentSignal ? parentSignal : controller.signal,
|
|
416
|
+
expired: () => expired,
|
|
417
|
+
assertActive: () => {
|
|
418
|
+
if (expired)
|
|
419
|
+
throw new ExecBridgeRunTimedOutError();
|
|
420
|
+
if (parentSignal?.aborted)
|
|
421
|
+
throw new ExecBridgeRunCancelledError();
|
|
422
|
+
},
|
|
423
|
+
run: async (operation) => {
|
|
424
|
+
if (expired)
|
|
425
|
+
throw new ExecBridgeRunTimedOutError();
|
|
426
|
+
if (parentSignal?.aborted)
|
|
427
|
+
throw new ExecBridgeRunCancelledError();
|
|
428
|
+
const value = await Promise.race([Promise.resolve().then(operation), interrupt]);
|
|
429
|
+
if (expired)
|
|
430
|
+
throw new ExecBridgeRunTimedOutError();
|
|
431
|
+
if (parentSignal?.aborted)
|
|
432
|
+
throw new ExecBridgeRunCancelledError();
|
|
433
|
+
return value;
|
|
434
|
+
},
|
|
435
|
+
dispose: () => {
|
|
436
|
+
if (timer)
|
|
437
|
+
clearTimeout(timer);
|
|
438
|
+
parentSignal?.removeEventListener('abort', onParentAbort);
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
class AgentRunBeforeManagedWorktreeError extends Error {
|
|
443
|
+
constructor() {
|
|
444
|
+
super('agent run failed before a managed worktree was created');
|
|
445
|
+
this.name = 'AgentRunBeforeManagedWorktreeError';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
class GitProofError extends Error {
|
|
449
|
+
constructor(message) {
|
|
450
|
+
super(message);
|
|
451
|
+
this.name = 'GitProofError';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
class GitOutputTruncatedError extends GitProofError {
|
|
455
|
+
constructor(bytes) {
|
|
456
|
+
super(`git output exceeded the capture limit (${String(bytes ?? 'unknown')} bytes)`);
|
|
457
|
+
this.name = 'GitOutputTruncatedError';
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function cancelledExecutionResult(run) {
|
|
461
|
+
return {
|
|
462
|
+
outcome: { status: 'failed', score: 0.1, reason: 'execution cancelled' },
|
|
463
|
+
strongEvidence: false,
|
|
464
|
+
failureKind: 'cancelled',
|
|
465
|
+
exitCode: run?.exitCode ?? null,
|
|
466
|
+
...(run ? { sessionLog: run.error ? `${run.output}\n${run.error}` : run.output } : {}),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function timedOutExecutionResult(run) {
|
|
470
|
+
return {
|
|
471
|
+
outcome: { status: 'failed', score: 0.1, reason: 'execution timed out' },
|
|
472
|
+
strongEvidence: false,
|
|
473
|
+
failureKind: 'timeout',
|
|
474
|
+
exitCode: run?.exitCode ?? null,
|
|
475
|
+
...(run ? { sessionLog: run.error ? `${run.output}\n${run.error}` : run.output } : {}),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function failedProofExecutionResult(run, error, proofOfWork) {
|
|
479
|
+
return {
|
|
480
|
+
outcome: { status: 'failed', score: 0.1, reason: `execution proof failed: ${error.message}` },
|
|
481
|
+
...(proofOfWork ? { proofOfWork } : {}),
|
|
482
|
+
strongEvidence: false,
|
|
483
|
+
failureKind: run.failureKind ?? 'runtime_error',
|
|
484
|
+
exitCode: run.exitCode ?? null,
|
|
485
|
+
sessionLog: run.error ? `${run.output}\n${run.error}` : run.output,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function failedAgentRunResult(run) {
|
|
489
|
+
return {
|
|
490
|
+
outcome: { status: 'failed', score: 0.1, reason: run.error ?? run.failureKind ?? 'agent run failed' },
|
|
491
|
+
strongEvidence: false,
|
|
492
|
+
...(run.failureKind !== undefined ? { failureKind: run.failureKind } : {}),
|
|
493
|
+
...(run.exitCode !== undefined ? { exitCode: run.exitCode } : {}),
|
|
494
|
+
sessionLog: run.error ? `${run.output}\n${run.error}` : run.output,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/** Default git runner: every incomplete command result fails closed. */
|
|
498
|
+
export const defaultGitRunner = async (args, cwd, signal, options) => {
|
|
499
|
+
let result;
|
|
129
500
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
501
|
+
result = await spawnCapture('git', args, {
|
|
502
|
+
cwd,
|
|
503
|
+
timeoutMs: 30_000,
|
|
504
|
+
env: scrubAgentEnv(process.env),
|
|
505
|
+
...(signal ? { signal } : {}),
|
|
506
|
+
...(options?.processSignalMode ? { processSignalMode: options.processSignalMode } : {}),
|
|
507
|
+
});
|
|
132
508
|
}
|
|
133
509
|
catch {
|
|
134
|
-
|
|
510
|
+
throw new GitProofError('git command failed to start or capture output');
|
|
135
511
|
}
|
|
512
|
+
if (result.termination === 'cancelled')
|
|
513
|
+
throw new ExecBridgeRunCancelledError();
|
|
514
|
+
if (result.termination === 'timeout')
|
|
515
|
+
throw new GitProofError('git command timed out');
|
|
516
|
+
if (result.stdoutTruncated)
|
|
517
|
+
throw new GitOutputTruncatedError(result.stdoutBytes);
|
|
518
|
+
if (result.code !== 0)
|
|
519
|
+
throw new GitProofError(`git command exited with code ${result.code}`);
|
|
520
|
+
return result.stdout;
|
|
136
521
|
};
|
|
522
|
+
/** Stream a complete git patch to disk so large diffs never need to be retained in the Node heap. */
|
|
523
|
+
async function writeDefaultGitPatch(args, cwd, destination, signal, onDestinationOpened) {
|
|
524
|
+
const result = await spawnCapture('git', args, {
|
|
525
|
+
cwd,
|
|
526
|
+
timeoutMs: 30_000,
|
|
527
|
+
env: scrubAgentEnv(process.env),
|
|
528
|
+
stdoutFile: destination,
|
|
529
|
+
...(onDestinationOpened ? { onStdoutFileOpened: onDestinationOpened } : {}),
|
|
530
|
+
...(signal ? { signal } : {}),
|
|
531
|
+
});
|
|
532
|
+
if (result.termination === 'cancelled')
|
|
533
|
+
throw new ExecBridgeRunCancelledError();
|
|
534
|
+
if (result.termination === 'timeout')
|
|
535
|
+
throw new GitProofError('git patch capture timed out');
|
|
536
|
+
if (result.code !== 0)
|
|
537
|
+
throw new GitProofError(`git patch capture exited with code ${result.code}`);
|
|
538
|
+
}
|
|
539
|
+
export const defaultGitPatchWriter = (args, cwd, destination, signal, onDestinationOpened) => (writeDefaultGitPatch(args, cwd, destination, signal, onDestinationOpened));
|
|
540
|
+
function writePrivatePatchFile(path, patch) {
|
|
541
|
+
let fd;
|
|
542
|
+
let ownsFile = false;
|
|
543
|
+
try {
|
|
544
|
+
fd = openSync(path, 'wx', 0o600);
|
|
545
|
+
ownsFile = true;
|
|
546
|
+
writeFileSync(fd, patch, { encoding: 'utf8' });
|
|
547
|
+
closeSync(fd);
|
|
548
|
+
fd = undefined;
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
if (fd !== undefined) {
|
|
552
|
+
try {
|
|
553
|
+
closeSync(fd);
|
|
554
|
+
}
|
|
555
|
+
catch { /* best-effort close before removing our artifact */ }
|
|
556
|
+
}
|
|
557
|
+
if (ownsFile) {
|
|
558
|
+
try {
|
|
559
|
+
rmSync(path, { force: true });
|
|
560
|
+
}
|
|
561
|
+
catch { /* preserve the persistence failure */ }
|
|
562
|
+
}
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
137
566
|
/**
|
|
138
567
|
* Build the `execute` function CycleEngine/runEvolutionCycle consume. Default-off: throws
|
|
139
568
|
* ExecBridgeDisabledError on first call unless enabled.
|
|
140
569
|
*/
|
|
141
|
-
export function makeClaudeExecBridge(opts) {
|
|
570
|
+
export function makeClaudeExecBridge(opts, internal) {
|
|
571
|
+
if (internal?.cursorWorktreeRoot && !opts.agent) {
|
|
572
|
+
throw new Error('custom Cursor worktree root requires an injected agent');
|
|
573
|
+
}
|
|
574
|
+
const cursorWorktreeRoot = internal?.cursorWorktreeRoot ?? joinPath(homedir(), '.cursor', 'worktrees');
|
|
142
575
|
const enabled = opts.enabled ?? (process.env['EVOLVE_EXEC_BRIDGE'] === '1');
|
|
143
|
-
|
|
144
|
-
|
|
576
|
+
if (opts.agentOptions?.workspaceTrust === 'isolated-worktree' && opts.isolation !== 'worktree') {
|
|
577
|
+
throw new WorkspaceTrustRequiresIsolationError();
|
|
578
|
+
}
|
|
579
|
+
const runnerName = opts.runner ?? 'claude';
|
|
580
|
+
const unsupportedBuiltInClaude = runnerName === 'claude' && !opts.agent;
|
|
581
|
+
const unsupportedBuiltInCodex = runnerName === 'codex' && !opts.agent;
|
|
582
|
+
const spec = getRunnerSpec(runnerName); // #66: provider-neutral runner registry
|
|
583
|
+
const resume = opts.resume ? validateAgentSessionResume(opts.resume, spec.name) : undefined;
|
|
584
|
+
// Do not construct uncontained built-in runners. Keep the bridge factory side-effect free so
|
|
585
|
+
// default-OFF callers retain the documented first-call failure semantics.
|
|
586
|
+
const agent = opts.agent ?? (unsupportedBuiltInClaude || unsupportedBuiltInCodex
|
|
587
|
+
? (() => {
|
|
588
|
+
if (unsupportedBuiltInCodex)
|
|
589
|
+
throw new UnsupportedCodexBuiltInRunnerError();
|
|
590
|
+
throw new UnsupportedClaudeBuiltInRunnerError();
|
|
591
|
+
})
|
|
592
|
+
: spec.makeRunner(opts.agentOptions));
|
|
145
593
|
const git = opts.git ?? defaultGitRunner;
|
|
146
|
-
const
|
|
594
|
+
const gitPatchWriter = opts.gitPatchWriter ?? (git === defaultGitRunner ? defaultGitPatchWriter : undefined);
|
|
595
|
+
const timeoutMs = Math.min(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, opts.executionLimits?.maxRuntimeMs ?? Number.MAX_SAFE_INTEGER);
|
|
596
|
+
// 绑定 patch writer 属于受保护的执行生命周期。没有第四个 AbortSignal 参数的旧三参数 writer 无法
|
|
597
|
+
// 观察我们传入的取消信号,不能让它在截止时间后继续运行。
|
|
598
|
+
if (opts.executionLimits && opts.gitPatchWriter && opts.gitPatchWriter.length < 4) {
|
|
599
|
+
throw new ExecBridgeCancellationContractError('gitPatchWriter');
|
|
600
|
+
}
|
|
601
|
+
if (opts.executionLimits && opts.validate && opts.validate.length < 4) {
|
|
602
|
+
throw new ExecBridgeCancellationContractError('validate');
|
|
603
|
+
}
|
|
147
604
|
// secure by default; only THIS runner's auth env reaches it (claude never gets OPENAI_, codex never ANTHROPIC_, #66)
|
|
148
605
|
const agentEnv = opts.scrubEnv === false ? undefined : scrubAgentEnv(process.env, { allowPrefixes: spec.envAllow.prefixes, ...(spec.envAllow.keys ? { allowKeys: spec.envAllow.keys } : {}) });
|
|
149
606
|
// Fail-fast when a run that can write/execute against the tree with no inner sandbox we control is requested
|
|
150
607
|
// without the throwaway worktree as containment (allowedRoots gates the cwd but can't stop a write outside it).
|
|
151
608
|
// Only when WE build the runner — an injected `agent` bypasses the built-in runner:
|
|
152
|
-
// - codex:
|
|
609
|
+
// - codex: fail-closed above because workspace-write does not contain host reads.
|
|
153
610
|
// - cursor: gated UNCONDITIONALLY. cursor-agent base `-p` already has write+shell access, its skipPermissions
|
|
154
611
|
// path is refused by runnerRegistry, and the runner is an unverified scaffold (#66/#181), so we do not let
|
|
155
612
|
// default cursor touch the real tree until run-verified. (Bugbot High #181)
|
|
156
|
-
//
|
|
157
|
-
const needsIsolation = opts.runner === 'cursor'
|
|
158
|
-
|| (opts.runner === 'codex' && opts.agentOptions?.skipPermissions === true);
|
|
613
|
+
// Built-in Claude and Codex are fail-closed above.
|
|
614
|
+
const needsIsolation = opts.runner === 'cursor' || opts.runner === 'gemini';
|
|
159
615
|
if (!opts.agent && needsIsolation && opts.isolation !== 'worktree') {
|
|
160
616
|
throw new UnsandboxedFullAccessRequiresIsolationError();
|
|
161
617
|
}
|
|
618
|
+
if (opts.runner === 'cursor' && !opts.agent) {
|
|
619
|
+
throw new UnsupportedCursorBuiltInRunnerError();
|
|
620
|
+
}
|
|
162
621
|
return async (mutation, decision) => {
|
|
163
622
|
if (!enabled)
|
|
164
623
|
throw new ExecBridgeDisabledError();
|
|
165
624
|
// Deny-by-default guardrail: an autonomous agent may only edit allowlisted repos. Checked before the
|
|
166
625
|
// agent is ever spawned, so a forbidden cwd never runs anything.
|
|
167
|
-
|
|
626
|
+
const repoCwd = opts.allowedRoots === undefined
|
|
627
|
+
? opts.cwd
|
|
628
|
+
: canonicalPathWithinRoots(opts.cwd, opts.allowedRoots);
|
|
629
|
+
if (!repoCwd) {
|
|
168
630
|
throw new ExecBridgeForbiddenError(opts.cwd);
|
|
169
631
|
}
|
|
632
|
+
if (unsupportedBuiltInClaude)
|
|
633
|
+
throw new UnsupportedClaudeBuiltInRunnerError();
|
|
634
|
+
if (unsupportedBuiltInCodex)
|
|
635
|
+
throw new UnsupportedCodexBuiltInRunnerError();
|
|
636
|
+
const assertRepoCwdStillAllowed = () => {
|
|
637
|
+
if (opts.allowedRoots !== undefined
|
|
638
|
+
&& canonicalPathWithinRoots(repoCwd, opts.allowedRoots) !== repoCwd) {
|
|
639
|
+
throw new ExecBridgeForbiddenError(opts.cwd);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
170
642
|
const resolveId = decision.selectedAssetId ?? decision.selectedGeneId;
|
|
171
643
|
const resolved = resolveId && opts.resolveGene ? await opts.resolveGene(resolveId) : null;
|
|
172
644
|
// Trust gate (finding #39.3): for unattended runs, only embed a gene we trust — an untrusted gene's strategy
|
|
@@ -180,63 +652,360 @@ export function makeClaudeExecBridge(opts) {
|
|
|
180
652
|
// use-case ①: inject the personality style block from the state applySelectForRun just persisted.
|
|
181
653
|
...(opts.personality ? { personality: opts.personality.currentState() } : {}),
|
|
182
654
|
});
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
const workDir = isolate ? joinPath(tmpdir(), `evolver-wt-${mutation.id}`) : opts.cwd;
|
|
186
|
-
if (isolate)
|
|
187
|
-
await git(['worktree', 'add', '--detach', workDir, 'HEAD'], opts.cwd);
|
|
655
|
+
assertRepoCwdStillAllowed();
|
|
656
|
+
const executionDeadline = createExecutionDeadline(opts.signal, opts.executionLimits?.maxRuntimeMs);
|
|
188
657
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
658
|
+
// Isolation: atomically reserve an unpredictable private temp container, then let git create the absent
|
|
659
|
+
// worktree path inside it. Cleanup is armed only after git succeeds and the resulting directory is verified.
|
|
660
|
+
const isolate = opts.isolation === 'worktree';
|
|
661
|
+
const managedCursorIsolation = isolate && opts.runner === 'cursor' && resume?.runner === 'cursor';
|
|
662
|
+
if (executionDeadline.expired())
|
|
663
|
+
return timedOutExecutionResult(undefined);
|
|
664
|
+
if (opts.signal?.aborted)
|
|
665
|
+
return cancelledExecutionResult(undefined);
|
|
666
|
+
const reservation = isolate ? reserveWorktreePath() : undefined;
|
|
667
|
+
let workDir = reservation?.workDir ?? repoCwd;
|
|
668
|
+
let worktreeIdentity;
|
|
669
|
+
let managedCursorIdentity;
|
|
670
|
+
const managedWorktreeName = managedCursorIsolation ? `evolver-${randomUUID()}` : undefined;
|
|
671
|
+
let result;
|
|
672
|
+
let observedRun;
|
|
200
673
|
let patchRef;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
674
|
+
let ownsPatchRef = false;
|
|
675
|
+
let preservePatchRef = false;
|
|
676
|
+
let failedProof;
|
|
677
|
+
const proofGit = async (args, cwd, checkCancellationAfter = true) => {
|
|
678
|
+
executionDeadline.assertActive();
|
|
679
|
+
if (cwd === repoCwd)
|
|
680
|
+
assertRepoCwdStillAllowed();
|
|
681
|
+
try {
|
|
682
|
+
const output = await executionDeadline.run(() => git(args, cwd, executionDeadline.signal));
|
|
683
|
+
if (checkCancellationAfter)
|
|
684
|
+
executionDeadline.assertActive();
|
|
685
|
+
return output;
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
if (error instanceof ExecBridgeRunCancelledError || error instanceof ExecBridgeRunTimedOutError || opts.signal?.aborted || executionDeadline.expired())
|
|
689
|
+
throw error;
|
|
690
|
+
if (error instanceof GitProofError)
|
|
691
|
+
throw error;
|
|
692
|
+
throw new GitProofError('git state proof failed');
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
try {
|
|
696
|
+
if (reservation) {
|
|
697
|
+
await proofGit(['worktree', 'add', '--detach', workDir, 'HEAD'], repoCwd, false);
|
|
698
|
+
worktreeIdentity = verifyWorktreePath(reservation);
|
|
699
|
+
if (opts.signal?.aborted)
|
|
700
|
+
throw new ExecBridgeRunCancelledError();
|
|
701
|
+
}
|
|
702
|
+
if (!reservation)
|
|
703
|
+
assertRepoCwdStillAllowed();
|
|
704
|
+
const run = await executionDeadline.run(() => agent(prompt, {
|
|
705
|
+
cwd: workDir,
|
|
706
|
+
timeoutMs,
|
|
707
|
+
...(agentEnv ? { env: agentEnv } : {}),
|
|
708
|
+
...(executionDeadline.signal ? { signal: executionDeadline.signal } : {}),
|
|
709
|
+
...(resume ? { resume } : {}),
|
|
710
|
+
...(managedWorktreeName ? { managedWorktreeName } : {}),
|
|
711
|
+
}));
|
|
712
|
+
observedRun = run;
|
|
713
|
+
await executionDeadline.run(() => opts.executionObserver?.onToolDecision?.({
|
|
714
|
+
tool_name: 'agent_runner',
|
|
715
|
+
decision: 'allowed',
|
|
716
|
+
status: run.ok ? 'completed' : 'failed',
|
|
717
|
+
}, executionDeadline.signal));
|
|
718
|
+
if (managedWorktreeName) {
|
|
719
|
+
try {
|
|
720
|
+
assertRepoCwdStillAllowed();
|
|
721
|
+
managedCursorIdentity = await executionDeadline.run(() => resolveManagedCursorWorktree(run.managedWorktreePath, managedWorktreeName, repoCwd, git, cursorWorktreeRoot));
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
if (error instanceof ExecBridgeRunTimedOutError || executionDeadline.expired())
|
|
725
|
+
throw new ExecBridgeRunTimedOutError();
|
|
726
|
+
if (run.failureKind === 'cancelled' || opts.signal?.aborted)
|
|
727
|
+
throw new ExecBridgeRunCancelledError();
|
|
728
|
+
if (!run.ok)
|
|
729
|
+
throw new AgentRunBeforeManagedWorktreeError();
|
|
730
|
+
if (error instanceof UnsafeWorktreePathError) {
|
|
731
|
+
throw new GitProofError(`Cursor managed worktree verification failed: ${error.message}`);
|
|
732
|
+
}
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
if (!managedCursorIdentity) {
|
|
736
|
+
if (executionDeadline.expired())
|
|
737
|
+
throw new ExecBridgeRunTimedOutError();
|
|
738
|
+
if (run.failureKind === 'cancelled' || opts.signal?.aborted)
|
|
739
|
+
throw new ExecBridgeRunCancelledError();
|
|
740
|
+
if (!run.ok)
|
|
741
|
+
throw new AgentRunBeforeManagedWorktreeError();
|
|
742
|
+
throw new GitProofError('Cursor managed worktree could not be verified');
|
|
743
|
+
}
|
|
744
|
+
workDir = managedCursorIdentity.path;
|
|
745
|
+
}
|
|
746
|
+
// Learning trace (slice 2): one model.called per agent spawn — the headless runner is one opaque
|
|
747
|
+
// model-driven turn from the bridge's viewpoint (per-request fidelity arrives via the proxy's llm_turn
|
|
748
|
+
// records; recordLlmTurn folds those when a caller has them). Best-effort: never affects the result.
|
|
749
|
+
try {
|
|
750
|
+
opts.traceRecorder?.modelCalled({
|
|
751
|
+
...(opts.runner !== undefined ? { provider: opts.runner } : { provider: 'claude' }),
|
|
752
|
+
...(opts.agentOptions?.model !== undefined ? { model: opts.agentOptions.model } : {}),
|
|
753
|
+
...(run.exitCode !== undefined && run.exitCode !== null ? { stopReason: `exit_${run.exitCode}` } : {}),
|
|
754
|
+
});
|
|
755
|
+
// Prefer an authoritative native session id from the runner when present. This covers Gemini and
|
|
756
|
+
// any future harness that reports a session id without also writing proxy llm_turn rows.
|
|
757
|
+
if (typeof run.sessionId === 'string' && run.sessionId.length > 0) {
|
|
758
|
+
try {
|
|
759
|
+
opts.traceRecorder?.bindSessionId(run.sessionId);
|
|
760
|
+
}
|
|
761
|
+
catch { /* observability only */ }
|
|
762
|
+
}
|
|
763
|
+
if (!run.ok) {
|
|
764
|
+
opts.traceRecorder?.toolFailed({
|
|
765
|
+
toolName: 'agent_runner',
|
|
766
|
+
error: run.error ?? run.failureKind ?? 'agent run failed',
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch { /* trace emission is observability only */ }
|
|
771
|
+
executionDeadline.assertActive();
|
|
772
|
+
// A worktree can contain three independent change surfaces after the agent exits: staged tracked changes,
|
|
773
|
+
// unstaged tracked changes, and untracked files. `git diff` alone sees only the second. In an isolated
|
|
774
|
+
// worktree it is safe to mark untracked files intent-to-add temporarily, which makes one `git diff HEAD`
|
|
775
|
+
// snapshot cover all three without staging their contents or disturbing the agent's existing staged state.
|
|
776
|
+
// Reset only those temporary index entries before validation so hooks observe the state the agent left.
|
|
777
|
+
const untrackedFiles = isolate
|
|
778
|
+
? (await proofGit(['ls-files', '--others', '--exclude-standard', '-z'], workDir)).split('\0').filter(Boolean)
|
|
779
|
+
: [];
|
|
780
|
+
if (untrackedFiles.length > 0)
|
|
781
|
+
await proofGit(['add', '--intent-to-add', '--', ...untrackedFiles], workDir);
|
|
782
|
+
let stat;
|
|
783
|
+
let changedFiles;
|
|
784
|
+
let numstat;
|
|
785
|
+
let patch = '';
|
|
786
|
+
const resetIntentToAdd = async () => {
|
|
787
|
+
if (untrackedFiles.length > 0) {
|
|
788
|
+
await executionDeadline.run(() => git(['reset', '--quiet', '--', ...untrackedFiles], workDir, executionDeadline.signal));
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
try {
|
|
792
|
+
stat = parseGitShortstat(await proofGit(['diff', '--shortstat', 'HEAD'], workDir));
|
|
793
|
+
changedFiles = (await proofGit(['diff', '--name-only', 'HEAD'], workDir)).split('\n').map((s) => s.trim()).filter(Boolean);
|
|
794
|
+
numstat = await proofGit(['diff', '--numstat', 'HEAD'], workDir);
|
|
795
|
+
if (isolate && stat.files > 0) {
|
|
796
|
+
if (gitPatchWriter) {
|
|
797
|
+
if (workDir === repoCwd)
|
|
798
|
+
assertRepoCwdStillAllowed();
|
|
799
|
+
const patchDestination = joinPath(tmpdir(), `evolver-patch-${randomUUID()}.diff`);
|
|
800
|
+
patchRef = patchDestination;
|
|
801
|
+
try {
|
|
802
|
+
await executionDeadline.run(() => gitPatchWriter(['diff', '--binary', '--full-index', 'HEAD'], workDir, patchDestination, executionDeadline.signal, () => { ownsPatchRef = true; }));
|
|
803
|
+
// A successful writer owns its result even when an older injected implementation ignores the
|
|
804
|
+
// optional callback. On rejection, only the callback can prove that a partial file is ours.
|
|
805
|
+
ownsPatchRef = true;
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
if (error instanceof ExecBridgeRunCancelledError || error instanceof ExecBridgeRunTimedOutError || opts.signal?.aborted || executionDeadline.expired())
|
|
809
|
+
throw error;
|
|
810
|
+
if (error instanceof SpawnCaptureFinalizeError) {
|
|
811
|
+
if (error.result.termination === 'cancelled')
|
|
812
|
+
throw new ExecBridgeRunCancelledError();
|
|
813
|
+
if (error.result.termination === 'timeout')
|
|
814
|
+
throw new GitProofError('git patch capture timed out');
|
|
815
|
+
}
|
|
816
|
+
if (error instanceof GitProofError)
|
|
817
|
+
throw error;
|
|
818
|
+
throw new GitProofError('git patch capture failed');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
patch = await proofGit(['diff', '--binary', '--full-index', 'HEAD'], workDir);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
try {
|
|
828
|
+
await resetIntentToAdd();
|
|
829
|
+
}
|
|
830
|
+
catch (cleanupError) {
|
|
831
|
+
if (cleanupError instanceof ExecBridgeRunCancelledError || opts.signal?.aborted) {
|
|
832
|
+
throw new ExecBridgeRunCancelledError();
|
|
833
|
+
}
|
|
834
|
+
if (cleanupError instanceof ExecBridgeRunTimedOutError || executionDeadline.expired()) {
|
|
835
|
+
throw new ExecBridgeRunTimedOutError();
|
|
836
|
+
}
|
|
837
|
+
// Preserve the primary proof failure for non-cancellation cleanup errors.
|
|
838
|
+
}
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
await resetIntentToAdd();
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
if (error instanceof ExecBridgeRunCancelledError || error instanceof ExecBridgeRunTimedOutError || opts.signal?.aborted || executionDeadline.expired())
|
|
846
|
+
throw error;
|
|
847
|
+
if (patchRef && ownsPatchRef) {
|
|
848
|
+
failedProof = gitDiffProof(stat, patchRef);
|
|
849
|
+
preservePatchRef = true;
|
|
850
|
+
}
|
|
851
|
+
throw new GitProofError('git index cleanup failed');
|
|
852
|
+
}
|
|
853
|
+
executionDeadline.assertActive();
|
|
854
|
+
// ENFORCE policy against the ACTUAL diff (finding: prompt.ts only ADVISES the agent "touch at most N
|
|
855
|
+
// file(s) / never modify X"; this is the hard gate). checkPolicy ALWAYS runs the global guards — the
|
|
856
|
+
// system blast hard cap (EVOLVER_HARD_CAP_FILES/LINES), the critical-protected paths (.env, MEMORY.md,
|
|
857
|
+
// package.json, the evolver skill, …), and destructive deletes of those paths — so a no-gene / no-
|
|
858
|
+
// constraints run is no longer un-guarded. The gene's max_files/max_lines/forbidden_paths layer on top.
|
|
859
|
+
// Any violation fails the cycle no matter what the agent did — even when validation would pass.
|
|
860
|
+
const bindingConstraints = opts.executionLimits
|
|
861
|
+
? { max_files: opts.executionLimits.maxFiles, max_lines: opts.executionLimits.maxLines }
|
|
862
|
+
: undefined;
|
|
863
|
+
const constraints = gene?.constraints && bindingConstraints
|
|
864
|
+
? {
|
|
865
|
+
max_files: Math.min(gene.constraints.max_files ?? Number.MAX_SAFE_INTEGER, bindingConstraints.max_files ?? Number.MAX_SAFE_INTEGER),
|
|
866
|
+
max_lines: Math.min(gene.constraints.max_lines ?? Number.MAX_SAFE_INTEGER, bindingConstraints.max_lines ?? Number.MAX_SAFE_INTEGER),
|
|
867
|
+
...(gene.constraints.forbidden_paths ? { forbidden_paths: gene.constraints.forbidden_paths } : {}),
|
|
868
|
+
}
|
|
869
|
+
: gene?.constraints ?? bindingConstraints;
|
|
870
|
+
const violations = checkPolicy({ stat, changedFiles, numstat, ...(constraints ? { constraints } : {}) });
|
|
871
|
+
await executionDeadline.run(() => opts.executionObserver?.onPolicyDecision?.({ version: 'policy.v1', allowed: violations.length === 0, violations }, executionDeadline.signal));
|
|
872
|
+
if (isolate && stat.files > 0 && !patchRef) {
|
|
873
|
+
// preserve the isolated edits as a patch (the worktree itself is removed); the real repo is untouched
|
|
874
|
+
const destination = joinPath(tmpdir(), `evolver-patch-${randomUUID()}.diff`);
|
|
875
|
+
try {
|
|
876
|
+
(opts.writePatchFile ?? writePrivatePatchFile)(destination, patch);
|
|
877
|
+
patchRef = destination;
|
|
878
|
+
ownsPatchRef = true;
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
if (error instanceof GitProofError)
|
|
882
|
+
throw error;
|
|
883
|
+
throw new GitProofError('git patch persistence failed');
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const proof = gitDiffProof(stat, patchRef);
|
|
887
|
+
if (proof.git_diff?.patch_ref) {
|
|
888
|
+
const patchReference = proof.git_diff.patch_ref;
|
|
889
|
+
await executionDeadline.run(() => opts.executionObserver?.onProofReference?.({ kind: proof.kind, ref: patchReference }, executionDeadline.signal));
|
|
890
|
+
}
|
|
891
|
+
// Success: prefer the authoritative validation hook; otherwise "agent succeeded AND produced a diff".
|
|
892
|
+
let passed = run.ok && stat.files > 0;
|
|
893
|
+
let score = passed ? 0.7 : run.ok ? 0.4 : 0.1; // ran-but-no-change is weak, not a clean failure
|
|
894
|
+
// Only validate a change that already respects the constraints — a constraint-violating diff is never
|
|
895
|
+
// a success regardless of what its tests say.
|
|
896
|
+
if (run.ok && stat.files > 0 && opts.validate && violations.length === 0) {
|
|
897
|
+
executionDeadline.assertActive();
|
|
898
|
+
const v = await executionDeadline.run(() => opts.validate(mutation, decision, workDir, executionDeadline.signal));
|
|
899
|
+
if (v.validator)
|
|
900
|
+
await executionDeadline.run(() => opts.executionObserver?.onValidatorDecision?.(v.validator, executionDeadline.signal));
|
|
901
|
+
else
|
|
902
|
+
await executionDeadline.run(() => opts.executionObserver?.onValidatorDecision?.({
|
|
903
|
+
id: 'validation', version: 'validation.v1', status: 'ran', passed: v.passed, score: v.score,
|
|
904
|
+
results: [], skipped: [], isolated: false,
|
|
905
|
+
}, executionDeadline.signal));
|
|
906
|
+
passed = v.passed;
|
|
907
|
+
score = v.score ?? (v.passed ? 0.9 : 0.2);
|
|
908
|
+
}
|
|
909
|
+
else if (opts.validate && violations.length > 0) {
|
|
910
|
+
await executionDeadline.run(() => opts.executionObserver?.onValidatorDecision?.({
|
|
911
|
+
id: 'validation', version: 'validation.v1', status: 'not_run', reason: 'policy_denied',
|
|
912
|
+
results: [], skipped: [], isolated: false,
|
|
913
|
+
}, executionDeadline.signal));
|
|
914
|
+
}
|
|
915
|
+
let reason;
|
|
916
|
+
if (violations.length > 0) {
|
|
917
|
+
passed = false;
|
|
918
|
+
score = Math.min(score, 0.1);
|
|
919
|
+
reason = summarizeViolations(violations);
|
|
920
|
+
}
|
|
921
|
+
preservePatchRef = patchRef !== undefined && ownsPatchRef;
|
|
922
|
+
const failureIdentity = !passed && stat.files > 0
|
|
923
|
+
? { rootAttemptId: mutation.id, executionId: randomUUID() }
|
|
924
|
+
: undefined;
|
|
925
|
+
result = {
|
|
926
|
+
outcome: { status: passed ? 'success' : 'failed', score, ...(reason ? { reason } : {}) },
|
|
927
|
+
proofOfWork: proof,
|
|
928
|
+
strongEvidence: passed && stat.files > 0,
|
|
929
|
+
...(failureIdentity ? { failureIdentity } : {}),
|
|
930
|
+
...(run.failureKind !== undefined ? { failureKind: run.failureKind } : {}),
|
|
931
|
+
...(run.exitCode !== undefined ? { exitCode: run.exitCode } : {}),
|
|
932
|
+
...(run.sessionId ? { nativeSessionId: run.sessionId } : {}),
|
|
933
|
+
// On a FAILED outcome, hand the agent transcript (stdout + stderr) to the cycle engine as host-side
|
|
934
|
+
// triage context (#279): an empty transcript -> host_no_transcript, a provider-error string ->
|
|
935
|
+
// host_provider_error. Omitted on success (failure-only context; never persisted).
|
|
936
|
+
...(passed ? {} : { sessionLog: run.error ? `${run.output}\n${run.error}` : run.output }),
|
|
937
|
+
};
|
|
205
938
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
939
|
+
catch (error) {
|
|
940
|
+
if (error instanceof ExecBridgeRunTimedOutError || executionDeadline.expired()) {
|
|
941
|
+
result = timedOutExecutionResult(observedRun);
|
|
942
|
+
}
|
|
943
|
+
else if (error instanceof ExecBridgeRunCancelledError || opts.signal?.aborted) {
|
|
944
|
+
result = cancelledExecutionResult(observedRun);
|
|
945
|
+
}
|
|
946
|
+
else if (error instanceof AgentRunBeforeManagedWorktreeError && observedRun) {
|
|
947
|
+
result = failedAgentRunResult(observedRun);
|
|
948
|
+
}
|
|
949
|
+
else if (error instanceof GitProofError && observedRun) {
|
|
950
|
+
result = failedProofExecutionResult(observedRun, error, failedProof);
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
if (managedCursorIdentity) {
|
|
954
|
+
try {
|
|
955
|
+
assertRepoCwdStillAllowed();
|
|
956
|
+
await cleanupManagedCursorWorktree(managedCursorIdentity, git, repoCwd);
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
// Preserve the primary execution error; the verified worktree remains diagnosable.
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (reservation) {
|
|
963
|
+
try {
|
|
964
|
+
assertRepoCwdStillAllowed();
|
|
965
|
+
await cleanupWorktreeReservation(reservation, worktreeIdentity, git, repoCwd);
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
// Preserve the primary execution error. The abandoned reservation remains private and diagnosable.
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
216
973
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
974
|
+
finally {
|
|
975
|
+
if (!preservePatchRef && ownsPatchRef && patchRef) {
|
|
976
|
+
try {
|
|
977
|
+
(opts.removePatchFile ?? ((path) => rmSync(path, { force: true })))(patchRef);
|
|
978
|
+
}
|
|
979
|
+
catch { /* best-effort cleanup must not replace the execution result or its primary failure */ }
|
|
980
|
+
}
|
|
222
981
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
proofOfWork: proof,
|
|
226
|
-
strongEvidence: passed && stat.files > 0,
|
|
227
|
-
// On a FAILED outcome, hand the agent transcript (stdout + stderr) to the cycle engine as host-side
|
|
228
|
-
// triage context (#279): an empty transcript -> host_no_transcript, a provider-error string ->
|
|
229
|
-
// host_provider_error. Omitted on success (failure-only context; never persisted).
|
|
230
|
-
...(passed ? {} : { sessionLog: run.error ? `${run.output}\n${run.error}` : run.output }),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
finally {
|
|
234
|
-
if (isolate) {
|
|
982
|
+
let cleanupError;
|
|
983
|
+
if (managedCursorIdentity) {
|
|
235
984
|
try {
|
|
236
|
-
|
|
985
|
+
assertRepoCwdStillAllowed();
|
|
986
|
+
await cleanupManagedCursorWorktree(managedCursorIdentity, git, repoCwd);
|
|
987
|
+
}
|
|
988
|
+
catch (error) {
|
|
989
|
+
cleanupError = error;
|
|
237
990
|
}
|
|
238
|
-
catch { /* best-effort cleanup */ }
|
|
239
991
|
}
|
|
992
|
+
if (reservation) {
|
|
993
|
+
try {
|
|
994
|
+
assertRepoCwdStillAllowed();
|
|
995
|
+
await cleanupWorktreeReservation(reservation, worktreeIdentity, git, repoCwd);
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
cleanupError ??= error;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// A successful run must not hide abandoned edits/resources. Both cleanup paths are attempted first so one
|
|
1002
|
+
// failure cannot strand the other worktree. Failed and cancelled results retain their primary classification.
|
|
1003
|
+
if (cleanupError && result.outcome.status === 'success')
|
|
1004
|
+
throw cleanupError;
|
|
1005
|
+
return result;
|
|
1006
|
+
}
|
|
1007
|
+
finally {
|
|
1008
|
+
executionDeadline.dispose();
|
|
240
1009
|
}
|
|
241
1010
|
};
|
|
242
1011
|
}
|