openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pakrym/originator

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

331lines · 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 if (!env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE) {
142 env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE = "codex_github_action";
143 }
144 let extraEnv = "";
145 if (codexHome != null) {
146 env.CODEX_HOME = codexHome;
147 extraEnv = `CODEX_HOME=${codexHome} `;
148 }
149
150 // Split the `program` from the `args` for `spawn()`.
151 const program = command.shift()!;
152 console.log(
153 `Running: ${extraEnv}${program} ${command
154 .map((a) => JSON.stringify(a))
155 .join(" ")}`
156 );
157 try {
158 await new Promise((resolve, reject) => {
159 const child = spawn(program, command, {
160 env,
161 stdio: ["pipe", "inherit", "inherit"],
162 });
163 child.stdin.write(input);
164 child.stdin.end();
165
166 child.on("error", reject);
167
168 child.on("close", async (code) => {
169 if (code !== 0) {
170 reject(new Error(`${program} exited with code ${code}`));
171 return;
172 }
173
174 try {
175 await finalizeExecution(outputFile, runAsUser);
176 resolve(undefined);
177 } catch (err) {
178 reject(err);
179 }
180 });
181 });
182 } finally {
183 await cleanupOutputSchema(resolvedOutputSchema);
184 }
185}
186
187async function finalizeExecution(
188 outputFile: OutputFile,
189 runAsUser: string | null
190): Promise<void> {
191 try {
192 let lastMessage: string;
193 if (runAsUser == null) {
194 lastMessage = await readFile(outputFile.file, "utf8");
195 setOutput("final-message", lastMessage);
196 } else {
197 lastMessage = await checkOutput([
198 "sudo",
199 "-u",
200 runAsUser,
201 "cat",
202 outputFile.file,
203 ]);
204 }
205 setOutput("final-message", lastMessage);
206 } finally {
207 await cleanupTempOutput(outputFile, runAsUser);
208 }
209}
210
211type OutputFile =
212 | {
213 type: "explicit";
214 file: string;
215 }
216 | {
217 type: "temp";
218 file: string;
219 };
220
221type ResolvedOutputSchema =
222 | {
223 type: "explicit";
224 file: string;
225 }
226 | {
227 type: "temp";
228 file: string;
229 dir: string;
230 };
231
232async function createTempOutputFile({
233 runAsUser,
234}: {
235 runAsUser: string | null;
236}): Promise<OutputFile> {
237 const dir = await createTempDir("codex-exec-", runAsUser);
238 return { type: "temp", file: path.join(dir, "output.md") };
239}
240
241async function cleanupTempOutput(
242 outputFile: OutputFile,
243 runAsUser: string | null
244): Promise<void> {
245 switch (outputFile.type) {
246 case "explicit":
247 // Do not delete user-specified output files.
248 return;
249 case "temp": {
250 const { file } = outputFile;
251 if (runAsUser == null) {
252 const dir = path.dirname(file);
253 await rm(dir, { recursive: true, force: true });
254 } else {
255 await checkOutput(["sudo", "rm", "-rf", path.dirname(file)]);
256 }
257 break;
258 }
259 }
260}
261
262async function resolveOutputSchema(
263 schema: OutputSchemaSource | null,
264 runAsUser: string | null
265): Promise<ResolvedOutputSchema | null> {
266 if (schema == null) {
267 return null;
268 }
269
270 switch (schema.type) {
271 case "file":
272 return { type: "explicit", file: schema.path };
273 case "inline": {
274 const dir = await createTempDir("codex-output-schema-", runAsUser);
275 const file = path.join(dir, "schema.json");
276 await writeFile(file, schema.content);
277 return { type: "temp", file, dir };
278 }
279 }
280}
281
282async function cleanupOutputSchema(
283 schema: ResolvedOutputSchema | null
284): Promise<void> {
285 if (schema == null) {
286 return;
287 }
288
289 switch (schema.type) {
290 case "explicit":
291 return;
292 case "temp":
293 await rm(schema.dir, { recursive: true, force: true });
294 return;
295 }
296}
297
298async function createTempDir(
299 prefix: string,
300 runAsUser: string | null
301): Promise<string> {
302 if (runAsUser == null) {
303 return await mkdtemp(path.join(os.tmpdir(), prefix));
304 } else {
305 return (
306 await checkOutput([
307 "sudo",
308 "-u",
309 runAsUser,
310 "mktemp",
311 "-d",
312 "-t",
313 `${prefix}.XXXXXX`,
314 ])
315 ).trim();
316 }
317}
318
319async function determineSandboxMode({
320 safetyStrategy,
321 requestedSandbox,
322}: {
323 safetyStrategy: SafetyStrategy;
324 requestedSandbox: SandboxMode;
325}): Promise<SandboxMode> {
326 if (safetyStrategy === "read-only") {
327 return "read-only";
328 } else {
329 return requestedSandbox;
330 }
331}
332