self-bench 0.3.3 → 0.3.4
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/Dockerfile.sandbox +1 -1
- package/README.md +20 -0
- package/bun.lock +48 -0
- package/compose.yaml +7 -1
- package/dist/agent-smoke-main.js +3 -2
- package/dist/agent-smoke-main.js.map +1 -1
- package/dist/agent-smoke.d.ts +2 -1
- package/dist/agent-smoke.d.ts.map +1 -1
- package/dist/agent-smoke.js +6 -1
- package/dist/agent-smoke.js.map +1 -1
- package/dist/api.d.ts +24 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +25 -18
- package/dist/api.js.map +1 -1
- package/dist/build-metadata.js +1 -1
- package/dist/cli.js +67 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +36 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +89 -15
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +22 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +31 -6
- package/dist/contracts.js.map +1 -1
- package/dist/eval-main.js +3 -2
- package/dist/eval-main.js.map +1 -1
- package/dist/evaluate.d.ts +2 -1
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +2 -1
- package/dist/evaluate.js.map +1 -1
- package/dist/harbor-environment.d.ts +2 -0
- package/dist/harbor-environment.d.ts.map +1 -0
- package/dist/harbor-environment.js +15 -0
- package/dist/harbor-environment.js.map +1 -0
- package/dist/harbor-results.d.ts.map +1 -1
- package/dist/harbor-results.js +2 -0
- package/dist/harbor-results.js.map +1 -1
- package/dist/process.d.ts +2 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js.map +1 -1
- package/dist/providers.d.ts +8 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +14 -0
- package/dist/providers.js.map +1 -0
- package/dist/reaudit-main.js +5 -5
- package/dist/reaudit-main.js.map +1 -1
- package/dist/repair-main.js +5 -5
- package/dist/repair-main.js.map +1 -1
- package/dist/sandbox-author.bundle.js +40 -6
- package/dist/sandbox-timeout.d.ts +11 -0
- package/dist/sandbox-timeout.d.ts.map +1 -0
- package/dist/sandbox-timeout.js +41 -0
- package/dist/sandbox-timeout.js.map +1 -0
- package/dist/sandbox-validation-repair.bundle.js +40 -6
- package/dist/sandbox.d.ts +2 -2
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +10 -3
- package/dist/sandbox.js.map +1 -1
- package/dist/temporal/activities.d.ts +2 -2
- package/dist/temporal/activities.d.ts.map +1 -1
- package/dist/temporal/activities.js +7 -1
- package/dist/temporal/activities.js.map +1 -1
- package/dist/temporal/worker-main.js +2 -2
- package/dist/temporal/worker-main.js.map +1 -1
- package/dist/terminal-prompts.d.ts +26 -0
- package/dist/terminal-prompts.d.ts.map +1 -0
- package/dist/terminal-prompts.js +137 -0
- package/dist/terminal-prompts.js.map +1 -0
- package/dist/terminal-reporter.d.ts +30 -0
- package/dist/terminal-reporter.d.ts.map +1 -0
- package/dist/terminal-reporter.js +90 -0
- package/dist/terminal-reporter.js.map +1 -0
- package/dist/validate-main.js +8 -2
- package/dist/validate-main.js.map +1 -1
- package/dist/vercel-cli.d.ts +64 -0
- package/dist/vercel-cli.d.ts.map +1 -0
- package/dist/vercel-cli.js +303 -0
- package/dist/vercel-cli.js.map +1 -0
- package/dist/vercel-command.d.ts +14 -0
- package/dist/vercel-command.d.ts.map +1 -0
- package/dist/vercel-command.js +93 -0
- package/dist/vercel-command.js.map +1 -0
- package/dist/vercel-executor.d.ts +14 -0
- package/dist/vercel-executor.d.ts.map +1 -0
- package/dist/vercel-executor.js +356 -0
- package/dist/vercel-executor.js.map +1 -0
- package/dist/vercel-fetch.d.ts +6 -0
- package/dist/vercel-fetch.d.ts.map +1 -0
- package/dist/vercel-fetch.js +39 -0
- package/dist/vercel-fetch.js.map +1 -0
- package/dist/vercel-profile.d.ts +34 -0
- package/dist/vercel-profile.d.ts.map +1 -0
- package/dist/vercel-profile.js +244 -0
- package/dist/vercel-profile.js.map +1 -0
- package/dist/vercel-runtime-image.d.ts +51 -0
- package/dist/vercel-runtime-image.d.ts.map +1 -0
- package/dist/vercel-runtime-image.js +123 -0
- package/dist/vercel-runtime-image.js.map +1 -0
- package/dist/vercel-setup-probe.d.ts +49 -0
- package/dist/vercel-setup-probe.d.ts.map +1 -0
- package/dist/vercel-setup-probe.js +151 -0
- package/dist/vercel-setup-probe.js.map +1 -0
- package/dist/vercel-setup.d.ts +42 -0
- package/dist/vercel-setup.d.ts.map +1 -0
- package/dist/vercel-setup.js +280 -0
- package/dist/vercel-setup.js.map +1 -0
- package/docs/evaluations.md +2 -0
- package/docs/operations.md +128 -6
- package/package.json +3 -1
- package/src/agent-smoke-main.ts +3 -2
- package/src/agent-smoke.ts +8 -2
- package/src/api.ts +31 -18
- package/src/cli.ts +72 -11
- package/src/config.ts +150 -25
- package/src/contracts.ts +33 -6
- package/src/eval-main.ts +3 -2
- package/src/evaluate.ts +5 -3
- package/src/harbor-environment.ts +17 -0
- package/src/harbor-results.ts +2 -0
- package/src/process.ts +3 -1
- package/src/providers.ts +22 -0
- package/src/reaudit-main.ts +8 -5
- package/src/repair-main.ts +8 -5
- package/src/sandbox-timeout.ts +56 -0
- package/src/sandbox.ts +15 -5
- package/src/temporal/activities.ts +9 -3
- package/src/temporal/worker-main.ts +2 -2
- package/src/terminal-prompts.ts +197 -0
- package/src/terminal-reporter.ts +126 -0
- package/src/validate-main.ts +9 -3
- package/src/vercel-cli.ts +403 -0
- package/src/vercel-command.ts +127 -0
- package/src/vercel-executor.ts +448 -0
- package/src/vercel-fetch.ts +45 -0
- package/src/vercel-profile.ts +319 -0
- package/src/vercel-runtime-image.ts +204 -0
- package/src/vercel-setup-probe.ts +229 -0
- package/src/vercel-setup.ts +419 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { APIError, Sandbox } from "@vercel/sandbox";
|
|
4
|
+
import {
|
|
5
|
+
isDigestPinnedOciImage,
|
|
6
|
+
type SelfBenchWorkerConfig,
|
|
7
|
+
type VercelCredentials,
|
|
8
|
+
} from "./config.js";
|
|
9
|
+
import { RollingOutput } from "./process.js";
|
|
10
|
+
import type {
|
|
11
|
+
SandboxExecutor,
|
|
12
|
+
SandboxRequest,
|
|
13
|
+
SandboxResult,
|
|
14
|
+
SandboxRunOptions,
|
|
15
|
+
} from "./sandbox.js";
|
|
16
|
+
import { executeVercelCommand, VERCEL_WORK_DIRECTORY } from "./vercel-command.js";
|
|
17
|
+
import { preventAmbiguousVercelCommandStartRetries } from "./vercel-fetch.js";
|
|
18
|
+
|
|
19
|
+
const CLEANUP_TIMEOUT_MS = 60_000;
|
|
20
|
+
const CLEANUP_RETRY_DELAY_MS = 1_000;
|
|
21
|
+
const CREATE_RATE_LIMIT_RETRIES = 2;
|
|
22
|
+
const CREATE_RATE_LIMIT_FALLBACK_DELAY_MS = 30_000;
|
|
23
|
+
const CREATE_RATE_LIMIT_MAX_DELAY_MS = 60_000;
|
|
24
|
+
const CREATE_REQUEST_TIMEOUT_MS = 60_000;
|
|
25
|
+
const HARD_TIMEOUT_EXIT_CODE = 124;
|
|
26
|
+
const LATE_CREATE_RECOVERY_DELAYS_MS = [0, 250, 750, 1_500] as const;
|
|
27
|
+
|
|
28
|
+
type VercelExecutionConfig = Extract<
|
|
29
|
+
SelfBenchWorkerConfig["execution"],
|
|
30
|
+
{ readonly kind: "vercel" }
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
type RunOutcome =
|
|
34
|
+
| { readonly ok: true; readonly result: SandboxResult }
|
|
35
|
+
| { readonly ok: false; readonly error: unknown };
|
|
36
|
+
|
|
37
|
+
type Sleep = (delayMs: number, signal: AbortSignal) => Promise<void>;
|
|
38
|
+
|
|
39
|
+
class VercelHardTimeoutError extends Error {
|
|
40
|
+
constructor(name: string, stage: string, timeoutMs: number) {
|
|
41
|
+
super(`Vercel sandbox ${name} stage ${stage} exceeded ${timeoutMs}ms`);
|
|
42
|
+
// Passed as the abort reason to in-flight SDK calls, so it carries the
|
|
43
|
+
// standard DOMException timeout name. run() converts it into an exit-124
|
|
44
|
+
// result rather than throwing.
|
|
45
|
+
this.name = "TimeoutError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class VercelSandboxExecutor implements SandboxExecutor {
|
|
50
|
+
readonly #config: VercelExecutionConfig;
|
|
51
|
+
readonly #fetch: typeof globalThis.fetch;
|
|
52
|
+
readonly #sleep: Sleep;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
config: VercelExecutionConfig,
|
|
56
|
+
fetch = globalThis.fetch,
|
|
57
|
+
sleep: Sleep = abortableDelay,
|
|
58
|
+
) {
|
|
59
|
+
this.#config = validateConfig(config);
|
|
60
|
+
this.#fetch = preventAmbiguousVercelCommandStartRetries(fetch);
|
|
61
|
+
this.#sleep = sleep;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async run(request: SandboxRequest, options: SandboxRunOptions = {}): Promise<SandboxResult> {
|
|
65
|
+
options.signal?.throwIfAborted();
|
|
66
|
+
const resources = validateRequest(request);
|
|
67
|
+
|
|
68
|
+
const name = sandboxName(request.runId, request.stage);
|
|
69
|
+
const tags = {
|
|
70
|
+
selfbench_run: request.runId.slice(0, 256),
|
|
71
|
+
selfbench_stage: request.stage.slice(0, 256),
|
|
72
|
+
};
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const stdout = new RollingOutput();
|
|
75
|
+
const stderr = new RollingOutput();
|
|
76
|
+
let terminationError: unknown;
|
|
77
|
+
let sandbox: Sandbox | undefined;
|
|
78
|
+
let deletePromise: Promise<void> | undefined;
|
|
79
|
+
let allocationMayExist = false;
|
|
80
|
+
|
|
81
|
+
const deleteHandle = (): Promise<void> => {
|
|
82
|
+
if (!sandbox) {
|
|
83
|
+
return Promise.resolve();
|
|
84
|
+
}
|
|
85
|
+
deletePromise ??= sandbox.delete({ signal: AbortSignal.timeout(CLEANUP_TIMEOUT_MS) });
|
|
86
|
+
return deletePromise;
|
|
87
|
+
};
|
|
88
|
+
const terminate = (error: unknown): void => {
|
|
89
|
+
if (terminationError !== undefined) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
terminationError = error;
|
|
93
|
+
controller.abort(error);
|
|
94
|
+
if (sandbox) {
|
|
95
|
+
void deleteHandle().catch(() => undefined);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const abort = (): void => terminate(abortReason(options.signal));
|
|
99
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
100
|
+
if (options.signal?.aborted) {
|
|
101
|
+
abort();
|
|
102
|
+
}
|
|
103
|
+
const hardTimeout = setTimeout(() => {
|
|
104
|
+
terminate(new VercelHardTimeoutError(name, request.stage, request.timeoutMs));
|
|
105
|
+
}, request.timeoutMs);
|
|
106
|
+
hardTimeout.unref();
|
|
107
|
+
|
|
108
|
+
let outcome: RunOutcome;
|
|
109
|
+
try {
|
|
110
|
+
sandbox = await this.#createSandbox(
|
|
111
|
+
name,
|
|
112
|
+
tags,
|
|
113
|
+
resources.vcpus,
|
|
114
|
+
request.timeoutMs,
|
|
115
|
+
controller.signal,
|
|
116
|
+
(value) => {
|
|
117
|
+
allocationMayExist = value;
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
allocationMayExist = false;
|
|
121
|
+
throwIfTerminated(terminationError);
|
|
122
|
+
|
|
123
|
+
const session = sandbox.currentSession();
|
|
124
|
+
if (session.status !== "running") {
|
|
125
|
+
throw new Error(`Vercel sandbox ${name} was created in unexpected state ${session.status}`);
|
|
126
|
+
}
|
|
127
|
+
if (session.cwd !== VERCEL_WORK_DIRECTORY) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Vercel image ${this.#config.image} has workdir ${session.cwd}; expected ${VERCEL_WORK_DIRECTORY}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (session.vcpus !== resources.vcpus || session.memory !== resources.memoryMiB) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Vercel sandbox ${name} returned ${session.vcpus} vCPU/${session.memory} MiB; expected ${resources.vcpus} vCPU/${resources.memoryMiB} MiB`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (request.files && request.files.length > 0) {
|
|
139
|
+
await session.writeFiles(
|
|
140
|
+
request.files.map((file) => ({ path: file.path, content: file.contents })),
|
|
141
|
+
{ signal: controller.signal },
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
throwIfTerminated(terminationError);
|
|
145
|
+
|
|
146
|
+
const outputs = await executeVercelCommand({
|
|
147
|
+
session,
|
|
148
|
+
request,
|
|
149
|
+
options,
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
terminate,
|
|
152
|
+
stdout,
|
|
153
|
+
stderr,
|
|
154
|
+
});
|
|
155
|
+
throwIfTerminated(terminationError);
|
|
156
|
+
outcome = { ok: true, result: { sandboxId: sandbox.name, ...outputs } };
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const failure = terminationError ?? error;
|
|
159
|
+
outcome =
|
|
160
|
+
failure instanceof VercelHardTimeoutError
|
|
161
|
+
? {
|
|
162
|
+
ok: true,
|
|
163
|
+
result: {
|
|
164
|
+
sandboxId: sandbox?.name ?? name,
|
|
165
|
+
exitCode: HARD_TIMEOUT_EXIT_CODE,
|
|
166
|
+
stdout: stdout.text(),
|
|
167
|
+
stderr: stderr.text(),
|
|
168
|
+
outputs: {},
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
: { ok: false, error: failure };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
clearTimeout(hardTimeout);
|
|
175
|
+
options.signal?.removeEventListener("abort", abort);
|
|
176
|
+
try {
|
|
177
|
+
await this.#cleanup(name, sandbox, deleteHandle, allocationMayExist);
|
|
178
|
+
} catch (cleanupError) {
|
|
179
|
+
const publicCleanupError = sanitizeCleanupError(cleanupError, this.#config.credentials.token);
|
|
180
|
+
outcome = outcome.ok
|
|
181
|
+
? { ok: false, error: publicCleanupError }
|
|
182
|
+
: {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: attachCleanupError(outcome.error, publicCleanupError),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (!outcome.ok) {
|
|
188
|
+
throw outcome.error;
|
|
189
|
+
}
|
|
190
|
+
return outcome.result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
close(): void {}
|
|
194
|
+
|
|
195
|
+
async #createSandbox(
|
|
196
|
+
name: string,
|
|
197
|
+
tags: Readonly<Record<string, string>>,
|
|
198
|
+
vcpus: number,
|
|
199
|
+
timeoutMs: number,
|
|
200
|
+
signal: AbortSignal,
|
|
201
|
+
setAllocationMayExist: (value: boolean) => void,
|
|
202
|
+
): Promise<Sandbox> {
|
|
203
|
+
for (let retries = 0; ; retries += 1) {
|
|
204
|
+
signal.throwIfAborted();
|
|
205
|
+
setAllocationMayExist(true);
|
|
206
|
+
try {
|
|
207
|
+
return await Sandbox.create({
|
|
208
|
+
...this.#config.credentials,
|
|
209
|
+
fetch: this.#fetch,
|
|
210
|
+
image: this.#config.image,
|
|
211
|
+
name,
|
|
212
|
+
persistent: false,
|
|
213
|
+
resources: { vcpus },
|
|
214
|
+
signal: withTimeout(signal, CREATE_REQUEST_TIMEOUT_MS),
|
|
215
|
+
tags: { ...tags },
|
|
216
|
+
timeout: timeoutMs,
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (createErrorConfirmsNoAllocation(error)) {
|
|
220
|
+
setAllocationMayExist(false);
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
!(error instanceof APIError) ||
|
|
224
|
+
error.response.status !== 429 ||
|
|
225
|
+
retries >= CREATE_RATE_LIMIT_RETRIES
|
|
226
|
+
) {
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
await this.#sleep(createRetryDelayMs(error.response), signal);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async #cleanup(
|
|
235
|
+
name: string,
|
|
236
|
+
sandbox: Sandbox | undefined,
|
|
237
|
+
deleteHandle: () => Promise<void>,
|
|
238
|
+
allocationMayExist: boolean,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
let directDeleteError: unknown;
|
|
241
|
+
if (sandbox) {
|
|
242
|
+
try {
|
|
243
|
+
await deleteHandle();
|
|
244
|
+
return;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
directDeleteError = error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!sandbox && !allocationMayExist) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const recoveryDelays = sandbox ? [CLEANUP_RETRY_DELAY_MS] : LATE_CREATE_RECOVERY_DELAYS_MS;
|
|
255
|
+
for (const delayMs of recoveryDelays) {
|
|
256
|
+
if (delayMs > 0) {
|
|
257
|
+
await this.#sleep(delayMs, AbortSignal.timeout(CLEANUP_TIMEOUT_MS));
|
|
258
|
+
}
|
|
259
|
+
let handle: Sandbox;
|
|
260
|
+
try {
|
|
261
|
+
handle = await Sandbox.get({
|
|
262
|
+
...this.#config.credentials,
|
|
263
|
+
fetch: this.#fetch,
|
|
264
|
+
name,
|
|
265
|
+
resume: false,
|
|
266
|
+
signal: AbortSignal.timeout(CLEANUP_TIMEOUT_MS),
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error instanceof APIError && error.response.status === 404) {
|
|
270
|
+
if (sandbox) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
await handle.delete({ signal: AbortSignal.timeout(CLEANUP_TIMEOUT_MS) });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error instanceof APIError && error.response.status === 404) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
throw new Error("sandbox absence remained unconfirmed after an ambiguous create failure");
|
|
288
|
+
} catch (recoveryError) {
|
|
289
|
+
const errors =
|
|
290
|
+
directDeleteError === undefined ? [recoveryError] : [directDeleteError, recoveryError];
|
|
291
|
+
throw new AggregateError(
|
|
292
|
+
errors,
|
|
293
|
+
`failed to delete Vercel sandbox ${name}: ${errors.map(errorMessage).join("; ")}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function validateConfig(config: VercelExecutionConfig): VercelExecutionConfig {
|
|
300
|
+
const credentials: VercelCredentials = {
|
|
301
|
+
token: config.credentials.token.trim(),
|
|
302
|
+
teamId: config.credentials.teamId.trim(),
|
|
303
|
+
projectId: config.credentials.projectId.trim(),
|
|
304
|
+
};
|
|
305
|
+
if (!credentials.token || !credentials.teamId || !credentials.projectId) {
|
|
306
|
+
throw new Error("Vercel execution requires a complete nonblank credential triple");
|
|
307
|
+
}
|
|
308
|
+
const image = config.image.trim();
|
|
309
|
+
if (!isDigestPinnedOciImage(image)) {
|
|
310
|
+
throw new Error("Vercel execution requires a digest-pinned image");
|
|
311
|
+
}
|
|
312
|
+
return { kind: "vercel", credentials, image, timeoutCapMs: config.timeoutCapMs };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function validateRequest(request: SandboxRequest): {
|
|
316
|
+
readonly vcpus: number;
|
|
317
|
+
readonly memoryMiB: number;
|
|
318
|
+
} {
|
|
319
|
+
if (!Number.isInteger(request.timeoutMs) || request.timeoutMs < 100) {
|
|
320
|
+
throw new Error("sandbox timeout must be an integer of at least 100ms");
|
|
321
|
+
}
|
|
322
|
+
if (request.timeoutMs > 24 * 60 * 60 * 1_000) {
|
|
323
|
+
throw new Error("Vercel sandbox timeout cannot exceed 24 hours");
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
request.inactivityTimeoutMs !== undefined &&
|
|
327
|
+
(!Number.isInteger(request.inactivityTimeoutMs) || request.inactivityTimeoutMs < 1)
|
|
328
|
+
) {
|
|
329
|
+
throw new Error("sandbox inactivity timeout must be a positive integer");
|
|
330
|
+
}
|
|
331
|
+
if (request.command.length === 0 || !request.command[0]) {
|
|
332
|
+
throw new Error("sandbox command must not be empty");
|
|
333
|
+
}
|
|
334
|
+
for (const path of [
|
|
335
|
+
...(request.files ?? []).map((file) => file.path),
|
|
336
|
+
...(request.outputPaths ?? []),
|
|
337
|
+
]) {
|
|
338
|
+
assertWorkPath(path);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const vcpus = request.cpu ?? 4;
|
|
342
|
+
if (!Number.isInteger(vcpus) || (vcpus !== 1 && (vcpus < 2 || vcpus > 32 || vcpus % 2 !== 0))) {
|
|
343
|
+
throw new Error("Vercel sandbox CPU must be 1 or an even integer from 2 through 32");
|
|
344
|
+
}
|
|
345
|
+
const memoryMiB = request.memoryMiB ?? vcpus * 2048;
|
|
346
|
+
if (memoryMiB !== vcpus * 2048) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Vercel fixes memory at 2048 MiB per vCPU; ${vcpus} vCPU requires ${vcpus * 2048} MiB`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return { vcpus, memoryMiB };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function assertWorkPath(path: string): void {
|
|
355
|
+
const normalized = posix.normalize(path);
|
|
356
|
+
if (
|
|
357
|
+
!path.startsWith(`${VERCEL_WORK_DIRECTORY}/`) ||
|
|
358
|
+
!normalized.startsWith(`${VERCEL_WORK_DIRECTORY}/`)
|
|
359
|
+
) {
|
|
360
|
+
throw new Error(`sandbox path must be beneath ${VERCEL_WORK_DIRECTORY}: ${path}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function sandboxName(runId: string, stage: string): string {
|
|
365
|
+
const suffix = crypto.randomUUID().replaceAll("-", "").slice(0, 12);
|
|
366
|
+
const context = `${runId}-${stage}`.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
367
|
+
const prefix = `selfbench-${context}`.slice(0, 128 - suffix.length - 1).replace(/-+$/g, "");
|
|
368
|
+
return `${prefix}-${suffix}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function withTimeout(signal: AbortSignal, timeoutMs: number): AbortSignal {
|
|
372
|
+
return AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function abortableDelay(delayMs: number, signal: AbortSignal): Promise<void> {
|
|
376
|
+
await delay(delayMs, undefined, { signal });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function createRetryDelayMs(response: Response): number {
|
|
380
|
+
const header = response.headers.get("retry-after")?.trim();
|
|
381
|
+
if (!header) {
|
|
382
|
+
return CREATE_RATE_LIMIT_FALLBACK_DELAY_MS;
|
|
383
|
+
}
|
|
384
|
+
const seconds = Number(header);
|
|
385
|
+
const requestedDelay = Number.isFinite(seconds)
|
|
386
|
+
? seconds * 1_000
|
|
387
|
+
: Date.parse(header) - Date.now();
|
|
388
|
+
if (!Number.isFinite(requestedDelay)) {
|
|
389
|
+
return CREATE_RATE_LIMIT_FALLBACK_DELAY_MS;
|
|
390
|
+
}
|
|
391
|
+
return Math.min(CREATE_RATE_LIMIT_MAX_DELAY_MS, Math.max(0, Math.ceil(requestedDelay)));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function createErrorConfirmsNoAllocation(error: unknown): boolean {
|
|
395
|
+
if (!(error instanceof APIError)) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
const status = error.response.status;
|
|
399
|
+
return status >= 400 && status < 500 && status !== 408 && status !== 409 && status !== 499;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function throwIfTerminated(error: unknown): void {
|
|
403
|
+
if (error !== undefined) {
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function abortReason(signal: AbortSignal | undefined): unknown {
|
|
409
|
+
return signal?.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function attachCleanupError(primaryError: unknown, cleanupError: unknown): unknown {
|
|
413
|
+
if (primaryError instanceof Error) {
|
|
414
|
+
const cleanupMessage = errorMessage(cleanupError).slice(0, 500);
|
|
415
|
+
const message = `${primaryError.message}; Vercel sandbox cleanup also failed: ${cleanupMessage}`;
|
|
416
|
+
try {
|
|
417
|
+
Object.defineProperties(primaryError, {
|
|
418
|
+
cleanupError: { configurable: true, value: cleanupError },
|
|
419
|
+
message: { configurable: true, value: message, writable: true },
|
|
420
|
+
});
|
|
421
|
+
return primaryError;
|
|
422
|
+
} catch {
|
|
423
|
+
const wrapped = new Error(message, { cause: primaryError });
|
|
424
|
+
wrapped.name = primaryError.name;
|
|
425
|
+
Object.defineProperty(wrapped, "cleanupError", {
|
|
426
|
+
configurable: true,
|
|
427
|
+
value: cleanupError,
|
|
428
|
+
});
|
|
429
|
+
return wrapped;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return new AggregateError(
|
|
433
|
+
[primaryError, cleanupError],
|
|
434
|
+
"Vercel sandbox execution and cleanup both failed",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function sanitizeCleanupError(error: unknown, redactedValue: string): Error {
|
|
439
|
+
const sanitized = new Error(
|
|
440
|
+
errorMessage(error).replaceAll(redactedValue, "[redacted]").slice(0, 1_000),
|
|
441
|
+
);
|
|
442
|
+
sanitized.name = "VercelSandboxCleanupError";
|
|
443
|
+
return sanitized;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function errorMessage(error: unknown): string {
|
|
447
|
+
return error instanceof Error ? error.message : String(error);
|
|
448
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class VercelCommandStartError extends Error {
|
|
2
|
+
constructor(message: string, cause?: unknown) {
|
|
3
|
+
super(`${message}; automatic retry was suppressed to prevent duplicate execution`, { cause });
|
|
4
|
+
// @vercel/sandbox's retry layer only honors AbortError as a non-retryable
|
|
5
|
+
// transport failure. The command layer restores the truthful public name.
|
|
6
|
+
this.name = "AbortError";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
restorePublicName(): void {
|
|
10
|
+
this.name = "VercelCommandStartError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function preventAmbiguousVercelCommandStartRetries(
|
|
15
|
+
fetch: typeof globalThis.fetch,
|
|
16
|
+
): typeof globalThis.fetch {
|
|
17
|
+
const wrapped = async (
|
|
18
|
+
input: Parameters<typeof fetch>[0],
|
|
19
|
+
init?: Parameters<typeof fetch>[1],
|
|
20
|
+
) => {
|
|
21
|
+
const url = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
|
22
|
+
const isCommandStart =
|
|
23
|
+
init?.method === "POST" && /\/v2\/sandboxes\/sessions\/[^/]+\/cmd$/.test(url.pathname);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(input, init);
|
|
26
|
+
if (isCommandStart && response.status >= 500) {
|
|
27
|
+
await response.body?.cancel().catch(() => undefined);
|
|
28
|
+
throw new VercelCommandStartError(`Vercel command start returned HTTP ${response.status}`);
|
|
29
|
+
}
|
|
30
|
+
return response;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof VercelCommandStartError || !isCommandStart || init?.signal?.aborted) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
throw new VercelCommandStartError("Vercel command start response was not received", error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return Object.assign(wrapped, {
|
|
39
|
+
preconnect: (...args: Parameters<typeof fetch.preconnect>): void => {
|
|
40
|
+
if (typeof fetch.preconnect === "function") {
|
|
41
|
+
fetch.preconnect(...args);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|