openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr22

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

328lines · modeblame

e9d0a695Michael Bolin10 months ago1import { spawn } from "child_process";
ffac27a1Michael Bolin10 months ago2import { chmod, mkdtemp, readFile, rm, writeFile } from "fs/promises";
e9d0a695Michael Bolin10 months ago3import path from "path";
ffac27a1Michael Bolin10 months ago4import os from "os";
e9d0a695Michael Bolin10 months ago5import { setOutput } from "@actions/core";
07db7939Michael Bolin10 months ago6import { checkOutput } from "./checkOutput";
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
bcb3128dMichael Bolin10 months ago72const runAsUser: string | null =
73safetyStrategy === "unprivileged-user" ? codexUser : null;
ffac27a1Michael Bolin10 months ago74
e9d0a695Michael Bolin10 months ago75let outputFile: OutputFile;
76if (explicitOutputFile != null) {
77outputFile = { type: "explicit", file: explicitOutputFile };
78} else {
bcb3128dMichael Bolin10 months ago79outputFile = await createTempOutputFile({ runAsUser });
e9d0a695Michael Bolin10 months ago80}
81
ffac27a1Michael Bolin10 months ago82const resolvedOutputSchema = await resolveOutputSchema(
83outputSchema,
bcb3128dMichael Bolin10 months ago84runAsUser
ffac27a1Michael Bolin10 months ago85);
911c5cb9Michael Bolin10 months ago86const sandboxMode = await determineSandboxMode({
87safetyStrategy,
88requestedSandbox: sandbox,
89});
bc00fb04Michael Bolin10 months ago90
e9d0a695Michael Bolin10 months ago91const command: Array<string> = [];
92
efba5096Michael Bolin10 months ago93let pathToCodex = "codex";
d49a23a3Michael Bolin10 months ago94if (safetyStrategy === "unprivileged-user") {
e9d0a695Michael Bolin10 months ago95if (codexUser == null) {
96throw new Error(
d49a23a3Michael Bolin10 months ago97"codexUser must be specified when using the 'unprivileged-user' safety strategy."
e9d0a695Michael Bolin10 months ago98);
99}
100
efba5096Michael Bolin10 months ago101if (process.platform === "win32") {
102throw new Error(
103"the 'unprivileged-user' safety strategy is not supported on Windows."
104);
105}
106
107// We are currently running as a privileged user, but `codexUser` will run
108// with a different $PATH variable, so we need to find the full path to
109// `codex`.
07db7939Michael Bolin10 months ago110pathToCodex = (await checkOutput(["which", "codex"])).trim();
111if (!pathToCodex) {
efba5096Michael Bolin10 months ago112throw new Error("could not find 'codex' in PATH");
113}
114
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 {
bcb3128dMichael Bolin10 months ago172await finalizeExecution(outputFile, runAsUser);
bc00fb04Michael Bolin10 months ago173resolve(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
bcb3128dMichael Bolin10 months ago184async function finalizeExecution(
185outputFile: OutputFile,
186runAsUser: string | null
187): Promise<void> {
e9d0a695Michael Bolin10 months ago188try {
bcb3128dMichael Bolin10 months ago189let lastMessage: string;
190if (runAsUser == null) {
191lastMessage = await readFile(outputFile.file, "utf8");
192setOutput("final-message", lastMessage);
193} else {
194lastMessage = await checkOutput([
195"sudo",
196"-u",
197runAsUser,
198"cat",
199outputFile.file,
200]);
201}
d49a23a3Michael Bolin10 months ago202setOutput("final-message", lastMessage);
e9d0a695Michael Bolin10 months ago203} finally {
bcb3128dMichael Bolin10 months ago204await cleanupTempOutput(outputFile, runAsUser);
e9d0a695Michael Bolin10 months ago205}
206}
207
208type OutputFile =
209| {
210type: "explicit";
211file: string;
212}
213| {
214type: "temp";
215file: string;
216};
217
bc00fb04Michael Bolin10 months ago218type ResolvedOutputSchema =
219| {
220type: "explicit";
221file: string;
222}
223| {
224type: "temp";
225file: string;
226dir: string;
227};
228
ffac27a1Michael Bolin10 months ago229async function createTempOutputFile({
bcb3128dMichael Bolin10 months ago230runAsUser,
ffac27a1Michael Bolin10 months ago231}: {
bcb3128dMichael Bolin10 months ago232runAsUser: string | null;
ffac27a1Michael Bolin10 months ago233}): Promise<OutputFile> {
bcb3128dMichael Bolin10 months ago234const dir = await createTempDir("codex-exec-", runAsUser);
e9d0a695Michael Bolin10 months ago235return { type: "temp", file: path.join(dir, "output.md") };
236}
237
bcb3128dMichael Bolin10 months ago238async function cleanupTempOutput(
239outputFile: OutputFile,
240runAsUser: string | null
241): Promise<void> {
e9d0a695Michael Bolin10 months ago242switch (outputFile.type) {
243case "explicit":
244// Do not delete user-specified output files.
245return;
246case "temp": {
247const { file } = outputFile;
bcb3128dMichael Bolin10 months ago248if (runAsUser == null) {
249const dir = path.dirname(file);
250await rm(dir, { recursive: true, force: true });
251} else {
252await checkOutput(["sudo", "rm", "-rf", path.dirname(file)]);
253}
e9d0a695Michael Bolin10 months ago254break;
255}
256}
257}
bc00fb04Michael Bolin10 months ago258
259async function resolveOutputSchema(
ffac27a1Michael Bolin10 months ago260schema: OutputSchemaSource | null,
bcb3128dMichael Bolin10 months ago261runAsUser: string | null
bc00fb04Michael Bolin10 months ago262): Promise<ResolvedOutputSchema | null> {
263if (schema == null) {
264return null;
265}
266
267switch (schema.type) {
268case "file":
269return { type: "explicit", file: schema.path };
270case "inline": {
bcb3128dMichael Bolin10 months ago271const dir = await createTempDir("codex-output-schema-", runAsUser);
bc00fb04Michael Bolin10 months ago272const file = path.join(dir, "schema.json");
273await writeFile(file, schema.content);
274return { type: "temp", file, dir };
275}
276}
277}
278
279async function cleanupOutputSchema(
280schema: ResolvedOutputSchema | null
281): Promise<void> {
282if (schema == null) {
283return;
284}
285
286switch (schema.type) {
287case "explicit":
288return;
289case "temp":
290await rm(schema.dir, { recursive: true, force: true });
291return;
292}
293}
911c5cb9Michael Bolin10 months ago294
bcb3128dMichael Bolin10 months ago295async function createTempDir(
296prefix: string,
297runAsUser: string | null
298): Promise<string> {
299if (runAsUser == null) {
300return await mkdtemp(path.join(os.tmpdir(), prefix));
301} else {
302return (
303await checkOutput([
304"sudo",
305"-u",
306runAsUser,
307"mktemp",
308"-d",
309"-t",
310`${prefix}.XXXXXX`,
311])
312).trim();
ffac27a1Michael Bolin10 months ago313}
314}
315
911c5cb9Michael Bolin10 months ago316async function determineSandboxMode({
317safetyStrategy,
318requestedSandbox,
319}: {
320safetyStrategy: SafetyStrategy;
321requestedSandbox: SandboxMode;
322}): Promise<SandboxMode> {
d49a23a3Michael Bolin10 months ago323if (safetyStrategy === "read-only") {
911c5cb9Michael Bolin10 months ago324return "read-only";
325} else {
326return requestedSandbox;
327}
328}