mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
feat: add support for --sandbox flag
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
28
codex-rs/common/src/sandbox_mode_cli_arg.rs
Normal file
28
codex-rs/common/src/sandbox_mode_cli_arg.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user