openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr20

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

293lines · modeblame

e9d0a695Michael Bolin10 months ago1import { spawn } from "child_process";
c01235fbMichael Bolin10 months ago2import { chmod, mkdtemp, readFile, rm, writeFile } from "fs/promises";
e9d0a695Michael Bolin10 months ago3import path from "path";
c01235fbMichael Bolin10 months ago4import os from "os";
e9d0a695Michael Bolin10 months ago5import { setOutput } from "@actions/core";
efba5096Michael Bolin10 months ago6import { which } from "./which";
e9d0a695Michael Bolin10 months ago7
8export type PromptSource =
9| {
bc00fb04Michael Bolin10 months ago10type: "inline";
e9d0a695Michael Bolin10 months ago11content: string;
12}
13| {
14type: "file";
15path: string;
16};
17
18export type SafetyStrategy =
d49a23a3Michael Bolin10 months ago19| "drop-sudo"
20| "read-only"
21| "unprivileged-user"
e9d0a695Michael Bolin10 months ago22| "unsafe";
23
911c5cb9Michael Bolin10 months ago24export type SandboxMode =
25| "read-only"
26| "workspace-write"
27| "danger-full-access";
28
bc00fb04Michael Bolin10 months ago29export type OutputSchemaSource =
30| {
31type: "file";
32path: string;
33}
34| {
35type: "inline";
36content: string;
37};
38
e9d0a695Michael Bolin10 months ago39export async function runCodexExec({
40prompt,
41codexHome,
42cd,
43extraArgs,
44explicitOutputFile,
bc00fb04Michael Bolin10 months ago45outputSchema,
7304b0a7Michael Bolin10 months ago46model,
e9d0a695Michael Bolin10 months ago47safetyStrategy,
48codexUser,
911c5cb9Michael Bolin10 months ago49sandbox,
e9d0a695Michael Bolin10 months ago50}: {
51prompt: PromptSource;
52codexHome: string | null;
53cd: string;
54extraArgs: Array<string>;
55explicitOutputFile: string | null;
bc00fb04Michael Bolin10 months ago56outputSchema: OutputSchemaSource | null;
7304b0a7Michael Bolin10 months ago57model: string | null;
e9d0a695Michael Bolin10 months ago58safetyStrategy: SafetyStrategy;
59codexUser: string | null;
911c5cb9Michael Bolin10 months ago60sandbox: SandboxMode;
e9d0a695Michael Bolin10 months ago61}): Promise<void> {
62let input: string;
63switch (prompt.type) {
bc00fb04Michael Bolin10 months ago64case "inline":
e9d0a695Michael Bolin10 months ago65input = prompt.content;
66break;
67case "file":
68input = await readFile(prompt.path, "utf8");
69break;
70}
71
c01235fbMichael Bolin10 months ago72const needsSharedTempDir = safetyStrategy === "unprivileged-user";
73
e9d0a695Michael Bolin10 months ago74let outputFile: OutputFile;
75if (explicitOutputFile != null) {
76outputFile = { type: "explicit", file: explicitOutputFile };
77} else {
c01235fbMichael Bolin10 months ago78outputFile = await createTempOutputFile({ shared: needsSharedTempDir });
e9d0a695Michael Bolin10 months ago79}
80
c01235fbMichael Bolin10 months ago81const resolvedOutputSchema = await resolveOutputSchema(
82outputSchema,
83needsSharedTempDir
84);
911c5cb9Michael Bolin10 months ago85const sandboxMode = await determineSandboxMode({
86safetyStrategy,
87requestedSandbox: sandbox,
88});
bc00fb04Michael Bolin10 months ago89
e9d0a695Michael Bolin10 months ago90const command: Array<string> = [];
91
efba5096Michael Bolin10 months ago92let pathToCodex = "codex";
d49a23a3Michael Bolin10 months ago93if (safetyStrategy === "unprivileged-user") {
e9d0a695Michael Bolin10 months ago94if (codexUser == null) {
95throw new Error(
d49a23a3Michael Bolin10 months ago96"codexUser must be specified when using the 'unprivileged-user' safety strategy."
e9d0a695Michael Bolin10 months ago97);
98}
99
efba5096Michael Bolin10 months ago100if (process.platform === "win32") {
101throw new Error(
102"the 'unprivileged-user' safety strategy is not supported on Windows."
103);
104}
105
106// We are currently running as a privileged user, but `codexUser` will run
107// with a different $PATH variable, so we need to find the full path to
108// `codex`.
109const whichResult = await which("codex");
110if (whichResult == null) {
111throw new Error("could not find 'codex' in PATH");
112}
113
114pathToCodex = whichResult;
e9d0a695Michael Bolin10 months ago115command.push("sudo", "-u", codexUser, "--");
116}
117
118command.push(
efba5096Michael Bolin10 months ago119pathToCodex,
e9d0a695Michael Bolin10 months ago120"exec",
121"--skip-git-repo-check",
122"--cd",
123cd,
124"--output-last-message",
125outputFile.file
126);
b8896131Michael Bolin10 months ago127
bc00fb04Michael Bolin10 months ago128if (resolvedOutputSchema != null) {
129command.push("--output-schema", resolvedOutputSchema.file);
b8896131Michael Bolin10 months ago130}
131
7304b0a7Michael Bolin10 months ago132if (model != null) {
133command.push("--model", model);
134}
135
e9d0a695Michael Bolin10 months ago136command.push(...extraArgs);
137
911c5cb9Michael Bolin10 months ago138command.push("--sandbox", sandboxMode);
e9d0a695Michael Bolin10 months ago139
140const env = { ...process.env };
141let extraEnv = "";
142if (codexHome != null) {
143env.CODEX_HOME = codexHome;
144extraEnv = `CODEX_HOME=${codexHome} `;
145}
146
147// Split the `program` from the `args` for `spawn()`.
148const program = command.shift()!;
149console.log(
150`Running: ${extraEnv}${program} ${command
151.map((a) => JSON.stringify(a))
152.join(" ")}`
153);
bc00fb04Michael Bolin10 months ago154try {
155await new Promise((resolve, reject) => {
156const child = spawn(program, command, {
157env,
158stdio: ["pipe", "inherit", "inherit"],
159});
160child.stdin.write(input);
161child.stdin.end();
162
163child.on("error", reject);
164
165child.on("close", async (code) => {
166if (code !== 0) {
167reject(new Error(`${program} exited with code ${code}`));
168return;
169}
170
171try {
172await finalizeExecution(outputFile);
173resolve(undefined);
174} catch (err) {
175reject(err);
176}
177});
e9d0a695Michael Bolin10 months ago178});
bc00fb04Michael Bolin10 months ago179} finally {
180await cleanupOutputSchema(resolvedOutputSchema);
181}
e9d0a695Michael Bolin10 months ago182}
183
184async function finalizeExecution(outputFile: OutputFile): Promise<void> {
185try {
186const lastMessage = await readFile(outputFile.file, "utf8");
d49a23a3Michael Bolin10 months ago187setOutput("final-message", lastMessage);
e9d0a695Michael Bolin10 months ago188} finally {
189await cleanupTempOutput(outputFile);
190}
191}
192
193type OutputFile =
194| {
195type: "explicit";
196file: string;
197}
198| {
199type: "temp";
200file: string;
201};
202
bc00fb04Michael Bolin10 months ago203type ResolvedOutputSchema =
204| {
205type: "explicit";
206file: string;
207}
208| {
209type: "temp";
210file: string;
211dir: string;
212};
213
c01235fbMichael Bolin10 months ago214async function createTempOutputFile({
215shared,
216}: {
217shared: boolean;
218}): Promise<OutputFile> {
219const dir = await createTempDir("codex-exec-", shared);
e9d0a695Michael Bolin10 months ago220return { type: "temp", file: path.join(dir, "output.md") };
221}
222
223async function cleanupTempOutput(outputFile: OutputFile): Promise<void> {
224switch (outputFile.type) {
225case "explicit":
226// Do not delete user-specified output files.
227return;
228case "temp": {
229const { file } = outputFile;
230const dir = path.dirname(file);
231await rm(dir, { recursive: true, force: true });
232break;
233}
234}
235}
bc00fb04Michael Bolin10 months ago236
237async function resolveOutputSchema(
c01235fbMichael Bolin10 months ago238schema: OutputSchemaSource | null,
239sharedTempDir: boolean
bc00fb04Michael Bolin10 months ago240): Promise<ResolvedOutputSchema | null> {
241if (schema == null) {
242return null;
243}
244
245switch (schema.type) {
246case "file":
247return { type: "explicit", file: schema.path };
248case "inline": {
c01235fbMichael Bolin10 months ago249const dir = await createTempDir("codex-output-schema-", sharedTempDir);
bc00fb04Michael Bolin10 months ago250const file = path.join(dir, "schema.json");
251await writeFile(file, schema.content);
252return { type: "temp", file, dir };
253}
254}
255}
256
257async function cleanupOutputSchema(
258schema: ResolvedOutputSchema | null
259): Promise<void> {
260if (schema == null) {
261return;
262}
263
264switch (schema.type) {
265case "explicit":
266return;
267case "temp":
268await rm(schema.dir, { recursive: true, force: true });
269return;
270}
271}
911c5cb9Michael Bolin10 months ago272
c01235fbMichael Bolin10 months ago273async function createTempDir(prefix: string, shared: boolean): Promise<string> {
274const dir = await mkdtemp(path.join(os.tmpdir(), prefix));
275if (shared) {
276await chmod(dir, 0o755);
277}
278return dir;
279}
280
911c5cb9Michael Bolin10 months ago281async function determineSandboxMode({
282safetyStrategy,
283requestedSandbox,
284}: {
285safetyStrategy: SafetyStrategy;
286requestedSandbox: SandboxMode;
287}): Promise<SandboxMode> {
d49a23a3Michael Bolin10 months ago288if (safetyStrategy === "read-only") {
911c5cb9Michael Bolin10 months ago289return "read-only";
290} else {
291return requestedSandbox;
292}
293}