feat: add support for --sandbox flag

This commit is contained in:
Michael Bolin
2025-07-07 19:03:44 -07:00
parent 0a44c42533
commit 6d16e22376
11 changed files with 160 additions and 58 deletions

View File

@@ -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<PathBuf>,
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
}
}

View File

@@ -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;

View File

@@ -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<SandboxModeCliArg> for SandboxMode {
fn from(value: SandboxModeCliArg) -> Self {
match value {
SandboxModeCliArg::ReadOnly => SandboxMode::ReadOnly,
SandboxModeCliArg::WorkspaceWrite => SandboxMode::WorkspaceWrite,
SandboxModeCliArg::DangerFullAccess => SandboxMode::DangerFullAccess,
}
}
}

View File

@@ -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.

View File

@@ -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<SandboxPolicy>,
/// Sandbox mode to use.
pub sandbox: Option<SandboxMode>,
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
pub sandbox_workspace_write: Option<SandboxWorkplaceWrite>,
/// 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<ReasoningSummary>,
}
impl ConfigToml {
/// Derive the effective sandbox policy from the configuration.
fn derive_sandbox_policy(&self, sandbox_mode_override: Option<SandboxMode>) -> 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<String>,
pub cwd: Option<PathBuf>,
pub approval_policy: Option<AskForApproval>,
pub sandbox_policy: Option<SandboxPolicy>,
pub sandbox_mode: Option<SandboxMode>,
pub model_provider: Option<String>,
pub config_profile: Option<String>,
pub codex_linux_sandbox_exe: Option<PathBuf>,
@@ -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::<ConfigToml>(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::<ConfigToml>(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::<ConfigToml>(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)
);
}

View File

@@ -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<PathBuf>,
#[serde(default)]
pub network_access: bool,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum ShellEnvironmentPolicyInherit {

View File

@@ -14,6 +14,11 @@ pub struct Cli {
#[arg(long, short = 'm')]
pub model: Option<String>,
/// Select the sandbox policy to use when executing model-generated shell
/// commands. This is a convenience alias for `-c sandbox_mode=<mode>`.
#[arg(long = "sandbox", short = 's')]
pub sandbox_mode: Option<codex_common::SandboxModeCliArg>,
/// Configuration profile from config.toml to specify default options.
#[arg(long = "profile", short = 'p')]
pub config_profile: Option<String>,

View File

@@ -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<PathBuf>) -> 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<PathBuf>) -> 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::<SandboxMode>::into)
};
// Load configuration and determine approval policy
@@ -99,7 +100,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> 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,

View File

@@ -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,
};

View File

@@ -21,6 +21,11 @@ pub struct Cli {
#[arg(long = "profile", short = 'p')]
pub config_profile: Option<String>,
/// Select the sandbox policy to use when executing model-generated shell
/// commands. This is a convenience alias for `-c sandbox_mode=<mode>`.
#[arg(long = "sandbox", short = 's')]
pub sandbox_mode: Option<codex_common::SandboxModeCliArg>,
/// Configure when the model requires human approval before executing a command.
#[arg(long = "ask-for-approval", short = 'a')]
pub approval_policy: Option<ApprovalModeCliArg>,

View File

@@ -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<PathBuf>) -> 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::<SandboxMode>::into),
cli.approval_policy.map(Into::into),
)
};
let config = {
@@ -68,7 +70,7 @@ pub fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> 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(),