diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index a21cd4e73e..905b746168 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -3,11 +3,11 @@ use std::path::PathBuf; use codex_common::CliConfigOverrides; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config_types::SandboxMode; use codex_core::exec::StdioPolicy; use codex_core::exec::spawn_command_under_linux_sandbox; use codex_core::exec::spawn_command_under_seatbelt; use codex_core::exec_env::create_env; -use codex_core::protocol::SandboxPolicy; use crate::LandlockCommand; use crate::SeatbeltCommand; @@ -63,14 +63,14 @@ async fn run_command_under_sandbox( codex_linux_sandbox_exe: Option, sandbox_type: SandboxType, ) -> anyhow::Result<()> { - let sandbox_policy = create_sandbox_policy(full_auto); + let sandbox_mode = create_sandbox_mode(full_auto); let cwd = std::env::current_dir()?; let config = Config::load_with_cli_overrides( config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?, ConfigOverrides { - sandbox_policy: Some(sandbox_policy), + sandbox_mode: Some(sandbox_mode), codex_linux_sandbox_exe, ..Default::default() }, @@ -104,10 +104,10 @@ async fn run_command_under_sandbox( handle_exit_status(status); } -pub fn create_sandbox_policy(full_auto: bool) -> SandboxPolicy { +pub fn create_sandbox_mode(full_auto: bool) -> SandboxMode { if full_auto { - SandboxPolicy::new_workspace_write_policy() + SandboxMode::WorkspaceWrite } else { - SandboxPolicy::new_read_only_policy() + SandboxMode::ReadOnly } } diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs index 18ed49e5a7..3d498a8e2c 100644 --- a/codex-rs/common/src/lib.rs +++ b/codex-rs/common/src/lib.rs @@ -7,6 +7,12 @@ pub mod elapsed; #[cfg(feature = "cli")] pub use approval_mode_cli_arg::ApprovalModeCliArg; +#[cfg(feature = "cli")] +mod sandbox_mode_cli_arg; + +#[cfg(feature = "cli")] +pub use sandbox_mode_cli_arg::SandboxModeCliArg; + #[cfg(any(feature = "cli", test))] mod config_override; diff --git a/codex-rs/common/src/sandbox_mode_cli_arg.rs b/codex-rs/common/src/sandbox_mode_cli_arg.rs new file mode 100644 index 0000000000..588637aebb --- /dev/null +++ b/codex-rs/common/src/sandbox_mode_cli_arg.rs @@ -0,0 +1,28 @@ +//! Standard type to use with the `--sandbox` (`-s`) CLI option. +//! +//! This mirrors the variants of [`codex_core::protocol::SandboxPolicy`], but +//! without any of the associated data so it can be expressed as a simple flag +//! on the command-line. Users that need to tweak the advanced options for +//! `workspace-write` can continue to do so via `-c` overrides or their +//! `config.toml`. + +use clap::ValueEnum; +use codex_core::config_types::SandboxMode; + +#[derive(Clone, Copy, Debug, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub enum SandboxModeCliArg { + ReadOnly, + WorkspaceWrite, + DangerFullAccess, +} + +impl From for SandboxMode { + fn from(value: SandboxModeCliArg) -> Self { + match value { + SandboxModeCliArg::ReadOnly => SandboxMode::ReadOnly, + SandboxModeCliArg::WorkspaceWrite => SandboxMode::WorkspaceWrite, + SandboxModeCliArg::DangerFullAccess => SandboxMode::DangerFullAccess, + } + } +} diff --git a/codex-rs/config.md b/codex-rs/config.md index 2eaae76079..54b185b431 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -206,34 +206,37 @@ model_reasoning_summary = "none" # disable reasoning summaries ## sandbox -The `sandbox` configuration determines the _sandbox policy_ that Codex uses to execute untrusted commands. The `mode` determines the "base policy." Currently, only `workspace-write` supports additional configuration options, but this may change in the future. +Codex executes model-generated shell commands inside an OS-level sandbox. -The default policy is `read-only`, which means commands can read any file on disk, but attempts to write a file or access the network will be blocked. +In most cases you can pick the desired behaviour with a single option: ```toml -[sandbox] -mode = "read-only" +# same as `--sandbox read-only` +sandbox = "read-only" ``` -A more relaxed policy is `workspace-write`. When specified, the current working directory for the Codex task will be writable (as well as `$TMPDIR` on macOS). Note that the CLI defaults to using `cwd` where it was spawned, though this can be overridden using `--cwd/-C`. +The default policy is `read-only`, which means commands can read any file on +disk, but attempts to write a file or access the network will be blocked. + +A more relaxed policy is `workspace-write`. When specified, the current working directory for the Codex task will be writable (as well as `$TMPDIR` on macOS). Note that the CLI defaults to using the directory where it was spawned as `cwd`, though this can be overridden using `--cwd/-C`. ```toml -[sandbox] -mode = "workspace-write" +sandbox = "workspace-write" -# By default, only the cwd for the Codex session will be writable (and $TMPDIR on macOS), -# but you can specify additional writable folders in this array. -writable_roots = [ - "/tmp", -] -network_access = false # Like read-only, this also defaults to false and can be omitted. +# Extra settings that only apply when `sandbox = "workspace-write"`. +[sandbox_workspace_write] +# By default, only the cwd for the Codex session will be writable (and $TMPDIR +# on macOS), but you can specify additional writable folders in this array. +writable_roots = ["/tmp"] +# Allow the command being run inside the sandbox to make outbound network +# requests. Disabled by default. +network_access = false ``` To disable sandboxing altogether, specify `danger-full-access` like so: ```toml -[sandbox] -mode = "danger-full-access" +sandbox = "danger-full-access" ``` This is reasonable to use if Codex is running in an environment that provides its own sandboxing (such as a Docker container) such that further sandboxing is unnecessary. diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 18c4ec2366..462bef0b4c 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -3,6 +3,8 @@ use crate::config_types::History; use crate::config_types::McpServerConfig; use crate::config_types::ReasoningEffort; use crate::config_types::ReasoningSummary; +use crate::config_types::SandboxMode; +use crate::config_types::SandboxWorkplaceWrite; use crate::config_types::ShellEnvironmentPolicy; use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::Tui; @@ -253,8 +255,11 @@ pub struct ConfigToml { #[serde(default)] pub shell_environment_policy: ShellEnvironmentPolicyToml, - /// If omitted, Codex defaults to the restrictive `read-only` policy. - pub sandbox: Option, + /// Sandbox mode to use. + pub sandbox: Option, + + /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. + pub sandbox_workspace_write: Option, /// Disable server-side response storage (sends the full conversation /// context with every request). Currently necessary for OpenAI customers @@ -305,13 +310,31 @@ pub struct ConfigToml { pub model_reasoning_summary: Option, } +impl ConfigToml { + /// Derive the effective sandbox policy from the configuration. + fn derive_sandbox_policy(&self, sandbox_mode_override: Option) -> SandboxPolicy { + let sandbox = sandbox_mode_override.or(self.sandbox).unwrap_or_default(); + match sandbox { + SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(), + SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { + Some(s) => SandboxPolicy::WorkspaceWrite { + writable_roots: s.writable_roots.clone(), + network_access: s.network_access, + }, + None => SandboxPolicy::new_workspace_write_policy(), + }, + SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess, + } + } +} + /// Optional overrides for user configuration (e.g., from CLI flags). #[derive(Default, Debug, Clone)] pub struct ConfigOverrides { pub model: Option, pub cwd: Option, pub approval_policy: Option, - pub sandbox_policy: Option, + pub sandbox_mode: Option, pub model_provider: Option, pub config_profile: Option, pub codex_linux_sandbox_exe: Option, @@ -332,16 +355,16 @@ impl Config { model, cwd, approval_policy, - sandbox_policy, + sandbox_mode, model_provider, config_profile: config_profile_key, codex_linux_sandbox_exe, } = overrides; - let config_profile = match config_profile_key.or(cfg.profile) { + let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) { Some(key) => cfg .profiles - .get(&key) + .get(key) .ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::NotFound, @@ -352,10 +375,7 @@ impl Config { None => ConfigProfile::default(), }; - let sandbox_policy = sandbox_policy.unwrap_or_else(|| { - cfg.sandbox - .unwrap_or_else(SandboxPolicy::new_read_only_policy) - }); + let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode); let mut model_providers = built_in_model_providers(); // Merge user-defined providers into the built-in list. @@ -549,30 +569,38 @@ persistence = "none" #[test] fn test_sandbox_config_parsing() { let sandbox_full_access = r#" -[sandbox] -mode = "danger-full-access" +sandbox = "danger-full-access" + +[sandbox_workspace_write] network_access = false # This should be ignored. "#; let sandbox_full_access_cfg = toml::from_str::(sandbox_full_access) .expect("TOML deserialization should succeed"); + let sandbox_mode_override = None; assert_eq!( - Some(SandboxPolicy::DangerFullAccess), - sandbox_full_access_cfg.sandbox + SandboxPolicy::DangerFullAccess, + sandbox_full_access_cfg.derive_sandbox_policy(sandbox_mode_override) ); let sandbox_read_only = r#" -[sandbox] -mode = "read-only" +sandbox = "read-only" + +[sandbox_workspace_write] network_access = true # This should be ignored. "#; let sandbox_read_only_cfg = toml::from_str::(sandbox_read_only) .expect("TOML deserialization should succeed"); - assert_eq!(Some(SandboxPolicy::ReadOnly), sandbox_read_only_cfg.sandbox); + let sandbox_mode_override = None; + assert_eq!( + SandboxPolicy::ReadOnly, + sandbox_read_only_cfg.derive_sandbox_policy(sandbox_mode_override) + ); let sandbox_workspace_write = r#" -[sandbox] -mode = "workspace-write" +sandbox = "workspace-write" + +[sandbox_workspace_write] writable_roots = [ "/tmp", ] @@ -580,12 +608,13 @@ writable_roots = [ let sandbox_workspace_write_cfg = toml::from_str::(sandbox_workspace_write) .expect("TOML deserialization should succeed"); + let sandbox_mode_override = None; assert_eq!( - Some(SandboxPolicy::WorkspaceWrite { + SandboxPolicy::WorkspaceWrite { writable_roots: vec![PathBuf::from("/tmp")], - network_access: false - }), - sandbox_workspace_write_cfg.sandbox + network_access: false, + }, + sandbox_workspace_write_cfg.derive_sandbox_policy(sandbox_mode_override) ); } diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index a7152d1462..83fe613c86 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -4,6 +4,7 @@ // definitions that do not contain business logic. use std::collections::HashMap; +use std::path::PathBuf; use strum_macros::Display; use wildmatch::WildMatchPattern; @@ -90,6 +91,28 @@ pub struct Tui { pub disable_mouse_capture: bool, } +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default)] +#[serde(rename_all = "kebab-case")] +pub enum SandboxMode { + #[serde(rename = "read-only")] + #[default] + ReadOnly, + + #[serde(rename = "workspace-write")] + WorkspaceWrite, + + #[serde(rename = "danger-full-access")] + DangerFullAccess, +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Default)] +pub struct SandboxWorkplaceWrite { + #[serde(default)] + pub writable_roots: Vec, + #[serde(default)] + pub network_access: bool, +} + #[derive(Deserialize, Debug, Clone, PartialEq, Default)] #[serde(rename_all = "kebab-case")] pub enum ShellEnvironmentPolicyInherit { diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index d9d577ebe6..5a0b420f3f 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -14,6 +14,11 @@ pub struct Cli { #[arg(long, short = 'm')] pub model: Option, + /// Select the sandbox policy to use when executing model-generated shell + /// commands. This is a convenience alias for `-c sandbox_mode=`. + #[arg(long = "sandbox", short = 's')] + pub sandbox_mode: Option, + /// Configuration profile from config.toml to specify default options. #[arg(long = "profile", short = 'p')] pub config_profile: Option, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 8603a753d9..44dddd4d0f 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -11,12 +11,12 @@ pub use cli::Cli; use codex_core::codex_wrapper; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; -use codex_core::protocol::SandboxPolicy; use codex_core::protocol::TaskCompleteEvent; use codex_core::util::is_inside_git_repo; use event_processor::EventProcessor; @@ -36,6 +36,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any skip_git_repo_check, color, last_message_file, + sandbox_mode: sandbox_mode_cli_arg, prompt, config_overrides, } = cli; @@ -84,12 +85,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any ), }; - let sandbox_policy = if full_auto { - Some(SandboxPolicy::new_workspace_write_policy()) + let sandbox_mode = if full_auto { + Some(SandboxMode::WorkspaceWrite) } else if dangerously_bypass_approvals_and_sandbox { - Some(SandboxPolicy::DangerFullAccess) + Some(SandboxMode::DangerFullAccess) } else { - None + sandbox_mode_cli_arg.map(Into::::into) }; // Load configuration and determine approval policy @@ -99,7 +100,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // This CLI is intended to be headless and has no affordances for asking // the user for approval. approval_policy: Some(AskForApproval::Never), - sandbox_policy, + sandbox_mode, cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)), model_provider: None, codex_linux_sandbox_exe, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 86541a0b9a..9e6850a6ef 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -115,7 +115,7 @@ impl CodexToolCallParam { approval_policy: approval_policy.map(Into::into), // Note we may want to expose a field on CodexToolCallParam to // facilitate configuring the sandbox policy. - sandbox_policy: None, + sandbox_mode: None, model_provider: None, codex_linux_sandbox_exe, }; diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index cb6bb92318..585ff2e34e 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -21,6 +21,11 @@ pub struct Cli { #[arg(long = "profile", short = 'p')] pub config_profile: Option, + /// Select the sandbox policy to use when executing model-generated shell + /// commands. This is a convenience alias for `-c sandbox_mode=`. + #[arg(long = "sandbox", short = 's')] + pub sandbox_mode: Option, + /// Configure when the model requires human approval before executing a command. #[arg(long = "ask-for-approval", short = 'a')] pub approval_policy: Option, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 317cd57fcb..07ddbc4168 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -5,11 +5,11 @@ use app::App; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config_types::SandboxMode; use codex_core::openai_api_key::OPENAI_API_KEY_ENV_VAR; use codex_core::openai_api_key::get_openai_api_key; use codex_core::openai_api_key::set_openai_api_key; use codex_core::protocol::AskForApproval; -use codex_core::protocol::SandboxPolicy; use codex_core::util::is_inside_git_repo; use codex_login::try_read_openai_api_key; use log_layer::TuiLogLayer; @@ -48,19 +48,21 @@ mod user_approval_widget; pub use cli::Cli; pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> std::io::Result<()> { - let (sandbox_policy, approval_policy) = if cli.full_auto { + let (sandbox_mode, approval_policy) = if cli.full_auto { ( - Some(SandboxPolicy::new_workspace_write_policy()), + Some(SandboxMode::WorkspaceWrite), Some(AskForApproval::OnFailure), ) } else if cli.dangerously_bypass_approvals_and_sandbox { ( - Some(SandboxPolicy::DangerFullAccess), + Some(SandboxMode::DangerFullAccess), Some(AskForApproval::Never), ) } else { - let sandbox_policy = None; - (sandbox_policy, cli.approval_policy.map(Into::into)) + ( + cli.sandbox_mode.map(Into::::into), + cli.approval_policy.map(Into::into), + ) }; let config = { @@ -68,7 +70,7 @@ pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> std::io:: let overrides = ConfigOverrides { model: cli.model.clone(), approval_policy, - sandbox_policy, + sandbox_mode, cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)), model_provider: None, config_profile: cli.config_profile.clone(),