openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr5

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

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