openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr15

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

257lines · modeblame

e9d0a695Michael Bolin10 months ago1import { spawn } from "child_process";
bc00fb04Michael Bolin10 months ago2import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
e9d0a695Michael Bolin10 months ago3import path from "path";
4import { setOutput } from "@actions/core";
5
6export type PromptSource =
7| {
bc00fb04Michael Bolin10 months ago8type: "inline";
e9d0a695Michael Bolin10 months ago9content: string;
10}
11| {
12type: "file";
13path: string;
14};
15
16export type SafetyStrategy =
d49a23a3Michael Bolin10 months ago17| "drop-sudo"
18| "read-only"
19| "unprivileged-user"
e9d0a695Michael Bolin10 months ago20| "unsafe";
21
911c5cb9Michael Bolin10 months ago22export type SandboxMode =
23| "read-only"
24| "workspace-write"
25| "danger-full-access";
26
bc00fb04Michael Bolin10 months ago27export type OutputSchemaSource =
28| {
29type: "file";
30path: string;
31}
32| {
33type: "inline";
34content: string;
35};
36
e9d0a695Michael Bolin10 months ago37export async function runCodexExec({
38prompt,
39codexHome,
40cd,
41extraArgs,
42explicitOutputFile,
bc00fb04Michael Bolin10 months ago43outputSchema,
7304b0a7Michael Bolin10 months ago44model,
e9d0a695Michael Bolin10 months ago45safetyStrategy,
46codexUser,
911c5cb9Michael Bolin10 months ago47sandbox,
e9d0a695Michael Bolin10 months ago48}: {
49prompt: PromptSource;
50codexHome: string | null;
51cd: string;
52extraArgs: Array<string>;
53explicitOutputFile: string | null;
bc00fb04Michael Bolin10 months ago54outputSchema: OutputSchemaSource | null;
7304b0a7Michael Bolin10 months ago55model: string | null;
e9d0a695Michael Bolin10 months ago56safetyStrategy: SafetyStrategy;
57codexUser: string | null;
911c5cb9Michael Bolin10 months ago58sandbox: SandboxMode;
e9d0a695Michael Bolin10 months ago59}): Promise<void> {
60let input: string;
61switch (prompt.type) {
bc00fb04Michael Bolin10 months ago62case "inline":
e9d0a695Michael Bolin10 months ago63input = prompt.content;
64break;
65case "file":
66input = await readFile(prompt.path, "utf8");
67break;
68}
69
70let outputFile: OutputFile;
71if (explicitOutputFile != null) {
72outputFile = { type: "explicit", file: explicitOutputFile };
73} else {
74outputFile = await createTempOutputFile();
75}
76
bc00fb04Michael Bolin10 months ago77const resolvedOutputSchema = await resolveOutputSchema(outputSchema);
911c5cb9Michael Bolin10 months ago78const sandboxMode = await determineSandboxMode({
79safetyStrategy,
80requestedSandbox: sandbox,
81});
bc00fb04Michael Bolin10 months ago82
e9d0a695Michael Bolin10 months ago83const command: Array<string> = [];
84
d49a23a3Michael Bolin10 months ago85if (safetyStrategy === "unprivileged-user") {
e9d0a695Michael Bolin10 months ago86if (codexUser == null) {
87throw new Error(
d49a23a3Michael Bolin10 months ago88"codexUser must be specified when using the 'unprivileged-user' safety strategy."
e9d0a695Michael Bolin10 months ago89);
90}
91
92command.push("sudo", "-u", codexUser, "--");
93}
94
95command.push(
96"codex",
97"exec",
98"--skip-git-repo-check",
99"--cd",
100cd,
101"--output-last-message",
102outputFile.file
103);
b8896131Michael Bolin10 months ago104
bc00fb04Michael Bolin10 months ago105if (resolvedOutputSchema != null) {
106command.push("--output-schema", resolvedOutputSchema.file);
b8896131Michael Bolin10 months ago107}
108
7304b0a7Michael Bolin10 months ago109if (model != null) {
110command.push("--model", model);
111}
112
e9d0a695Michael Bolin10 months ago113command.push(...extraArgs);
114
911c5cb9Michael Bolin10 months ago115command.push("--sandbox", sandboxMode);
e9d0a695Michael Bolin10 months ago116
117const env = { ...process.env };
118let extraEnv = "";
119if (codexHome != null) {
120env.CODEX_HOME = codexHome;
121extraEnv = `CODEX_HOME=${codexHome} `;
122}
123
124// Split the `program` from the `args` for `spawn()`.
125const program = command.shift()!;
126console.log(
127`Running: ${extraEnv}${program} ${command
128.map((a) => JSON.stringify(a))
129.join(" ")}`
130);
bc00fb04Michael Bolin10 months ago131try {
132await new Promise((resolve, reject) => {
133const child = spawn(program, command, {
134env,
135stdio: ["pipe", "inherit", "inherit"],
136});
137child.stdin.write(input);
138child.stdin.end();
139
140child.on("error", reject);
141
142child.on("close", async (code) => {
143if (code !== 0) {
144reject(new Error(`${program} exited with code ${code}`));
145return;
146}
147
148try {
149await finalizeExecution(outputFile);
150resolve(undefined);
151} catch (err) {
152reject(err);
153}
154});
e9d0a695Michael Bolin10 months ago155});
bc00fb04Michael Bolin10 months ago156} finally {
157await cleanupOutputSchema(resolvedOutputSchema);
158}
e9d0a695Michael Bolin10 months ago159}
160
161async function finalizeExecution(outputFile: OutputFile): Promise<void> {
162try {
163const lastMessage = await readFile(outputFile.file, "utf8");
d49a23a3Michael Bolin10 months ago164setOutput("final-message", lastMessage);
e9d0a695Michael Bolin10 months ago165} finally {
166await cleanupTempOutput(outputFile);
167}
168}
169
170type OutputFile =
171| {
172type: "explicit";
173file: string;
174}
175| {
176type: "temp";
177file: string;
178};
179
bc00fb04Michael Bolin10 months ago180type ResolvedOutputSchema =
181| {
182type: "explicit";
183file: string;
184}
185| {
186type: "temp";
187file: string;
188dir: string;
189};
190
e9d0a695Michael Bolin10 months ago191async function createTempOutputFile(): Promise<OutputFile> {
192const dir = await mkdtemp("codex-exec-");
193return { type: "temp", file: path.join(dir, "output.md") };
194}
195
196async function cleanupTempOutput(outputFile: OutputFile): Promise<void> {
197switch (outputFile.type) {
198case "explicit":
199// Do not delete user-specified output files.
200return;
201case "temp": {
202const { file } = outputFile;
203const dir = path.dirname(file);
204await rm(dir, { recursive: true, force: true });
205break;
206}
207}
208}
bc00fb04Michael Bolin10 months ago209
210async function resolveOutputSchema(
211schema: OutputSchemaSource | null
212): Promise<ResolvedOutputSchema | null> {
213if (schema == null) {
214return null;
215}
216
217switch (schema.type) {
218case "file":
219return { type: "explicit", file: schema.path };
220case "inline": {
221const dir = await mkdtemp("codex-output-schema-");
222const file = path.join(dir, "schema.json");
223await writeFile(file, schema.content);
224return { type: "temp", file, dir };
225}
226}
227}
228
229async function cleanupOutputSchema(
230schema: ResolvedOutputSchema | null
231): Promise<void> {
232if (schema == null) {
233return;
234}
235
236switch (schema.type) {
237case "explicit":
238return;
239case "temp":
240await rm(schema.dir, { recursive: true, force: true });
241return;
242}
243}
911c5cb9Michael Bolin10 months ago244
245async function determineSandboxMode({
246safetyStrategy,
247requestedSandbox,
248}: {
249safetyStrategy: SafetyStrategy;
250requestedSandbox: SandboxMode;
251}): Promise<SandboxMode> {
d49a23a3Michael Bolin10 months ago252if (safetyStrategy === "read-only") {
911c5cb9Michael Bolin10 months ago253return "read-only";
254} else {
255return requestedSandbox;
256}
257}