openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr16

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

257lines · modecode

1import { spawn } from "child_process";
2import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
3import path from "path";
4import { setOutput } from "@actions/core";
5
6export type PromptSource =
7 | {
8 type: "inline";
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 type SandboxMode =
23 | "read-only"
24 | "workspace-write"
25 | "danger-full-access";
26
27export type OutputSchemaSource =
28 | {
29 type: "file";
30 path: string;
31 }
32 | {
33 type: "inline";
34 content: string;
35 };
36
37export async function runCodexExec({
38 prompt,
39 codexHome,
40 cd,
41 extraArgs,
42 explicitOutputFile,
43 outputSchema,
44 model,
45 safetyStrategy,
46 codexUser,
47 sandbox,
48}: {
49 prompt: PromptSource;
50 codexHome: string | null;
51 cd: string;
52 extraArgs: Array<string>;
53 explicitOutputFile: string | null;
54 outputSchema: OutputSchemaSource | null;
55 model: string | null;
56 safetyStrategy: SafetyStrategy;
57 codexUser: string | null;
58 sandbox: SandboxMode;
59}): Promise<void> {
60 let input: string;
61 switch (prompt.type) {
62 case "inline":
63 input = prompt.content;
64 break;
65 case "file":
66 input = await readFile(prompt.path, "utf8");
67 break;
68 }
69
70 let outputFile: OutputFile;
71 if (explicitOutputFile != null) {
72 outputFile = { type: "explicit", file: explicitOutputFile };
73 } else {
74 outputFile = await createTempOutputFile();
75 }
76
77 const resolvedOutputSchema = await resolveOutputSchema(outputSchema);
78 const sandboxMode = await determineSandboxMode({
79 safetyStrategy,
80 requestedSandbox: sandbox,
81 });
82
83 const command: Array<string> = [];
84
85 if (safetyStrategy === "unprivileged-user") {
86 if (codexUser == null) {
87 throw new Error(
88 "codexUser must be specified when using the 'unprivileged-user' safety strategy."
89 );
90 }
91
92 command.push("sudo", "-u", codexUser, "--");
93 }
94
95 command.push(
96 "codex",
97 "exec",
98 "--skip-git-repo-check",
99 "--cd",
100 cd,
101 "--output-last-message",
102 outputFile.file
103 );
104
105 if (resolvedOutputSchema != null) {
106 command.push("--output-schema", resolvedOutputSchema.file);
107 }
108
109 if (model != null) {
110 command.push("--model", model);
111 }
112
113 command.push(...extraArgs);
114
115 command.push("--sandbox", sandboxMode);
116
117 const env = { ...process.env };
118 let extraEnv = "";
119 if (codexHome != null) {
120 env.CODEX_HOME = codexHome;
121 extraEnv = `CODEX_HOME=${codexHome} `;
122 }
123
124 // Split the `program` from the `args` for `spawn()`.
125 const program = command.shift()!;
126 console.log(
127 `Running: ${extraEnv}${program} ${command
128 .map((a) => JSON.stringify(a))
129 .join(" ")}`
130 );
131 try {
132 await new Promise((resolve, reject) => {
133 const child = spawn(program, command, {
134 env,
135 stdio: ["pipe", "inherit", "inherit"],
136 });
137 child.stdin.write(input);
138 child.stdin.end();
139
140 child.on("error", reject);
141
142 child.on("close", async (code) => {
143 if (code !== 0) {
144 reject(new Error(`${program} exited with code ${code}`));
145 return;
146 }
147
148 try {
149 await finalizeExecution(outputFile);
150 resolve(undefined);
151 } catch (err) {
152 reject(err);
153 }
154 });
155 });
156 } finally {
157 await cleanupOutputSchema(resolvedOutputSchema);
158 }
159}
160
161async function finalizeExecution(outputFile: OutputFile): Promise<void> {
162 try {
163 const lastMessage = await readFile(outputFile.file, "utf8");
164 setOutput("final-message", lastMessage);
165 } finally {
166 await cleanupTempOutput(outputFile);
167 }
168}
169
170type OutputFile =
171 | {
172 type: "explicit";
173 file: string;
174 }
175 | {
176 type: "temp";
177 file: string;
178 };
179
180type ResolvedOutputSchema =
181 | {
182 type: "explicit";
183 file: string;
184 }
185 | {
186 type: "temp";
187 file: string;
188 dir: string;
189 };
190
191async function createTempOutputFile(): Promise<OutputFile> {
192 const dir = await mkdtemp("codex-exec-");
193 return { type: "temp", file: path.join(dir, "output.md") };
194}
195
196async function cleanupTempOutput(outputFile: OutputFile): Promise<void> {
197 switch (outputFile.type) {
198 case "explicit":
199 // Do not delete user-specified output files.
200 return;
201 case "temp": {
202 const { file } = outputFile;
203 const dir = path.dirname(file);
204 await rm(dir, { recursive: true, force: true });
205 break;
206 }
207 }
208}
209
210async function resolveOutputSchema(
211 schema: OutputSchemaSource | null
212): Promise<ResolvedOutputSchema | null> {
213 if (schema == null) {
214 return null;
215 }
216
217 switch (schema.type) {
218 case "file":
219 return { type: "explicit", file: schema.path };
220 case "inline": {
221 const dir = await mkdtemp("codex-output-schema-");
222 const file = path.join(dir, "schema.json");
223 await writeFile(file, schema.content);
224 return { type: "temp", file, dir };
225 }
226 }
227}
228
229async function cleanupOutputSchema(
230 schema: ResolvedOutputSchema | null
231): Promise<void> {
232 if (schema == null) {
233 return;
234 }
235
236 switch (schema.type) {
237 case "explicit":
238 return;
239 case "temp":
240 await rm(schema.dir, { recursive: true, force: true });
241 return;
242 }
243}
244
245async function determineSandboxMode({
246 safetyStrategy,
247 requestedSandbox,
248}: {
249 safetyStrategy: SafetyStrategy;
250 requestedSandbox: SandboxMode;
251}): Promise<SandboxMode> {
252 if (safetyStrategy === "read-only") {
253 return "read-only";
254 } else {
255 return requestedSandbox;
256 }
257}
258