openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr21

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

292lines · 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 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 pathToCodex = (await checkOutput(["which", "codex"])).trim();
110 if (!pathToCodex) {
111 throw new Error("could not find 'codex' in PATH");
112 }
113
114 command.push("sudo", "-u", codexUser, "--");
115 }
116
117 command.push(
118 pathToCodex,
119 "exec",
120 "--skip-git-repo-check",
121 "--cd",
122 cd,
123 "--output-last-message",
124 outputFile.file
125 );
126
127 if (resolvedOutputSchema != null) {
128 command.push("--output-schema", resolvedOutputSchema.file);
129 }
130
131 if (model != null) {
132 command.push("--model", model);
133 }
134
135 command.push(...extraArgs);
136
137 command.push("--sandbox", sandboxMode);
138
139 const env = { ...process.env };
140 let extraEnv = "";
141 if (codexHome != null) {
142 env.CODEX_HOME = codexHome;
143 extraEnv = `CODEX_HOME=${codexHome} `;
144 }
145
146 // Split the `program` from the `args` for `spawn()`.
147 const program = command.shift()!;
148 console.log(
149 `Running: ${extraEnv}${program} ${command
150 .map((a) => JSON.stringify(a))
151 .join(" ")}`
152 );
153 try {
154 await new Promise((resolve, reject) => {
155 const child = spawn(program, command, {
156 env,
157 stdio: ["pipe", "inherit", "inherit"],
158 });
159 child.stdin.write(input);
160 child.stdin.end();
161
162 child.on("error", reject);
163
164 child.on("close", async (code) => {
165 if (code !== 0) {
166 reject(new Error(`${program} exited with code ${code}`));
167 return;
168 }
169
170 try {
171 await finalizeExecution(outputFile);
172 resolve(undefined);
173 } catch (err) {
174 reject(err);
175 }
176 });
177 });
178 } finally {
179 await cleanupOutputSchema(resolvedOutputSchema);
180 }
181}
182
183async function finalizeExecution(outputFile: OutputFile): Promise<void> {
184 try {
185 const lastMessage = await readFile(outputFile.file, "utf8");
186 setOutput("final-message", lastMessage);
187 } finally {
188 await cleanupTempOutput(outputFile);
189 }
190}
191
192type OutputFile =
193 | {
194 type: "explicit";
195 file: string;
196 }
197 | {
198 type: "temp";
199 file: string;
200 };
201
202type ResolvedOutputSchema =
203 | {
204 type: "explicit";
205 file: string;
206 }
207 | {
208 type: "temp";
209 file: string;
210 dir: string;
211 };
212
213async function createTempOutputFile({
214 shared,
215}: {
216 shared: boolean;
217}): Promise<OutputFile> {
218 const dir = await createTempDir("codex-exec-", shared);
219 return { type: "temp", file: path.join(dir, "output.md") };
220}
221
222async function cleanupTempOutput(outputFile: OutputFile): Promise<void> {
223 switch (outputFile.type) {
224 case "explicit":
225 // Do not delete user-specified output files.
226 return;
227 case "temp": {
228 const { file } = outputFile;
229 const dir = path.dirname(file);
230 await rm(dir, { recursive: true, force: true });
231 break;
232 }
233 }
234}
235
236async function resolveOutputSchema(
237 schema: OutputSchemaSource | null,
238 sharedTempDir: boolean
239): Promise<ResolvedOutputSchema | null> {
240 if (schema == null) {
241 return null;
242 }
243
244 switch (schema.type) {
245 case "file":
246 return { type: "explicit", file: schema.path };
247 case "inline": {
248 const dir = await createTempDir("codex-output-schema-", sharedTempDir);
249 const file = path.join(dir, "schema.json");
250 await writeFile(file, schema.content);
251 return { type: "temp", file, dir };
252 }
253 }
254}
255
256async function cleanupOutputSchema(
257 schema: ResolvedOutputSchema | null
258): Promise<void> {
259 if (schema == null) {
260 return;
261 }
262
263 switch (schema.type) {
264 case "explicit":
265 return;
266 case "temp":
267 await rm(schema.dir, { recursive: true, force: true });
268 return;
269 }
270}
271
272async function createTempDir(prefix: string, shared: boolean): Promise<string> {
273 const dir = await mkdtemp(path.join(os.tmpdir(), prefix));
274 if (shared) {
275 await chmod(dir, 0o755);
276 }
277 return dir;
278}
279
280async function determineSandboxMode({
281 safetyStrategy,
282 requestedSandbox,
283}: {
284 safetyStrategy: SafetyStrategy;
285 requestedSandbox: SandboxMode;
286}): Promise<SandboxMode> {
287 if (safetyStrategy === "read-only") {
288 return "read-only";
289 } else {
290 return requestedSandbox;
291 }
292}
293