Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-runner/src/index.ts
Snapshot at 34e3175
1import { DurableObject, RpcTarget, WorkerEntrypoint } from "cloudflare:workers";
2import { Snapshot } from "./snapshot";
3import type { QuickPorts } from "./quick";
4import { forwardGitRead } from "./git";
5import { runReviewChecks } from "./runner";
6import type { ContainerPort, ForkRepository, QuickFileStat, Receipt, ReviewAuthority, RunRequest, StoredReceipt } from "./types";
7
8export { QuickFiles } from "./snapshot";
9export { requirePassingReceipt } from "./runner";
10export type * from "./types";
11
12export interface RunnerEnv {
13 LOADER: WorkerLoader;
14 ARTIFACTS: { get(name: string): Promise<ForkRepository> };
15}
16
17declare global {
18 namespace Cloudflare {
19 interface GlobalProps {
20 mainModule: typeof import("./index");
21 durableNamespaces: "ReceiptRunner";
22 }
23 }
24}
25
26/** Loopback capability is scoped to one authorized fork by the controlling Durable Object. */
27export class GitReadGateway extends WorkerEntrypoint<RunnerEnv, { remote: string }> {
28 override fetch(request: Request): Promise<Response> {
29 return forwardGitRead(request, this.ctx.props.remote);
30 }
31}
32
33/** Sandbox 1.0 native adapter. Export only through trusted bindings; there is no public fetch route. */
34export class ReceiptRunner extends DurableObject<RunnerEnv> {
35 private busy = false;
36 #snapshots = new Map<string, Snapshot>();
37
38 protected quickPorts(): QuickPorts {
39 return { loader: this.env.LOADER, evidence: "dynamic-worker", files: snapshot => {
40 const snapshotId = crypto.randomUUID();
41 this.#snapshots.set(snapshotId, snapshot);
42 return this.ctx.exports.QuickFiles({ props: { runnerId: this.ctx.id.toString(), snapshotId } });
43 } };
44 }
45 async readSnapshot(id: string, path: string): Promise<string | null> {
46 const snapshot = this.#snapshots.get(id);
47 if (!snapshot) throw new Error("Snapshot unavailable.");
48 return snapshot.read(path);
49 }
50 async listSnapshot(id: string, prefix: string): Promise<string[]> {
51 const snapshot = this.#snapshots.get(id);
52 if (!snapshot) throw new Error("Snapshot unavailable.");
53 return snapshot.list(prefix);
54 }
55 async statSnapshot(id: string, path: string): Promise<QuickFileStat | null> {
56 const snapshot = this.#snapshots.get(id);
57 if (!snapshot) throw new Error("Snapshot unavailable.");
58 return snapshot.stat(path);
59 }
60
61 constructor(ctx: DurableObjectState, env: RunnerEnv) {
62 super(ctx, env);
63 // A restarted DO must not inherit untrusted processes/files from its old execution.
64 ctx.blockConcurrencyWhile(async () => {
65 if (ctx.container?.running) await ctx.container.destroy();
66 });
67 }
68
69 protected containerPort(): ContainerPort | undefined { return this.ctx.container; }
70 protected evidence(): Receipt["evidence"] { return "container"; }
71
72 async run(request: RunRequest, authority: ReviewAuthority & RpcTarget): Promise<StoredReceipt> {
73 if (this.busy) throw new Error("Runner already in use");
74 const container = this.containerPort();
75 this.busy = true;
76 try {
77 let authorizedFork: string | undefined;
78 return await runReviewChecks(request, {
79 authority: {
80 authorize: async (input) => {
81 const review = await authority.authorize(input);
82 authorizedFork = review.forkRepoName;
83 return review;
84 },
85 storeReceipt: (record) => authority.storeReceipt(record),
86 setReviewReceiptDigest: (review, digest) => authority.setReviewReceiptDigest(review, digest),
87 },
88 getFork: (name) => {
89 if (!authorizedFork || name !== authorizedFork) throw new Error("Unauthorized fork");
90 return this.env.ARTIFACTS.get(name);
91 }, container,
92 gitGateway: (remote) => this.ctx.exports.GitReadGateway({ props: { remote } }),
93 evidence: this.evidence(),
94 quick: this.quickPorts(),
95 });
96 } finally {
97 for (const snapshot of this.#snapshots.values()) snapshot.close();
98 this.#snapshots.clear();
99 this.busy = false;
100 }
101 }
102}
103