Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-runner/src/quick.ts
Snapshot at 34e3175
1import { Snapshot } from "./snapshot";
2import { sha256 } from "./runner";
3import type { AuthorizedReview, CheckReceipt, ForkRepository, QuickCheck } from "./types";
4
5export const QUICK_TIMEOUT_MS = 10_000;
6export const QUICK_MESSAGE_BYTES = 4096;
7export interface QuickPorts {
8 loader: Pick<WorkerLoader, "load">;
9 files(snapshot: Snapshot): Fetcher | Promise<Fetcher>;
10 /** Trusted adapter provenance; never part of a caller request. */
11 evidence: "dynamic-worker" | "mocked-worker-loader";
12}
13
14/** A trusted main module adapts the check's default function, as in upstream's code harness.
15 * No environment/entrypoint imports, tail, secrets, or compatibility with Node are supplied. */
16export function quickHarness(module: string): string {
17 return `import check from ${JSON.stringify("./" + module)};
18export default {
19 async fetch(request, env) {
20 if (request.method === "GET") {
21 if (typeof check !== "function") throw new Error("Invalid check module");
22 return new Response("ready");
23 }
24 try {
25 const result = await check(Object.freeze({
26 read: path => env.FILES.read(path), list: prefix => env.FILES.list(prefix),
27 stat: path => env.FILES.stat(path)
28 }));
29 if (!result || typeof result.ok !== "boolean" ||
30 (result.message !== undefined && typeof result.message !== "string")) throw new Error();
31 // Bound inside the isolate AND revalidate outside it: project code is untrusted.
32 const message = typeof result.message === "string" ? result.message.slice(0, 4096) : undefined;
33 return Response.json({ ok: result.ok, message });
34 } catch { return Response.json({ ok: false, message: "Quick check threw or returned an invalid result." }); }
35 }
36};`;
37}
38
39function messageText(value: string): string {
40 // Remove control/bidi escapes and stack-shaped lines; never preserve exception objects.
41 const clean = [...value].map(character => {
42 const code = character.charCodeAt(0);
43 return code < 32 || (code >= 127 && code <= 159) || (code >= 0x202a && code <= 0x202e) ||
44 (code >= 0x2066 && code <= 0x2069) ? " " : character;
45 }).join("").replace(/\s+at\s+.*$/s, "");
46 const bytes = new TextEncoder().encode(clean);
47 // stream:true drops an incomplete final codepoint instead of adding bytes for U+FFFD.
48 return new TextDecoder().decode(bytes.subarray(0, QUICK_MESSAGE_BYTES), { stream: true });
49}
50
51async function responseText(response: Response, limit: number): Promise<string> {
52 if (!response.ok || !response.body) throw new Error("Invalid quick response");
53 const reader = response.body.getReader();
54 const chunks: Uint8Array[] = [];
55 let size = 0;
56 try {
57 while (true) {
58 const { done, value } = await reader.read();
59 if (done) break;
60 size += value.byteLength;
61 if (size > limit) throw new Error("Quick response too large");
62 chunks.push(value);
63 }
64 } finally { await reader.cancel(); }
65 const bytes = new Uint8Array(size);
66 let offset = 0;
67 for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
68 return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
69}
70
71export async function runQuickCheck(ports: QuickPorts | undefined, repo: ForkRepository,
72 review: AuthorizedReview, check: QuickCheck): Promise<CheckReceipt> {
73 const started = Date.now();
74 const controller = new AbortController();
75 const snapshot = new Snapshot(repo, review.commit, started + QUICK_TIMEOUT_MS);
76 let imageDigest = "";
77 let outcome: CheckReceipt["outcome"] = "infrastructure-error";
78 let message = "Quick worker could not start.";
79 let running = false;
80 let timer: ReturnType<typeof setTimeout> | undefined;
81 try {
82 const deadline = new Promise<never>((_resolve, reject) => {
83 timer = setTimeout(() => {
84 snapshot.close();
85 controller.abort();
86 reject(new Error("Quick deadline"));
87 }, QUICK_TIMEOUT_MS);
88 });
89 await Promise.race([deadline, (async () => {
90 const source = await snapshot.read(check.module);
91 controller.signal.throwIfAborted();
92 if (source === null || !ports) throw new Error("Module or loader unavailable");
93 imageDigest = `module:sha256:${await sha256(source)}`;
94 controller.signal.throwIfAborted();
95 const entrypoint = ports.loader.load({
96 compatibilityDate: "2026-10-11", compatibilityFlags: ["disallow_importable_env"],
97 mainModule: "harness.js",
98 modules: { "harness.js": quickHarness(check.module), [check.module]: source },
99 env: { FILES: await ports.files(snapshot) }, globalOutbound: null,
100 limits: { cpuMs: QUICK_TIMEOUT_MS },
101 }).getEntrypoint();
102 // Startup errors (including syntax/import/initialization) are distinct from check exceptions.
103 await responseText(await entrypoint.fetch("https://quick.invalid/", { signal: controller.signal }), 32);
104 controller.signal.throwIfAborted();
105 running = true;
106 const body = await responseText(await entrypoint.fetch("https://quick.invalid/", {
107 method: "POST", signal: controller.signal,
108 }), 32_768);
109 controller.signal.throwIfAborted();
110 const result: unknown = JSON.parse(body);
111 if (!result || typeof result !== "object" || !("ok" in result) || typeof result.ok !== "boolean" ||
112 ("message" in result && result.message !== undefined && typeof result.message !== "string"))
113 throw new Error("Invalid result");
114 message = "message" in result && typeof result.message === "string" ? messageText(result.message) : "";
115 outcome = result.ok ? "passed" : "failed";
116 })()]);
117 } catch {
118 outcome = controller.signal.aborted ? "timed-out" : running ? "failed" : "infrastructure-error";
119 message = outcome === "timed-out" ? "Quick check exceeded its deadline."
120 : running ? "Quick check failed." : "Quick worker could not start.";
121 } finally {
122 if (timer !== undefined) clearTimeout(timer);
123 snapshot.close();
124 // Abort the dispatched fetch, not just the host's wait. Late RPC completions are fenced too.
125 controller.abort();
126 }
127 const finished = Date.now();
128 const logs = { stdout: "", stderr: "", truncated: false };
129 return { commit: review.commit, check, runtime: "dynamic-worker", imageDigest,
130 exitStatus: (outcome as CheckReceipt["outcome"]) === "passed" ? 0 : outcome === "failed" ? 1 : null, outcome, message,
131 startedAt: new Date(started).toISOString(), finishedAt: new Date(finished).toISOString(),
132 durationMs: Math.max(0, finished - started), logs, logDigest: await sha256(JSON.stringify(logs)) };
133}
134