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.

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