self-bench 0.3.0 → 0.3.2

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.
Files changed (66) hide show
  1. package/.dockerignore +10 -0
  2. package/Dockerfile +37 -0
  3. package/Dockerfile.sandbox +24 -0
  4. package/README.md +34 -26
  5. package/biome.json +18 -0
  6. package/bun.lock +1182 -0
  7. package/compose.yaml +85 -0
  8. package/dist/agent-smoke-main.js +1 -1
  9. package/dist/build-metadata.d.ts +2 -0
  10. package/dist/build-metadata.d.ts.map +1 -0
  11. package/dist/build-metadata.js +2 -0
  12. package/dist/build-metadata.js.map +1 -0
  13. package/dist/cli.js +18 -8
  14. package/dist/cli.js.map +1 -1
  15. package/dist/eval-main.js +1 -1
  16. package/dist/reaudit-main.js +1 -1
  17. package/dist/repair-main.js +1 -1
  18. package/dist/validate-main.js +1 -1
  19. package/docs/evaluations.md +67 -0
  20. package/docs/operations.md +178 -0
  21. package/docs/task-construction.md +94 -0
  22. package/package.json +28 -15
  23. package/scripts/verify-package.ts +57 -0
  24. package/scripts/write-build-metadata.ts +27 -0
  25. package/src/activities.ts +1236 -0
  26. package/src/agent-smoke-main.ts +63 -0
  27. package/src/agent-smoke.ts +132 -0
  28. package/src/api-main.ts +12 -0
  29. package/src/api.ts +239 -0
  30. package/src/artifacts.ts +361 -0
  31. package/src/audit.ts +106 -0
  32. package/src/build-metadata.ts +3 -0
  33. package/src/cli.ts +350 -0
  34. package/src/codex-review.ts +220 -0
  35. package/src/config.ts +117 -0
  36. package/src/contracts.ts +209 -0
  37. package/src/coupling.ts +259 -0
  38. package/src/docker-executor.ts +115 -0
  39. package/src/eval-main.ts +92 -0
  40. package/src/evaluate.ts +293 -0
  41. package/src/github.ts +26 -0
  42. package/src/harbor-results.ts +142 -0
  43. package/src/harbor-task.ts +528 -0
  44. package/src/hash.ts +5 -0
  45. package/src/modal-auth.ts +11 -0
  46. package/src/modal-executor.ts +176 -0
  47. package/src/parallel.ts +24 -0
  48. package/src/process.ts +165 -0
  49. package/src/provenance.ts +458 -0
  50. package/src/reaudit-main.ts +192 -0
  51. package/src/repair-main.ts +203 -0
  52. package/src/repair.ts +55 -0
  53. package/src/run-wait.ts +40 -0
  54. package/src/sandbox-author.ts +19 -0
  55. package/src/sandbox-repair.ts +160 -0
  56. package/src/sandbox-review.ts +17 -0
  57. package/src/sandbox-validation-repair.ts +174 -0
  58. package/src/sandbox.ts +51 -0
  59. package/src/subscription-auth.ts +67 -0
  60. package/src/temporal.ts +23 -0
  61. package/src/validate-main.ts +171 -0
  62. package/src/validation-repair.ts +94 -0
  63. package/src/worker-main.ts +32 -0
  64. package/src/workflow.ts +519 -0
  65. package/tsconfig.build.json +13 -0
  66. package/tsconfig.json +21 -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,5 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function sha256(value: Uint8Array | string): string {
4
+ return createHash("sha256").update(value).digest("hex");
5
+ }
@@ -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
+ }