// Runs one ordered agent profile. Credential decryption and reviewer posting // live in separate processes so neither privileged secret reaches this runner. import { spawn } from "node:child_process"; import { chmod, chown, readFile, writeFile } from "node:fs/promises"; import process from "node:process"; import { createInterface } from "node:readline"; import type { Readable } from "node:stream"; import { text as readText } from "node:stream/consumers"; import { finished } from "node:stream/promises"; import { setTimeout as sleep } from "node:timers/promises"; import { type ClaudeFailureFields, type CodexAuthentication, codexAuthentication, collectOpenCodeOutput, type CommandResult, isClaudeAuthFailure, isCodexAuthFailure, openCodeArgs, openCodeError, parseOpenCodeEvents, persistCodexAuth, retryInvalidToken, stageCodexAuth, } from "#agents"; import { field, isObject } from "#common"; import { echo, exitCode, killProfileProcesses, readProfileFile, } from "#processes"; import { AgentAuthError, agentEnvironment, type ProfileFailure, type ProfileIdentity, runProfiles, sanitizedEnvironment, withProfile, } from "#profiles"; import { readRuntimeManifest, type RuntimeOutcome, type RuntimeProfile, } from "#runtime"; interface AttemptResult { body: string; reviewPath: string; anchorsPath: string; profile: ProfileIdentity; } // Claude Code's stream-json `result` event, with camelCase field names. interface ClaudeResult extends ClaudeFailureFields { isError?: boolean; subtype: string; } interface RenderedPrompt { prompt: string; reviewPath: string; anchorsPath: string; } const env = (name: string): string => { const value = process.env[name]; if (value === undefined) { throw new Error(`${name} is not set`); } return value; }; function definedEnvironment(): Map { const result = new Map(); for (const [name, value] of Object.entries(process.env)) { if (value !== undefined) { result.set(name, value); } } return result; } const API = env("GITEA_API_URL"); const REPO = env("GITEA_REPOSITORY"); const INDEX = env("ISSUE_INDEX"); const EVENT = env("EVENT_NAME"); const AUTHOR_TOKEN = env("GITEA_TOKEN"); const WORKSPACE = env("GITHUB_WORKSPACE"); const MANIFEST = await readRuntimeManifest(env("BOT_AGENTS_MANIFEST")); const PROCESS_ENV = sanitizedEnvironment(definedEnvironment()); const RULES_PATH = "repos/commons/code-rules/raw/README.md"; const CODEX_AUTH = "/root/.codex/auth.json"; const SKILLS_INSTALL = "skills add obra/superpowers"; async function gitea(path: string, body?: unknown): Promise { const response = await fetch(`${API}/${path}`, { method: body === undefined ? "GET" : "POST", headers: { Authorization: `token ${AUTHOR_TOKEN}`, "Content-Type": "application/json", }, body: body === undefined ? undefined : JSON.stringify(body), }); if (!response.ok) { throw new Error(`${path}: ${response.status} ${await response.text()}`); } return response; } async function postAuthorComment(body: string): Promise { await gitea(`repos/${REPO}/issues/${INDEX}/comments`, { body }); } async function runRoot(command: string, args: string[]): Promise { const code = await exitCode( spawn(command, args, { env: Object.fromEntries(PROCESS_ENV), stdio: "ignore", }), ); if (code !== 0) { throw new Error(`${command} ${args[0]} exited with ${code}`); } } async function configureGitAuthor(): Promise { const userResponse = await gitea("user"); const user: unknown = await userResponse.json(); const login = field(user, "login"); const email = field(user, "email"); if (typeof login !== "string" || typeof email !== "string") { throw new TypeError("user: unexpected Gitea response"); } await runRoot("git", ["config", "user.name", login]); await runRoot("git", ["config", "user.email", email]); } async function readProfileToken( profile: RuntimeProfile, ): Promise { const token = profile.tokenFile === null ? null : await readFile(profile.tokenFile, "utf8"); return token; } async function runAgentCommand( profile: RuntimeProfile, command: string, args: string[], token?: string, ): Promise { const child = spawn(command, args, { cwd: WORKSPACE, env: Object.fromEntries(agentEnvironment(PROCESS_ENV, profile, token)), uid: profile.uid, gid: profile.gid, stdio: "ignore", }); const code = await exitCode(child); if (code !== 0) { throw new Error(`${command} ${args[0]} exited with ${code}`); } } // The agent name the skills installer knows each profile type by. function skillsAgent(type: RuntimeProfile["type"]): string { switch (type) { case "CLAUDE": return "claude-code"; case "CODEX": return "codex"; case "OPENCODE": return "opencode"; } } async function prepareProfile(profile: RuntimeProfile): Promise { await runAgentCommand(profile, "git", [ "config", "--global", "--add", "safe.directory", WORKSPACE, ]); await runAgentCommand(profile, "npx", [ "-y", ...SKILLS_INSTALL.split(" "), "--global", "--agent", skillsAgent(profile.type), "--yes", ]); } // One issue comment as the prompt quotes it. function formatComment(comment: unknown): string { const login = field(field(comment, "user"), "login"); const createdAt = field(comment, "created_at"); const body = field(comment, "body"); return `## ${String(login)} at ${String(createdAt)}\n\n${String(body)}\n`; } async function renderPrompt(profile: RuntimeProfile): Promise { const reviewPath = await makeProfileFile(profile, "review.md"); const anchorsPath = await makeProfileFile(profile, "anchors.json"); const commentsResponse = await gitea( `repos/${REPO}/issues/${INDEX}/comments?limit=100`, ); const comments: unknown = await commentsResponse.json(); if (!Array.isArray(comments)) { throw new TypeError("comments: unexpected Gitea response"); } const rulesResponse = await gitea(RULES_PATH); const codeRules = await rulesResponse.text(); const values = new Map([ ["COMMENT", env("COMMENT")], [ "ISSUE_COMMENTS", comments.map((comment: unknown) => formatComment(comment)).join("\n"), ], ["CODE_RULES", codeRules], ["EVENT_NAME", EVENT], ["GITEA_API_URL", API], ["GITEA_REPOSITORY", REPO], ["ISSUE_INDEX", INDEX], ["REVIEW_PATH", reviewPath], ["ANCHORS_PATH", anchorsPath], ]); const template = await readFile( new URL("../../prompt.md", import.meta.url), "utf8", ); return { prompt: template.replaceAll( /\$\{(?\w+)\}/gu, (match: string, ...args: unknown[]) => { const groups = args.at(-1); const name = isObject(groups) ? field(groups, "name") : undefined; return typeof name === "string" ? (values.get(name) ?? match) : match; }, ), reviewPath, anchorsPath, }; } async function runClaude( profile: RuntimeProfile, token: string, prompt: string, ): Promise { const claude = spawn( "claude", [ "--print", "--dangerously-skip-permissions", "--model", profile.model, "--output-format", "stream-json", "--verbose", prompt, ], { cwd: WORKSPACE, env: Object.fromEntries( agentEnvironment(PROCESS_ENV, profile, token).set("IS_SANDBOX", "1"), ), uid: profile.uid, gid: profile.gid, stdio: ["ignore", "pipe", "pipe"], }, ); const status = exitCode(claude); let result: ClaudeResult | null = null; let error = ""; const copyError = (async (): Promise => { claude.stderr.setEncoding("utf8"); for await (const chunk of claude.stderr) { const part = String(chunk); error += part; console.error(part); } })(); for await (const line of createInterface({ input: claude.stdout, crlfDelay: Infinity, })) { if (line !== "") { const event: unknown = JSON.parse(line); if (isObject(event)) { const content = field(field(event, "message"), "content"); for (const part of Array.isArray(content) ? content : []) { if (isObject(part)) { const text = field(part, "thinking") ?? field(part, "text") ?? field(part, "name"); if (typeof text === "string" && text !== "") { console.log(text); } } } if (field(event, "type") === "result") { const isError = field(event, "is_error"); const answer = field(event, "result"); const subtype = field(event, "subtype"); const apiStatus = field(event, "api_error_status"); result = { isError: typeof isError === "boolean" ? isError : undefined, result: typeof answer === "string" ? answer : undefined, subtype: String(subtype), apiErrorStatus: typeof apiStatus === "number" ? apiStatus : undefined, }; } } } } const [code] = await Promise.all([status, copyError]); if (result?.isError === true) { const failure = new Error( result.result ?? `claude ended with ${result.subtype}`, ); if (isClaudeAuthFailure(result)) { throw new AgentAuthError("claude sign-in failed", { cause: failure }); } throw failure; } if (result?.result === undefined) { throw new Error( error.trim() || `claude ended with ${result?.subtype ?? "no result"}`, ); } if (code !== 0) { throw new Error(error.trim() || `claude exited with ${code}`); } return result.result; } async function codexDeviceLogin( profile: RuntimeProfile, environment: ReadonlyMap, ): Promise { const login = spawn("codex", ["login", "--device-auth"], { cwd: WORKSPACE, env: Object.fromEntries(environment), uid: profile.uid, gid: profile.gid, stdio: ["ignore", "pipe", "pipe"], }); const status = exitCode(login); let shown = ""; const collect = async (stream: Readable): Promise => { stream.setEncoding("utf8"); for await (const chunk of stream) { shown += String(chunk); } }; const drained = Promise.all([collect(login.stdout), collect(login.stderr)]); await sleep(3000); while (shown.trim() === "") { const exited = await Promise.race([ status.then(() => true), sleep(100, false), ]); if (exited) { throw new AgentAuthError("AI bot login produced no instructions"); } } await postAuthorComment(withProfile(shown, profile)); const [code] = await Promise.all([status, drained]); if (code !== 0) { throw new AgentAuthError(`codex login exited with ${code}`); } } async function codexApiLogin( profile: RuntimeProfile, environment: ReadonlyMap, token: string, ): Promise { const login = spawn("codex", ["login", "--with-api-key"], { cwd: WORKSPACE, env: Object.fromEntries(environment), uid: profile.uid, gid: profile.gid, stdio: ["pipe", "inherit", "pipe"], }); const status = exitCode(login); login.stdin.end(`${token}\n`); await finished(login.stdin); const [code, error] = await Promise.all([status, readText(login.stderr)]); if (code !== 0) { throw new Error(error.trim() || `codex login exited with ${code}`); } } async function makeProfileFile( profile: RuntimeProfile, name: string, ): Promise { const path = `${profile.home}/${name}`; await writeFile(path, "", { mode: 0o600 }); await chmod(path, 0o600); await chown(path, profile.uid, profile.gid); return path; } async function runCodex( profile: RuntimeProfile, prompt: string, authentication: CodexAuthentication, ): Promise { const token = authentication.kind === "api-key" ? authentication.token : undefined; const environment = agentEnvironment(PROCESS_ENV, profile, token); if (authentication.kind === "api-key") { await codexApiLogin(profile, environment, authentication.token); } else { const check = spawn("codex", ["login", "status"], { cwd: WORKSPACE, env: Object.fromEntries(environment), uid: profile.uid, gid: profile.gid, stdio: "ignore", }); if ((await exitCode(check)) !== 0) { await codexDeviceLogin(profile, environment); } } const outputPath = await makeProfileFile(profile, "result.md"); const attempt = async (): Promise => { const codex = spawn( "codex", [ "exec", "--model", profile.model, "--dangerously-bypass-approvals-and-sandbox", "--output-last-message", outputPath, prompt, ], { cwd: WORKSPACE, env: Object.fromEntries(environment), uid: profile.uid, gid: profile.gid, stdio: ["ignore", "inherit", "pipe"], }, ); const status = exitCode(codex); codex.stderr.setEncoding("utf8"); let error = ""; for await (const chunk of codex.stderr) { const part = String(chunk); await echo(process.stderr, part); error += part; } // Codex echoes the prompt, whose comments may quote auth errors. return { code: await status, error: error.replaceAll(prompt, "") }; }; const result = authentication.kind === "api-key" ? await attempt() : await retryInvalidToken(attempt, async () => { await runAgentCommand(profile, "codex", ["logout"]); await codexDeviceLogin(profile, environment); }); if (result.code !== 0) { const failure = new Error( result.error.trim() || `codex exited with ${result.code}`, ); if (isCodexAuthFailure(result.error)) { throw new AgentAuthError("codex sign-in failed", { cause: failure }); } throw failure; } return outputPath; } async function runOpenCode( profile: RuntimeProfile, token: string, prompt: string, ): Promise { const child = spawn("opencode", openCodeArgs(profile.model, prompt), { cwd: WORKSPACE, env: Object.fromEntries(agentEnvironment(PROCESS_ENV, profile, token)), uid: profile.uid, gid: profile.gid, stdio: ["ignore", "pipe", "pipe"], }); const status = exitCode(child); const collected = collectOpenCodeOutput( child.stdout, child.stderr, async (chunk) => { await echo(process.stdout, chunk); }, async (chunk) => { await echo(process.stderr, chunk); }, ); const [code, streams] = await Promise.all([status, collected]); const error = streams.stderr.trim(); if (code !== 0) { throw ( openCodeError(streams.stdout) ?? new Error(error || `opencode exited with ${code}`) ); } return parseOpenCodeEvents(streams.stdout); } async function runProfile(profile: RuntimeProfile): Promise { const token = await readProfileToken(profile); const authentication = profile.type === "CODEX" ? codexAuthentication(token ?? undefined) : null; let stagedCodexAuth: string | null = null; let body: string | null = null; let resultPath: string | null = null; let reviewPath: string | null = null; let anchorsPath: string | null = null; try { if (authentication?.kind === "persistent") { stagedCodexAuth = await stageCodexAuth( CODEX_AUTH, profile.home, profile.uid, profile.gid, ); } await prepareProfile(profile); const rendered = await renderPrompt(profile); reviewPath = rendered.reviewPath; anchorsPath = rendered.anchorsPath; if (profile.type === "CLAUDE") { body = await runClaude(profile, token ?? "", rendered.prompt); } else if (profile.type === "CODEX") { resultPath = await runCodex( profile, rendered.prompt, authentication ?? codexAuthentication(token ?? undefined), ); } else { body = await runOpenCode(profile, token ?? "", rendered.prompt); } } finally { await killProfileProcesses(profile.uid); if (stagedCodexAuth !== null) { await persistCodexAuth(stagedCodexAuth, CODEX_AUTH); } } if (resultPath !== null) { body = await readProfileFile(profile, resultPath); } if (body === null || reviewPath === null || anchorsPath === null) { throw new Error("agent attempt produced no result"); } return { body, reviewPath, anchorsPath, profile: { type: profile.type, model: profile.model }, }; } async function writeOutcome(outcome: RuntimeOutcome): Promise { await writeFile(MANIFEST.outcomeFile, JSON.stringify(outcome), { mode: 0o600, }); await chmod(MANIFEST.outcomeFile, 0o600); } const failures: ProfileFailure[] = []; try { await configureGitAuthor(); await writeOutcome({ ok: true, ...(await runProfiles(MANIFEST.profiles, runProfile, failures)), }); } catch (error) { console.error(error); await writeOutcome({ ok: false, failures }); throw error; }