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.

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