self-bench 0.3.0 → 0.3.3
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/.dockerignore +9 -0
- package/Dockerfile +38 -0
- package/Dockerfile.sandbox +24 -0
- package/README.md +103 -171
- package/biome.json +18 -0
- package/bun.lock +1182 -0
- package/compose.yaml +85 -0
- package/dist/agent-smoke-main.js +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +40 -2
- package/dist/api.js.map +1 -1
- package/dist/artifacts.d.ts +0 -1
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +0 -3
- package/dist/artifacts.js.map +1 -1
- package/dist/build-metadata.d.ts +2 -0
- package/dist/build-metadata.d.ts.map +1 -0
- package/dist/build-metadata.js +2 -0
- package/dist/build-metadata.js.map +1 -0
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/codex-review.d.ts +2 -1
- package/dist/codex-review.d.ts.map +1 -1
- package/dist/codex-review.js +26 -22
- package/dist/codex-review.js.map +1 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -2
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/eval-main.js +3 -3
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +12 -7
- package/dist/evaluate.js.map +1 -1
- package/dist/provenance.d.ts +0 -1
- package/dist/provenance.d.ts.map +1 -1
- package/dist/provenance.js.map +1 -1
- package/dist/reaudit-main.js +10 -7
- package/dist/reaudit-main.js.map +1 -1
- package/dist/repair-main.js +8 -5
- package/dist/repair-main.js.map +1 -1
- package/dist/review/assets/index-CZ3G759O.js +1568 -0
- package/dist/review/assets/index-DgSLzX8l.css +1 -0
- package/dist/review/assets/{mojo-Blby8dQK.js → mojo-DJz3ZmWd.js} +1 -1
- package/dist/review/assets/typst-BUadGCkm.js +1 -0
- package/dist/review/index.html +2 -2
- package/dist/sandbox-repair.bundle.js +8 -7
- package/dist/sandbox-repair.js +7 -7
- package/dist/sandbox-repair.js.map +1 -1
- package/dist/sandbox-review.bundle.js +29 -25
- package/dist/sandbox-review.js +5 -3
- package/dist/sandbox-review.js.map +1 -1
- package/dist/sandbox-validation-repair.bundle.js +9 -9
- package/dist/sandbox-validation-repair.js +11 -9
- package/dist/sandbox-validation-repair.js.map +1 -1
- package/dist/subscription-auth.d.ts +12 -1
- package/dist/subscription-auth.d.ts.map +1 -1
- package/dist/subscription-auth.js +16 -16
- package/dist/subscription-auth.js.map +1 -1
- package/dist/{activities.d.ts → temporal/activities.d.ts} +2 -3
- package/dist/temporal/activities.d.ts.map +1 -0
- package/dist/{activities.js → temporal/activities.js} +44 -33
- package/dist/temporal/activities.js.map +1 -0
- package/dist/{temporal.d.ts → temporal/connection.d.ts} +2 -2
- package/dist/temporal/connection.d.ts.map +1 -0
- package/dist/{temporal.js → temporal/connection.js} +1 -1
- package/dist/temporal/connection.js.map +1 -0
- package/dist/{worker-main.d.ts.map → temporal/worker-main.d.ts.map} +1 -1
- package/dist/{worker-main.js → temporal/worker-main.js} +5 -9
- package/dist/temporal/worker-main.js.map +1 -0
- package/dist/{workflow.d.ts → temporal/workflow.d.ts} +1 -1
- package/dist/temporal/workflow.d.ts.map +1 -0
- package/dist/temporal/workflow.js.map +1 -0
- package/dist/validate-main.js +1 -1
- package/docs/evaluations.md +68 -0
- package/docs/operations.md +169 -0
- package/docs/task-construction.md +94 -0
- package/package.json +33 -20
- package/scripts/verify-package.ts +57 -0
- package/scripts/write-build-metadata.ts +27 -0
- package/src/agent-smoke-main.ts +63 -0
- package/src/agent-smoke.ts +132 -0
- package/src/api-main.ts +12 -0
- package/src/api.ts +277 -0
- package/src/artifacts.ts +357 -0
- package/src/audit.ts +106 -0
- package/src/build-metadata.ts +3 -0
- package/src/cli.ts +359 -0
- package/src/codex-review.ts +229 -0
- package/src/config.ts +114 -0
- package/src/contracts.ts +204 -0
- package/src/coupling.ts +259 -0
- package/src/docker-executor.ts +115 -0
- package/src/eval-main.ts +92 -0
- package/src/evaluate.ts +298 -0
- package/src/github.ts +26 -0
- package/src/harbor-results.ts +142 -0
- package/src/harbor-task.ts +528 -0
- package/src/hash.ts +5 -0
- package/src/modal-auth.ts +11 -0
- package/src/modal-executor.ts +176 -0
- package/src/parallel.ts +24 -0
- package/src/process.ts +165 -0
- package/src/provenance.ts +457 -0
- package/src/reaudit-main.ts +195 -0
- package/src/repair-main.ts +206 -0
- package/src/repair.ts +55 -0
- package/src/run-wait.ts +40 -0
- package/src/sandbox-author.ts +19 -0
- package/src/sandbox-repair.ts +156 -0
- package/src/sandbox-review.ts +19 -0
- package/src/sandbox-validation-repair.ts +177 -0
- package/src/sandbox.ts +51 -0
- package/src/subscription-auth.ts +80 -0
- package/src/temporal/activities.ts +1241 -0
- package/src/temporal/connection.ts +23 -0
- package/src/temporal/worker-main.ts +28 -0
- package/src/temporal/workflow.ts +519 -0
- package/src/validate-main.ts +171 -0
- package/src/validation-repair.ts +94 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +21 -0
- package/dist/activities.d.ts.map +0 -1
- package/dist/activities.js.map +0 -1
- package/dist/extensions/review.d.ts +0 -3
- package/dist/extensions/review.d.ts.map +0 -1
- package/dist/extensions/review.js +0 -44
- package/dist/extensions/review.js.map +0 -1
- package/dist/review/assets/index-Dq-6DFzI.css +0 -1
- package/dist/review/assets/index-DurW_RD7.js +0 -1567
- package/dist/review/assets/typst-DHCkPAjA.js +0 -1
- package/dist/temporal.d.ts.map +0 -1
- package/dist/temporal.js.map +0 -1
- package/dist/worker-main.js.map +0 -1
- package/dist/workflow.d.ts.map +0 -1
- package/dist/workflow.js.map +0 -1
- package/src/extensions/review.ts +0 -54
- /package/dist/{worker-main.d.ts → temporal/worker-main.d.ts} +0 -0
- /package/dist/{workflow.js → temporal/workflow.js} +0 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { chmod, cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, posix, resolve, sep } from "node:path";
|
|
3
|
+
import { type TaskDefinition, taskDefinitionSchema } from "./contracts.js";
|
|
4
|
+
import { sha256 } from "./hash.js";
|
|
5
|
+
import { runCommand } from "./process.js";
|
|
6
|
+
import { patchPaths } from "./repair.js";
|
|
7
|
+
|
|
8
|
+
const HARBOR_SCHEMA_VERSION = "1.4";
|
|
9
|
+
const COMPILER_REVISION = 23;
|
|
10
|
+
|
|
11
|
+
export interface AuthoredTaskFiles {
|
|
12
|
+
readonly definition: TaskDefinition;
|
|
13
|
+
readonly testPatch: string;
|
|
14
|
+
readonly goldPatch: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadAuthoredTask(directory: string): Promise<AuthoredTaskFiles> {
|
|
18
|
+
const definition = taskDefinitionSchema.parse(
|
|
19
|
+
JSON.parse(await readFile(join(directory, "definition.json"), "utf8")),
|
|
20
|
+
);
|
|
21
|
+
assertSafeTaskPaths(definition);
|
|
22
|
+
const [testPatch, goldPatch] = await Promise.all([
|
|
23
|
+
readFile(join(directory, "test.patch"), "utf8"),
|
|
24
|
+
readFile(join(directory, "gold.patch"), "utf8"),
|
|
25
|
+
]);
|
|
26
|
+
if (!testPatch.startsWith("diff --git ")) {
|
|
27
|
+
throw new Error("test.patch is not a Git patch");
|
|
28
|
+
}
|
|
29
|
+
assertSafePatchPaths(testPatch);
|
|
30
|
+
if (!goldPatch.startsWith("diff --git ")) {
|
|
31
|
+
throw new Error("gold.patch is not a Git patch");
|
|
32
|
+
}
|
|
33
|
+
return { definition, testPatch, goldPatch };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function compileHarborTask(
|
|
37
|
+
authoredDirectory: string,
|
|
38
|
+
repositoryDirectory: string,
|
|
39
|
+
outputDirectory: string,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const task = await loadAuthoredTask(authoredDirectory);
|
|
42
|
+
const dependencySetupPatch = dependencyManifestPatch(task.goldPatch);
|
|
43
|
+
const preinstallGoldDependencies = dependencySetupPatch.length > 0;
|
|
44
|
+
await runCommand("git", [
|
|
45
|
+
"-C",
|
|
46
|
+
repositoryDirectory,
|
|
47
|
+
"cat-file",
|
|
48
|
+
"-e",
|
|
49
|
+
`${task.definition.baseCommit}^{commit}`,
|
|
50
|
+
]);
|
|
51
|
+
await rm(outputDirectory, { recursive: true, force: true });
|
|
52
|
+
const environment = join(outputDirectory, "environment");
|
|
53
|
+
const solution = join(outputDirectory, "solution");
|
|
54
|
+
const tests = join(outputDirectory, "tests");
|
|
55
|
+
await Promise.all([
|
|
56
|
+
mkdir(environment, { recursive: true }),
|
|
57
|
+
mkdir(solution, { recursive: true }),
|
|
58
|
+
mkdir(tests, { recursive: true }),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const snapshot = join(outputDirectory, ".repo.tar.gz");
|
|
62
|
+
await runCommand("git", [
|
|
63
|
+
"-C",
|
|
64
|
+
repositoryDirectory,
|
|
65
|
+
"archive",
|
|
66
|
+
"--format=tar.gz",
|
|
67
|
+
`--output=${snapshot}`,
|
|
68
|
+
task.definition.baseCommit,
|
|
69
|
+
]);
|
|
70
|
+
await Promise.all([
|
|
71
|
+
cp(snapshot, join(environment, "repo.tar.gz")),
|
|
72
|
+
cp(snapshot, join(tests, "repo.tar.gz")),
|
|
73
|
+
writeFile(join(outputDirectory, "instruction.md"), `${task.definition.prompt.trim()}\n`),
|
|
74
|
+
writeFile(join(solution, "gold.patch"), task.goldPatch),
|
|
75
|
+
writeFile(join(solution, "solve.sh"), solutionScript()),
|
|
76
|
+
writeFile(join(tests, "test.patch"), task.testPatch),
|
|
77
|
+
writeFile(join(tests, "test.sh"), testScript(task.definition, task.testPatch)),
|
|
78
|
+
writeFile(join(environment, "Dockerfile"), agentDockerfile(task.definition)),
|
|
79
|
+
writeFile(
|
|
80
|
+
join(tests, "Dockerfile"),
|
|
81
|
+
verifierDockerfile(task.definition, preinstallGoldDependencies),
|
|
82
|
+
),
|
|
83
|
+
writeFile(join(outputDirectory, "task.toml"), taskToml(task.definition)),
|
|
84
|
+
...(preinstallGoldDependencies
|
|
85
|
+
? [writeFile(join(tests, "dependency-setup.patch"), dependencySetupPatch)]
|
|
86
|
+
: []),
|
|
87
|
+
]);
|
|
88
|
+
await rm(snapshot);
|
|
89
|
+
await Promise.all([
|
|
90
|
+
chmod(join(solution, "solve.sh"), 0o755),
|
|
91
|
+
chmod(join(tests, "test.sh"), 0o755),
|
|
92
|
+
]);
|
|
93
|
+
await writeFile(
|
|
94
|
+
join(outputDirectory, ".selfbench-manifest.json"),
|
|
95
|
+
`${JSON.stringify(
|
|
96
|
+
{
|
|
97
|
+
generator: "selfbench",
|
|
98
|
+
harborSchemaVersion: HARBOR_SCHEMA_VERSION,
|
|
99
|
+
compilerRevision: COMPILER_REVISION,
|
|
100
|
+
taskId: task.definition.taskId,
|
|
101
|
+
difficulty: task.definition.difficulty,
|
|
102
|
+
definitionSha256: sha256(JSON.stringify(task.definition)),
|
|
103
|
+
testPatchSha256: sha256(task.testPatch),
|
|
104
|
+
goldPatchSha256: sha256(task.goldPatch),
|
|
105
|
+
},
|
|
106
|
+
null,
|
|
107
|
+
2,
|
|
108
|
+
)}\n`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function refreshHarborTask(
|
|
113
|
+
outputDirectory: string,
|
|
114
|
+
definition: TaskDefinition,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const manifestPath = join(outputDirectory, ".selfbench-manifest.json");
|
|
117
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, unknown>;
|
|
118
|
+
if (manifest.taskId !== definition.taskId) {
|
|
119
|
+
throw new Error(`bundle task ${String(manifest.taskId)} does not match ${definition.taskId}`);
|
|
120
|
+
}
|
|
121
|
+
const [goldPatch, testPatch] = await Promise.all([
|
|
122
|
+
readFile(join(outputDirectory, "solution/gold.patch"), "utf8"),
|
|
123
|
+
readFile(join(outputDirectory, "tests/test.patch"), "utf8"),
|
|
124
|
+
]);
|
|
125
|
+
assertSafePatchPaths(testPatch);
|
|
126
|
+
const dependencySetupPatch = dependencyManifestPatch(goldPatch);
|
|
127
|
+
const preinstallGoldDependencies = dependencySetupPatch.length > 0;
|
|
128
|
+
await writeFile(join(outputDirectory, "tests/test.sh"), testScript(definition, testPatch));
|
|
129
|
+
await Promise.all([
|
|
130
|
+
writeFile(join(outputDirectory, "task.toml"), taskToml(definition)),
|
|
131
|
+
writeFile(join(outputDirectory, "environment/Dockerfile"), agentDockerfile(definition)),
|
|
132
|
+
writeFile(
|
|
133
|
+
join(outputDirectory, "tests/Dockerfile"),
|
|
134
|
+
verifierDockerfile(definition, preinstallGoldDependencies),
|
|
135
|
+
),
|
|
136
|
+
preinstallGoldDependencies
|
|
137
|
+
? writeFile(join(outputDirectory, "tests/dependency-setup.patch"), dependencySetupPatch)
|
|
138
|
+
: rm(join(outputDirectory, "tests/dependency-setup.patch"), { force: true }),
|
|
139
|
+
chmod(join(outputDirectory, "tests/test.sh"), 0o755),
|
|
140
|
+
]);
|
|
141
|
+
await writeFile(
|
|
142
|
+
manifestPath,
|
|
143
|
+
`${JSON.stringify(
|
|
144
|
+
{
|
|
145
|
+
...manifest,
|
|
146
|
+
compilerRevision: COMPILER_REVISION,
|
|
147
|
+
difficulty: definition.difficulty,
|
|
148
|
+
definitionSha256: sha256(JSON.stringify(definition)),
|
|
149
|
+
testPatchSha256: sha256(testPatch),
|
|
150
|
+
goldPatchSha256: sha256(goldPatch),
|
|
151
|
+
},
|
|
152
|
+
null,
|
|
153
|
+
2,
|
|
154
|
+
)}\n`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function taskToml(task: TaskDefinition): string {
|
|
159
|
+
const metadata = {
|
|
160
|
+
selfbench_task_id: task.taskId,
|
|
161
|
+
difficulty: task.difficulty,
|
|
162
|
+
repo: task.repo,
|
|
163
|
+
base_commit: task.baseCommit,
|
|
164
|
+
workdir: task.workdir,
|
|
165
|
+
source_pr: task.sourcePr,
|
|
166
|
+
compiler_revision: COMPILER_REVISION,
|
|
167
|
+
};
|
|
168
|
+
return `${[
|
|
169
|
+
`schema_version = ${tomlString(HARBOR_SCHEMA_VERSION)}`,
|
|
170
|
+
'artifacts = ["/opt/selfbench/agent.patch"]',
|
|
171
|
+
"",
|
|
172
|
+
"[task]",
|
|
173
|
+
`name = ${tomlString(`selfbench/${task.taskId}`)}`,
|
|
174
|
+
'version = "1.0.0"',
|
|
175
|
+
`description = ${tomlString(`Reproduce ${task.taskId} from its authentic engineer request.`)}`,
|
|
176
|
+
`keywords = ["software-engineering", "private-swe", "selfbench", ${tomlString(task.difficulty)}]`,
|
|
177
|
+
"",
|
|
178
|
+
"[metadata]",
|
|
179
|
+
...Object.entries(metadata).map(([key, value]) => `${key} = ${tomlValue(value)}`),
|
|
180
|
+
"",
|
|
181
|
+
"[agent]",
|
|
182
|
+
`timeout_sec = ${task.timeouts.agentSeconds}.0`,
|
|
183
|
+
'user = "agent"',
|
|
184
|
+
'network_mode = "allowlist"',
|
|
185
|
+
'allowed_hosts = ["chatgpt.com", "*.chatgpt.com", "openai.com", "*.openai.com"]',
|
|
186
|
+
"",
|
|
187
|
+
"[verifier]",
|
|
188
|
+
`timeout_sec = ${task.timeouts.setupSeconds + task.timeouts.testsSeconds}.0`,
|
|
189
|
+
'user = "root"',
|
|
190
|
+
'environment_mode = "separate"',
|
|
191
|
+
'network_mode = "public"',
|
|
192
|
+
"",
|
|
193
|
+
"[[verifier.collect]]",
|
|
194
|
+
'service = "main"',
|
|
195
|
+
'user = "root"',
|
|
196
|
+
`timeout_sec = ${Math.min(task.timeouts.testsSeconds, 300)}.0`,
|
|
197
|
+
'command = "git --git-dir=/opt/selfbench/base.git --work-tree=/app add -A && git --git-dir=/opt/selfbench/base.git --work-tree=/app diff --cached --binary HEAD > /opt/selfbench/agent.patch"',
|
|
198
|
+
"",
|
|
199
|
+
"[environment]",
|
|
200
|
+
'network_mode = "public"',
|
|
201
|
+
`build_timeout_sec = ${task.timeouts.setupSeconds + 600}.0`,
|
|
202
|
+
`cpus = ${task.resources.cpus}`,
|
|
203
|
+
`memory_mb = ${task.resources.memoryMb}`,
|
|
204
|
+
`storage_mb = ${task.resources.storageMb}`,
|
|
205
|
+
"",
|
|
206
|
+
"[verifier.environment]",
|
|
207
|
+
'network_mode = "public"',
|
|
208
|
+
`build_timeout_sec = ${task.timeouts.setupSeconds + 600}.0`,
|
|
209
|
+
`cpus = ${task.resources.cpus}`,
|
|
210
|
+
`memory_mb = ${task.resources.memoryMb}`,
|
|
211
|
+
`storage_mb = ${task.resources.storageMb}`,
|
|
212
|
+
].join("\n")}\n`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function agentDockerfile(task: TaskDefinition): string {
|
|
216
|
+
return `${toolchainDockerfile(task.toolchains)}
|
|
217
|
+
RUN useradd --create-home --shell /bin/bash agent
|
|
218
|
+
${repositoryDockerfile(task)}
|
|
219
|
+
RUN git -C /app reset --hard -q HEAD \\
|
|
220
|
+
&& git -C /app clean -fdq \\
|
|
221
|
+
&& mkdir -p /opt/selfbench \\
|
|
222
|
+
&& cp -a /app/.git /opt/selfbench/base.git \\
|
|
223
|
+
&& chown -R agent:agent /app /home/agent /opt/uv-cache \\
|
|
224
|
+
&& chown -R root:root /opt/selfbench \\
|
|
225
|
+
&& chmod 700 /opt/selfbench \\
|
|
226
|
+
&& mkdir -p /home/agent/.cache/uv \\
|
|
227
|
+
&& chown -R agent:agent /home/agent/.cache
|
|
228
|
+
ENV UV_CACHE_DIR=/home/agent/.cache/uv \\
|
|
229
|
+
UV_NO_BUILD_ISOLATION=1
|
|
230
|
+
USER agent
|
|
231
|
+
WORKDIR /app
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function verifierDockerfile(task: TaskDefinition, preinstallGoldDependencies: boolean): string {
|
|
236
|
+
return `${toolchainDockerfile(task.toolchains)}
|
|
237
|
+
${repositoryDockerfile(task)}
|
|
238
|
+
${preinstallGoldDependencies ? goldDependencySetupLayer(task) : ""}
|
|
239
|
+
RUN useradd --create-home --shell /bin/bash verifier \\
|
|
240
|
+
&& chown -R verifier:verifier /app /opt/uv-cache \\
|
|
241
|
+
&& mkdir -p /opt/selfbench \\
|
|
242
|
+
&& chmod 700 /opt/selfbench \\
|
|
243
|
+
&& mkdir -p /home/verifier/.cache/uv \\
|
|
244
|
+
&& chown -R verifier:verifier /home/verifier/.cache
|
|
245
|
+
ENV UV_CACHE_DIR=/home/verifier/.cache/uv \\
|
|
246
|
+
UV_NO_BUILD_ISOLATION=1
|
|
247
|
+
COPY test.patch test.sh /tests/
|
|
248
|
+
RUN chmod 700 /tests && chmod 600 /tests/test.patch && chmod +x /tests/test.sh
|
|
249
|
+
WORKDIR /app
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function goldDependencySetupLayer(task: TaskDefinition): string {
|
|
254
|
+
return `COPY dependency-setup.patch /tmp/selfbench-dependency-setup.patch
|
|
255
|
+
RUN git -C /app apply --binary --whitespace=nowarn /tmp/selfbench-dependency-setup.patch \\
|
|
256
|
+
&& cd ${shellQuote(`/app/${task.workdir}`)} \\
|
|
257
|
+
&& bash -lc ${shellQuote(task.setupCommand)} \\
|
|
258
|
+
&& git -C /app reset --hard -q HEAD \\
|
|
259
|
+
&& git -C /app clean -fdq \\
|
|
260
|
+
&& rm /tmp/selfbench-dependency-setup.patch
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function goldPatchChangesDependencyManifests(patch: string): boolean {
|
|
265
|
+
return dependencyManifestPatch(patch).length > 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function dependencyManifestPatch(patch: string): string {
|
|
269
|
+
const sections = patch.split(/(?=^diff --git )/m);
|
|
270
|
+
const selected = sections.filter((section) => {
|
|
271
|
+
const header = section.split("\n", 1)[0] ?? "";
|
|
272
|
+
const match = /^diff --git a\/(.+) b\/(.+)$/.exec(header);
|
|
273
|
+
return match?.[2] ? isDependencyManifest(match[2]) : false;
|
|
274
|
+
});
|
|
275
|
+
return selected.length > 0 ? `${selected.join("").trimEnd()}\n` : "";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isDependencyManifest(path: string): boolean {
|
|
279
|
+
const name = posix.basename(path);
|
|
280
|
+
return (
|
|
281
|
+
/^(?:package(?:-lock)?\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|deno\.lock)$/.test(
|
|
282
|
+
name,
|
|
283
|
+
) ||
|
|
284
|
+
/^(?:pyproject\.toml|uv\.lock|poetry\.lock|Pipfile(?:\.lock)?|requirements[^/]*\.txt)$/.test(
|
|
285
|
+
name,
|
|
286
|
+
) ||
|
|
287
|
+
/^(?:go\.(?:mod|sum)|Cargo\.(?:toml|lock))$/.test(name)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function toolchainDockerfile(toolchains: readonly string[]): string {
|
|
292
|
+
const layers: Record<string, string> = {
|
|
293
|
+
uv: "RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh",
|
|
294
|
+
python:
|
|
295
|
+
"RUN uv python install 3.11 3.12 3.13 && ln -sf /usr/local/bin/python3.12 /usr/local/bin/python3 && ln -sf /usr/local/bin/python3.12 /usr/local/bin/python",
|
|
296
|
+
node: `ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright
|
|
297
|
+
RUN mkdir -p "$PLAYWRIGHT_BROWSERS_PATH" && chmod 755 "$PLAYWRIGHT_BROWSERS_PATH" \\
|
|
298
|
+
&& arch="$(dpkg --print-architecture)" && case "$arch" in arm64) node_arch=arm64 ;; amd64) node_arch=x64 ;; *) exit 1 ;; esac \\
|
|
299
|
+
&& curl -fsSL "https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-\${node_arch}.tar.xz" | tar -C /usr/local --strip-components=1 -xJ \\
|
|
300
|
+
&& mkdir -p /opt/corepack && chmod 755 /opt/corepack \\
|
|
301
|
+
&& corepack enable`,
|
|
302
|
+
bun: "RUN npm install --global bun@1.3.14",
|
|
303
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: the output is a shell variable.
|
|
304
|
+
go: `RUN arch="$(dpkg --print-architecture)" && curl -fsSL "https://go.dev/dl/go1.25.0.linux-${"${arch}"}.tar.gz" | tar -C /usr/local -xz`,
|
|
305
|
+
rust: "RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | env RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo sh -s -- -y --no-modify-path --profile minimal --default-toolchain 1.90.0",
|
|
306
|
+
};
|
|
307
|
+
const selected = new Set(toolchains);
|
|
308
|
+
if (selected.has("python")) {
|
|
309
|
+
selected.add("uv");
|
|
310
|
+
}
|
|
311
|
+
if (selected.has("bun")) {
|
|
312
|
+
selected.add("node");
|
|
313
|
+
}
|
|
314
|
+
const order = ["uv", "python", "node", "bun", "go", "rust"];
|
|
315
|
+
return `FROM ubuntu:24.04
|
|
316
|
+
ENV DEBIAN_FRONTEND=noninteractive \\
|
|
317
|
+
UV_LINK_MODE=copy \\
|
|
318
|
+
UV_CACHE_DIR=/opt/uv-cache \\
|
|
319
|
+
UV_PYTHON_INSTALL_DIR=/usr/local/share/uv/python \\
|
|
320
|
+
UV_PYTHON_BIN_DIR=/usr/local/bin \\
|
|
321
|
+
RUSTUP_HOME=/usr/local/rustup \\
|
|
322
|
+
CARGO_HOME=/usr/local/cargo \\
|
|
323
|
+
COREPACK_HOME=/opt/corepack \\
|
|
324
|
+
PATH=/usr/local/go/bin:/usr/local/cargo/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
|
|
325
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
326
|
+
bash build-essential ca-certificates curl git jq passwd pkg-config procps unzip xz-utils \\
|
|
327
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
328
|
+
${order
|
|
329
|
+
.filter((name) => selected.has(name))
|
|
330
|
+
.map((name) => layers[name])
|
|
331
|
+
.filter(Boolean)
|
|
332
|
+
.join("\n")}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function repositoryDockerfile(task: TaskDefinition): string {
|
|
336
|
+
const pythonBuildDependencies = task.toolchains.includes("python")
|
|
337
|
+
? ` \\
|
|
338
|
+
&& find /app -path '*/.venv/bin/python' -exec uv pip install --python '{}' 'setuptools>=70' wheel ';'`
|
|
339
|
+
: "";
|
|
340
|
+
return `COPY repo.tar.gz /tmp/repo.tar.gz
|
|
341
|
+
RUN mkdir -p /app && tar -xzf /tmp/repo.tar.gz -C /app && rm /tmp/repo.tar.gz \\
|
|
342
|
+
&& git -C /app init -q \\
|
|
343
|
+
&& git -C /app config user.email selfbench@local \\
|
|
344
|
+
&& git -C /app config user.name selfbench \\
|
|
345
|
+
&& git -C /app add -A \\
|
|
346
|
+
&& git -C /app commit -qm base
|
|
347
|
+
RUN mkdir -p /opt/uv-cache && chmod 777 /opt/uv-cache \\
|
|
348
|
+
&& cd ${shellQuote(`/app/${task.workdir}`)} \\
|
|
349
|
+
&& bash -lc ${shellQuote(task.setupCommand)}${pythonBuildDependencies} \\
|
|
350
|
+
&& chmod -R a+rwX /opt/uv-cache`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function solutionScript(): string {
|
|
354
|
+
return `#!/bin/bash
|
|
355
|
+
set -euo pipefail
|
|
356
|
+
git -C /app apply --binary --whitespace=nowarn /solution/gold.patch
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function testScript(task: TaskDefinition, testPatch: string): string {
|
|
361
|
+
const repositoryTestPaths = [
|
|
362
|
+
...new Set([
|
|
363
|
+
...task.testPaths.map((path) => repositoryRelativePath(task, path)),
|
|
364
|
+
...patchPaths(testPatch),
|
|
365
|
+
]),
|
|
366
|
+
].sort();
|
|
367
|
+
const exclusions = repositoryTestPaths
|
|
368
|
+
.flatMap((path) => [
|
|
369
|
+
`--exclude=${shellQuote(path.replace(/\/$/, ""))}`,
|
|
370
|
+
`--exclude=${shellQuote(`${path.replace(/\/$/, "")}/*`)}`,
|
|
371
|
+
])
|
|
372
|
+
.join(" ");
|
|
373
|
+
const protectedPaths = repositoryTestPaths.map(shellQuote).join(" ");
|
|
374
|
+
const protectedAbsolute = repositoryTestPaths.map((path) => shellQuote(`/app/${path}`)).join(" ");
|
|
375
|
+
const f2p = taskCommand(task, task.failToPass);
|
|
376
|
+
const p2p = task.passToPass.length > 0 ? taskCommand(task, task.passToPass) : "true";
|
|
377
|
+
return `#!/bin/bash
|
|
378
|
+
set -uo pipefail
|
|
379
|
+
mkdir -p /logs/verifier
|
|
380
|
+
patch_applied=1
|
|
381
|
+
fail_to_pass=0
|
|
382
|
+
pass_to_pass=0
|
|
383
|
+
deterministic=0
|
|
384
|
+
setup_completed=0
|
|
385
|
+
fail_to_pass_exit_code=-1
|
|
386
|
+
fail_to_pass_repeat_exit_code=-1
|
|
387
|
+
pass_to_pass_exit_code=-1
|
|
388
|
+
verifier_cache=""
|
|
389
|
+
|
|
390
|
+
kill_verifier_processes() { pkill -KILL -u "$(id -u verifier)" 2>/dev/null || true; }
|
|
391
|
+
# Some toolchains fetch dependencies at test runtime (e.g. Next.js e2e installs),
|
|
392
|
+
# so a single registry connection reset must not be misread as a dead test. Retry
|
|
393
|
+
# only infrastructure-style failures with backoff; real assertion failures fail fast.
|
|
394
|
+
run_verifier_command() {
|
|
395
|
+
local logfile
|
|
396
|
+
logfile="$(mktemp /tmp/selfbench-verifier-command-XXXXXX.log)"
|
|
397
|
+
local attempt=1
|
|
398
|
+
local status=1
|
|
399
|
+
while [ "$attempt" -le 3 ]; do
|
|
400
|
+
: > "$logfile"
|
|
401
|
+
runuser -u verifier -- env PATH="/usr/local/go/bin:/usr/local/cargo/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin" UV_CACHE_DIR="$verifier_cache" UV_NO_BUILD_ISOLATION=1 bash -lc "$1" >"$logfile" 2>&1
|
|
402
|
+
status=$?
|
|
403
|
+
if [ "$status" -eq 0 ]; then
|
|
404
|
+
break
|
|
405
|
+
fi
|
|
406
|
+
if [ "$attempt" -lt 3 ] && grep -qE 'ECONNRESET|ETIMEDOUT|ESOCKETTIMEDOUT|ENOTFOUND|EAI_AGAIN|META_FETCH_FAIL|FetchError|EPIPE|EPERM|registry\\.npmjs' "$logfile"; then
|
|
407
|
+
sleep "$((10 * attempt))"
|
|
408
|
+
attempt=$((attempt + 1))
|
|
409
|
+
continue
|
|
410
|
+
fi
|
|
411
|
+
break
|
|
412
|
+
done
|
|
413
|
+
cat "$logfile"
|
|
414
|
+
rm -f "$logfile"
|
|
415
|
+
kill_verifier_processes
|
|
416
|
+
return "$status"
|
|
417
|
+
}
|
|
418
|
+
protect_held_out_path() {
|
|
419
|
+
local path="$1"
|
|
420
|
+
chown -R root:root -- "$path"
|
|
421
|
+
chmod -R a-w,go+rX -- "$path"
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if [ ! -f /opt/selfbench/agent.patch ]; then
|
|
425
|
+
patch_applied=0
|
|
426
|
+
elif [ -s /opt/selfbench/agent.patch ]; then
|
|
427
|
+
git -C /app apply --binary --whitespace=nowarn ${exclusions} /opt/selfbench/agent.patch || patch_applied=0
|
|
428
|
+
fi
|
|
429
|
+
|
|
430
|
+
if [ "$patch_applied" -eq 1 ]; then setup_completed=1; fi
|
|
431
|
+
|
|
432
|
+
if [ "$patch_applied" -eq 1 ] && [ "$setup_completed" -eq 1 ]; then
|
|
433
|
+
kill_verifier_processes
|
|
434
|
+
for protected_path in ${protectedPaths}; do
|
|
435
|
+
git -C /app restore --source=HEAD --staged --worktree -- "$protected_path" 2>/dev/null || true
|
|
436
|
+
git -C /app clean -fd -- "$protected_path" >/dev/null 2>&1 || true
|
|
437
|
+
done
|
|
438
|
+
git -C /app apply --binary --whitespace=nowarn /tests/test.patch || patch_applied=0
|
|
439
|
+
if [ "$patch_applied" -eq 1 ]; then
|
|
440
|
+
for protected_path in ${protectedAbsolute}; do protect_held_out_path "$protected_path"; done
|
|
441
|
+
fi
|
|
442
|
+
rm -f /tests/test.patch
|
|
443
|
+
fi
|
|
444
|
+
|
|
445
|
+
if [ "$patch_applied" -eq 1 ] && [ "$setup_completed" -eq 1 ]; then
|
|
446
|
+
cd ${shellQuote(`/app/${task.workdir}`)}
|
|
447
|
+
verifier_cache="$(mktemp -d /tmp/selfbench-verifier-uv-XXXXXX)"
|
|
448
|
+
cp -a /opt/uv-cache/. "$verifier_cache"/
|
|
449
|
+
chown -R verifier:verifier "$verifier_cache"
|
|
450
|
+
if run_verifier_command ${shellQuote(f2p)}; then
|
|
451
|
+
fail_to_pass_exit_code=0
|
|
452
|
+
fail_to_pass=1
|
|
453
|
+
if run_verifier_command ${shellQuote(f2p)}; then
|
|
454
|
+
fail_to_pass_repeat_exit_code=0
|
|
455
|
+
deterministic=1
|
|
456
|
+
else
|
|
457
|
+
fail_to_pass_repeat_exit_code=$?
|
|
458
|
+
fi
|
|
459
|
+
else
|
|
460
|
+
fail_to_pass_exit_code=$?
|
|
461
|
+
fi
|
|
462
|
+
if run_verifier_command ${shellQuote(p2p)}; then
|
|
463
|
+
pass_to_pass_exit_code=0
|
|
464
|
+
pass_to_pass=1
|
|
465
|
+
else
|
|
466
|
+
pass_to_pass_exit_code=$?
|
|
467
|
+
fi
|
|
468
|
+
rm -rf "$verifier_cache"
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
reward=0
|
|
472
|
+
if [ "$patch_applied" -eq 1 ] && [ "$fail_to_pass" -eq 1 ] && [ "$pass_to_pass" -eq 1 ] && [ "$deterministic" -eq 1 ]; then reward=1; fi
|
|
473
|
+
cat > /logs/verifier/reward.json <<EOF
|
|
474
|
+
{"reward": $reward, "patch_applied": $patch_applied, "fail_to_pass": $fail_to_pass, "pass_to_pass": $pass_to_pass, "deterministic": $deterministic, "setup_completed": $setup_completed, "fail_to_pass_exit_code": $fail_to_pass_exit_code, "fail_to_pass_repeat_exit_code": $fail_to_pass_repeat_exit_code, "pass_to_pass_exit_code": $pass_to_pass_exit_code}
|
|
475
|
+
EOF
|
|
476
|
+
exit 0
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function taskCommand(task: TaskDefinition, tests: readonly string[]): string {
|
|
481
|
+
return task.testCommand.replaceAll("{tests}", tests.map(shellQuote).join(" "));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function assertSafeTaskPaths(task: TaskDefinition): void {
|
|
485
|
+
for (const path of [
|
|
486
|
+
task.workdir,
|
|
487
|
+
...task.testPaths.map((value) => posix.join(task.workdir, value)),
|
|
488
|
+
]) {
|
|
489
|
+
const resolved = resolve("/repo", path);
|
|
490
|
+
if (resolved !== "/repo" && !resolved.startsWith(`/repo${sep}`)) {
|
|
491
|
+
throw new Error(`task path escapes repository: ${path}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function assertSafePatchPaths(patch: string): void {
|
|
497
|
+
const paths = patchPaths(patch);
|
|
498
|
+
if (paths.length === 0) {
|
|
499
|
+
throw new Error("test.patch changes no files");
|
|
500
|
+
}
|
|
501
|
+
for (const path of paths) {
|
|
502
|
+
const resolved = resolve("/repo", path);
|
|
503
|
+
if (
|
|
504
|
+
resolved === "/repo" ||
|
|
505
|
+
!resolved.startsWith(`/repo${sep}`) ||
|
|
506
|
+
resolved.startsWith(`/repo${sep}.git${sep}`) ||
|
|
507
|
+
resolved === `/repo${sep}.git`
|
|
508
|
+
) {
|
|
509
|
+
throw new Error(`test patch path escapes repository: ${path}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function repositoryRelativePath(task: TaskDefinition, path: string): string {
|
|
515
|
+
return posix.normalize(posix.join(task.workdir, path)).replace(/^\.\//, "");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function shellQuote(value: string): string {
|
|
519
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function tomlString(value: string): string {
|
|
523
|
+
return JSON.stringify(value);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function tomlValue(value: string | number): string {
|
|
527
|
+
return typeof value === "string" ? tomlString(value) : String(value);
|
|
528
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const MODAL_CREDENTIAL_KEYS = ["MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"] as const;
|
|
2
|
+
|
|
3
|
+
export function removeEmptyModalCredentialOverrides(
|
|
4
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
5
|
+
): void {
|
|
6
|
+
for (const key of MODAL_CREDENTIAL_KEYS) {
|
|
7
|
+
if (environment[key]?.trim() === "") {
|
|
8
|
+
delete environment[key];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|