import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
import { spawn, spawnSync } from "node:child_process";
import { createServer } from "node:http";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { basename, join, resolve } from "node:path";
const SHA = /^[0-9a-f]{40}$/;
const ID = /^[A-Za-z0-9_-]{1,200}$/;
const BRANCH = /^refs\/heads\/millwork\/[A-Za-z0-9/_-]{1,160}$/;
const REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
const MAX_BODY = 16_384;
function required(value, name) {
if (!value || typeof value !== "string") throw new Error(`Set ${name}`);
return value;
}
export function adapterConfig(env = process.env) {
const repository = required(env.REVIEW_REPOSITORY, "REVIEW_REPOSITORY");
if (!REPOSITORY.test(repository)) throw new Error("REVIEW_REPOSITORY must be owner/repo");
const bearerToken = required(env.REVIEW_ADAPTER_TOKEN, "REVIEW_ADAPTER_TOKEN");
if (bearerToken.length < 32) throw new Error("REVIEW_ADAPTER_TOKEN must have at least 32 characters");
const remoteUrl = required(env.REVIEW_REMOTE_URL, "REVIEW_REMOTE_URL");
if (!/^git@github\.com:[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/.test(remoteUrl)
&& !/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/.test(remoteUrl)) {
throw new Error("REVIEW_REMOTE_URL must be a GitHub SSH or credential-helper HTTPS URL without a secret");
}
const remoteRepository = remoteUrl.replace(/^git@github\.com:/, "").replace(/^https:\/\/github\.com\//, "").replace(/\.git$/, "");
if (remoteRepository.toLowerCase() !== repository.toLowerCase()) throw new Error("REVIEW_REMOTE_URL must match REVIEW_REPOSITORY");
const workRoot = resolve(required(env.REVIEW_WORK_ROOT, "REVIEW_WORK_ROOT"));
const agentArgv = JSON.parse(env.REVIEW_AGENT_ARGV_JSON ?? '["codex","exec","--approve-for-me","-"]');
if (!Array.isArray(agentArgv) || !agentArgv.length || agentArgv.some((arg) => typeof arg !== "string" || !arg)) {
throw new Error("REVIEW_AGENT_ARGV_JSON must be a nonempty JSON argv array");
}
return { repository, bearerToken, remoteUrl, workRoot, agentArgv };
}
/** Catch incompatible Codex flags before accepting a paid agent start. */
export function assertAgentCommandReady(config, probe = spawnSync) {
const [binary, ...args] = config.agentArgv;
if (basename(binary) !== "codex") return;
if (args[0] !== "exec" || args.at(-1) !== "-") {
throw new Error("Codex agent command must use codex exec and read its prompt from stdin (-)");
}
const result = probe(binary, [...args.slice(0, -1), "--help"], {
encoding: "utf8", timeout: 10_000, maxBuffer: 64 * 1024,
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error || result.status !== 0) {
const detail = String(result.stderr ?? "").trim().split("\n")[0].slice(0, 160);
throw new Error(`Codex agent command preflight failed${detail ? `: ${detail}` : ""}`);
}
}
/** Refuse jobs when this checkout cannot authenticate to its configured repo. */
export function assertGitRemoteReady(config, probe = spawnSync) {
const result = probe("git", ["ls-remote", "--heads", config.remoteUrl], {
encoding: "utf8", timeout: 20_000, maxBuffer: 64 * 1024,
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error || result.status !== 0 || !/^[0-9a-f]{40}\s+refs\/heads\//m.test(String(result.stdout ?? ""))) {
throw new Error("Git remote preflight failed; check repository access and the credential helper");
}
}
function run(argv, options = {}) {
return new Promise((resolveRun, rejectRun) => {
// The adapter bearer token authenticates Millwork to this server. Neither
// the coding agent nor any git helper it launches needs that authority.
const childEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0", ...options.env };
delete childEnv.REVIEW_ADAPTER_TOKEN;
const child = spawn(argv[0], argv.slice(1), {
cwd: options.cwd,
env: childEnv,
stdio: [options.input === undefined ? "ignore" : "pipe", "ignore", "ignore"],
shell: false,
signal: options.signal,
});
if (options.input !== undefined) {
child.stdin.on("error", () => { /* a failed child may close stdin early */ });
child.stdin.end(options.input);
}
child.on("error", rejectRun);
child.on("exit", (code, signal) => {
if (code === 0) resolveRun();
else rejectRun(new Error(`${argv[0]} exited ${code ?? signal}`));
});
});
}
function validStart(input, config) {
if (input.protocol !== "async_job_v1" || input.operation !== "start"
|| !ID.test(input.execution_id) || !ID.test(input.job_attempt_id) || !ID.test(input.idempotency_key)
|| input.repository !== config.repository || !SHA.test(input.base_sha)
|| !BRANCH.test(input.branch) || input.branch !== `refs/heads/millwork/run/${input.execution_id}`
|| typeof input.base_ref !== "string"
|| !/^refs\/heads\/[A-Za-z0-9/_-]{1,160}$/.test(input.base_ref)
|| !Number.isFinite(Date.parse(input.deadline_at)) || Date.parse(input.deadline_at) <= Date.now()
|| typeof input.task?.objective !== "string" || !input.task.objective.trim()
|| input.task.objective.length > 4_000) {
throw new Error("invalid start identity");
}
}
function jobId(attemptId, idempotencyKey) {
return `job_${createHash("sha256").update(`${attemptId}\0${idempotencyKey}`).digest("hex").slice(0, 32)}`;
}
function equalToken(actual, expected) {
const one = Buffer.from(actual ?? "");
const two = Buffer.from(expected);
return one.length === two.length && timingSafeEqual(one, two);
}
function comparableStart(input) {
const { protocol, operation, execution_id, job_attempt_id, idempotency_key, repository,
base_ref, base_sha, branch, deadline_at, task } = input;
return { protocol, operation, execution_id, job_attempt_id, idempotency_key, repository,
base_ref, base_sha, branch, deadline_at, task };
}
export class AsyncJobAdapter {
constructor(config, command = run) {
this.config = config;
this.command = command;
this.jobs = new Map();
this.controllers = new Map();
this.startLocks = new Map();
}
path(id) { return join(this.config.workRoot, "jobs", `${id}.json`); }
async persist(job) {
await mkdir(join(this.config.workRoot, "jobs"), { recursive: true, mode: 0o700 });
const path = this.path(job.job_id);
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
await writeFile(temp, JSON.stringify(job), { mode: 0o600 });
await rename(temp, path);
}
async load(id) {
if (this.jobs.has(id)) return this.jobs.get(id);
let job;
try { job = JSON.parse(await readFile(this.path(id), "utf8")); }
catch (error) { if (error.code === "ENOENT") return null; throw error; }
if (job.state === "queued" || job.state === "running") {
// A process restart has lost supervision of an agent attempt. Never
// dispatch it again under the same logical job identity.
job.state = "failed";
job.failure = "adapter_restarted_during_attempt";
await this.persist(job);
}
this.jobs.set(id, job);
return job;
}
async start(input) {
validStart(input, this.config);
const id = jobId(input.job_attempt_id, input.idempotency_key);
const pending = this.startLocks.get(id);
if (pending) {
await pending;
return this.start(input);
}
const dispatch = (async () => {
const previous = await this.load(id);
if (previous) {
if (JSON.stringify(previous.start) !== JSON.stringify(comparableStart(input))) {
throw new Error("logical job identity reused with different start request");
}
return { operation: "start", job_id: id };
}
const job = { job_id: id, start: comparableStart(input), state: "queued", usage_usd: 0 };
await this.persist(job);
this.jobs.set(id, job);
const controller = new AbortController();
this.controllers.set(id, controller);
void this.execute(job, controller).finally(() => this.controllers.delete(id));
return { operation: "start", job_id: id };
})();
this.startLocks.set(id, dispatch);
try { return await dispatch; }
finally { this.startLocks.delete(id); }
}
async execute(job, controller) {
const { start } = job;
const worktree = join(this.config.workRoot, "worktrees", job.job_id);
const deadlineMs = Date.parse(start.deadline_at) - Date.now();
const timer = setTimeout(() => controller.abort(), Math.max(1, deadlineMs));
try {
job.state = "running";
await this.persist(job);
await mkdir(join(this.config.workRoot, "worktrees"), { recursive: true, mode: 0o700 });
await this.command(["git", "clone", "--no-checkout", this.config.remoteUrl, worktree], { signal: controller.signal });
await this.command(["git", "-C", worktree, "fetch", "origin", start.branch], { signal: controller.signal });
const fetched = await this.capture(["git", "-C", worktree, "rev-parse", "FETCH_HEAD"], controller.signal);
if (fetched !== start.base_sha) throw new Error("Millwork run branch did not start at accepted base SHA");
await this.command(["git", "-C", worktree, "checkout", "-B", start.branch.slice("refs/heads/".length), fetched], { signal: controller.signal });
const prompt = `${start.task.objective}\n\nWork only in this checkout. Commit no secret. Do not push; the adapter will commit and push after you finish.`;
await this.command(this.config.agentArgv, { cwd: worktree, signal: controller.signal, input: prompt });
await this.command(["git", "-C", worktree, "add", "-A"], { signal: controller.signal });
await this.command(["git", "-C", worktree, "-c", "user.name=Millwork Agent", "-c", "user.email=agent@getmillwork.dev", "commit", "--allow-empty", "-m", `Complete Millwork run ${start.execution_id}`], { signal: controller.signal });
const sha = await this.capture(["git", "-C", worktree, "rev-parse", "HEAD"], controller.signal);
if (!SHA.test(sha)) throw new Error("final SHA unavailable");
await this.command(["git", "-C", worktree, "push", "origin", `HEAD:${start.branch}`], { signal: controller.signal });
if (controller.signal.aborted) throw new Error("cancelled after push");
const completed = { ...job, state: "completed", claimed_sha: sha };
await this.persist(completed);
Object.assign(job, completed);
} catch (error) {
const failed = { ...job, state: controller.signal.aborted ? "cancelled" : "failed",
failure: String(error.message ?? error).slice(0, 200) };
await this.persist(failed);
Object.assign(job, failed);
} finally { clearTimeout(timer); }
}
async capture(argv, signal) {
return new Promise((resolveCapture, rejectCapture) => {
const child = spawn(argv[0], argv.slice(1), { stdio: ["ignore", "pipe", "ignore"], shell: false, signal });
let out = "";
child.stdout.on("data", (chunk) => { out += chunk; if (out.length > 256) child.kill(); });
child.on("error", rejectCapture);
child.on("exit", (code) => code === 0 ? resolveCapture(out.trim()) : rejectCapture(new Error(`${argv[0]} read failed`)));
});
}
async identify(input) {
if (input.protocol !== "async_job_v1" || !ID.test(input.job_attempt_id)
|| !ID.test(input.idempotency_key) || (input.job_id !== null && input.job_id !== undefined && !ID.test(input.job_id))) {
throw new Error("invalid job identity");
}
const id = jobId(input.job_attempt_id, input.idempotency_key);
if (input.job_id && input.job_id !== id) throw new Error("job ID mismatch");
const job = await this.load(id);
if (!job) throw new Error("job not found");
return job;
}
async status(input) {
const job = await this.identify(input);
return { operation: "status", job_id: job.job_id, state: job.state,
...(job.state === "completed" ? { claimed_sha: job.claimed_sha, usage_usd: job.usage_usd } : {}),
...(["failed", "cancelled"].includes(job.state) ? { usage_usd: job.usage_usd } : {}) };
}
async cancel(input) {
const job = await this.identify(input);
this.controllers.get(job.job_id)?.abort();
return { operation: "cancel", accepted: true };
}
async handle(input) {
if (input?.operation === "start") return this.start(input);
if (input?.operation === "status") return this.status(input);
if (input?.operation === "cancel") return this.cancel(input);
throw new Error("unknown operation");
}
}
export function createAdapterServer(adapter) {
return createServer(async (request, response) => {
const send = (status, body) => { response.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" }); response.end(JSON.stringify(body)); };
if (!equalToken(request.headers.authorization?.replace(/^Bearer /, ""), adapter.config.bearerToken)) {
send(401, { error: "unauthorized" }); return;
}
if (request.method === "HEAD") { response.writeHead(200); response.end(); return; }
if (request.method !== "POST") { send(405, { error: "method_not_allowed" }); return; }
let raw = "";
for await (const chunk of request) {
raw += chunk;
if (raw.length > MAX_BODY) { send(413, { error: "body_too_large" }); return; }
}
try { send(200, await adapter.handle(JSON.parse(raw))); }
catch (error) { send(400, { error: String(error.message ?? error).slice(0, 200) }); }
});
}
if (process.argv[1] && import.meta.url === new URL(`file://${resolve(process.argv[1])}`).href) {
const config = adapterConfig();
assertAgentCommandReady(config);
assertGitRemoteReady(config);
const adapter = new AsyncJobAdapter(config);
const port = Number(process.env.REVIEW_ADAPTER_PORT ?? 8731);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid REVIEW_ADAPTER_PORT");
createAdapterServer(adapter).listen(port, "127.0.0.1", () => {
process.stdout.write(`Review Composer agent adapter listening on localhost:${port}\n`);
});
}