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 · modeblame

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