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.

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