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/main.ts

224lines · modeblame

e9d0a695Michael Bolin10 months ago1import { Command, Option } from "commander";
2import pkg from "../package.json" assert { type: "json" };
3
4import { readServerInfo } from "./readServerInfo";
5import { PromptSource, runCodexExec, SafetyStrategy } from "./runCodexExec";
6import { dropSudo } from "./dropSudo";
7import { ensureActorHasWriteAccess } from "./checkActorPermissions";
8import parseArgsStringToArgv from "string-argv";
9
10export async function main() {
11const program = new Command();
12
13program
151b4352Michael Bolin10 months ago14.name("codex-action")
e9d0a695Michael Bolin10 months ago15.version(pkg.version)
151b4352Michael Bolin10 months ago16.description("Multitool to support openai/codex-action.");
e9d0a695Michael Bolin10 months ago17
18program
19.command("read-server-info")
20.description("Read server info from the responses API proxy")
21.argument("<serverInfoFile>", "Path to the server info file")
22.action(async (serverInfoFile: string) => {
23await readServerInfo(serverInfoFile);
24});
25
26program
27.command("drop-sudo")
28.description("Drops sudo privileges for the configured user.")
29.addOption(new Option("--user <user>", "User to modify").default("runner"))
30.addOption(
31new Option("--group <group>", "Group granting sudo privileges").default(
32"sudo"
33)
34)
35.addOption(new Option("--root-phase", "internal").default(false).hideHelp())
36.action(
37async (options: { user: string; group: string; rootPhase: boolean }) => {
38await dropSudo({
39user: options.user,
40group: options.group,
41rootPhase: options.rootPhase,
42});
43}
44);
45
46program
47.command("run-codex-exec")
48.description("Invokes `codex exec` with the appropriate arguments")
49.requiredOption("--prompt <prompt>", "Prompt to pass to `codex exec`.")
50.requiredOption(
51"--prompt-file <FILE>",
52"File containing the prompt to pass to `codex exec`."
53)
54.requiredOption(
55"--codex-home <DIRECTORY>",
56"Path to the Codex CLI home directory (where config files are stored)."
57)
58.requiredOption("--cd <DIRECTORY>", "Working directory for Codex")
59.requiredOption(
60"--proxy-port <port>",
61"Port of the Responses API Proxy",
62parseIntStrict
63)
64.requiredOption(
65"--extra-args <args>",
66"Additional args to pass through to `codex exec` as JSON array or shell string.",
67parseExtraArgs
68)
69.requiredOption(
70"--output-file <FILE>",
71"Path where the final message from `codex exec` will be written."
72)
b8896131Michael Bolin10 months ago73.requiredOption(
74"--output-schema-file <FILE>",
75"Path to a schema file to pass to `codex exec --output-schema`."
76)
b2426cd1Michael Bolin10 months ago77.requiredOption("--model <model>", "Model the agent should use")
e9d0a695Michael Bolin10 months ago78.requiredOption(
79"--safety-strategy <strategy>",
80"Safety strategy to use. One of 'drop_sudo', 'read_only', 'unprivileged_user', or 'unsafe'."
81)
82.requiredOption(
83"--codex-user <user>",
84"User to run codex exec as when using the 'unprivileged_user' safety strategy."
85)
86.action(
87async (options: {
88prompt: string;
89promptFile: string;
90codexHome: string;
91cd: string;
92proxyPort: number;
93extraArgs: Array<string>;
94outputFile: string;
b8896131Michael Bolin10 months ago95outputSchemaFile: string;
b2426cd1Michael Bolin10 months ago96model: string;
e9d0a695Michael Bolin10 months ago97safetyStrategy: string;
98codexUser: string;
99}) => {
100const {
101prompt,
102promptFile,
103codexHome,
104cd,
105proxyPort,
106extraArgs,
107outputFile,
b8896131Michael Bolin10 months ago108outputSchemaFile,
b2426cd1Michael Bolin10 months ago109model,
e9d0a695Michael Bolin10 months ago110safetyStrategy,
111codexUser,
112} = options;
113
114const normalizedPrompt = emptyAsNull(prompt);
115const normalizedPromptFile = emptyAsNull(promptFile);
116let promptSource: PromptSource;
117if (normalizedPrompt != null) {
118promptSource = { type: "text", content: normalizedPrompt };
119} else if (normalizedPromptFile != null) {
120promptSource = { type: "file", path: normalizedPromptFile };
121} else {
122throw new Error(
123"Either `prompt` or `prompt_file` must be specified."
124);
125}
126
127// Custom option processing to coerces to null does not work with
128// Commander.js's requiredOption, so we have to post-process here.
129await runCodexExec({
130prompt: promptSource,
131codexHome: emptyAsNull(codexHome),
132cd,
133proxyPort,
134extraArgs,
135explicitOutputFile: emptyAsNull(outputFile),
b8896131Michael Bolin10 months ago136outputSchemaFile: emptyAsNull(outputSchemaFile),
b2426cd1Michael Bolin10 months ago137model: emptyAsNull(model),
e9d0a695Michael Bolin10 months ago138safetyStrategy: toSafetyStrategy(safetyStrategy),
139codexUser: emptyAsNull(codexUser),
140});
141}
142);
143
144program
145.command("check-write-access")
146.description(
147"Checks that the triggering actor has write access to the repository"
148)
149.option(
150"--allow-bots <boolean>",
151"Allow GitHub App and bot actors to bypass the write-access check (default: true).",
152parseBoolean,
153true
154)
155.action(async ({ allowBots }: { allowBots: boolean }) => {
156const result = await ensureActorHasWriteAccess({
157allowBotActors: allowBots,
158});
159switch (result.status) {
160case "approved": {
161console.log(`Actor '${result.actor}' is permitted to continue.`);
162break;
163}
164case "rejected": {
165const message = `Actor '${result.actor}' is not permitted to run this action: ${result.reason}`;
166console.error(message);
167throw new Error(message);
168}
169}
170});
171
172program.parse();
173}
174
175function parseIntStrict(value: string): number {
176const parsed = parseInt(value, 10);
177if (isNaN(parsed)) {
178throw new Error(`Invalid integer: ${value}`);
179}
180return parsed;
181}
182
183function parseExtraArgs(value: string): Array<string> {
184if (value.length === 0) {
185return [];
186}
187
188if (value.startsWith("[")) {
189return JSON.parse(value);
190} else {
191return parseArgsStringToArgv(value);
192}
193}
194
195function toSafetyStrategy(value: string): SafetyStrategy {
196switch (value) {
197case "drop_sudo":
198case "read_only":
199case "unprivileged_user":
200case "unsafe":
201return value;
202default:
203throw new Error(
204`Invalid safety strategy: ${value}. Must be one of 'drop_sudo', 'read_only', 'unprivileged_user', or 'unsafe'.`
205);
206}
207}
208
209function emptyAsNull(value: string): string | null {
210return value.trim().length == 0 ? null : value;
211}
212
213function parseBoolean(value: string): boolean {
214const normalized = value.trim().toLowerCase();
215if (["true", "1", "yes", "y"].includes(normalized)) {
216return true;
217}
218if (["false", "0", "no", "n"].includes(normalized)) {
219return false;
220}
221throw new Error(`Invalid boolean value: ${value}`);
222}
223
224main();