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.

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