openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/main.ts

213lines · 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
14.name("codex-exec-action")
15.version(pkg.version)
16.description("Multitool to support @openai/codex-exec-action.");
17
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)
73.requiredOption(
74"--safety-strategy <strategy>",
75"Safety strategy to use. One of 'drop_sudo', 'read_only', 'unprivileged_user', or 'unsafe'."
76)
77.requiredOption(
78"--codex-user <user>",
79"User to run codex exec as when using the 'unprivileged_user' safety strategy."
80)
81.action(
82async (options: {
83prompt: string;
84promptFile: string;
85codexHome: string;
86cd: string;
87proxyPort: number;
88extraArgs: Array<string>;
89outputFile: string;
90safetyStrategy: string;
91codexUser: string;
92}) => {
93const {
94prompt,
95promptFile,
96codexHome,
97cd,
98proxyPort,
99extraArgs,
100outputFile,
101safetyStrategy,
102codexUser,
103} = options;
104
105const normalizedPrompt = emptyAsNull(prompt);
106const normalizedPromptFile = emptyAsNull(promptFile);
107let promptSource: PromptSource;
108if (normalizedPrompt != null) {
109promptSource = { type: "text", content: normalizedPrompt };
110} else if (normalizedPromptFile != null) {
111promptSource = { type: "file", path: normalizedPromptFile };
112} else {
113throw new Error(
114"Either `prompt` or `prompt_file` must be specified."
115);
116}
117
118// Custom option processing to coerces to null does not work with
119// Commander.js's requiredOption, so we have to post-process here.
120await runCodexExec({
121prompt: promptSource,
122codexHome: emptyAsNull(codexHome),
123cd,
124proxyPort,
125extraArgs,
126explicitOutputFile: emptyAsNull(outputFile),
127safetyStrategy: toSafetyStrategy(safetyStrategy),
128codexUser: emptyAsNull(codexUser),
129});
130}
131);
132
133program
134.command("check-write-access")
135.description(
136"Checks that the triggering actor has write access to the repository"
137)
138.option(
139"--allow-bots <boolean>",
140"Allow GitHub App and bot actors to bypass the write-access check (default: true).",
141parseBoolean,
142true
143)
144.action(async ({ allowBots }: { allowBots: boolean }) => {
145const result = await ensureActorHasWriteAccess({
146allowBotActors: allowBots,
147});
148switch (result.status) {
149case "approved": {
150console.log(`Actor '${result.actor}' is permitted to continue.`);
151break;
152}
153case "rejected": {
154const message = `Actor '${result.actor}' is not permitted to run this action: ${result.reason}`;
155console.error(message);
156throw new Error(message);
157}
158}
159});
160
161program.parse();
162}
163
164function parseIntStrict(value: string): number {
165const parsed = parseInt(value, 10);
166if (isNaN(parsed)) {
167throw new Error(`Invalid integer: ${value}`);
168}
169return parsed;
170}
171
172function parseExtraArgs(value: string): Array<string> {
173if (value.length === 0) {
174return [];
175}
176
177if (value.startsWith("[")) {
178return JSON.parse(value);
179} else {
180return parseArgsStringToArgv(value);
181}
182}
183
184function toSafetyStrategy(value: string): SafetyStrategy {
185switch (value) {
186case "drop_sudo":
187case "read_only":
188case "unprivileged_user":
189case "unsafe":
190return value;
191default:
192throw new Error(
193`Invalid safety strategy: ${value}. Must be one of 'drop_sudo', 'read_only', 'unprivileged_user', or 'unsafe'.`
194);
195}
196}
197
198function emptyAsNull(value: string): string | null {
199return value.trim().length == 0 ? null : value;
200}
201
202function parseBoolean(value: string): boolean {
203const normalized = value.trim().toLowerCase();
204if (["true", "1", "yes", "y"].includes(normalized)) {
205return true;
206}
207if (["false", "0", "no", "n"].includes(normalized)) {
208return false;
209}
210throw new Error(`Invalid boolean value: ${value}`);
211}
212
213main();