Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ runs:
- name: Ensure Node.js available
# Pin to a commit hash because some repositories require it:
# https://github.com/openai/codex-action/issues/43
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"

Expand Down Expand Up @@ -266,7 +266,7 @@ runs:
--safety-strategy "$SAFETY_STRATEGY"

- name: Enable Linux user namespaces for bubblewrap
if: ${{ runner.os == 'Linux' && runner.environment == 'github-hosted' && (inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '') }}
if: ${{ runner.os == 'Linux' && (inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '') }}
shell: bash
run: |
set -euo pipefail
Expand Down
218 changes: 184 additions & 34 deletions dist/main.js

Large diffs are not rendered by default.

31 changes: 20 additions & 11 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { dropSudo } from "./dropSudo";
import { ensureActorHasWriteAccess } from "./checkActorPermissions";
import parseArgsStringToArgv from "string-argv";
import { parsePort } from "./ports";
import { writeProxyConfig } from "./writeProxyConfig";
import { checkOutput } from "./checkOutput";

Expand Down Expand Up @@ -80,7 +81,7 @@ export async function main() {
"Write the OpenAI Proxy model provider config into CODEX_HOME/config.toml"
)
.requiredOption("--codex-home <DIRECTORY>", "Path to Codex home directory")
.requiredOption("--port <port>", "Proxy server port", parseIntStrict)
.requiredOption("--port <port>", "Proxy server port", parsePort)
.requiredOption(
"--safety-strategy <strategy>",
"Safety strategy to use. One of 'drop-sudo', 'read-only', 'unprivileged-user', or 'unsafe'."
Expand Down Expand Up @@ -307,21 +308,29 @@ export async function main() {
program.parse();
}

function parseIntStrict(value: string): number {
const parsed = parseInt(value, 10);
if (isNaN(parsed)) {
throw new Error(`Invalid integer: ${value}`);
}
return parsed;
}

function parseExtraArgs(value: string): Array<string> {
if (value.length === 0) {
return [];
}

if (value.startsWith("[")) {
return JSON.parse(value);
const trimmed = value.trimStart();
if (trimmed.startsWith("[")) {
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch (error) {
const message =
error instanceof Error ? error.message : "unknown JSON parse error";
throw new Error(`Invalid --extra-args JSON: ${message}`);
}

if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) {
throw new Error(
"Invalid --extra-args JSON: expected a JSON array of strings."
);
}

return parsed;
} else {
return parseArgsStringToArgv(value);
Comment thread
Alanperry1 marked this conversation as resolved.
}
Expand Down
49 changes: 49 additions & 0 deletions src/ports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const MIN_PORT = 1;
const MAX_PORT = 65535;

export function isValidPort(value: unknown): value is number {
return (
typeof value === "number" &&
Number.isInteger(value) &&
value >= MIN_PORT &&
value <= MAX_PORT
);
}

export function parsePort(value: string): number {
const trimmed = value.trim();
if (!/^\d+$/.test(trimmed)) {
throw invalidPortError(value);
}

const port = Number.parseInt(trimmed, 10);
if (!isValidPort(port)) {
throw invalidPortError(value);
}

return port;
}

export function ensureValidPort(value: unknown): number {
if (!isValidPort(value)) {
throw invalidPortError(formatPortValue(value));
}

return value;
}

function invalidPortError(value: string): Error {
return new Error(
`Invalid port: ${value}. Expected an integer between ${MIN_PORT} and ${MAX_PORT}.`
);
}

function formatPortValue(value: unknown): string {
if (typeof value === "string") {
return JSON.stringify(value);
}
if (value === undefined) {
return "undefined";
}
return String(value);
}
28 changes: 23 additions & 5 deletions src/readServerInfo.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
import * as core from "@actions/core";
import * as fs from "fs/promises";
import { ensureValidPort } from "./ports";

/**
* In theory, this is not called until `serverInfoFile` is non-empty, but we
* will poll in the rare case that it was a partial write.
*/
export async function readServerInfo(serverInfoFile: string): Promise<void> {
let seenEnoent = false;

for (let attempt = 0; attempt < 100; attempt++) {
try {
const contents = await fs.readFile(serverInfoFile, { encoding: "utf8" });
const { port } = JSON.parse(contents);
if (typeof port !== "number") {
continue;
}
const parsedPort = ensureValidPort(port);

core.setOutput("port", port.toString());
core.setOutput("port", parsedPort.toString());
return;
} catch (error) {
if (isEnoent(error)) {
seenEnoent = true;
}
console.error(`Error reading server info: ${error}`);
await sleep(100);
}
}

throw Error(`Failed to read server info from ${serverInfoFile}`);
const hint = seenEnoent
? "\nHint: the server info file was never created — check that the" +
" 'openai-api-key' input is set and the Responses API proxy started" +
" successfully."
: "";
throw Error(`Failed to read server info from ${serverInfoFile}${hint}`);
}

function isEnoent(err: unknown): boolean {
return (
err != null &&
typeof err === "object" &&
"code" in err &&
(err as { code: unknown }).code === "ENOENT"
);
}

async function sleep(ms: number): Promise<void> {
Expand Down
38 changes: 34 additions & 4 deletions src/runCodexExec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export async function runCodexExec({
});
});
} finally {
await cleanupOutputSchema(resolvedOutputSchema);
await cleanupOutputSchema(resolvedOutputSchema, runAsUser);
}
}

Expand Down Expand Up @@ -280,14 +280,15 @@ async function resolveOutputSchema(
case "inline": {
const dir = await createTempDir("codex-output-schema-", runAsUser);
const file = path.join(dir, "schema.json");
await writeFile(file, schema.content);
await writeTempFile(file, schema.content, runAsUser);
return { type: "temp", file, dir };
}
}
}

async function cleanupOutputSchema(
schema: ResolvedOutputSchema | null
schema: ResolvedOutputSchema | null,
runAsUser: string | null
): Promise<void> {
if (schema == null) {
return;
Expand All @@ -297,11 +298,40 @@ async function cleanupOutputSchema(
case "explicit":
return;
case "temp":
await rm(schema.dir, { recursive: true, force: true });
if (runAsUser == null) {
await rm(schema.dir, { recursive: true, force: true });
} else {
await checkOutput(["sudo", "rm", "-rf", schema.dir]);
}
return;
}
}

async function writeTempFile(
file: string,
contents: string,
runAsUser: string | null
): Promise<void> {
if (runAsUser == null) {
await writeFile(file, contents);
return;
}

const stagingDir = await mkdtemp(
path.join(os.tmpdir(), "codex-output-schema-staging-")
);
const stagingFile = path.join(stagingDir, path.basename(file));

try {
await writeFile(stagingFile, contents);
await checkOutput(["sudo", "cp", stagingFile, file]);
await checkOutput(["sudo", "chown", runAsUser, file]);
await checkOutput(["sudo", "chmod", "600", file]);
} finally {
await rm(stagingDir, { recursive: true, force: true });
}
}

async function createTempDir(
prefix: string,
runAsUser: string | null
Expand Down
Loading
Loading