Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

156 lines · 6.9 KBRaw History
1/**
2 * Diffinery runner agent entry point.
3 *
4 * agent.mjs fetch the job from DIFFINERY_API_URL and run it
5 * agent.mjs --spec job.json run a JobSpec from a file (reports to spec.apiUrl,
6 * or DIFFINERY_API_URL; `--offline` logs to stdout only)
7 * agent.mjs --sandbox AI-agent sandbox HTTP server on :8080
8 * agent.mjs --sandbox-pool many sandboxes in one process, for local development and
9 * end-to-end tests (see sandbox-pool.ts)
10 * agent.mjs --jail ... run a terminal task's command in its jail (see jail.ts)
11 * agent.mjs --registry-relay HTTPS proxy for the Docker daemon that splits big
12 * image layer uploads to the Diffinery registry (see relay.ts)
13 *
14 * With DIFFINERY_IMPORT_ID and DIFFINERY_IMPORT_TOKEN set (instead of a job), it
15 * runs a GitHub import's Git transfer (see importer.ts) and exits.
16 */
17import { readFile } from "node:fs/promises";
18import type { JobSpec } from "@diffinery/shared";
19import { apiBaseFor, runAgentJob } from "./agent.js";
20import { trustEgressCa } from "./egress.js";
21import { RunnerApi } from "./api.js";
22import { runJail } from "./jail.js";
23import { runImportAgent } from "./importer.js";
24import { startSandbox } from "./sandbox.js";
25import { startSandboxPool } from "./sandbox-pool.js";
26import { startRelay } from "./relay.js";
27
28interface Args {
29 spec?: string;
30 sandbox: boolean;
31 pool: boolean;
32 relay: boolean;
33 offline: boolean;
34 workRoot?: string;
35}
36
37function parseArgs(argv: string[]): Args {
38 const a: Args = { sandbox: false, pool: false, relay: false, offline: false };
39 for (let i = 0; i < argv.length; i++) {
40 const v = argv[i]!;
41 if (v === "--spec") a.spec = argv[++i];
42 else if (v.startsWith("--spec=")) a.spec = v.slice(7);
43 else if (v === "--sandbox") a.sandbox = true;
44 else if (v === "--sandbox-pool") a.pool = true;
45 else if (v === "--registry-relay") a.relay = true;
46 else if (v === "--offline") a.offline = true;
47 else if (v === "--work-root") a.workRoot = argv[++i];
48 else if (v === "--help" || v === "-h") {
49 process.stdout.write("usage: agent.mjs [--spec job.json [--offline]] [--sandbox | --sandbox-pool] [--work-root dir]\n");
50 process.exit(0);
51 }
52 }
53 return a;
54}
55
56function fail(msg: string): never {
57 process.stderr.write(`[diffinery-runner] ${msg}\n`);
58 process.exit(2);
59}
60
61async function main(): Promise<void> {
62 if (process.argv[2] === "--jail") process.exit(await runJail(process.argv.slice(3)));
63 const args = parseArgs(process.argv.slice(2));
64 const env = process.env;
65 const workRoot = args.workRoot ?? env.DIFFINERY_WORK_ROOT ?? "/workspace";
66 await trustEgressCa(env);
67
68 if (args.relay) {
69 const { DIFFINERY_RELAY_HOST: host, DIFFINERY_RELAY_UPSTREAM: upstream, DIFFINERY_RELAY_CERT: cert, DIFFINERY_RELAY_KEY: key } = env;
70 if (!host || !upstream || !cert || !key) fail("DIFFINERY_RELAY_HOST, DIFFINERY_RELAY_UPSTREAM, DIFFINERY_RELAY_CERT and DIFFINERY_RELAY_KEY are required with --registry-relay");
71 await startRelay({ port: Number(env.DIFFINERY_RELAY_PORT ?? 3129), host, upstream, chunkSize: env.DIFFINERY_RELAY_CHUNK ? Number(env.DIFFINERY_RELAY_CHUNK) : undefined, cert: await readFile(cert, "utf8"), key: await readFile(key, "utf8") });
72 return;
73 }
74
75 if (args.pool) {
76 const secret = env.DIFFINERY_SANDBOX_POOL_SECRET;
77 if (!secret) fail("DIFFINERY_SANDBOX_POOL_SECRET is required in --sandbox-pool mode");
78 const pool = await startSandboxPool({ port: Number(env.DIFFINERY_SANDBOX_PORT ?? env.PORT ?? 8080), secret, workRoot });
79 process.stdout.write(`[sandbox-pool] listening on :${pool.port}\n`);
80 const stop = () => void pool.close().then(() => process.exit(0));
81 process.on("SIGTERM", stop);
82 process.on("SIGINT", stop);
83 return;
84 }
85 if (args.sandbox) {
86 const secret = env.DIFFINERY_SANDBOX_SECRET;
87 if (!secret) fail("DIFFINERY_SANDBOX_SECRET is required in --sandbox mode");
88 const h = await startSandbox({
89 port: Number(env.DIFFINERY_SANDBOX_PORT ?? env.PORT ?? 8080),
90 secret,
91 workRoot,
92 cloneUrl: env.DIFFINERY_CLONE_URL,
93 ref: env.DIFFINERY_REF,
94 token: env.DIFFINERY_JOB_TOKEN,
95 });
96 process.stdout.write(`[sandbox] listening on :${h.port}\n`);
97 const stop = () => void h.close().then(() => process.exit(0));
98 process.on("SIGTERM", stop);
99 process.on("SIGINT", stop);
100 return;
101 }
102
103 if (env.DIFFINERY_IMPORT_ID) {
104 const api = env.DIFFINERY_API_URL;
105 const token = env.DIFFINERY_IMPORT_TOKEN;
106 if (!api || !token) fail("DIFFINERY_API_URL and DIFFINERY_IMPORT_TOKEN are required in import mode");
107 const chunk = Number(env.DIFFINERY_IMPORT_CHUNK_BYTES);
108 const large = Number(env.DIFFINERY_IMPORT_LARGE_BLOB_BYTES);
109 const result = await runImportAgent({ apiUrl: api, importId: env.DIFFINERY_IMPORT_ID, token, workRoot, chunkBytes: Number.isFinite(chunk) && chunk > 0 ? chunk : undefined, largeBlobBytes: Number.isFinite(large) && large > 0 ? large : undefined });
110 process.stdout.write(`[diffinery-import] ${result.ok ? "finished" : `failed: ${result.error}`}\n`);
111 process.exit(result.ok ? 0 : 1);
112 }
113
114 let spec: JobSpec;
115 let apiBase: string | null;
116 if (args.spec) {
117 spec = JSON.parse(await readFile(args.spec, "utf8")) as JobSpec;
118 if (env.DIFFINERY_JOB_TOKEN && !spec.token) spec.token = env.DIFFINERY_JOB_TOKEN;
119 apiBase = args.offline ? null : apiBaseFor(env.DIFFINERY_API_URL ?? spec.apiUrl, spec.jobId);
120 } else {
121 const api = env.DIFFINERY_API_URL;
122 const jobId = env.DIFFINERY_JOB_ID;
123 const token = env.DIFFINERY_JOB_TOKEN;
124 if (!api || !jobId || !token) fail("DIFFINERY_API_URL, DIFFINERY_JOB_ID and DIFFINERY_JOB_TOKEN are required (or pass --spec / --sandbox)");
125 apiBase = apiBaseFor(api, jobId);
126 const client = new RunnerApi(apiBase, token);
127 try {
128 spec = await client.getSpec();
129 } catch (e) {
130 const msg = e instanceof Error ? e.message : String(e);
131 try {
132 await client.complete({ conclusion: "failure", outputs: {}, failure: { message: `Runner could not fetch the job spec: ${msg}` } });
133 } catch {
134 /* nothing more we can do */
135 }
136 fail(`could not fetch job spec: ${msg}`);
137 }
138 spec.token ||= token;
139 spec.apiUrl ||= api;
140 }
141
142 const completion = await runAgentJob(spec, {
143 apiBase,
144 workRoot,
145 toolCache: env.RUNNER_TOOL_CACHE,
146 echoLogs: env.DIFFINERY_ECHO_LOGS === "1" || apiBase === null,
147 handleSignals: true,
148 // Cloudflare gives a container 15 minutes after SIGTERM during a rollout; the last
149 // two are for cancelling the steps and reporting the result before SIGKILL.
150 drainOnSigtermMs: args.spec ? undefined : 13 * 60_000,
151 });
152 process.stdout.write(`[diffinery-runner] job finished: ${completion.conclusion}\n`);
153 process.exit(completion.conclusion === "success" ? 0 : 1);
154}
155
156main().catch((e: unknown) => fail(e instanceof Error ? (e.stack ?? e.message) : String(e)));