#!/usr/bin/env node const fs = require("fs"); const os = require("os"); const path = require("path"); const { spawn } = require("child_process"); const DEFAULT_PROMPT = "請自我介紹"; const createdPaths = new Set(); function removeIfCreated(filePath) { if (!filePath || !createdPaths.has(filePath)) { return; } try { fs.rmSync(filePath, { force: true }); } catch { // Best-effort cleanup only. } } function cleanup() { for (const filePath of Array.from(createdPaths).reverse()) { removeIfCreated(filePath); } } function makeTempFile(dir, prefix) { const random = `${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}`; const filePath = path.join(dir, `${prefix}.${random}`); const fd = fs.openSync(filePath, "wx", 0o600); fs.closeSync(fd); createdPaths.add(filePath); return filePath; } function appendGithubOutput(status, output) { const outputFile = process.env.GITHUB_OUTPUT; if (!outputFile) { return; } let delimiter; do { delimiter = `CODEX_OUTPUT_${Math.random().toString(36).slice(2)}${Date.now()}`; } while (output.includes(delimiter)); fs.appendFileSync( outputFile, `status=${status}\noutput<<${delimiter}\n${output}${output.endsWith("\n") ? "" : "\n"}${delimiter}\n`, { encoding: "utf8", mode: 0o600 }, ); } function fail(message, code = 1) { console.error(message); appendGithubOutput("failed", message); cleanup(); process.exit(code); } function validateAuth(encodedAuth, authFile) { const decoded = Buffer.from(encodedAuth, "base64"); if (decoded.length === 0 && encodedAuth.length > 0) { fail("OAUTH must be valid base64 encoded Codex auth.json."); } const normalized = encodedAuth.replace(/\s+/g, ""); if (decoded.toString("base64").replace(/=+$/, "") !== normalized.replace(/=+$/, "")) { fail("OAUTH must be valid base64 encoded Codex auth.json."); } fs.writeFileSync(authFile, decoded, { mode: 0o600 }); let parsed; try { parsed = JSON.parse(decoded.toString("utf8")); } catch { fail("Decoded OAUTH must be a JSON object."); } if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") { fail("Decoded OAUTH must be a JSON object."); } } function runCodex(model, prompt) { return new Promise((resolve) => { const child = spawn( "codex", [ "exec", "--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check", "--model", model, prompt, ], { stdio: ["ignore", "pipe", "pipe"] }, ); let output = ""; child.stdout.on("data", (chunk) => { process.stdout.write(chunk); output += chunk.toString(); }); child.stderr.on("data", (chunk) => { process.stdout.write(chunk); output += chunk.toString(); }); child.on("error", (error) => { output += `${error.message}\n`; resolve({ status: 1, output }); }); child.on("close", (code) => { resolve({ status: code ?? 1, output }); }); }); } async function main() { process.on("exit", cleanup); process.on("SIGINT", () => { cleanup(); process.exit(130); }); process.on("SIGTERM", () => { cleanup(); process.exit(143); }); const oauth = process.env.OAUTH || ""; const model = process.env.MODEL || ""; const codexHome = process.env.CODEX_HOME || "/root/.codex"; const prompt = process.env.PROMPT || DEFAULT_PROMPT; if (!oauth) { fail("OAUTH is required: provide base64 encoded Codex auth.json."); } if (!model) { fail("MODEL is required."); } try { fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 }); } catch { fail("Unable to create CODEX_HOME."); } const authFile = makeTempFile(codexHome, "auth"); const authPath = path.join(codexHome, "auth.json"); const lockPath = path.join(os.tmpdir(), `codex-auth-${Buffer.from(codexHome).toString("hex")}.lock`); let lockHandle; try { lockHandle = fs.openSync(lockPath, "wx", 0o600); createdPaths.add(lockPath); } catch { fail("Unable to lock Codex auth.json."); } validateAuth(oauth, authFile); if (fs.existsSync(authPath)) { fail("Refusing to overwrite existing Codex auth.json."); } fs.copyFileSync(authFile, authPath); fs.chmodSync(authPath, 0o600); createdPaths.add(authPath); removeIfCreated(authFile); const result = await runCodex(model, prompt); appendGithubOutput(result.status === 0 ? "completed" : "failed", result.output); if (lockHandle !== undefined) { fs.closeSync(lockHandle); } cleanup(); process.exit(result.status); } main().catch((error) => { fail(error instanceof Error ? error.message : String(error)); });