From c0b16cfc6b653fe62a49ddb8982ea879dff87751 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 22 May 2026 10:23:59 -0700 Subject: [PATCH] cli: infer host sandbox backend (#24102) ## Why `codex sandbox` previously required an OS subcommand like `linux`, `macos`, or `windows`, even though the command can only run the sandbox backend available on the current host. That made the CLI imply a cross-OS choice that does not exist. ## What changed - Collapse `codex sandbox ` into `codex sandbox [COMMAND]...` by wiring the `sandbox` parser directly to the host-specific backend args with `cfg`. - Keep the existing backend runners for Seatbelt, Linux sandbox, and Windows restricted token. - Rename the public Windows debug sandbox runner to `run_command_under_windows_sandbox` for clarity. - Update the Rust sandbox docs and related README references to describe host OS selection and avoid pointing readers at legacy `sandbox_mode` config. ## Arg0 compatibility The `codex-linux-sandbox` helper path is still handled before normal CLI parsing. `arg0_dispatch()` checks whether the executable basename is `codex-linux-sandbox` and directly calls `codex_linux_sandbox::run_main()`, so removing the `sandbox linux` parser branch does not affect the arg0 helper flow. ## Verification - `cargo test -p codex-cli` - `cargo test -p codex-arg0` - `just fix -p codex-cli` --- codex-rs/README.md | 22 ++--- codex-rs/cli/src/debug_sandbox.rs | 5 +- codex-rs/cli/src/lib.rs | 6 +- codex-rs/cli/src/main.rs | 138 ++++++++++++------------------ codex-rs/core/README.md | 2 +- codex-rs/linux-sandbox/README.md | 2 +- 6 files changed, 70 insertions(+), 105 deletions(-) diff --git a/codex-rs/README.md b/codex-rs/README.md index d219061a35..e315f7fd9b 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -55,26 +55,17 @@ Use `codex exec --ephemeral ...` to run without persisting session rollout files ### Experimenting with the Codex Sandbox -To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI: +To test to see what happens when a command is run under the sandbox provided by Codex, use the `sandbox` subcommand in Codex CLI: ``` -# macOS -codex sandbox macos [--log-denials] [COMMAND]... +# Uses the sandbox implementation for the current host OS: +# Seatbelt on macOS, the Linux sandbox on Linux, and Windows restricted token on Windows. +codex sandbox [COMMAND]... -# Linux -codex sandbox linux [COMMAND]... - -# Windows -codex sandbox windows [COMMAND]... - -# Legacy aliases -codex debug seatbelt [--log-denials] [COMMAND]... -codex debug landlock [COMMAND]... +# macOS-only diagnostic option +codex sandbox --log-denials [COMMAND]... ``` -To try a writable legacy sandbox mode with these commands, pass an explicit config override such -as `-c 'sandbox_mode="workspace-write"'`. - ### Selecting a sandbox policy via `--sandbox` The Rust CLI exposes a dedicated `--sandbox` (`-s`) flag that lets you pick the sandbox policy **without** having to reach for the generic `-c/--config` option: @@ -90,7 +81,6 @@ codex --sandbox workspace-write codex --sandbox danger-full-access ``` -The same setting can be persisted in `~/.codex/config.toml` via the top-level `sandbox_mode = "MODE"` key, e.g. `sandbox_mode = "workspace-write"`. In `workspace-write`, Codex also includes `~/.codex/memories` in its writable roots so memory maintenance does not require an extra approval. ## Code Organization diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index e83d90e8ff..4833ff2c6d 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -110,7 +110,7 @@ pub async fn run_command_under_landlock( .await } -pub async fn run_command_under_windows( +pub async fn run_command_under_windows_sandbox( command: WindowsCommand, codex_linux_sandbox_exe: Option, ) -> anyhow::Result<()> { @@ -672,8 +672,7 @@ async fn load_debug_sandbox_config_with_codex_home( // For legacy configs, `codex sandbox` historically defaulted to read-only // instead of inheriting ambient `sandbox_mode` settings from user/system // config. Keep that behavior unless this invocation explicitly passes a - // legacy `sandbox_mode` CLI override, which is now the documented writable - // replacement for the removed `--full-auto` flag. + // legacy `sandbox_mode` CLI override for compatibility with older callers. let uses_legacy_sandbox_mode_override = cli_overrides_use_legacy_sandbox_mode(&cli_overrides); let config = build_debug_sandbox_config( cli_overrides.clone(), diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index 5bea8ce78d..d9e1efe980 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; pub use debug_sandbox::run_command_under_landlock; pub use debug_sandbox::run_command_under_seatbelt; -pub use debug_sandbox::run_command_under_windows; +pub use debug_sandbox::run_command_under_windows_sandbox; pub use login::read_access_token_from_stdin; pub use login::read_api_key_from_stdin; pub use login::run_login_status; @@ -20,8 +20,8 @@ pub use login::run_login_with_device_code; pub use login::run_login_with_device_code_fallback_to_browser; pub use login::run_logout; -// TODO: Deduplicate these shared sandbox options if we remove the explicit -// `codex sandbox ` platform subcommands. +// These command structs share common sandbox options, but remain separate +// because each host backend has a slightly different option surface. #[derive(Debug, Parser)] pub struct SeatbeltCommand { /// Named permissions profile to apply from the active configuration stack. diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 929d1b8654..aefc022221 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -10,9 +10,6 @@ use codex_arg0::Arg0DispatchPaths; use codex_arg0::arg0_dispatch_or_else; use codex_chatgpt::apply_command::ApplyCommand; use codex_chatgpt::apply_command::run_apply_command; -use codex_cli::LandlockCommand; -use codex_cli::SeatbeltCommand; -use codex_cli::WindowsCommand; use codex_cli::read_access_token_from_stdin; use codex_cli::read_api_key_from_stdin; use codex_cli::run_login_status; @@ -160,7 +157,7 @@ enum Subcommand { Doctor(DoctorCommand), /// Run commands within a Codex-provided sandbox. - Sandbox(SandboxArgs), + Sandbox(HostSandboxArgs), /// Debugging tools. Debug(DebugCommand), @@ -343,24 +340,25 @@ struct ForkCommand { config_overrides: TuiCli, } +#[cfg(target_os = "macos")] +type HostSandboxArgs = codex_cli::SeatbeltCommand; +#[cfg(target_os = "linux")] +type HostSandboxArgs = codex_cli::LandlockCommand; +#[cfg(target_os = "windows")] +type HostSandboxArgs = codex_cli::WindowsCommand; + +#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] +type HostSandboxArgs = UnsupportedSandboxArgs; + +#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] #[derive(Debug, Parser)] -struct SandboxArgs { - #[command(subcommand)] - cmd: SandboxCommand, -} +struct UnsupportedSandboxArgs { + #[clap(skip)] + pub config_overrides: CliConfigOverrides, -#[derive(Debug, clap::Subcommand)] -enum SandboxCommand { - /// Run a command under Seatbelt (macOS only). - #[clap(visible_alias = "seatbelt")] - Macos(SeatbeltCommand), - - /// Run a command under the Linux sandbox (bubblewrap by default). - #[clap(visible_alias = "landlock")] - Linux(LandlockCommand), - - /// Run a command under Windows restricted token (Windows only). - Windows(WindowsCommand), + /// Full command args to run under the host sandbox. + #[arg(trailing_var_arg = true)] + pub command: Vec, } #[derive(Debug, Parser)] @@ -1238,56 +1236,37 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { codex_cloud_tasks::run_main(cloud_cli, arg0_paths.codex_linux_sandbox_exe.clone()) .await?; } - Some(Subcommand::Sandbox(sandbox_args)) => match sandbox_args.cmd { - SandboxCommand::Macos(mut seatbelt_cli) => { - reject_remote_mode_for_subcommand( - root_remote.as_deref(), - root_remote_auth_token_env.as_deref(), - "sandbox macos", - )?; - prepend_config_flags( - &mut seatbelt_cli.config_overrides, - root_config_overrides.clone(), - ); - codex_cli::run_command_under_seatbelt( - seatbelt_cli, - arg0_paths.codex_linux_sandbox_exe.clone(), - ) - .await?; - } - SandboxCommand::Linux(mut landlock_cli) => { - reject_remote_mode_for_subcommand( - root_remote.as_deref(), - root_remote_auth_token_env.as_deref(), - "sandbox linux", - )?; - prepend_config_flags( - &mut landlock_cli.config_overrides, - root_config_overrides.clone(), - ); - codex_cli::run_command_under_landlock( - landlock_cli, - arg0_paths.codex_linux_sandbox_exe.clone(), - ) - .await?; - } - SandboxCommand::Windows(mut windows_cli) => { - reject_remote_mode_for_subcommand( - root_remote.as_deref(), - root_remote_auth_token_env.as_deref(), - "sandbox windows", - )?; - prepend_config_flags( - &mut windows_cli.config_overrides, - root_config_overrides.clone(), - ); - codex_cli::run_command_under_windows( - windows_cli, - arg0_paths.codex_linux_sandbox_exe.clone(), - ) - .await?; - } - }, + Some(Subcommand::Sandbox(mut sandbox_cli)) => { + reject_remote_mode_for_subcommand( + root_remote.as_deref(), + root_remote_auth_token_env.as_deref(), + "sandbox", + )?; + prepend_config_flags( + &mut sandbox_cli.config_overrides, + root_config_overrides.clone(), + ); + #[cfg(target_os = "macos")] + codex_cli::run_command_under_seatbelt( + sandbox_cli, + arg0_paths.codex_linux_sandbox_exe.clone(), + ) + .await?; + #[cfg(target_os = "linux")] + codex_cli::run_command_under_landlock( + sandbox_cli, + arg0_paths.codex_linux_sandbox_exe.clone(), + ) + .await?; + #[cfg(target_os = "windows")] + codex_cli::run_command_under_windows_sandbox( + sandbox_cli, + arg0_paths.codex_linux_sandbox_exe.clone(), + ) + .await?; + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + anyhow::bail!("`codex sandbox` is not supported on this operating system"); + } Some(Subcommand::Debug(DebugCommand { subcommand })) => match subcommand { DebugSubcommand::Models(cmd) => { reject_remote_mode_for_subcommand( @@ -2500,12 +2479,12 @@ mod tests { assert!(matches!(cli.subcommand, Some(Subcommand::Update))); } + #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] #[test] - fn sandbox_macos_parses_permissions_profile() { + fn sandbox_parses_permissions_profile() { let cli = MultitoolCli::try_parse_from([ "codex", "sandbox", - "macos", "--permissions-profile", ":workspace", "--", @@ -2513,20 +2492,18 @@ mod tests { ]) .expect("parse"); - let Some(Subcommand::Sandbox(SandboxArgs { - cmd: SandboxCommand::Macos(command), - })) = cli.subcommand - else { - panic!("expected sandbox macos command"); + let Some(Subcommand::Sandbox(command)) = cli.subcommand else { + panic!("expected sandbox command"); }; assert_eq!(command.permissions_profile.as_deref(), Some(":workspace")); assert_eq!(command.command, vec!["echo"]); } + #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] #[test] - fn sandbox_macos_rejects_explicit_profile_controls_without_profile() { - let err = MultitoolCli::try_parse_from(["codex", "sandbox", "macos", "-C", "/tmp"]) + fn sandbox_rejects_explicit_profile_controls_without_profile() { + let err = MultitoolCli::try_parse_from(["codex", "sandbox", "-C", "/tmp"]) .expect_err("parse should fail"); assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument); @@ -2579,8 +2556,7 @@ mod tests { #[test] fn sandbox_full_auto_no_longer_parses() { - let result = - MultitoolCli::try_parse_from(["codex", "sandbox", "linux", "--full-auto", "--"]); + let result = MultitoolCli::try_parse_from(["codex", "sandbox", "--full-auto", "--"]); assert!(result.is_err()); } diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 3283ba2c3e..57b9e53f6b 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -22,7 +22,7 @@ Seatbelt also keeps the legacy default preferences read access ### Linux -Expects the binary containing `codex-core` to run the equivalent of `codex sandbox linux` (legacy alias: `codex debug landlock`) when `arg0` is `codex-linux-sandbox`. See the `codex-arg0` crate for details. +Expects the binary containing `codex-core` to run the equivalent of `codex sandbox` when `arg0` is `codex-linux-sandbox`. See the `codex-arg0` crate for details. Legacy `SandboxPolicy` / `sandbox_mode` configs are still supported on Linux. They can continue to use the legacy Landlock path when the split filesystem diff --git a/codex-rs/linux-sandbox/README.md b/codex-rs/linux-sandbox/README.md index 4fc65c7499..07c6796470 100644 --- a/codex-rs/linux-sandbox/README.md +++ b/codex-rs/linux-sandbox/README.md @@ -94,4 +94,4 @@ commands that would enter the bubblewrap path. you can skip this in restrictive container environments with `--no-proc`. **Notes** -- The CLI surface still uses legacy names like `codex debug landlock`. +- The CLI surface is `codex sandbox`; the host OS selects the sandbox backend.