diff --git a/codex-cli/scripts/run_in_container.sh b/codex-cli/scripts/run_in_container.sh index 01070cf04b1d..607ec297a6c8 100755 --- a/codex-cli/scripts/run_in_container.sh +++ b/codex-cli/scripts/run_in_container.sh @@ -92,4 +92,4 @@ quoted_args="" for arg in "$@"; do quoted_args+=" $(printf '%q' "$arg")" done -docker exec -it "$CONTAINER_NAME" bash -c "cd \"/app$WORK_DIR\" && codex --full-auto ${quoted_args}" +docker exec -it "$CONTAINER_NAME" bash -c "cd \"/app$WORK_DIR\" && codex --sandbox workspace-write --ask-for-approval on-request ${quoted_args}" diff --git a/codex-rs/README.md b/codex-rs/README.md index 31bae56235fc..d219061a350e 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -59,19 +59,22 @@ To test to see what happens when a command is run under the sandbox provided by ``` # macOS -codex sandbox macos [--full-auto] [--log-denials] [COMMAND]... +codex sandbox macos [--log-denials] [COMMAND]... # Linux -codex sandbox linux [--full-auto] [COMMAND]... +codex sandbox linux [COMMAND]... # Windows -codex sandbox windows [--full-auto] [COMMAND]... +codex sandbox windows [COMMAND]... # Legacy aliases -codex debug seatbelt [--full-auto] [--log-denials] [COMMAND]... -codex debug landlock [--full-auto] [COMMAND]... +codex debug seatbelt [--log-denials] [COMMAND]... +codex debug landlock [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: diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index c85da0f5f2d0..a08a81a529ed 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -41,14 +41,12 @@ pub async fn run_command_under_seatbelt( codex_linux_sandbox_exe: Option, ) -> anyhow::Result<()> { let SeatbeltCommand { - full_auto, allow_unix_sockets, log_denials, config_overrides, command, } = command; run_command_under_sandbox( - full_auto, command, config_overrides, codex_linux_sandbox_exe, @@ -72,12 +70,10 @@ pub async fn run_command_under_landlock( codex_linux_sandbox_exe: Option, ) -> anyhow::Result<()> { let LandlockCommand { - full_auto, config_overrides, command, } = command; run_command_under_sandbox( - full_auto, command, config_overrides, codex_linux_sandbox_exe, @@ -93,12 +89,10 @@ pub async fn run_command_under_windows( codex_linux_sandbox_exe: Option, ) -> anyhow::Result<()> { let WindowsCommand { - full_auto, config_overrides, command, } = command; run_command_under_sandbox( - full_auto, command, config_overrides, codex_linux_sandbox_exe, @@ -117,7 +111,6 @@ enum SandboxType { } async fn run_command_under_sandbox( - full_auto: bool, command: Vec, config_overrides: CliConfigOverrides, codex_linux_sandbox_exe: Option, @@ -131,7 +124,6 @@ async fn run_command_under_sandbox( .parse_overrides() .map_err(anyhow::Error::msg)?, codex_linux_sandbox_exe, - full_auto, ) .await?; @@ -406,14 +398,6 @@ async fn run_command_under_windows_session( std::process::exit(exit_code); } -pub fn create_sandbox_mode(full_auto: bool) -> SandboxMode { - if full_auto { - SandboxMode::WorkspaceWrite - } else { - SandboxMode::ReadOnly - } -} - async fn spawn_debug_sandbox_child( program: PathBuf, args: Vec, @@ -583,12 +567,10 @@ mod windows_stdio_bridge { async fn load_debug_sandbox_config( cli_overrides: Vec<(String, TomlValue)>, codex_linux_sandbox_exe: Option, - full_auto: bool, ) -> anyhow::Result { load_debug_sandbox_config_with_codex_home( cli_overrides, codex_linux_sandbox_exe, - full_auto, /*codex_home*/ None, ) .await @@ -597,9 +579,14 @@ async fn load_debug_sandbox_config( async fn load_debug_sandbox_config_with_codex_home( cli_overrides: Vec<(String, TomlValue)>, codex_linux_sandbox_exe: Option, - full_auto: bool, codex_home: Option, ) -> anyhow::Result { + // 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. + let uses_legacy_sandbox_mode_override = cli_overrides_use_legacy_sandbox_mode(&cli_overrides); let config = build_debug_sandbox_config( cli_overrides.clone(), ConfigOverrides { @@ -610,19 +597,14 @@ async fn load_debug_sandbox_config_with_codex_home( ) .await?; - if config_uses_permission_profiles(&config) { - if full_auto { - anyhow::bail!( - "`codex sandbox --full-auto` is only supported for legacy `sandbox_mode` configs; choose a writable `[permissions]` profile instead" - ); - } + if config_uses_permission_profiles(&config) || uses_legacy_sandbox_mode_override { return Ok(config); } build_debug_sandbox_config( cli_overrides, ConfigOverrides { - sandbox_mode: Some(create_sandbox_mode(full_auto)), + sandbox_mode: Some(SandboxMode::ReadOnly), codex_linux_sandbox_exe, ..Default::default() }, @@ -656,9 +638,14 @@ fn config_uses_permission_profiles(config: &Config) -> bool { .is_some() } +fn cli_overrides_use_legacy_sandbox_mode(cli_overrides: &[(String, TomlValue)]) -> bool { + cli_overrides.iter().any(|(key, _)| key == "sandbox_mode") +} + #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; use tempfile::TempDir; fn escape_toml_path(path: &std::path::Path) -> String { @@ -705,7 +692,7 @@ mod tests { let legacy_config = build_debug_sandbox_config( Vec::new(), ConfigOverrides { - sandbox_mode: Some(create_sandbox_mode(/*full_auto*/ false)), + sandbox_mode: Some(SandboxMode::ReadOnly), ..Default::default() }, Some(codex_home_path.clone()), @@ -715,7 +702,6 @@ mod tests { let config = load_debug_sandbox_config_with_codex_home( Vec::new(), /*codex_linux_sandbox_exe*/ None, - /*full_auto*/ false, Some(codex_home_path), ) .await?; @@ -739,25 +725,90 @@ mod tests { } #[tokio::test] - async fn debug_sandbox_rejects_full_auto_for_permission_profiles() -> anyhow::Result<()> { + async fn debug_sandbox_honors_explicit_legacy_sandbox_mode() -> anyhow::Result<()> { let codex_home = TempDir::new()?; - let sandbox_paths = TempDir::new()?; - let docs = sandbox_paths.path().join("docs"); - let private = docs.join("private"); - write_permissions_profile_config(&codex_home, &docs, &private)?; + let codex_home_path = codex_home.path().to_path_buf(); + let cli_overrides = vec![( + "sandbox_mode".to_string(), + TomlValue::String("workspace-write".to_string()), + )]; - let err = load_debug_sandbox_config_with_codex_home( + let workspace_write_config = build_debug_sandbox_config( + cli_overrides.clone(), + ConfigOverrides::default(), + Some(codex_home_path.clone()), + ) + .await?; + let read_only_config = build_debug_sandbox_config( Vec::new(), + ConfigOverrides { + sandbox_mode: Some(SandboxMode::ReadOnly), + ..Default::default() + }, + Some(codex_home_path.clone()), + ) + .await?; + + let config = load_debug_sandbox_config_with_codex_home( + cli_overrides, /*codex_linux_sandbox_exe*/ None, - /*full_auto*/ true, - Some(codex_home.path().to_path_buf()), + Some(codex_home_path), ) - .await - .expect_err("full-auto should be rejected for active permission profiles"); + .await?; - assert!( - err.to_string().contains("--full-auto"), - "unexpected error: {err}" + if cfg!(target_os = "windows") { + assert_eq!( + workspace_write_config + .permissions + .file_system_sandbox_policy(), + read_only_config.permissions.file_system_sandbox_policy(), + "workspace-write downgrades to read-only when the Windows sandbox is disabled" + ); + } else { + assert_ne!( + workspace_write_config + .permissions + .file_system_sandbox_policy(), + read_only_config.permissions.file_system_sandbox_policy(), + "test fixture should distinguish explicit workspace-write from read-only" + ); + } + assert_eq!( + config.permissions.file_system_sandbox_policy(), + workspace_write_config + .permissions + .file_system_sandbox_policy(), + ); + + Ok(()) + } + + #[tokio::test] + async fn debug_sandbox_defaults_legacy_configs_to_read_only() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + let codex_home_path = codex_home.path().to_path_buf(); + + let read_only_config = build_debug_sandbox_config( + Vec::new(), + ConfigOverrides { + sandbox_mode: Some(SandboxMode::ReadOnly), + ..Default::default() + }, + Some(codex_home_path.clone()), + ) + .await?; + + let config = load_debug_sandbox_config_with_codex_home( + Vec::new(), + /*codex_linux_sandbox_exe*/ None, + Some(codex_home_path), + ) + .await?; + + assert!(!config_uses_permission_profiles(&config)); + assert_eq!( + config.permissions.file_system_sandbox_policy(), + read_only_config.permissions.file_system_sandbox_policy(), ); Ok(()) diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index 3f3448c64c3d..1f396854588a 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -21,10 +21,6 @@ pub use login::run_logout; #[derive(Debug, Parser)] pub struct SeatbeltCommand { - /// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR) - #[arg(long = "full-auto", default_value_t = false)] - pub full_auto: bool, - /// Allow the sandboxed command to bind/connect AF_UNIX sockets rooted at this path. Relative paths are resolved against the current directory. Repeat to allow multiple paths. #[arg(long = "allow-unix-socket", value_parser = parse_allow_unix_socket_path)] pub allow_unix_sockets: Vec, @@ -48,10 +44,6 @@ fn parse_allow_unix_socket_path(raw: &str) -> Result { #[derive(Debug, Parser)] pub struct LandlockCommand { - /// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR) - #[arg(long = "full-auto", default_value_t = false)] - pub full_auto: bool, - #[clap(skip)] pub config_overrides: CliConfigOverrides, @@ -62,10 +54,6 @@ pub struct LandlockCommand { #[derive(Debug, Parser)] pub struct WindowsCommand { - /// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR) - #[arg(long = "full-auto", default_value_t = false)] - pub full_auto: bool, - #[clap(skip)] pub config_overrides: CliConfigOverrides, diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 1c2eabf986a3..01e44fd8ca33 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -1352,16 +1352,12 @@ async fn run_debug_prompt_input_command( )); } - let approval_policy = if shared.full_auto { - Some(AskForApproval::OnRequest) - } else if shared.dangerously_bypass_approvals_and_sandbox { + let approval_policy = if shared.dangerously_bypass_approvals_and_sandbox { Some(AskForApproval::Never) } else { interactive.approval_policy.map(Into::into) }; - let sandbox_mode = if shared.full_auto { - Some(codex_protocol::config_types::SandboxMode::WorkspaceWrite) - } else if shared.dangerously_bypass_approvals_and_sandbox { + let sandbox_mode = if shared.dangerously_bypass_approvals_and_sandbox { Some(codex_protocol::config_types::SandboxMode::DangerFullAccess) } else { shared.sandbox_mode.map(Into::into) @@ -1950,6 +1946,35 @@ mod tests { assert!(remove_result.is_err()); } + #[test] + fn full_auto_no_longer_parses_at_top_level() { + let result = MultitoolCli::try_parse_from(["codex", "--full-auto"]); + + assert!(result.is_err()); + } + + #[test] + fn exec_full_auto_reports_migration_path() { + let cli = MultitoolCli::try_parse_from(["codex", "exec", "--full-auto", "summarize"]) + .expect("exec should accept removed flag long enough to report a migration path"); + let Some(Subcommand::Exec(exec)) = cli.subcommand else { + panic!("expected exec subcommand"); + }; + + assert_eq!( + exec.removed_full_auto_warning(), + Some("warning: `--full-auto` is deprecated; use `--sandbox workspace-write` instead.") + ); + } + + #[test] + fn sandbox_full_auto_no_longer_parses() { + let result = + MultitoolCli::try_parse_from(["codex", "sandbox", "linux", "--full-auto", "--"]); + + assert!(result.is_err()); + } + fn sample_exit_info(conversation_id: Option<&str>, thread_name: Option<&str>) -> AppExitInfo { let token_usage = TokenUsage { output_tokens: 2, @@ -2080,14 +2105,13 @@ mod tests { } #[test] - fn resume_merges_option_flags_and_full_auto() { + fn resume_merges_option_flags() { let interactive = finalize_resume_from_args( [ "codex", "resume", "sid", "--oss", - "--full-auto", "--search", "--sandbox", "workspace-write", @@ -2116,7 +2140,6 @@ mod tests { interactive.approval_policy, Some(codex_utils_cli::ApprovalModeCliArg::OnRequest) ); - assert!(interactive.full_auto); assert_eq!( interactive.cwd.as_deref(), Some(std::path::Path::new("/tmp")) diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index cbdf695562a1..2b12898c3cdf 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -35,6 +35,16 @@ pub struct Cli { #[arg(long = "ignore-rules", global = true, default_value_t = false)] pub ignore_rules: bool, + /// Legacy compatibility trap for the removed `--full-auto` flag. + #[arg( + long = "full-auto", + hide = true, + global = true, + default_value_t = false, + conflicts_with = "dangerously_bypass_approvals_and_sandbox" + )] + pub removed_full_auto: bool, + /// Path to a JSON Schema file describing the model's final response shape. #[arg(long = "output-schema", value_name = "FILE")] pub output_schema: Option, @@ -85,6 +95,18 @@ impl std::ops::DerefMut for Cli { } } +impl Cli { + pub fn removed_full_auto_warning(&self) -> Option<&'static str> { + if self.removed_full_auto { + return Some( + "warning: `--full-auto` is deprecated; use `--sandbox workspace-write` instead.", + ); + } + + None + } +} + #[derive(Debug, Default)] pub struct ExecSharedCliOptions(SharedCliOptions); @@ -130,7 +152,6 @@ impl FromArgMatches for ExecSharedCliOptions { fn mark_exec_global_args(cmd: clap::Command) -> clap::Command { cmd.mut_arg("model", |arg| arg.global(true)) - .mut_arg("full_auto", |arg| arg.global(true)) .mut_arg("dangerously_bypass_approvals_and_sandbox", |arg| { arg.global(true) }) diff --git a/codex-rs/exec/src/cli_tests.rs b/codex-rs/exec/src/cli_tests.rs index dfa202884b00..45f2aa330d8a 100644 --- a/codex-rs/exec/src/cli_tests.rs +++ b/codex-rs/exec/src/cli_tests.rs @@ -70,3 +70,13 @@ fn parses_config_isolation_flags() { assert!(cli.ignore_user_config); assert!(cli.ignore_rules); } + +#[test] +fn removed_full_auto_flag_reports_migration_path() { + let cli = Cli::parse_from(["codex-exec", "--full-auto", "summarize"]); + + assert_eq!( + cli.removed_full_auto_warning(), + Some("warning: `--full-auto` is deprecated; use `--sandbox workspace-write` instead.") + ); +} diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index d4044a728d86..fbc2d7e9ff1f 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -216,6 +216,11 @@ fn exec_root_span() -> tracing::Span { } pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { + #[allow(clippy::print_stderr)] + if let Some(message) = cli.removed_full_auto_warning() { + eprintln!("{message}"); + } + if let Err(err) = set_default_originator("codex_exec".to_string()) { tracing::warn!(?err, "Failed to set codex exec originator override {err:?}"); } @@ -227,6 +232,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result ephemeral, ignore_user_config, ignore_rules, + removed_full_auto, color, last_message_file, json: json_mode, @@ -242,7 +248,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result oss_provider, config_profile, sandbox_mode: sandbox_mode_cli_arg, - full_auto, dangerously_bypass_approvals_and_sandbox, cwd, add_dir, @@ -269,7 +274,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result .with_writer(std::io::stderr) .with_filter(env_filter); - let sandbox_mode = if full_auto { + let sandbox_mode = if removed_full_auto { Some(SandboxMode::WorkspaceWrite) } else if dangerously_bypass_approvals_and_sandbox { Some(SandboxMode::DangerFullAccess) diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 977fcc4b6d2d..f1efd61ef540 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -696,12 +696,7 @@ pub async fn run_main( .cwd .clone() .filter(|_| matches!(app_server_target, AppServerTarget::Remote { .. })); - let (sandbox_mode, approval_policy) = if cli.full_auto { - ( - Some(SandboxMode::WorkspaceWrite), - Some(AskForApproval::OnRequest), - ) - } else if cli.dangerously_bypass_approvals_and_sandbox { + let (sandbox_mode, approval_policy) = if cli.dangerously_bypass_approvals_and_sandbox { ( Some(SandboxMode::DangerFullAccess), Some(AskForApproval::Never), diff --git a/codex-rs/utils/cli/src/shared_options.rs b/codex-rs/utils/cli/src/shared_options.rs index b9e47c8d8411..c174a7b5a654 100644 --- a/codex-rs/utils/cli/src/shared_options.rs +++ b/codex-rs/utils/cli/src/shared_options.rs @@ -38,17 +38,12 @@ pub struct SharedCliOptions { #[arg(long = "sandbox", short = 's')] pub sandbox_mode: Option, - /// Convenience alias for low-friction sandboxed automatic execution. - #[arg(long = "full-auto", default_value_t = false)] - pub full_auto: bool, - /// Skip all confirmation prompts and execute commands without sandboxing. /// EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed. #[arg( long = "dangerously-bypass-approvals-and-sandbox", alias = "yolo", - default_value_t = false, - conflicts_with = "full_auto" + default_value_t = false )] pub dangerously_bypass_approvals_and_sandbox: bool, @@ -63,9 +58,8 @@ pub struct SharedCliOptions { impl SharedCliOptions { pub fn inherit_exec_root_options(&mut self, root: &Self) { - let self_selected_sandbox_mode = self.sandbox_mode.is_some() - || self.full_auto - || self.dangerously_bypass_approvals_and_sandbox; + let self_selected_sandbox_mode = + self.sandbox_mode.is_some() || self.dangerously_bypass_approvals_and_sandbox; let Self { images, model, @@ -73,7 +67,6 @@ impl SharedCliOptions { oss_provider, config_profile, sandbox_mode, - full_auto, dangerously_bypass_approvals_and_sandbox, cwd, add_dir, @@ -85,7 +78,6 @@ impl SharedCliOptions { oss_provider: root_oss_provider, config_profile: root_config_profile, sandbox_mode: root_sandbox_mode, - full_auto: root_full_auto, dangerously_bypass_approvals_and_sandbox: root_dangerously_bypass_approvals_and_sandbox, cwd: root_cwd, add_dir: root_add_dir, @@ -107,7 +99,6 @@ impl SharedCliOptions { *sandbox_mode = *root_sandbox_mode; } if !self_selected_sandbox_mode { - *full_auto = *root_full_auto; *dangerously_bypass_approvals_and_sandbox = *root_dangerously_bypass_approvals_and_sandbox; } @@ -128,7 +119,6 @@ impl SharedCliOptions { pub fn apply_subcommand_overrides(&mut self, subcommand: Self) { let subcommand_selected_sandbox_mode = subcommand.sandbox_mode.is_some() - || subcommand.full_auto || subcommand.dangerously_bypass_approvals_and_sandbox; let Self { images, @@ -137,7 +127,6 @@ impl SharedCliOptions { oss_provider, config_profile, sandbox_mode, - full_auto, dangerously_bypass_approvals_and_sandbox, cwd, add_dir, @@ -157,7 +146,6 @@ impl SharedCliOptions { } if subcommand_selected_sandbox_mode { self.sandbox_mode = sandbox_mode; - self.full_auto = full_auto; self.dangerously_bypass_approvals_and_sandbox = dangerously_bypass_approvals_and_sandbox; } diff --git a/codex-rs/windows-sandbox-rs/sandbox_smoketests.py b/codex-rs/windows-sandbox-rs/sandbox_smoketests.py index f629a4c6d9d9..f6689587f5ce 100644 --- a/codex-rs/windows-sandbox-rs/sandbox_smoketests.py +++ b/codex-rs/windows-sandbox-rs/sandbox_smoketests.py @@ -74,11 +74,13 @@ def run_sbx( env.update(ENV_BASE) if env_extra: env.update(env_extra) - # Map policy to codex CLI flags - # read-only => default; workspace-write => --full-auto + # Map policy to codex CLI overrides. + # read-only => default; workspace-write => legacy sandbox_mode override if policy not in ("read-only", "workspace-write"): raise ValueError(f"unknown policy: {policy}") - policy_flags: List[str] = ["--full-auto"] if policy == "workspace-write" else [] + policy_flags: List[str] = ( + ["-c", 'sandbox_mode="workspace-write"'] if policy == "workspace-write" else [] + ) overrides: List[str] = [] if policy == "workspace-write" and additional_root is not None: