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,361 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream, createWriteStream } from "node:fs";
3
+ import { link, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, resolve, sep } from "node:path";
6
+ import { Readable, Transform } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
8
+ import { Storage } from "@google-cloud/storage";
9
+ import type { SelfBenchConfig } from "./config.js";
10
+ import type { ArtifactRef } from "./contracts.js";
11
+ import { sha256 } from "./hash.js";
12
+
13
+ export interface ArtifactStore {
14
+ put(key: string, value: Uint8Array, contentType: string): Promise<ArtifactRef>;
15
+ putFile(key: string, sourcePath: string, contentType: string): Promise<ArtifactRef>;
16
+ get(reference: ArtifactRef): Promise<Uint8Array>;
17
+ openRead(reference: ArtifactRef): Promise<Readable>;
18
+ getByKey(key: string): Promise<Uint8Array | undefined>;
19
+ }
20
+
21
+ export function createArtifactStore(config: SelfBenchConfig["artifact"]): ArtifactStore {
22
+ return config.kind === "gcs"
23
+ ? new GcsArtifactStore(config.bucket, config.prefix)
24
+ : new LocalArtifactStore(config.directory);
25
+ }
26
+
27
+ export class LocalArtifactStore implements ArtifactStore {
28
+ readonly #root: string;
29
+
30
+ constructor(root: string) {
31
+ this.#root = resolve(root);
32
+ }
33
+
34
+ async put(key: string, value: Uint8Array, contentType: string): Promise<ArtifactRef> {
35
+ const path = this.#pathFor(key);
36
+ await mkdir(dirname(path), { recursive: true });
37
+ const digest = sha256(value);
38
+ try {
39
+ await writeFile(path, value, { flag: "wx" });
40
+ } catch (error) {
41
+ const existing = await fileDigest(path).catch(() => undefined);
42
+ if (!existing || existing.sha256 !== digest || existing.sizeBytes !== value.byteLength) {
43
+ throw error;
44
+ }
45
+ }
46
+ return {
47
+ uri: `file://${path}`,
48
+ sha256: digest,
49
+ sizeBytes: value.byteLength,
50
+ contentType,
51
+ };
52
+ }
53
+
54
+ async putFile(key: string, sourcePath: string, contentType: string): Promise<ArtifactRef> {
55
+ const path = this.#pathFor(key);
56
+ await mkdir(dirname(path), { recursive: true });
57
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
58
+ try {
59
+ const digest = await copyWithDigest(sourcePath, temporaryPath);
60
+ try {
61
+ await link(temporaryPath, path);
62
+ } catch (error) {
63
+ if (!hasCode(error, "EEXIST")) {
64
+ throw error;
65
+ }
66
+ const existing = await fileDigest(path).catch(() => undefined);
67
+ if (
68
+ !existing ||
69
+ existing.sha256 !== digest.sha256 ||
70
+ existing.sizeBytes !== digest.sizeBytes
71
+ ) {
72
+ throw new Error(`artifact already exists with different contents: file://${path}`);
73
+ }
74
+ }
75
+ return {
76
+ uri: `file://${path}`,
77
+ ...digest,
78
+ contentType,
79
+ };
80
+ } finally {
81
+ await rm(temporaryPath, { force: true });
82
+ }
83
+ }
84
+
85
+ async get(reference: ArtifactRef): Promise<Uint8Array> {
86
+ const url = new URL(reference.uri);
87
+ if (url.protocol !== "file:") {
88
+ throw new Error(`local artifact store cannot read ${reference.uri}`);
89
+ }
90
+ const path = resolve(decodeURIComponent(url.pathname));
91
+ this.#assertInsideRoot(path);
92
+ const value = await readFile(path);
93
+ verifyArtifact(reference, value);
94
+ return value;
95
+ }
96
+
97
+ async openRead(reference: ArtifactRef): Promise<Readable> {
98
+ const path = this.#pathForReference(reference);
99
+ if ((await stat(path)).size !== reference.sizeBytes) {
100
+ throw new Error(`artifact integrity check failed: ${reference.uri}`);
101
+ }
102
+ return verifiedArtifactReadStream(reference, createReadStream(path));
103
+ }
104
+
105
+ async getByKey(key: string): Promise<Uint8Array | undefined> {
106
+ try {
107
+ return await readFile(this.#pathFor(key));
108
+ } catch (error) {
109
+ if (isNotFound(error)) {
110
+ return undefined;
111
+ }
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ #pathForReference(reference: ArtifactRef): string {
117
+ const url = new URL(reference.uri);
118
+ if (url.protocol !== "file:") {
119
+ throw new Error(`local artifact store cannot read ${reference.uri}`);
120
+ }
121
+ const path = resolve(decodeURIComponent(url.pathname));
122
+ this.#assertInsideRoot(path);
123
+ return path;
124
+ }
125
+
126
+ #pathFor(key: string): string {
127
+ if (!key || key.startsWith("/") || key.split("/").some((part) => part === "..")) {
128
+ throw new Error(`unsafe artifact key: ${key}`);
129
+ }
130
+ const path = resolve(this.#root, key);
131
+ this.#assertInsideRoot(path);
132
+ return path;
133
+ }
134
+
135
+ #assertInsideRoot(path: string): void {
136
+ if (path !== this.#root && !path.startsWith(`${this.#root}${sep}`)) {
137
+ throw new Error(`artifact path escapes root: ${path}`);
138
+ }
139
+ }
140
+ }
141
+
142
+ export class GcsArtifactStore implements ArtifactStore {
143
+ readonly #storage = new Storage();
144
+ readonly #bucket: string;
145
+ readonly #prefix: string;
146
+
147
+ constructor(bucket: string, prefix: string) {
148
+ this.#bucket = bucket;
149
+ this.#prefix = prefix.replace(/^\/+|\/+$/g, "");
150
+ }
151
+
152
+ async put(key: string, value: Uint8Array, contentType: string): Promise<ArtifactRef> {
153
+ const object = this.#objectFor(key);
154
+ const digest = sha256(value);
155
+ const file = this.#storage.bucket(this.#bucket).file(object);
156
+ try {
157
+ await file.save(value, {
158
+ resumable: false,
159
+ preconditionOpts: { ifGenerationMatch: 0 },
160
+ metadata: {
161
+ contentType,
162
+ metadata: { sha256: digest },
163
+ },
164
+ });
165
+ } catch (error) {
166
+ const [exists] = await file.exists();
167
+ if (!exists) {
168
+ throw error;
169
+ }
170
+ const [metadata] = await file.getMetadata();
171
+ if (metadata.metadata?.sha256 !== digest) {
172
+ throw new Error(
173
+ `artifact already exists with different contents: gs://${this.#bucket}/${object}`,
174
+ );
175
+ }
176
+ }
177
+ return {
178
+ uri: `gs://${this.#bucket}/${object}`,
179
+ sha256: digest,
180
+ sizeBytes: value.byteLength,
181
+ contentType,
182
+ };
183
+ }
184
+
185
+ async putFile(key: string, sourcePath: string, contentType: string): Promise<ArtifactRef> {
186
+ const object = this.#objectFor(key);
187
+ const bucket = this.#storage.bucket(this.#bucket);
188
+ const file = bucket.file(object);
189
+ const snapshotDirectory = await mkdtemp(join(tmpdir(), "selfbench-artifact-"));
190
+ const snapshotPath = join(snapshotDirectory, "upload");
191
+ try {
192
+ const digest = await copyWithDigest(sourcePath, snapshotPath);
193
+ try {
194
+ await bucket.upload(snapshotPath, {
195
+ destination: object,
196
+ resumable: true,
197
+ preconditionOpts: { ifGenerationMatch: 0 },
198
+ metadata: {
199
+ contentType,
200
+ metadata: { sha256: digest.sha256 },
201
+ },
202
+ });
203
+ } catch (error) {
204
+ const [exists] = await file.exists();
205
+ if (!exists) {
206
+ throw error;
207
+ }
208
+ const [metadata] = await file.getMetadata();
209
+ if (
210
+ metadata.metadata?.sha256 !== digest.sha256 ||
211
+ Number(metadata.size) !== digest.sizeBytes
212
+ ) {
213
+ throw new Error(
214
+ `artifact already exists with different contents: gs://${this.#bucket}/${object}`,
215
+ );
216
+ }
217
+ }
218
+ return {
219
+ uri: `gs://${this.#bucket}/${object}`,
220
+ ...digest,
221
+ contentType,
222
+ };
223
+ } finally {
224
+ await rm(snapshotDirectory, { recursive: true, force: true });
225
+ }
226
+ }
227
+
228
+ async get(reference: ArtifactRef): Promise<Uint8Array> {
229
+ const match = /^gs:\/\/([^/]+)\/(.+)$/.exec(reference.uri);
230
+ if (!match) {
231
+ throw new Error(`GCS artifact store cannot read ${reference.uri}`);
232
+ }
233
+ const [, bucket, object] = match;
234
+ if (bucket !== this.#bucket || !object) {
235
+ throw new Error(`artifact is outside configured bucket: ${reference.uri}`);
236
+ }
237
+ if (this.#prefix && object !== this.#prefix && !object.startsWith(`${this.#prefix}/`)) {
238
+ throw new Error(`artifact is outside configured bucket: ${reference.uri}`);
239
+ }
240
+ const [value] = await this.#storage.bucket(bucket).file(object).download();
241
+ verifyArtifact(reference, value);
242
+ return value;
243
+ }
244
+
245
+ async openRead(reference: ArtifactRef): Promise<Readable> {
246
+ const file = this.#fileForReference(reference);
247
+ const [metadata] = await file.getMetadata();
248
+ if (
249
+ Number(metadata.size) !== reference.sizeBytes ||
250
+ metadata.metadata?.sha256 !== reference.sha256
251
+ ) {
252
+ throw new Error(`artifact integrity check failed: ${reference.uri}`);
253
+ }
254
+ return verifiedArtifactReadStream(reference, file.createReadStream());
255
+ }
256
+
257
+ async getByKey(key: string): Promise<Uint8Array | undefined> {
258
+ const file = this.#storage.bucket(this.#bucket).file(this.#objectFor(key));
259
+ const [exists] = await file.exists();
260
+ if (!exists) {
261
+ return undefined;
262
+ }
263
+ const [value] = await file.download();
264
+ return value;
265
+ }
266
+
267
+ #fileForReference(reference: ArtifactRef) {
268
+ const match = /^gs:\/\/([^/]+)\/(.+)$/.exec(reference.uri);
269
+ if (!match) {
270
+ throw new Error(`GCS artifact store cannot read ${reference.uri}`);
271
+ }
272
+ const [, bucket, object] = match;
273
+ if (bucket !== this.#bucket || !object) {
274
+ throw new Error(`artifact is outside configured bucket: ${reference.uri}`);
275
+ }
276
+ if (this.#prefix && object !== this.#prefix && !object.startsWith(`${this.#prefix}/`)) {
277
+ throw new Error(`artifact is outside configured bucket: ${reference.uri}`);
278
+ }
279
+ return this.#storage.bucket(bucket).file(object);
280
+ }
281
+
282
+ #objectFor(key: string): string {
283
+ if (!key || key.startsWith("/") || key.split("/").some((part) => part === "..")) {
284
+ throw new Error(`unsafe artifact key: ${key}`);
285
+ }
286
+ return this.#prefix ? `${this.#prefix}/${key}` : key;
287
+ }
288
+ }
289
+
290
+ export function verifiedArtifactReadStream(reference: ArtifactRef, input: Readable): Readable {
291
+ return Readable.from(
292
+ (async function* () {
293
+ const hash = createHash("sha256");
294
+ let sizeBytes = 0;
295
+ for await (const chunk of input) {
296
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
297
+ hash.update(bytes);
298
+ sizeBytes += bytes.byteLength;
299
+ yield bytes;
300
+ }
301
+ if (sizeBytes !== reference.sizeBytes || hash.digest("hex") !== reference.sha256) {
302
+ throw new Error(`artifact integrity check failed: ${reference.uri}`);
303
+ }
304
+ })(),
305
+ );
306
+ }
307
+
308
+ async function copyWithDigest(
309
+ sourcePath: string,
310
+ destinationPath: string,
311
+ ): Promise<{ sha256: string; sizeBytes: number }> {
312
+ const hash = createHash("sha256");
313
+ let sizeBytes = 0;
314
+ const hasher = new Transform({
315
+ transform(chunk: Buffer, _encoding, callback) {
316
+ hash.update(chunk);
317
+ sizeBytes += chunk.byteLength;
318
+ callback(undefined, chunk);
319
+ },
320
+ });
321
+ await pipeline(
322
+ createReadStream(sourcePath),
323
+ hasher,
324
+ createWriteStream(destinationPath, { flags: "wx" }),
325
+ );
326
+ return { sha256: hash.digest("hex"), sizeBytes };
327
+ }
328
+
329
+ async function fileDigest(path: string): Promise<{ sha256: string; sizeBytes: number }> {
330
+ const hash = createHash("sha256");
331
+ let sizeBytes = 0;
332
+ for await (const chunk of createReadStream(path)) {
333
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
334
+ hash.update(bytes);
335
+ sizeBytes += bytes.byteLength;
336
+ }
337
+ return { sha256: hash.digest("hex"), sizeBytes };
338
+ }
339
+
340
+ function hasCode(error: unknown, code: string): boolean {
341
+ return (
342
+ typeof error === "object" &&
343
+ error !== null &&
344
+ "code" in error &&
345
+ (error as { code?: unknown }).code === code
346
+ );
347
+ }
348
+
349
+ function isNotFound(error: unknown): boolean {
350
+ return hasCode(error, "ENOENT");
351
+ }
352
+
353
+ function verifyArtifact(reference: ArtifactRef, value: Uint8Array): void {
354
+ if (value.byteLength !== reference.sizeBytes || sha256(value) !== reference.sha256) {
355
+ throw new Error(`artifact integrity check failed: ${reference.uri}`);
356
+ }
357
+ }
358
+
359
+ export async function artifactSize(path: string): Promise<number> {
360
+ return (await stat(path)).size;
361
+ }
package/src/audit.ts ADDED
@@ -0,0 +1,106 @@
1
+ import type { Difficulty, TaskDefinition } from "./contracts.js";
2
+
3
+ export interface StaticAuditReport {
4
+ readonly accepted: boolean;
5
+ readonly blockers: readonly string[];
6
+ readonly metrics: {
7
+ readonly implementationFiles: number;
8
+ readonly implementationChangedLines: number;
9
+ readonly testFiles: number;
10
+ };
11
+ }
12
+
13
+ const thresholds: Record<
14
+ Difficulty,
15
+ {
16
+ readonly implementationFiles: number;
17
+ readonly changedLines: number;
18
+ readonly passToPass: number;
19
+ }
20
+ > = {
21
+ easy: { implementationFiles: 1, changedLines: 20, passToPass: 0 },
22
+ medium: { implementationFiles: 2, changedLines: 50, passToPass: 1 },
23
+ hard: { implementationFiles: 3, changedLines: 100, passToPass: 2 },
24
+ };
25
+
26
+ export function auditTaskDefinition(
27
+ definition: TaskDefinition,
28
+ goldPatch: string,
29
+ testPatch: string,
30
+ ): StaticAuditReport {
31
+ const gold = patchMetrics(goldPatch);
32
+ const tests = patchMetrics(testPatch);
33
+ const threshold = thresholds[definition.difficulty];
34
+ const testPathSet = new Set(tests.files);
35
+ const blockers: string[] = [];
36
+ const overlap = gold.files.filter((path) => testPathSet.has(path));
37
+ if (overlap.length > 0) {
38
+ blockers.push(`gold and held-out test patches overlap: ${overlap.join(", ")}`);
39
+ }
40
+ if (gold.files.length < threshold.implementationFiles) {
41
+ blockers.push(
42
+ `${definition.difficulty} mode requires at least ${threshold.implementationFiles} implementation files; found ${gold.files.length}`,
43
+ );
44
+ }
45
+ if (gold.changedLines < threshold.changedLines) {
46
+ blockers.push(
47
+ `${definition.difficulty} mode requires at least ${threshold.changedLines} changed implementation lines; found ${gold.changedLines}`,
48
+ );
49
+ }
50
+ if (tests.files.length === 0) {
51
+ blockers.push("held-out test patch changes no files");
52
+ }
53
+ if (definition.passToPass.length < threshold.passToPass) {
54
+ blockers.push(
55
+ `${definition.difficulty} mode requires at least ${threshold.passToPass} pass-to-pass regression tests`,
56
+ );
57
+ }
58
+ if (
59
+ definition.failToPass.some((path) => testCommandHardcodesPath(definition.testCommand, path)) ||
60
+ definition.passToPass.some((path) => testCommandHardcodesPath(definition.testCommand, path))
61
+ ) {
62
+ blockers.push(
63
+ 'test command must not hard-code fail-to-pass or pass-to-pass paths outside "{tests}"',
64
+ );
65
+ }
66
+ if (definition.testCommand.split("{tests}").length !== 2) {
67
+ blockers.push('test command must contain "{tests}" exactly once');
68
+ }
69
+ if (/(["'])\{tests\}\1/.test(definition.testCommand)) {
70
+ blockers.push('test command must expand "{tests}" as an unquoted shell argument list');
71
+ }
72
+ return {
73
+ accepted: blockers.length === 0,
74
+ blockers,
75
+ metrics: {
76
+ implementationFiles: gold.files.length,
77
+ implementationChangedLines: gold.changedLines,
78
+ testFiles: tests.files.length,
79
+ },
80
+ };
81
+ }
82
+
83
+ function testCommandHardcodesPath(command: string, path: string): boolean {
84
+ return command.replace("{tests}", "").includes(path);
85
+ }
86
+
87
+ function patchMetrics(patch: string): { files: string[]; changedLines: number } {
88
+ const files: string[] = [];
89
+ let changedLines = 0;
90
+ for (const line of patch.split("\n")) {
91
+ if (line.startsWith("diff --git a/")) {
92
+ const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
93
+ if (match?.[2]) {
94
+ files.push(match[2]);
95
+ }
96
+ continue;
97
+ }
98
+ if (
99
+ (line.startsWith("+") && !line.startsWith("+++")) ||
100
+ (line.startsWith("-") && !line.startsWith("---"))
101
+ ) {
102
+ changedLines += 1;
103
+ }
104
+ }
105
+ return { files, changedLines };
106
+ }
@@ -0,0 +1,3 @@
1
+ const unsetBuildCommit = "0".repeat(40);
2
+
3
+ export const buildCommit = process.env.SELFBENCH_BUILD_COMMIT ?? unsetBuildCommit;