openai/codex-action

Public

mirrored from https://github.com/openai/codex-actionAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fb85e4a05fad8a68ac0f2a006707e0b921aa83c2

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

171lines · modecode

1import { spawn } from "child_process";
2import { mkdtemp, readFile, rm } from "fs/promises";
3import path from "path";
4import { setOutput } from "@actions/core";
5
6export type PromptSource =
7 | {
8 type: "text";
9 content: string;
10 }
11 | {
12 type: "file";
13 path: string;
14 };
15
16export type SafetyStrategy =
17 | "drop_sudo"
18 | "read_only"
19 | "unprivileged_user"
20 | "unsafe";
21
22export async function runCodexExec({
23 prompt,
24 codexHome,
25 cd,
26 proxyPort,
27 extraArgs,
28 explicitOutputFile,
29 safetyStrategy,
30 codexUser,
31}: {
32 prompt: PromptSource;
33 codexHome: string | null;
34 cd: string;
35 proxyPort: number;
36 extraArgs: Array<string>;
37 explicitOutputFile: string | null;
38 safetyStrategy: SafetyStrategy;
39 codexUser: string | null;
40}): Promise<void> {
41 let input: string;
42 switch (prompt.type) {
43 case "text":
44 input = prompt.content;
45 break;
46 case "file":
47 input = await readFile(prompt.path, "utf8");
48 break;
49 }
50
51 let outputFile: OutputFile;
52 if (explicitOutputFile != null) {
53 outputFile = { type: "explicit", file: explicitOutputFile };
54 } else {
55 outputFile = await createTempOutputFile();
56 }
57
58 const command: Array<string> = [];
59
60 if (safetyStrategy === "unprivileged_user") {
61 if (codexUser == null) {
62 throw new Error(
63 "codexUser must be specified when using the 'unprivileged_user' safety strategy."
64 );
65 }
66
67 command.push("sudo", "-u", codexUser, "--");
68 }
69
70 const providerBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
71 const providerId = "openai-proxy";
72 const providerConfig = `model_providers.${providerId}={ name = "OpenAI Proxy", base_url = "${providerBaseUrl}", wire_api = "responses" }`;
73
74 command.push(
75 "codex",
76 "exec",
77 "--skip-git-repo-check",
78 "--cd",
79 cd,
80 "--config",
81 providerConfig,
82 "--config",
83 `model_provider="${providerId}"`,
84 "--output-last-message",
85 outputFile.file
86 );
87 command.push(...extraArgs);
88
89 // Note that if profiles expand to support their own sandbox policies, a
90 // custom profile could override this setting.
91 if (safetyStrategy === "read_only") {
92 command.push("--config", 'sandbox_mode="read-only"');
93 }
94
95 const env = { ...process.env };
96 let extraEnv = "";
97 if (codexHome != null) {
98 env.CODEX_HOME = codexHome;
99 extraEnv = `CODEX_HOME=${codexHome} `;
100 }
101
102 // Split the `program` from the `args` for `spawn()`.
103 const program = command.shift()!;
104 console.log(
105 `Running: ${extraEnv}${program} ${command
106 .map((a) => JSON.stringify(a))
107 .join(" ")}`
108 );
109 return new Promise((resolve, reject) => {
110 const child = spawn(program, command, {
111 env,
112 stdio: ["pipe", "inherit", "inherit"],
113 });
114 child.stdin.write(input);
115 child.stdin.end();
116
117 child.on("error", reject);
118
119 child.on("close", async (code) => {
120 if (code !== 0) {
121 reject(new Error(`${program} exited with code ${code}`));
122 return;
123 }
124
125 try {
126 await finalizeExecution(outputFile);
127 resolve();
128 } catch (err) {
129 reject(err);
130 }
131 });
132 });
133}
134
135async function finalizeExecution(outputFile: OutputFile): Promise<void> {
136 try {
137 const lastMessage = await readFile(outputFile.file, "utf8");
138 setOutput("final_message", lastMessage);
139 } finally {
140 await cleanupTempOutput(outputFile);
141 }
142}
143
144type OutputFile =
145 | {
146 type: "explicit";
147 file: string;
148 }
149 | {
150 type: "temp";
151 file: string;
152 };
153
154async function createTempOutputFile(): Promise<OutputFile> {
155 const dir = await mkdtemp("codex-exec-");
156 return { type: "temp", file: path.join(dir, "output.md") };
157}
158
159async function cleanupTempOutput(outputFile: OutputFile): Promise<void> {
160 switch (outputFile.type) {
161 case "explicit":
162 // Do not delete user-specified output files.
163 return;
164 case "temp": {
165 const { file } = outputFile;
166 const dir = path.dirname(file);
167 await rm(dir, { recursive: true, force: true });
168 break;
169 }
170 }
171}
172