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 · modecode

1import { 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() {
11 const program = new Command();
12
13 program
14 .name("codex-action")
15 .version(pkg.version)
16 .description("Multitool to support openai/codex-action.");
17
18 program
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) => {
23 await readServerInfo(serverInfoFile);
24 });
25
26 program
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(
31 new Option("--group <group>", "Group granting sudo privileges").default(
32 "sudo"
33 )
34 )
35 .addOption(new Option("--root-phase", "internal").default(false).hideHelp())
36 .action(
37 async (options: { user: string; group: string; rootPhase: boolean }) => {
38 await dropSudo({
39 user: options.user,
40 group: options.group,
41 rootPhase: options.rootPhase,
42 });
43 }
44 );
45
46 program
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",
62 parseIntStrict
63 )
64 .requiredOption(
65 "--extra-args <args>",
66 "Additional args to pass through to `codex exec` as JSON array or shell string.",
67 parseExtraArgs
68 )
69 .requiredOption(
70 "--output-file <FILE>",
71 "Path where the final message from `codex exec` will be written."
72 )
73 .requiredOption(
74 "--output-schema-file <FILE>",
75 "Path to a schema file to pass to `codex exec --output-schema`."
76 )
77 .requiredOption("--model <model>", "Model the agent should use")
78 .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(
87 async (options: {
88 prompt: string;
89 promptFile: string;
90 codexHome: string;
91 cd: string;
92 proxyPort: number;
93 extraArgs: Array<string>;
94 outputFile: string;
95 outputSchemaFile: string;
96 model: string;
97 safetyStrategy: string;
98 codexUser: string;
99 }) => {
100 const {
101 prompt,
102 promptFile,
103 codexHome,
104 cd,
105 proxyPort,
106 extraArgs,
107 outputFile,
108 outputSchemaFile,
109 model,
110 safetyStrategy,
111 codexUser,
112 } = options;
113
114 const normalizedPrompt = emptyAsNull(prompt);
115 const normalizedPromptFile = emptyAsNull(promptFile);
116 let promptSource: PromptSource;
117 if (normalizedPrompt != null) {
118 promptSource = { type: "text", content: normalizedPrompt };
119 } else if (normalizedPromptFile != null) {
120 promptSource = { type: "file", path: normalizedPromptFile };
121 } else {
122 throw 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.
129 await runCodexExec({
130 prompt: promptSource,
131 codexHome: emptyAsNull(codexHome),
132 cd,
133 proxyPort,
134 extraArgs,
135 explicitOutputFile: emptyAsNull(outputFile),
136 outputSchemaFile: emptyAsNull(outputSchemaFile),
137 model: emptyAsNull(model),
138 safetyStrategy: toSafetyStrategy(safetyStrategy),
139 codexUser: emptyAsNull(codexUser),
140 });
141 }
142 );
143
144 program
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).",
152 parseBoolean,
153 true
154 )
155 .action(async ({ allowBots }: { allowBots: boolean }) => {
156 const result = await ensureActorHasWriteAccess({
157 allowBotActors: allowBots,
158 });
159 switch (result.status) {
160 case "approved": {
161 console.log(`Actor '${result.actor}' is permitted to continue.`);
162 break;
163 }
164 case "rejected": {
165 const message = `Actor '${result.actor}' is not permitted to run this action: ${result.reason}`;
166 console.error(message);
167 throw new Error(message);
168 }
169 }
170 });
171
172 program.parse();
173}
174
175function parseIntStrict(value: string): number {
176 const parsed = parseInt(value, 10);
177 if (isNaN(parsed)) {
178 throw new Error(`Invalid integer: ${value}`);
179 }
180 return parsed;
181}
182
183function parseExtraArgs(value: string): Array<string> {
184 if (value.length === 0) {
185 return [];
186 }
187
188 if (value.startsWith("[")) {
189 return JSON.parse(value);
190 } else {
191 return parseArgsStringToArgv(value);
192 }
193}
194
195function toSafetyStrategy(value: string): SafetyStrategy {
196 switch (value) {
197 case "drop_sudo":
198 case "read_only":
199 case "unprivileged_user":
200 case "unsafe":
201 return value;
202 default:
203 throw 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 {
210 return value.trim().length == 0 ? null : value;
211}
212
213function parseBoolean(value: string): boolean {
214 const normalized = value.trim().toLowerCase();
215 if (["true", "1", "yes", "y"].includes(normalized)) {
216 return true;
217 }
218 if (["false", "0", "no", "n"].includes(normalized)) {
219 return false;
220 }
221 throw new Error(`Invalid boolean value: ${value}`);
222}
223
224main();
225