diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 85d68691..b4e4ff0d 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -1,17 +1,19 @@ -import type { CommandConfirmation } from "./agent-loop.js"; -import type { AppConfig } from "../config.js"; -import type { ExecInput } from "./sandbox/interface.js"; import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; +import type { AppConfig } from "../config.js"; +import type { CommandConfirmation } from "./agent-loop.js"; +import type { ExecInput } from "./sandbox/interface.js"; import type { ResponseInputItem } from "openai/resources/responses/responses.mjs"; -import { exec, execApplyPatch } from "./exec.js"; -import { ReviewDecision } from "./review.js"; -import { FullAutoErrorMode } from "../auto-approval-mode.js"; -import { SandboxType } from "./sandbox/interface.js"; import { canAutoApprove } from "../../approvals.js"; import { formatCommandForDisplay } from "../../format-command.js"; +import { FullAutoErrorMode } from "../auto-approval-mode.js"; +import { CODEX_UNSAFE_ALLOW_NO_SANDBOX } from "../config.js"; +import { exec, execApplyPatch } from "./exec.js"; +import { ReviewDecision } from "./review.js"; import { isLoggingEnabled, log } from "../logger/log.js"; +import { SandboxType } from "./sandbox/interface.js"; import { access } from "fs/promises"; +import { execFile } from "node:child_process"; // --------------------------------------------------------------------------- // Session‑level cache of commands that the user has chosen to always approve. @@ -279,10 +281,48 @@ const isInLinux = async (): Promise => { } }; +/** + * Return `true` if the `sandbox-exec` binary can be located. This intentionally does **not** + * spawn the binary – we only care about its presence. + */ +export const isSandboxExecAvailable = (): Promise => + new Promise((res) => + execFile( + "command", + ["-v", "sandbox-exec"], + { signal: AbortSignal.timeout(200) }, + (err) => res(!err), // exit 0 ⇒ found + ), + ); + async function getSandbox(runInSandbox: boolean): Promise { if (runInSandbox) { if (process.platform === "darwin") { - return SandboxType.MACOS_SEATBELT; + // On macOS we rely on the system-provided `sandbox-exec` binary to + // enforce the Seatbelt profile. However, starting with macOS 14 the + // executable may be removed from the default installation or the user + // might be running the CLI on a stripped-down environment (for + // instance, inside certain CI images). Attempting to spawn a missing + // binary makes Node.js throw an *uncaught* `ENOENT` error further down + // the stack which crashes the whole CLI. + + // To provide a graceful degradation path we first check at runtime + // whether `sandbox-exec` can be found **and** is executable. If the + // check fails we fall back to the NONE sandbox while emitting a + // warning so users and maintainers are aware that the additional + // process isolation is not in effect. + + if (await isSandboxExecAvailable()) { + return SandboxType.MACOS_SEATBELT; + } + + if (CODEX_UNSAFE_ALLOW_NO_SANDBOX) { + log( + "WARNING: macOS Seatbelt sandbox requested but 'sandbox-exec' was not found in PATH. " + + "With `CODEX_UNSAFE_ALLOW_NO_SANDBOX` enabled, continuing without sandbox.", + ); + return SandboxType.NONE; + } } else if (await isInLinux()) { return SandboxType.NONE; } else if (process.platform === "win32") { diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 1bf05303..f778764f 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -65,6 +65,10 @@ export let OPENAI_API_KEY = process.env["OPENAI_API_KEY"] || ""; export const OPENAI_ORGANIZATION = process.env["OPENAI_ORGANIZATION"] || ""; export const OPENAI_PROJECT = process.env["OPENAI_PROJECT"] || ""; +export const CODEX_UNSAFE_ALLOW_NO_SANDBOX = Boolean( + process.env["CODEX_UNSAFE_ALLOW_NO_SANDBOX"] || "", +); + export function setApiKey(apiKey: string): void { OPENAI_API_KEY = apiKey; }