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
package/src/sandbox.ts ADDED
@@ -0,0 +1,51 @@
1
+ import type { SelfBenchConfig } from "./config.js";
2
+ import { DockerSandboxExecutor } from "./docker-executor.js";
3
+ import { ModalSandboxExecutor } from "./modal-executor.js";
4
+
5
+ export interface SandboxFile {
6
+ readonly path: string;
7
+ readonly contents: Uint8Array | string;
8
+ }
9
+
10
+ export interface SandboxRequest {
11
+ readonly runId: string;
12
+ readonly stage: string;
13
+ readonly command: readonly string[];
14
+ readonly files?: readonly SandboxFile[];
15
+ readonly outputPaths?: readonly string[];
16
+ readonly environment?: Readonly<Record<string, string>>;
17
+ readonly secrets?: Readonly<Record<string, string>>;
18
+ readonly timeoutMs: number;
19
+ readonly inactivityTimeoutMs?: number;
20
+ readonly cpu?: number;
21
+ readonly memoryMiB?: number;
22
+ }
23
+
24
+ export interface SandboxProgress {
25
+ readonly stream: "stdout" | "stderr";
26
+ readonly bytes: number;
27
+ }
28
+
29
+ export interface SandboxRunOptions {
30
+ readonly signal?: AbortSignal;
31
+ readonly onProgress?: (progress: SandboxProgress) => void;
32
+ }
33
+
34
+ export interface SandboxResult {
35
+ readonly sandboxId: string;
36
+ readonly exitCode: number;
37
+ readonly stdout: string;
38
+ readonly stderr: string;
39
+ readonly outputs: Readonly<Record<string, Uint8Array>>;
40
+ }
41
+
42
+ export interface SandboxExecutor {
43
+ run(request: SandboxRequest, options?: SandboxRunOptions): Promise<SandboxResult>;
44
+ close(): void;
45
+ }
46
+
47
+ export function createSandboxExecutor(config: SelfBenchConfig["execution"]): SandboxExecutor {
48
+ return config.kind === "modal"
49
+ ? new ModalSandboxExecutor(config)
50
+ : new DockerSandboxExecutor(config);
51
+ }
@@ -0,0 +1,67 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { runCommand } from "./process.js";
5
+
6
+ export async function loadPiSubscriptionAuth(): Promise<string> {
7
+ const raw =
8
+ process.env.SELFBENCH_PI_AUTH_JSON ??
9
+ (await readFile(join(homedir(), ".pi/agent/auth.json"), "utf8"));
10
+ const parsed = JSON.parse(raw) as unknown;
11
+ const credential = isRecord(parsed) ? parsed["openai-codex"] : undefined;
12
+ if (
13
+ !isRecord(credential) ||
14
+ credential.type !== "oauth" ||
15
+ typeof credential.access !== "string" ||
16
+ typeof credential.refresh !== "string"
17
+ ) {
18
+ throw new Error("Pi auth does not contain an openai-codex subscription credential");
19
+ }
20
+ return JSON.stringify({ "openai-codex": credential });
21
+ }
22
+
23
+ export function assertCodexSubscriptionAuth(value: unknown, source = "Codex auth"): void {
24
+ if (!isRecord(value) || value.auth_mode !== "chatgpt" || !isRecord(value.tokens)) {
25
+ throw new Error(`${source} does not contain a ChatGPT subscription token set`);
26
+ }
27
+ }
28
+
29
+ export async function loadCodexSubscriptionAuth(): Promise<string> {
30
+ const path = process.env.CODEX_AUTH_JSON_PATH ?? join(homedir(), ".codex/auth.json");
31
+ const parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
32
+ assertCodexSubscriptionAuth(parsed, path);
33
+ const auth = parsed as Record<string, unknown>;
34
+ return JSON.stringify({
35
+ auth_mode: "chatgpt",
36
+ tokens: auth.tokens,
37
+ ...(typeof auth.last_refresh === "string" ? { last_refresh: auth.last_refresh } : {}),
38
+ });
39
+ }
40
+
41
+ export async function subscriptionBearerToken(model: string): Promise<string> {
42
+ const result = await runCommand("pi", [
43
+ "auth",
44
+ "print-bearer-token",
45
+ "--provider",
46
+ "openai-codex",
47
+ "--model",
48
+ model,
49
+ ]);
50
+ const token = result.stdout.trim();
51
+ if (!token) {
52
+ throw new Error(`Pi returned no openai-codex subscription bearer token for ${model}`);
53
+ }
54
+ return token;
55
+ }
56
+
57
+ export async function githubToken(): Promise<string | undefined> {
58
+ if (process.env.GH_TOKEN) {
59
+ return process.env.GH_TOKEN;
60
+ }
61
+ const result = await runCommand("gh", ["auth", "token"], { allowFailure: true });
62
+ return result.exitCode === 0 && result.stdout.trim() ? result.stdout.trim() : undefined;
63
+ }
64
+
65
+ function isRecord(value: unknown): value is Record<string, unknown> {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ }
@@ -0,0 +1,23 @@
1
+ import { Connection } from "@temporalio/client";
2
+ import { NativeConnection } from "@temporalio/worker";
3
+ import type { SelfBenchConfig } from "./config.js";
4
+
5
+ export async function connectTemporalClient(
6
+ config: SelfBenchConfig["temporal"],
7
+ ): Promise<Connection> {
8
+ return await Connection.connect({
9
+ address: config.address,
10
+ tls: config.tls,
11
+ ...(config.apiKey ? { apiKey: config.apiKey } : {}),
12
+ });
13
+ }
14
+
15
+ export async function connectTemporalWorker(
16
+ config: SelfBenchConfig["temporal"],
17
+ ): Promise<NativeConnection> {
18
+ return await NativeConnection.connect({
19
+ address: config.address,
20
+ tls: config.tls,
21
+ ...(config.apiKey ? { apiKey: config.apiKey } : {}),
22
+ });
23
+ }
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, mkdir, readdir, writeFile } from "node:fs/promises";
4
+ import { basename, join, resolve } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import { harborInfrastructureError, readHarborJobResult } from "./harbor-results.js";
7
+ import { parallelMap } from "./parallel.js";
8
+ import { runCommand } from "./process.js";
9
+
10
+ const parsed = parseArgs({
11
+ options: {
12
+ tasks: { type: "string" },
13
+ jobs: { type: "string" },
14
+ environment: { type: "string", default: "modal" },
15
+ concurrency: { type: "string", default: "10" },
16
+ help: { type: "boolean", short: "h" },
17
+ },
18
+ strict: true,
19
+ });
20
+ if (parsed.values.help) {
21
+ console.log(`Run Harbor nop and oracle gates for expanded SelfBench tasks.
22
+
23
+ Usage:
24
+ self-bench-validate --tasks DIRECTORY --jobs DIRECTORY [options]
25
+
26
+ Options:
27
+ --environment NAME Harbor environment (default: modal)
28
+ --concurrency N Concurrent tasks (default: 10)
29
+ -h, --help Show this help`);
30
+ process.exit(0);
31
+ }
32
+
33
+ const tasksDirectory = resolve(parsed.values.tasks ?? fail("--tasks is required"));
34
+ const jobsDirectory = resolve(parsed.values.jobs ?? fail("--jobs is required"));
35
+ const environment = parsed.values.environment ?? "modal";
36
+ if (environment !== "modal" && environment !== "docker") {
37
+ throw new Error("--environment must be modal or docker");
38
+ }
39
+ const concurrency = positiveInteger(parsed.values.concurrency, "--concurrency");
40
+ const directories = (
41
+ await Promise.all(
42
+ (
43
+ await readdir(tasksDirectory)
44
+ ).map(async (name) => {
45
+ const directory = join(tasksDirectory, name);
46
+ return await access(join(directory, "task.toml")).then(
47
+ () => directory,
48
+ () => undefined,
49
+ );
50
+ }),
51
+ )
52
+ ).filter((directory): directory is string => directory !== undefined);
53
+ await mkdir(jobsDirectory, { recursive: true });
54
+
55
+ const tasks = await parallelMap(directories.sort(), concurrency, async (taskDirectory) => {
56
+ const taskId = basename(taskDirectory);
57
+ console.error(`validating ${taskId}`);
58
+ try {
59
+ const nop = await runGate(taskDirectory, taskId, "nop", environment, jobsDirectory);
60
+ const oracle = await runGate(taskDirectory, taskId, "oracle", environment, jobsDirectory);
61
+ const accepted = nop.passed && oracle.passed;
62
+ console.error(`${taskId}: ${accepted ? "accepted" : "rejected"}`);
63
+ return { taskId, status: "validated" as const, accepted, nop, oracle };
64
+ } catch (error) {
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ console.error(`${taskId}: error: ${message}`);
67
+ return { taskId, status: "error" as const, accepted: false, error: message };
68
+ }
69
+ });
70
+ const report = {
71
+ schemaVersion: 1,
72
+ generatedAt: new Date().toISOString(),
73
+ sourceTasks: tasksDirectory,
74
+ environment,
75
+ summary: {
76
+ total: tasks.length,
77
+ accepted: tasks.filter((task) => task.accepted).length,
78
+ rejected: tasks.filter((task) => task.status === "validated" && !task.accepted).length,
79
+ errors: tasks.filter((task) => task.status === "error").length,
80
+ },
81
+ tasks,
82
+ };
83
+ await writeFile(
84
+ join(jobsDirectory, "validation-summary.json"),
85
+ `${JSON.stringify(report, null, 2)}\n`,
86
+ );
87
+ console.log(JSON.stringify(report.summary, null, 2));
88
+
89
+ async function runGate(
90
+ taskDirectory: string,
91
+ taskId: string,
92
+ agent: "nop" | "oracle",
93
+ environment: "docker" | "modal",
94
+ jobsDirectory: string,
95
+ ): Promise<{ readonly passed: boolean; readonly rewards: Readonly<Record<string, number>> }> {
96
+ const jobName = `${taskId}-${agent}-${crypto.randomUUID().slice(0, 8)}`;
97
+ const process = await runCommand(
98
+ "harbor",
99
+ [
100
+ "run",
101
+ "--path",
102
+ taskDirectory,
103
+ "--agent",
104
+ agent,
105
+ "--env",
106
+ environment,
107
+ "--job-name",
108
+ jobName,
109
+ "--jobs-dir",
110
+ jobsDirectory,
111
+ "--n-concurrent",
112
+ "1",
113
+ "--max-retries",
114
+ "1",
115
+ "--delete",
116
+ "--yes",
117
+ "--quiet",
118
+ ],
119
+ { allowFailure: true, timeoutMs: 4 * 60 * 60 * 1000 },
120
+ );
121
+ if (process.exitCode !== 0) {
122
+ throw new Error(`Harbor ${agent} exited ${process.exitCode}: ${process.stderr.slice(-1_000)}`);
123
+ }
124
+ const result = await readHarborJobResult(jobsDirectory, jobName);
125
+ const infrastructure = harborInfrastructureError(result.trial);
126
+ if (infrastructure) {
127
+ throw new Error(`Harbor ${agent} infrastructure failure: ${infrastructure}`);
128
+ }
129
+ const rewards = rewardValues(result.trial);
130
+ const passed =
131
+ agent === "nop"
132
+ ? rewards.fail_to_pass === 0 &&
133
+ (rewards.pass_to_pass ?? 0) >= 1 &&
134
+ (rewards.setup_completed ?? 0) >= 1
135
+ : (rewards.patch_applied ?? 0) >= 1 &&
136
+ (rewards.fail_to_pass ?? 0) >= 1 &&
137
+ (rewards.pass_to_pass ?? 0) >= 1 &&
138
+ (rewards.deterministic ?? 0) >= 1 &&
139
+ (rewards.setup_completed ?? 0) >= 1;
140
+ return { passed, rewards };
141
+ }
142
+
143
+ function rewardValues(value: unknown): Readonly<Record<string, number>> {
144
+ if (!isRecord(value) || !isRecord(value.verifier_result)) {
145
+ return {};
146
+ }
147
+ const raw = value.verifier_result.rewards;
148
+ return isRecord(raw)
149
+ ? Object.fromEntries(
150
+ Object.entries(raw).filter(
151
+ (entry): entry is [string, number] => typeof entry[1] === "number",
152
+ ),
153
+ )
154
+ : {};
155
+ }
156
+
157
+ function positiveInteger(value: string | undefined, label: string): number {
158
+ const number = Number(value);
159
+ if (!Number.isInteger(number) || number < 1) {
160
+ throw new Error(`${label} must be a positive integer`);
161
+ }
162
+ return number;
163
+ }
164
+
165
+ function isRecord(value: unknown): value is Record<string, unknown> {
166
+ return typeof value === "object" && value !== null && !Array.isArray(value);
167
+ }
168
+
169
+ function fail(message: string): never {
170
+ throw new Error(message);
171
+ }
@@ -0,0 +1,94 @@
1
+ import type { TaskDefinition } from "./contracts.js";
2
+ import { patchPaths } from "./repair.js";
3
+
4
+ export function validationRepairPaths(originalTestPatch: string): readonly string[] {
5
+ const paths = patchPaths(originalTestPatch);
6
+ if (paths.length === 0) {
7
+ throw new Error("original held-out test patch changes no files");
8
+ }
9
+ const nonTestPaths = paths.filter((path) => !isTestOnlyPath(path));
10
+ if (nonTestPaths.length > 0) {
11
+ throw new Error(`validation repair requires test-only patch paths: ${nonTestPaths.join(", ")}`);
12
+ }
13
+ return paths;
14
+ }
15
+
16
+ function isTestOnlyPath(path: string): boolean {
17
+ const segments = path.toLowerCase().split("/");
18
+ const filename = segments.at(-1) ?? "";
19
+ return (
20
+ segments.some((segment) =>
21
+ /^(?:tests?|__tests__|e2e|integration|fixtures?|selfbench(?:-tests|_tests)?|\.selfbench-tests)$/.test(
22
+ segment,
23
+ ),
24
+ ) || /(?:^|\.)(?:test|spec)\.[^.]+$/.test(filename)
25
+ );
26
+ }
27
+
28
+ export function assertValidationRepair(
29
+ original: TaskDefinition,
30
+ repaired: TaskDefinition,
31
+ originalTestPatch: string,
32
+ changedPaths: readonly string[],
33
+ ): void {
34
+ const immutableKeys = [
35
+ "schemaVersion",
36
+ "difficulty",
37
+ "taskId",
38
+ "repo",
39
+ "baseCommit",
40
+ "workdir",
41
+ "sourcePr",
42
+ "sourceUrl",
43
+ "prompt",
44
+ ] as const;
45
+ for (const key of immutableKeys) {
46
+ if (JSON.stringify(repaired[key]) !== JSON.stringify(original[key])) {
47
+ throw new Error(`validation repair changed immutable definition field ${key}`);
48
+ }
49
+ }
50
+ const allowed = new Set(validationRepairPaths(originalTestPatch));
51
+ const outside = changedPaths.filter((path) => !allowed.has(path));
52
+ if (outside.length > 0) {
53
+ throw new Error(
54
+ `validation repair changed files outside held-out tests: ${outside.join(", ")}`,
55
+ );
56
+ }
57
+ }
58
+
59
+ export function validationRepairPrompt(input: {
60
+ readonly definition: TaskDefinition;
61
+ readonly authenticRequest: string;
62
+ readonly diagnostics: string;
63
+ readonly allowedPaths: readonly string[];
64
+ }): string {
65
+ return `Repair the verifier harness and held-out tests for SelfBench task ${input.definition.taskId}.
66
+
67
+ The repository is the exact base snapshot with the current held-out test patch already applied. You may edit only these existing held-out test files:
68
+
69
+ ${input.allowedPaths.map((path) => `- ${path}`).join("\n")}
70
+
71
+ The authentic engineer request is:
72
+
73
+ <authentic_request>
74
+ ${input.authenticRequest.trim()}
75
+ </authentic_request>
76
+
77
+ The failed nop/oracle validation diagnostics are:
78
+
79
+ <validation_diagnostics>
80
+ ${input.diagnostics.trim()}
81
+ </validation_diagnostics>
82
+
83
+ Fix the task so its repository-native verifier is rigorous and runnable. You may edit the allowed held-out tests and /work/definition.json. In definition.json you may change only setupCommand, testCommand, failToPass, passToPass, testPaths, toolchains, timeouts, and resources. Never change task identity, difficulty, repository, base commit, workdir, source pull request, prompt, or reference solution.
84
+
85
+ The required validation split is:
86
+ - nop: the base repository plus held-out tests must make failToPass fail while passToPass succeeds;
87
+ - oracle: after /work/task/solution/gold.patch is applied, failToPass and passToPass must both succeed deterministically.
88
+
89
+ Use the verifier diagnostics rather than guessing. Inspect repository package scripts and CI for the native test command. Keep {tests} as a list-safe placeholder; do not quote the whole placeholder, assign it to one scalar, or hard-code selected paths elsewhere. Use one test mode/bundler per command instead of chaining equivalent suites. Put dependency installation, native builds, fixture generation, and browser/runtime installation in setupCommand, not testCommand. Pin the repository's declared package manager and use frozen installs. Browser-backed tests must install required browser binaries and OS dependencies during setup. Run focused nop/oracle-style commands in the sandbox when feasible.
90
+
91
+ Do not weaken, delete, or skip requested behavioral coverage merely to make commands exit zero. A passing base with no solution is not acceptable. If no rigorous runnable split exists, leave the files and definition unchanged and explain why in the final response.
92
+
93
+ Inspect /work/gold.patch and /work/test.sh only to verify the intended behavior and oracle seam, never to couple assertions to private implementation details. Do not modify application code, the instruction, gold patch, or commit files. Finish with the repaired definition at /work/definition.json and any held-out test edits present in the working tree; do not commit.`;
94
+ }
@@ -0,0 +1,32 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { Worker } from "@temporalio/worker";
4
+ import { createActivities } from "./activities.js";
5
+ import { loadConfig } from "./config.js";
6
+ import { removeEmptyModalCredentialOverrides } from "./modal-auth.js";
7
+ import { runCommand } from "./process.js";
8
+ import { connectTemporalWorker } from "./temporal.js";
9
+
10
+ removeEmptyModalCredentialOverrides();
11
+ const config = loadConfig();
12
+ if (config.execution.kind === "docker" || config.harborEnvironment === "docker") {
13
+ await runCommand("docker", ["info"], { timeoutMs: 30_000 });
14
+ await runCommand("docker", ["compose", "version"], { timeoutMs: 30_000 });
15
+ }
16
+ const connection = await connectTemporalWorker(config.temporal);
17
+ const javascriptWorkflow = fileURLToPath(new URL("./workflow.js", import.meta.url));
18
+ const workflowsPath = existsSync(javascriptWorkflow)
19
+ ? javascriptWorkflow
20
+ : fileURLToPath(new URL("./workflow.ts", import.meta.url));
21
+ const worker = await Worker.create({
22
+ connection,
23
+ namespace: config.temporal.namespace,
24
+ taskQueue: config.temporal.taskQueue,
25
+ workflowsPath,
26
+ activities: createActivities(config),
27
+ maxConcurrentActivityTaskExecutions: config.activityConcurrency,
28
+ });
29
+ console.log(
30
+ `SelfBench worker polling ${config.temporal.namespace}/${config.temporal.taskQueue} with activity concurrency ${config.activityConcurrency}`,
31
+ );
32
+ await worker.run();