openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr10

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

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