openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
685900c1573bc72aae35d9984d72f5fd06cc8061

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

328lines · modecode

1import { spawn } from "child_process";
2import { chmod, mkdtemp, readFile, rm, writeFile } from "fs/promises";
3import path from "path";
4import os from "os";
5import { setOutput } from "@actions/core";
6import { checkOutput } from "./checkOutput";
7
8export type PromptSource =
9 | {
10 type: "inline";
11 content: string;
12 }
13 | {
14 type: "file";
15 path: string;
16 };
17
18export type SafetyStrategy =
19 | "drop-sudo"
20 | "read-only"
21 | "unprivileged-user"
22 | "unsafe";
23
24export type SandboxMode =
25 | "read-only"
26 | "workspace-write"
27 | "danger-full-access";
28
29export type OutputSchemaSource =
30 | {
31 type: "file";
32 path: string;
33 }
34 | {
35 type: "inline";
36 content: string;
37 };
38
39export async function runCodexExec({
40 prompt,
41 codexHome,
42 cd,
43 extraArgs,
44 explicitOutputFile,
45 outputSchema,
46 model,
47 safetyStrategy,
48 codexUser,
49 sandbox,
50}: {
51 prompt: PromptSource;
52 codexHome: string | null;
53 cd: string;
54 extraArgs: Array<string>;
55 explicitOutputFile: string | null;
56 outputSchema: OutputSchemaSource | null;
57 model: string | null;
58 safetyStrategy: SafetyStrategy;
59 codexUser: string | null;
60 sandbox: SandboxMode;
61}): Promise<void> {
62 let input: string;
63 switch (prompt.type) {
64 case "inline":
65 input = prompt.content;
66 break;
67 case "file":
68 input = await readFile(prompt.path, "utf8");
69 break;
70 }
71
72 const runAsUser: string | null =
73 safetyStrategy === "unprivileged-user" ? codexUser : null;
74
75 let outputFile: OutputFile;
76 if (explicitOutputFile != null) {
77 outputFile = { type: "explicit", file: explicitOutputFile };
78 } else {
79 outputFile = await createTempOutputFile({ runAsUser });
80 }
81
82 const resolvedOutputSchema = await resolveOutputSchema(
83 outputSchema,
84 runAsUser
85 );
86 const sandboxMode = await determineSandboxMode({
87 safetyStrategy,
88 requestedSandbox: sandbox,
89 });
90
91 const command: Array<string> = [];
92
93 let pathToCodex = "codex";
94 if (safetyStrategy === "unprivileged-user") {
95 if (codexUser == null) {
96 throw new Error(
97 "codexUser must be specified when using the 'unprivileged-user' safety strategy."
98 );
99 }
100
101 if (process.platform === "win32") {
102 throw new Error(
103 "the 'unprivileged-user' safety strategy is not supported on Windows."
104 );
105 }
106
107 // We are currently running as a privileged user, but `codexUser` will run
108 // with a different $PATH variable, so we need to find the full path to
109 // `codex`.
110 pathToCodex = (await checkOutput(["which", "codex"])).trim();
111 if (!pathToCodex) {
112 throw new Error("could not find 'codex' in PATH");
113 }
114
115 command.push("sudo", "-u", codexUser, "--");
116 }
117
118 command.push(
119 pathToCodex,
120 "exec",
121 "--skip-git-repo-check",
122 "--cd",
123 cd,
124 "--output-last-message",
125 outputFile.file
126 );
127
128 if (resolvedOutputSchema != null) {
129 command.push("--output-schema", resolvedOutputSchema.file);
130 }
131
132 if (model != null) {
133 command.push("--model", model);
134 }
135
136 command.push(...extraArgs);
137
138 command.push("--sandbox", sandboxMode);
139
140 const env = { ...process.env };
141 let extraEnv = "";
142 if (codexHome != null) {
143 env.CODEX_HOME = codexHome;
144 extraEnv = `CODEX_HOME=${codexHome} `;
145 }
146
147 // Split the `program` from the `args` for `spawn()`.
148 const program = command.shift()!;
149 console.log(
150 `Running: ${extraEnv}${program} ${command
151 .map((a) => JSON.stringify(a))
152 .join(" ")}`
153 );
154 try {
155 await new Promise((resolve, reject) => {
156 const child = spawn(program, command, {
157 env,
158 stdio: ["pipe", "inherit", "inherit"],
159 });
160 child.stdin.write(input);
161 child.stdin.end();
162
163 child.on("error", reject);
164
165 child.on("close", async (code) => {
166 if (code !== 0) {
167 reject(new Error(`${program} exited with code ${code}`));
168 return;
169 }
170
171 try {
172 await finalizeExecution(outputFile, runAsUser);
173 resolve(undefined);
174 } catch (err) {
175 reject(err);
176 }
177 });
178 });
179 } finally {
180 await cleanupOutputSchema(resolvedOutputSchema);
181 }
182}
183
184async function finalizeExecution(
185 outputFile: OutputFile,
186 runAsUser: string | null
187): Promise<void> {
188 try {
189 let lastMessage: string;
190 if (runAsUser == null) {
191 lastMessage = await readFile(outputFile.file, "utf8");
192 setOutput("final-message", lastMessage);
193 } else {
194 lastMessage = await checkOutput([
195 "sudo",
196 "-u",
197 runAsUser,
198 "cat",
199 outputFile.file,
200 ]);
201 }
202 setOutput("final-message", lastMessage);
203 } finally {
204 await cleanupTempOutput(outputFile, runAsUser);
205 }
206}
207
208type OutputFile =
209 | {
210 type: "explicit";
211 file: string;
212 }
213 | {
214 type: "temp";
215 file: string;
216 };
217
218type ResolvedOutputSchema =
219 | {
220 type: "explicit";
221 file: string;
222 }
223 | {
224 type: "temp";
225 file: string;
226 dir: string;
227 };
228
229async function createTempOutputFile({
230 runAsUser,
231}: {
232 runAsUser: string | null;
233}): Promise<OutputFile> {
234 const dir = await createTempDir("codex-exec-", runAsUser);
235 return { type: "temp", file: path.join(dir, "output.md") };
236}
237
238async function cleanupTempOutput(
239 outputFile: OutputFile,
240 runAsUser: string | null
241): Promise<void> {
242 switch (outputFile.type) {
243 case "explicit":
244 // Do not delete user-specified output files.
245 return;
246 case "temp": {
247 const { file } = outputFile;
248 if (runAsUser == null) {
249 const dir = path.dirname(file);
250 await rm(dir, { recursive: true, force: true });
251 } else {
252 await checkOutput(["sudo", "rm", "-rf", path.dirname(file)]);
253 }
254 break;
255 }
256 }
257}
258
259async function resolveOutputSchema(
260 schema: OutputSchemaSource | null,
261 runAsUser: string | null
262): Promise<ResolvedOutputSchema | null> {
263 if (schema == null) {
264 return null;
265 }
266
267 switch (schema.type) {
268 case "file":
269 return { type: "explicit", file: schema.path };
270 case "inline": {
271 const dir = await createTempDir("codex-output-schema-", runAsUser);
272 const file = path.join(dir, "schema.json");
273 await writeFile(file, schema.content);
274 return { type: "temp", file, dir };
275 }
276 }
277}
278
279async function cleanupOutputSchema(
280 schema: ResolvedOutputSchema | null
281): Promise<void> {
282 if (schema == null) {
283 return;
284 }
285
286 switch (schema.type) {
287 case "explicit":
288 return;
289 case "temp":
290 await rm(schema.dir, { recursive: true, force: true });
291 return;
292 }
293}
294
295async function createTempDir(
296 prefix: string,
297 runAsUser: string | null
298): Promise<string> {
299 if (runAsUser == null) {
300 return await mkdtemp(path.join(os.tmpdir(), prefix));
301 } else {
302 return (
303 await checkOutput([
304 "sudo",
305 "-u",
306 runAsUser,
307 "mktemp",
308 "-d",
309 "-t",
310 `${prefix}.XXXXXX`,
311 ])
312 ).trim();
313 }
314}
315
316async function determineSandboxMode({
317 safetyStrategy,
318 requestedSandbox,
319}: {
320 safetyStrategy: SafetyStrategy;
321 requestedSandbox: SandboxMode;
322}): Promise<SandboxMode> {
323 if (safetyStrategy === "read-only") {
324 return "read-only";
325 } else {
326 return requestedSandbox;
327 }
328}
329