diff --git a/codex-cli/src/utils/agent/exec.ts b/codex-cli/src/utils/agent/exec.ts index 3a0e653de1..79fe63747a 100644 --- a/codex-cli/src/utils/agent/exec.ts +++ b/codex-cli/src/utils/agent/exec.ts @@ -4,6 +4,7 @@ import type { ParseEntry } from "shell-quote"; import { process_patch } from "./apply-patch.js"; import { SandboxType } from "./sandbox/interface.js"; +import { execWithLandlock } from "./sandbox/landlock.js"; import { execWithSeatbelt } from "./sandbox/macos-seatbelt.js"; import { exec as rawExec } from "./sandbox/raw-exec.js"; import { formatCommandForDisplay } from "../../format-command.js"; @@ -42,26 +43,30 @@ export function exec( sandbox: SandboxType, abortSignal?: AbortSignal, ): Promise { - // This is a temporary measure to understand what are the common base commands - // until we start persisting and uploading rollouts - const opts: SpawnOptions = { timeout: timeoutInMillis || DEFAULT_TIMEOUT_MS, ...(requiresShell(cmd) ? { shell: true } : {}), ...(workdir ? { cwd: workdir } : {}), }; - // Merge default writable roots with any user-specified ones. - const writableRoots = [ - process.cwd(), - os.tmpdir(), - ...additionalWritableRoots, - ]; - if (sandbox === SandboxType.MACOS_SEATBELT) { - return execWithSeatbelt(cmd, opts, writableRoots, abortSignal); - } - // SandboxType.NONE (or any other) falls back to the raw exec implementation - return rawExec(cmd, opts, abortSignal); + switch (sandbox) { + case SandboxType.NONE: { + // SandboxType.NONE uses the raw exec implementation. + return rawExec(cmd, opts, abortSignal); + } + case SandboxType.MACOS_SEATBELT: { + // Merge default writable roots with any user-specified ones. + const writableRoots = [ + process.cwd(), + os.tmpdir(), + ...additionalWritableRoots, + ]; + return execWithSeatbelt(cmd, opts, writableRoots, abortSignal); + } + case SandboxType.LINUX_LANDLOCK: { + return execWithLandlock(cmd, opts, additionalWritableRoots, abortSignal); + } + } } export function execApplyPatch( diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index ec0ba617a9..44a5d48f94 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -303,6 +303,11 @@ async function getSandbox(runInSandbox: boolean): Promise { "Sandbox was mandated, but 'sandbox-exec' was not found in PATH!", ); } + } else if (process.platform === "linux") { + // TODO: Need to verify that the Landlock sandbox is working. For example, + // using Landlock in a Linux Docker container from a macOS host may not + // work. + return SandboxType.LINUX_LANDLOCK; } else if (CODEX_UNSAFE_ALLOW_NO_SANDBOX) { // Allow running without a sandbox if the user has explicitly marked the // environment as already being sufficiently locked-down. diff --git a/codex-cli/src/utils/agent/sandbox/landlock.ts b/codex-cli/src/utils/agent/sandbox/landlock.ts new file mode 100644 index 0000000000..78707b72b4 --- /dev/null +++ b/codex-cli/src/utils/agent/sandbox/landlock.ts @@ -0,0 +1,38 @@ +import type { ExecResult } from "./sandbox/interface"; +import type { SpawnOptions } from "child_process"; + +import { exec } from "./raw-exec.js"; + +export function execWithLandlock( + cmd: Array, + opts: SpawnOptions, + userProvidedWritableRoots: ReadonlyArray, + abortSignal?: AbortSignal, +): Promise { + // TODO(mbolin): Find the arch-appropriate sandbox executable. + const sandboxExecutable = "bin/codex-linux-sandbox-arm64"; + + const extraSandboxPermissions = userProvidedWritableRoots.flatMap( + (root: string) => ["--sandbox-permission", `disk-write-folder=${root}`], + ); + const fullCommand = [ + sandboxExecutable, + "--full-auto", + + "--sandbox-permission", + "disk-full-read-access", + + "--sandbox-permission", + "disk-write-cwd", + + "--sandbox-permission", + "disk-write-platform-user-temp-folder", + + ...extraSandboxPermissions, + + "--", + ...cmd, + ]; + + return exec(fullCommand, opts, abortSignal); +}