openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr6

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

184lines · modecode

1import { spawn } from "child_process";
2import { mkdtemp, readFile, rm } from "fs/promises";
3import path from "path";
4import { setOutput } from "@actions/core";
5
6export type PromptSource =
7 | {
8 type: "text";
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 async function runCodexExec({
23 prompt,
24 codexHome,
25 cd,
26 proxyPort,
27 extraArgs,
28 explicitOutputFile,
29 outputSchemaFile,
30 model,
31 safetyStrategy,
32 codexUser,
33}: {
34 prompt: PromptSource;
35 codexHome: string | null;
36 cd: string;
37 proxyPort: number;
38 extraArgs: Array<string>;
39 explicitOutputFile: string | null;
40 outputSchemaFile: string | null;
41 model: string | null;
42 safetyStrategy: SafetyStrategy;
43 codexUser: string | null;
44}): Promise<void> {
45 let input: string;
46 switch (prompt.type) {
47 case "text":
48 input = prompt.content;
49 break;
50 case "file":
51 input = await readFile(prompt.path, "utf8");
52 break;
53 }
54
55 let outputFile: OutputFile;
56 if (explicitOutputFile != null) {
57 outputFile = { type: "explicit", file: explicitOutputFile };
58 } else {
59 outputFile = await createTempOutputFile();
60 }
61
62 const command: Array<string> = [];
63
64 if (safetyStrategy === "unprivileged_user") {
65 if (codexUser == null) {
66 throw new Error(
67 "codexUser must be specified when using the 'unprivileged_user' safety strategy."
68 );
69 }
70
71 command.push("sudo", "-u", codexUser, "--");
72 }
73
74 const providerBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
75 const providerId = "openai-proxy";
76 const providerConfig = `model_providers.${providerId}={ name = "OpenAI Proxy", base_url = "${providerBaseUrl}", wire_api = "responses" }`;
77
78 command.push(
79 "codex",
80 "exec",
81 "--skip-git-repo-check",
82 "--cd",
83 cd,
84 "--config",
85 providerConfig,
86 "--config",
87 `model_provider="${providerId}"`,
88 "--output-last-message",
89 outputFile.file
90 );
91
92 if (outputSchemaFile != null) {
93 command.push("--output-schema", outputSchemaFile);
94 }
95
96 if (model != null) {
97 command.push("--model", model);
98 }
99
100 command.push(...extraArgs);
101
102 // Note that if profiles expand to support their own sandbox policies, a
103 // custom profile could override this setting.
104 if (safetyStrategy === "read_only") {
105 command.push("--config", 'sandbox_mode="read-only"');
106 }
107
108 const env = { ...process.env };
109 let extraEnv = "";
110 if (codexHome != null) {
111 env.CODEX_HOME = codexHome;
112 extraEnv = `CODEX_HOME=${codexHome} `;
113 }
114
115 // Split the `program` from the `args` for `spawn()`.
116 const program = command.shift()!;
117 console.log(
118 `Running: ${extraEnv}${program} ${command
119 .map((a) => JSON.stringify(a))
120 .join(" ")}`
121 );
122 return new Promise((resolve, reject) => {
123 const child = spawn(program, command, {
124 env,
125 stdio: ["pipe", "inherit", "inherit"],
126 });
127 child.stdin.write(input);
128 child.stdin.end();
129
130 child.on("error", reject);
131
132 child.on("close", async (code) => {
133 if (code !== 0) {
134 reject(new Error(`${program} exited with code ${code}`));
135 return;
136 }
137
138 try {
139 await finalizeExecution(outputFile);
140 resolve();
141 } catch (err) {
142 reject(err);
143 }
144 });
145 });
146}
147
148async function finalizeExecution(outputFile: OutputFile): Promise<void> {
149 try {
150 const lastMessage = await readFile(outputFile.file, "utf8");
151 setOutput("final_message", lastMessage);
152 } finally {
153 await cleanupTempOutput(outputFile);
154 }
155}
156
157type OutputFile =
158 | {
159 type: "explicit";
160 file: string;
161 }
162 | {
163 type: "temp";
164 file: string;
165 };
166
167async function createTempOutputFile(): Promise<OutputFile> {
168 const dir = await mkdtemp("codex-exec-");
169 return { type: "temp", file: path.join(dir, "output.md") };
170}
171
172async function cleanupTempOutput(outputFile: OutputFile): Promise<void> {
173 switch (outputFile.type) {
174 case "explicit":
175 // Do not delete user-specified output files.
176 return;
177 case "temp": {
178 const { file } = outputFile;
179 const dir = path.dirname(file);
180 await rm(dir, { recursive: true, force: true });
181 break;
182 }
183 }
184}
185