openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
ffac27a108c7752c85d8e0eef6466f0ba22dd5ec

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

293lines · 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 { which } from "./which";
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 needsSharedTempDir = safetyStrategy === "unprivileged-user";
73
74 let outputFile: OutputFile;
75 if (explicitOutputFile != null) {
76 outputFile = { type: "explicit", file: explicitOutputFile };
77 } else {
78 outputFile = await createTempOutputFile({ shared: needsSharedTempDir });
79 }
80
81 const resolvedOutputSchema = await resolveOutputSchema(
82 outputSchema,
83 needsSharedTempDir
84 );
85 const sandboxMode = await determineSandboxMode({
86 safetyStrategy,
87 requestedSandbox: sandbox,
88 });
89
90 const command: Array<string> = [];
91
92 let pathToCodex = "codex";
93 if (safetyStrategy === "unprivileged-user") {
94 if (codexUser == null) {
95 throw new Error(
96 "codexUser must be specified when using the 'unprivileged-user' safety strategy."
97 );
98 }
99
100 if (process.platform === "win32") {
101 throw new Error(
102 "the 'unprivileged-user' safety strategy is not supported on Windows."
103 );
104 }
105
106 // We are currently running as a privileged user, but `codexUser` will run
107 // with a different $PATH variable, so we need to find the full path to
108 // `codex`.
109 const whichResult = await which("codex");
110 if (whichResult == null) {
111 throw new Error("could not find 'codex' in PATH");
112 }
113
114 pathToCodex = whichResult;
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);
173 resolve(undefined);
174 } catch (err) {
175 reject(err);
176 }
177 });
178 });
179 } finally {
180 await cleanupOutputSchema(resolvedOutputSchema);
181 }
182}
183
184async function finalizeExecution(outputFile: OutputFile): Promise<void> {
185 try {
186 const lastMessage = await readFile(outputFile.file, "utf8");
187 setOutput("final-message", lastMessage);
188 } finally {
189 await cleanupTempOutput(outputFile);
190 }
191}
192
193type OutputFile =
194 | {
195 type: "explicit";
196 file: string;
197 }
198 | {
199 type: "temp";
200 file: string;
201 };
202
203type ResolvedOutputSchema =
204 | {
205 type: "explicit";
206 file: string;
207 }
208 | {
209 type: "temp";
210 file: string;
211 dir: string;
212 };
213
214async function createTempOutputFile({
215 shared,
216}: {
217 shared: boolean;
218}): Promise<OutputFile> {
219 const dir = await createTempDir("codex-exec-", shared);
220 return { type: "temp", file: path.join(dir, "output.md") };
221}
222
223async function cleanupTempOutput(outputFile: OutputFile): Promise<void> {
224 switch (outputFile.type) {
225 case "explicit":
226 // Do not delete user-specified output files.
227 return;
228 case "temp": {
229 const { file } = outputFile;
230 const dir = path.dirname(file);
231 await rm(dir, { recursive: true, force: true });
232 break;
233 }
234 }
235}
236
237async function resolveOutputSchema(
238 schema: OutputSchemaSource | null,
239 sharedTempDir: boolean
240): Promise<ResolvedOutputSchema | null> {
241 if (schema == null) {
242 return null;
243 }
244
245 switch (schema.type) {
246 case "file":
247 return { type: "explicit", file: schema.path };
248 case "inline": {
249 const dir = await createTempDir("codex-output-schema-", sharedTempDir);
250 const file = path.join(dir, "schema.json");
251 await writeFile(file, schema.content);
252 return { type: "temp", file, dir };
253 }
254 }
255}
256
257async function cleanupOutputSchema(
258 schema: ResolvedOutputSchema | null
259): Promise<void> {
260 if (schema == null) {
261 return;
262 }
263
264 switch (schema.type) {
265 case "explicit":
266 return;
267 case "temp":
268 await rm(schema.dir, { recursive: true, force: true });
269 return;
270 }
271}
272
273async function createTempDir(prefix: string, shared: boolean): Promise<string> {
274 const dir = await mkdtemp(path.join(os.tmpdir(), prefix));
275 if (shared) {
276 await chmod(dir, 0o755);
277 }
278 return dir;
279}
280
281async function determineSandboxMode({
282 safetyStrategy,
283 requestedSandbox,
284}: {
285 safetyStrategy: SafetyStrategy;
286 requestedSandbox: SandboxMode;
287}): Promise<SandboxMode> {
288 if (safetyStrategy === "read-only") {
289 return "read-only";
290 } else {
291 return requestedSandbox;
292 }
293}
294