openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
6fb86fda8b106930a296719ae7a9fdf05ae5f4f6

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

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