openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr56

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/runCodexExec.ts

338lines · 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";
685900c1Michael 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,
189029fdArthur Deygin9 months ago47effort,
e9d0a695Michael Bolin10 months ago48safetyStrategy,
49codexUser,
911c5cb9Michael Bolin10 months ago50sandbox,
e9d0a695Michael Bolin10 months ago51}: {
52prompt: PromptSource;
53codexHome: string | null;
54cd: string;
55extraArgs: Array<string>;
56explicitOutputFile: string | null;
bc00fb04Michael Bolin10 months ago57outputSchema: OutputSchemaSource | null;
7304b0a7Michael Bolin10 months ago58model: string | null;
189029fdArthur Deygin9 months ago59effort: string | null;
e9d0a695Michael Bolin10 months ago60safetyStrategy: SafetyStrategy;
61codexUser: string | null;
911c5cb9Michael Bolin10 months ago62sandbox: SandboxMode;
e9d0a695Michael Bolin10 months ago63}): Promise<void> {
64let input: string;
65switch (prompt.type) {
bc00fb04Michael Bolin10 months ago66case "inline":
e9d0a695Michael Bolin10 months ago67input = prompt.content;
68break;
69case "file":
70input = await readFile(prompt.path, "utf8");
71break;
72}
73
685900c1Michael Bolin10 months ago74const runAsUser: string | null =
75safetyStrategy === "unprivileged-user" ? codexUser : null;
ffac27a1Michael Bolin10 months ago76
e9d0a695Michael Bolin10 months ago77let outputFile: OutputFile;
78if (explicitOutputFile != null) {
79outputFile = { type: "explicit", file: explicitOutputFile };
80} else {
685900c1Michael Bolin10 months ago81outputFile = await createTempOutputFile({ runAsUser });
e9d0a695Michael Bolin10 months ago82}
83
ffac27a1Michael Bolin10 months ago84const resolvedOutputSchema = await resolveOutputSchema(
85outputSchema,
685900c1Michael Bolin10 months ago86runAsUser
ffac27a1Michael Bolin10 months ago87);
911c5cb9Michael Bolin10 months ago88const sandboxMode = await determineSandboxMode({
89safetyStrategy,
90requestedSandbox: sandbox,
91});
bc00fb04Michael Bolin10 months ago92
e9d0a695Michael Bolin10 months ago93const command: Array<string> = [];
94
efba5096Michael Bolin10 months ago95let pathToCodex = "codex";
d49a23a3Michael Bolin10 months ago96if (safetyStrategy === "unprivileged-user") {
e9d0a695Michael Bolin10 months ago97if (codexUser == null) {
98throw new Error(
d49a23a3Michael Bolin10 months ago99"codexUser must be specified when using the 'unprivileged-user' safety strategy."
e9d0a695Michael Bolin10 months ago100);
101}
102
efba5096Michael Bolin10 months ago103if (process.platform === "win32") {
104throw new Error(
105"the 'unprivileged-user' safety strategy is not supported on Windows."
106);
107}
108
109// We are currently running as a privileged user, but `codexUser` will run
110// with a different $PATH variable, so we need to find the full path to
111// `codex`.
685900c1Michael Bolin10 months ago112pathToCodex = (await checkOutput(["which", "codex"])).trim();
113if (!pathToCodex) {
efba5096Michael Bolin10 months ago114throw new Error("could not find 'codex' in PATH");
115}
116
e9d0a695Michael Bolin10 months ago117command.push("sudo", "-u", codexUser, "--");
118}
119
120command.push(
efba5096Michael Bolin10 months ago121pathToCodex,
e9d0a695Michael Bolin10 months ago122"exec",
123"--skip-git-repo-check",
124"--cd",
125cd,
126"--output-last-message",
127outputFile.file
128);
b8896131Michael Bolin10 months ago129
bc00fb04Michael Bolin10 months ago130if (resolvedOutputSchema != null) {
131command.push("--output-schema", resolvedOutputSchema.file);
b8896131Michael Bolin10 months ago132}
133
7304b0a7Michael Bolin10 months ago134if (model != null) {
135command.push("--model", model);
136}
137
189029fdArthur Deygin9 months ago138if (effort != null) {
139// https://github.com/openai/codex/blob/00debb6399eb51c4b9273f0bc012912c42fe6c91/docs/config.md#config
140// https://github.com/openai/codex/blob/00debb6399eb51c4b9273f0bc012912c42fe6c91/docs/config.md#model_reasoning_effort
141command.push("--config", `model_reasoning_effort="${effort}"`);
142}
143
e9d0a695Michael Bolin10 months ago144command.push(...extraArgs);
145
911c5cb9Michael Bolin10 months ago146command.push("--sandbox", sandboxMode);
e9d0a695Michael Bolin10 months ago147
148const env = { ...process.env };
0cb7e270pakrym-oai10 months ago149if (!env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE) {
150env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE = "codex_github_action";
151}
e9d0a695Michael Bolin10 months ago152let extraEnv = "";
153if (codexHome != null) {
154env.CODEX_HOME = codexHome;
155extraEnv = `CODEX_HOME=${codexHome} `;
156}
157
158// Split the `program` from the `args` for `spawn()`.
159const program = command.shift()!;
160console.log(
161`Running: ${extraEnv}${program} ${command
162.map((a) => JSON.stringify(a))
163.join(" ")}`
164);
bc00fb04Michael Bolin10 months ago165try {
166await new Promise((resolve, reject) => {
167const child = spawn(program, command, {
168env,
169stdio: ["pipe", "inherit", "inherit"],
170});
171child.stdin.write(input);
172child.stdin.end();
173
174child.on("error", reject);
175
176child.on("close", async (code) => {
177if (code !== 0) {
178reject(new Error(`${program} exited with code ${code}`));
179return;
180}
181
182try {
685900c1Michael Bolin10 months ago183await finalizeExecution(outputFile, runAsUser);
bc00fb04Michael Bolin10 months ago184resolve(undefined);
185} catch (err) {
186reject(err);
187}
188});
e9d0a695Michael Bolin10 months ago189});
bc00fb04Michael Bolin10 months ago190} finally {
191await cleanupOutputSchema(resolvedOutputSchema);
192}
e9d0a695Michael Bolin10 months ago193}
194
685900c1Michael Bolin10 months ago195async function finalizeExecution(
196outputFile: OutputFile,
197runAsUser: string | null
198): Promise<void> {
e9d0a695Michael Bolin10 months ago199try {
685900c1Michael Bolin10 months ago200let lastMessage: string;
201if (runAsUser == null) {
202lastMessage = await readFile(outputFile.file, "utf8");
203} else {
204lastMessage = await checkOutput([
205"sudo",
206"-u",
207runAsUser,
208"cat",
209outputFile.file,
210]);
211}
d49a23a3Michael Bolin10 months ago212setOutput("final-message", lastMessage);
e9d0a695Michael Bolin10 months ago213} finally {
685900c1Michael Bolin10 months ago214await cleanupTempOutput(outputFile, runAsUser);
e9d0a695Michael Bolin10 months ago215}
216}
217
218type OutputFile =
219| {
220type: "explicit";
221file: string;
222}
223| {
224type: "temp";
225file: string;
226};
227
bc00fb04Michael Bolin10 months ago228type ResolvedOutputSchema =
229| {
230type: "explicit";
231file: string;
232}
233| {
234type: "temp";
235file: string;
236dir: string;
237};
238
ffac27a1Michael Bolin10 months ago239async function createTempOutputFile({
685900c1Michael Bolin10 months ago240runAsUser,
ffac27a1Michael Bolin10 months ago241}: {
685900c1Michael Bolin10 months ago242runAsUser: string | null;
ffac27a1Michael Bolin10 months ago243}): Promise<OutputFile> {
685900c1Michael Bolin10 months ago244const dir = await createTempDir("codex-exec-", runAsUser);
e9d0a695Michael Bolin10 months ago245return { type: "temp", file: path.join(dir, "output.md") };
246}
247
685900c1Michael Bolin10 months ago248async function cleanupTempOutput(
249outputFile: OutputFile,
250runAsUser: string | null
251): Promise<void> {
e9d0a695Michael Bolin10 months ago252switch (outputFile.type) {
253case "explicit":
254// Do not delete user-specified output files.
255return;
256case "temp": {
257const { file } = outputFile;
685900c1Michael Bolin10 months ago258if (runAsUser == null) {
259const dir = path.dirname(file);
260await rm(dir, { recursive: true, force: true });
261} else {
262await checkOutput(["sudo", "rm", "-rf", path.dirname(file)]);
263}
e9d0a695Michael Bolin10 months ago264break;
265}
266}
267}
bc00fb04Michael Bolin10 months ago268
269async function resolveOutputSchema(
ffac27a1Michael Bolin10 months ago270schema: OutputSchemaSource | null,
685900c1Michael Bolin10 months ago271runAsUser: string | null
bc00fb04Michael Bolin10 months ago272): Promise<ResolvedOutputSchema | null> {
273if (schema == null) {
274return null;
275}
276
277switch (schema.type) {
278case "file":
279return { type: "explicit", file: schema.path };
280case "inline": {
685900c1Michael Bolin10 months ago281const dir = await createTempDir("codex-output-schema-", runAsUser);
bc00fb04Michael Bolin10 months ago282const file = path.join(dir, "schema.json");
283await writeFile(file, schema.content);
284return { type: "temp", file, dir };
285}
286}
287}
288
289async function cleanupOutputSchema(
290schema: ResolvedOutputSchema | null
291): Promise<void> {
292if (schema == null) {
293return;
294}
295
296switch (schema.type) {
297case "explicit":
298return;
299case "temp":
300await rm(schema.dir, { recursive: true, force: true });
301return;
302}
303}
911c5cb9Michael Bolin10 months ago304
685900c1Michael Bolin10 months ago305async function createTempDir(
306prefix: string,
307runAsUser: string | null
308): Promise<string> {
309if (runAsUser == null) {
310return await mkdtemp(path.join(os.tmpdir(), prefix));
311} else {
312return (
313await checkOutput([
314"sudo",
315"-u",
316runAsUser,
317"mktemp",
318"-d",
319"-t",
320`${prefix}.XXXXXX`,
321])
322).trim();
ffac27a1Michael Bolin10 months ago323}
324}
325
911c5cb9Michael Bolin10 months ago326async function determineSandboxMode({
327safetyStrategy,
328requestedSandbox,
329}: {
330safetyStrategy: SafetyStrategy;
331requestedSandbox: SandboxMode;
332}): Promise<SandboxMode> {
d49a23a3Michael Bolin10 months ago333if (safetyStrategy === "read-only") {
911c5cb9Michael Bolin10 months ago334return "read-only";
335} else {
336return requestedSandbox;
337}
338}