diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index fdb8a52af..d022adbc7 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -3,6 +3,7 @@ use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; use codex_network_proxy::NetworkProxy; +use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use std::collections::HashMap; use std::path::Path; @@ -14,9 +15,9 @@ use tokio::process::Child; /// isolation plus seccomp for network restrictions. /// /// Unlike macOS Seatbelt where we directly embed the policy text, the Linux -/// helper accepts a list of `--sandbox-permission`/`-s` flags mirroring the -/// public CLI. We convert the internal [`SandboxPolicy`] representation into -/// the equivalent CLI options. +/// helper is a separate executable. We pass the legacy [`SandboxPolicy`] plus +/// split filesystem/network policies as JSON so the helper can migrate +/// incrementally without breaking older call sites. #[allow(clippy::too_many_arguments)] pub async fn spawn_command_under_linux_sandbox

( codex_linux_sandbox_exe: P, @@ -32,9 +33,13 @@ pub async fn spawn_command_under_linux_sandbox

( where P: AsRef, { - let args = create_linux_sandbox_command_args( + let file_system_sandbox_policy = FileSystemSandboxPolicy::from(sandbox_policy); + let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy); + let args = create_linux_sandbox_command_args_for_policies( command, sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, sandbox_policy_cwd, use_bwrap_sandbox, allow_network_for_proxy(false), @@ -45,7 +50,7 @@ where args, arg0, cwd: command_cwd, - network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy), + network_sandbox_policy, network, stdio_policy, env, @@ -60,32 +65,43 @@ pub(crate) fn allow_network_for_proxy(enforce_managed_network: bool) -> bool { enforce_managed_network } -/// Converts the sandbox policy into the CLI invocation for `codex-linux-sandbox`. +/// Converts the sandbox policies into the CLI invocation for +/// `codex-linux-sandbox`. /// /// The helper performs the actual sandboxing (bubblewrap + seccomp) after -/// parsing these arguments. See `docs/linux_sandbox.md` for the Linux semantics. -pub(crate) fn create_linux_sandbox_command_args( +/// parsing these arguments. Policy JSON flags are emitted before helper feature +/// flags so the argv order matches the helper's CLI shape. See +/// `docs/linux_sandbox.md` for the Linux semantics. +#[allow(clippy::too_many_arguments)] +pub(crate) fn create_linux_sandbox_command_args_for_policies( command: Vec, sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, sandbox_policy_cwd: &Path, use_bwrap_sandbox: bool, allow_network_for_proxy: bool, ) -> Vec { - #[expect(clippy::expect_used)] + let sandbox_policy_json = serde_json::to_string(sandbox_policy) + .unwrap_or_else(|err| panic!("failed to serialize sandbox policy: {err}")); + let file_system_policy_json = serde_json::to_string(file_system_sandbox_policy) + .unwrap_or_else(|err| panic!("failed to serialize filesystem sandbox policy: {err}")); + let network_policy_json = serde_json::to_string(&network_sandbox_policy) + .unwrap_or_else(|err| panic!("failed to serialize network sandbox policy: {err}")); let sandbox_policy_cwd = sandbox_policy_cwd .to_str() - .expect("cwd must be valid UTF-8") + .unwrap_or_else(|| panic!("cwd must be valid UTF-8")) .to_string(); - #[expect(clippy::expect_used)] - let sandbox_policy_json = - serde_json::to_string(sandbox_policy).expect("Failed to serialize SandboxPolicy to JSON"); - let mut linux_cmd: Vec = vec![ "--sandbox-policy-cwd".to_string(), sandbox_policy_cwd, "--sandbox-policy".to_string(), sandbox_policy_json, + "--file-system-sandbox-policy".to_string(), + file_system_policy_json, + "--network-sandbox-policy".to_string(), + network_policy_json, ]; if use_bwrap_sandbox { linux_cmd.push("--use-bwrap-sandbox".to_string()); @@ -93,6 +109,32 @@ pub(crate) fn create_linux_sandbox_command_args( if allow_network_for_proxy { linux_cmd.push("--allow-network-for-proxy".to_string()); } + linux_cmd.push("--".to_string()); + linux_cmd.extend(command); + linux_cmd +} + +/// Converts the sandbox cwd and execution options into the CLI invocation for +/// `codex-linux-sandbox`. +#[cfg(test)] +pub(crate) fn create_linux_sandbox_command_args( + command: Vec, + sandbox_policy_cwd: &Path, + use_bwrap_sandbox: bool, + allow_network_for_proxy: bool, +) -> Vec { + let sandbox_policy_cwd = sandbox_policy_cwd + .to_str() + .unwrap_or_else(|| panic!("cwd must be valid UTF-8")) + .to_string(); + + let mut linux_cmd: Vec = vec!["--sandbox-policy-cwd".to_string(), sandbox_policy_cwd]; + if use_bwrap_sandbox { + linux_cmd.push("--use-bwrap-sandbox".to_string()); + } + if allow_network_for_proxy { + linux_cmd.push("--allow-network-for-proxy".to_string()); + } // Separator so that command arguments starting with `-` are not parsed as // options of the helper itself. @@ -113,16 +155,14 @@ mod tests { fn bwrap_flags_are_feature_gated() { let command = vec!["/bin/true".to_string()]; let cwd = Path::new("/tmp"); - let policy = SandboxPolicy::new_read_only_policy(); - let with_bwrap = - create_linux_sandbox_command_args(command.clone(), &policy, cwd, true, false); + let with_bwrap = create_linux_sandbox_command_args(command.clone(), cwd, true, false); assert_eq!( with_bwrap.contains(&"--use-bwrap-sandbox".to_string()), true ); - let without_bwrap = create_linux_sandbox_command_args(command, &policy, cwd, false, false); + let without_bwrap = create_linux_sandbox_command_args(command, cwd, false, false); assert_eq!( without_bwrap.contains(&"--use-bwrap-sandbox".to_string()), false @@ -133,15 +173,46 @@ mod tests { fn proxy_flag_is_included_when_requested() { let command = vec!["/bin/true".to_string()]; let cwd = Path::new("/tmp"); - let policy = SandboxPolicy::new_read_only_policy(); - let args = create_linux_sandbox_command_args(command, &policy, cwd, true, true); + let args = create_linux_sandbox_command_args(command, cwd, true, true); assert_eq!( args.contains(&"--allow-network-for-proxy".to_string()), true ); } + #[test] + fn split_policy_flags_are_included() { + let command = vec!["/bin/true".to_string()]; + let cwd = Path::new("/tmp"); + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy); + let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); + + let args = create_linux_sandbox_command_args_for_policies( + command, + &sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, + cwd, + true, + false, + ); + + assert_eq!( + args.windows(2).any(|window| { + window[0] == "--file-system-sandbox-policy" && !window[1].is_empty() + }), + true + ); + assert_eq!( + args.windows(2) + .any(|window| window[0] == "--network-sandbox-policy" + && window[1] == "\"restricted\""), + true + ); + } + #[test] fn proxy_network_requires_managed_requirements() { assert_eq!(allow_network_for_proxy(false), false); diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 49ecef923..69ab438e1 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -14,7 +14,7 @@ use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::execute_exec_request; use crate::landlock::allow_network_for_proxy; -use crate::landlock::create_linux_sandbox_command_args; +use crate::landlock::create_linux_sandbox_command_args_for_policies; use crate::protocol::SandboxPolicy; #[cfg(target_os = "macos")] use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; @@ -516,9 +516,11 @@ impl SandboxManager { let exe = codex_linux_sandbox_exe .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; let allow_proxy_network = allow_network_for_proxy(enforce_managed_network); - let mut args = create_linux_sandbox_command_args( + let mut args = create_linux_sandbox_command_args_for_policies( command.clone(), &effective_policy, + &effective_file_system_policy, + effective_network_policy, sandbox_policy_cwd, use_linux_sandbox_bwrap, allow_proxy_network, diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs index f9477a126..307f956a4 100644 --- a/codex-rs/linux-sandbox/src/landlock.rs +++ b/codex-rs/linux-sandbox/src/landlock.rs @@ -8,6 +8,7 @@ use std::path::Path; use codex_core::error::CodexErr; use codex_core::error::Result; use codex_core::error::SandboxErr; +use codex_protocol::protocol::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; @@ -40,13 +41,14 @@ use seccompiler::apply_filter; /// Filesystem restrictions are intentionally handled by bubblewrap. pub(crate) fn apply_sandbox_policy_to_current_thread( sandbox_policy: &SandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, cwd: &Path, apply_landlock_fs: bool, allow_network_for_proxy: bool, proxy_routed_network: bool, ) -> Result<()> { let network_seccomp_mode = network_seccomp_mode( - sandbox_policy, + network_sandbox_policy, allow_network_for_proxy, proxy_routed_network, ); @@ -91,20 +93,20 @@ enum NetworkSeccompMode { } fn should_install_network_seccomp( - sandbox_policy: &SandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, allow_network_for_proxy: bool, ) -> bool { // Managed-network sessions should remain fail-closed even for policies that // would normally grant full network access (for example, DangerFullAccess). - !sandbox_policy.has_full_network_access() || allow_network_for_proxy + !network_sandbox_policy.is_enabled() || allow_network_for_proxy } fn network_seccomp_mode( - sandbox_policy: &SandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, allow_network_for_proxy: bool, proxy_routed_network: bool, ) -> Option { - if !should_install_network_seccomp(sandbox_policy, allow_network_for_proxy) { + if !should_install_network_seccomp(network_sandbox_policy, allow_network_for_proxy) { None } else if proxy_routed_network { Some(NetworkSeccompMode::ProxyRouted) @@ -266,13 +268,13 @@ mod tests { use super::NetworkSeccompMode; use super::network_seccomp_mode; use super::should_install_network_seccomp; - use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::protocol::NetworkSandboxPolicy; use pretty_assertions::assert_eq; #[test] fn managed_network_enforces_seccomp_even_for_full_network_policy() { assert_eq!( - should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, true), + should_install_network_seccomp(NetworkSandboxPolicy::Enabled, true), true ); } @@ -280,7 +282,7 @@ mod tests { #[test] fn full_network_policy_without_managed_network_skips_seccomp() { assert_eq!( - should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, false), + should_install_network_seccomp(NetworkSandboxPolicy::Enabled, false), false ); } @@ -288,11 +290,11 @@ mod tests { #[test] fn restricted_network_policy_always_installs_seccomp() { assert!(should_install_network_seccomp( - &SandboxPolicy::new_read_only_policy(), + NetworkSandboxPolicy::Restricted, false )); assert!(should_install_network_seccomp( - &SandboxPolicy::new_read_only_policy(), + NetworkSandboxPolicy::Restricted, true )); } @@ -300,7 +302,7 @@ mod tests { #[test] fn managed_proxy_routes_use_proxy_routed_seccomp_mode() { assert_eq!( - network_seccomp_mode(&SandboxPolicy::DangerFullAccess, true, true), + network_seccomp_mode(NetworkSandboxPolicy::Enabled, true, true), Some(NetworkSeccompMode::ProxyRouted) ); } @@ -308,7 +310,7 @@ mod tests { #[test] fn restricted_network_without_proxy_routing_uses_restricted_mode() { assert_eq!( - network_seccomp_mode(&SandboxPolicy::new_read_only_policy(), false, false), + network_seccomp_mode(NetworkSandboxPolicy::Restricted, false, false), Some(NetworkSeccompMode::Restricted) ); } @@ -316,7 +318,7 @@ mod tests { #[test] fn full_network_without_managed_proxy_skips_network_seccomp_mode() { assert_eq!( - network_seccomp_mode(&SandboxPolicy::DangerFullAccess, false, false), + network_seccomp_mode(NetworkSandboxPolicy::Enabled, false, false), None ); } diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index c9fdd06a3..dad4a6d3a 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -14,6 +14,9 @@ use crate::proxy_routing::activate_proxy_routes_in_netns; use crate::proxy_routing::prepare_host_proxy_route_spec; use crate::vendored_bwrap::exec_vendored_bwrap; use crate::vendored_bwrap::run_vendored_bwrap_main; +use codex_protocol::protocol::FileSystemSandboxPolicy; +use codex_protocol::protocol::NetworkSandboxPolicy; +use codex_protocol::protocol::SandboxPolicy; #[derive(Debug, Parser)] /// CLI surface for the Linux sandbox helper. @@ -26,8 +29,18 @@ pub struct LandlockCommand { #[arg(long = "sandbox-policy-cwd")] pub sandbox_policy_cwd: PathBuf, - #[arg(long = "sandbox-policy")] - pub sandbox_policy: codex_protocol::protocol::SandboxPolicy, + /// Legacy compatibility policy. + /// + /// Newer callers pass split filesystem/network policies as well so the + /// helper can migrate incrementally without breaking older invocations. + #[arg(long = "sandbox-policy", hide = true)] + pub sandbox_policy: Option, + + #[arg(long = "file-system-sandbox-policy", hide = true)] + pub file_system_sandbox_policy: Option, + + #[arg(long = "network-sandbox-policy", hide = true)] + pub network_sandbox_policy: Option, /// Opt-in: use the bubblewrap-based Linux sandbox pipeline. /// @@ -77,6 +90,8 @@ pub fn run_main() -> ! { let LandlockCommand { sandbox_policy_cwd, sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, use_bwrap_sandbox, apply_seccomp_then_exec, allow_network_for_proxy, @@ -89,6 +104,16 @@ pub fn run_main() -> ! { panic!("No command specified to execute."); } ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_bwrap_sandbox); + let EffectiveSandboxPolicies { + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + } = resolve_sandbox_policies( + sandbox_policy_cwd.as_path(), + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + ); // Inner stage: apply seccomp/no_new_privs after bubblewrap has already // established the filesystem view. @@ -104,6 +129,7 @@ pub fn run_main() -> ! { let proxy_routing_active = allow_network_for_proxy; if let Err(e) = apply_sandbox_policy_to_current_thread( &sandbox_policy, + network_sandbox_policy, &sandbox_policy_cwd, false, allow_network_for_proxy, @@ -114,9 +140,10 @@ pub fn run_main() -> ! { exec_or_panic(command); } - if sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy { + if file_system_sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy { if let Err(e) = apply_sandbox_policy_to_current_thread( &sandbox_policy, + network_sandbox_policy, &sandbox_policy_cwd, false, allow_network_for_proxy, @@ -139,17 +166,20 @@ pub fn run_main() -> ! { } else { None }; - let inner = build_inner_seccomp_command( - &sandbox_policy_cwd, - &sandbox_policy, + let inner = build_inner_seccomp_command(InnerSeccompCommandArgs { + sandbox_policy_cwd: &sandbox_policy_cwd, + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, + network_sandbox_policy, use_bwrap_sandbox, allow_network_for_proxy, proxy_route_spec, command, - ); + }); run_bwrap_with_proc_fallback( &sandbox_policy_cwd, &sandbox_policy, + network_sandbox_policy, inner, !no_proc, allow_network_for_proxy, @@ -159,6 +189,7 @@ pub fn run_main() -> ! { // Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled. if let Err(e) = apply_sandbox_policy_to_current_thread( &sandbox_policy, + network_sandbox_policy, &sandbox_policy_cwd, true, allow_network_for_proxy, @@ -169,6 +200,59 @@ pub fn run_main() -> ! { exec_or_panic(command); } +#[derive(Debug, Clone)] +struct EffectiveSandboxPolicies { + sandbox_policy: SandboxPolicy, + file_system_sandbox_policy: FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, +} + +fn resolve_sandbox_policies( + sandbox_policy_cwd: &Path, + sandbox_policy: Option, + file_system_sandbox_policy: Option, + network_sandbox_policy: Option, +) -> EffectiveSandboxPolicies { + // Accept either a fully legacy policy, a fully split policy pair, or all + // three views together. Reject partial split-policy input so the helper + // never runs with mismatched filesystem/network state. + let split_policies = match (file_system_sandbox_policy, network_sandbox_policy) { + (Some(file_system_sandbox_policy), Some(network_sandbox_policy)) => { + Some((file_system_sandbox_policy, network_sandbox_policy)) + } + (None, None) => None, + _ => panic!("file-system and network sandbox policies must be provided together"), + }; + + match (sandbox_policy, split_policies) { + (Some(sandbox_policy), Some((file_system_sandbox_policy, network_sandbox_policy))) => { + EffectiveSandboxPolicies { + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + } + } + (Some(sandbox_policy), None) => EffectiveSandboxPolicies { + file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), + sandbox_policy, + }, + (None, Some((file_system_sandbox_policy, network_sandbox_policy))) => { + let sandbox_policy = file_system_sandbox_policy + .to_legacy_sandbox_policy(network_sandbox_policy, sandbox_policy_cwd) + .unwrap_or_else(|err| { + panic!("failed to derive legacy sandbox policy from split policies: {err}") + }); + EffectiveSandboxPolicies { + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + } + } + (None, None) => panic!("missing sandbox policy configuration"), + } +} + fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_sandbox: bool) { if apply_seccomp_then_exec && !use_bwrap_sandbox { panic!("--apply-seccomp-then-exec requires --use-bwrap-sandbox"); @@ -177,12 +261,13 @@ fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_san fn run_bwrap_with_proc_fallback( sandbox_policy_cwd: &Path, - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, + sandbox_policy: &SandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, inner: Vec, mount_proc: bool, allow_network_for_proxy: bool, ) -> ! { - let network_mode = bwrap_network_mode(sandbox_policy, allow_network_for_proxy); + let network_mode = bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy); let mut mount_proc = mount_proc; if mount_proc && !preflight_proc_mount_support(sandbox_policy_cwd, sandbox_policy, network_mode) @@ -200,12 +285,12 @@ fn run_bwrap_with_proc_fallback( } fn bwrap_network_mode( - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, allow_network_for_proxy: bool, ) -> BwrapNetworkMode { if allow_network_for_proxy { BwrapNetworkMode::ProxyOnly - } else if sandbox_policy.has_full_network_access() { + } else if network_sandbox_policy.is_enabled() { BwrapNetworkMode::FullAccess } else { BwrapNetworkMode::Isolated @@ -214,7 +299,7 @@ fn bwrap_network_mode( fn build_bwrap_argv( inner: Vec, - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, + sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, options: BwrapOptions, ) -> Vec { @@ -237,7 +322,7 @@ fn build_bwrap_argv( fn preflight_proc_mount_support( sandbox_policy_cwd: &Path, - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, + sandbox_policy: &SandboxPolicy, network_mode: BwrapNetworkMode, ) -> bool { let preflight_argv = @@ -248,7 +333,7 @@ fn preflight_proc_mount_support( fn build_preflight_bwrap_argv( sandbox_policy_cwd: &Path, - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, + sandbox_policy: &SandboxPolicy, network_mode: BwrapNetworkMode, ) -> Vec { let preflight_command = vec![resolve_true_command()]; @@ -358,15 +443,29 @@ fn is_proc_mount_failure(stderr: &str) -> bool { || stderr.contains("Permission denied")) } -/// Build the inner command that applies seccomp after bubblewrap. -fn build_inner_seccomp_command( - sandbox_policy_cwd: &Path, - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, +struct InnerSeccompCommandArgs<'a> { + sandbox_policy_cwd: &'a Path, + sandbox_policy: &'a SandboxPolicy, + file_system_sandbox_policy: &'a FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, use_bwrap_sandbox: bool, allow_network_for_proxy: bool, proxy_route_spec: Option, command: Vec, -) -> Vec { +} + +/// Build the inner command that applies seccomp after bubblewrap. +fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec { + let InnerSeccompCommandArgs { + sandbox_policy_cwd, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + use_bwrap_sandbox, + allow_network_for_proxy, + proxy_route_spec, + command, + } = args; let current_exe = match std::env::current_exe() { Ok(path) => path, Err(err) => panic!("failed to resolve current executable path: {err}"), @@ -375,6 +474,14 @@ fn build_inner_seccomp_command( Ok(json) => json, Err(err) => panic!("failed to serialize sandbox policy: {err}"), }; + let file_system_policy_json = match serde_json::to_string(file_system_sandbox_policy) { + Ok(json) => json, + Err(err) => panic!("failed to serialize filesystem sandbox policy: {err}"), + }; + let network_policy_json = match serde_json::to_string(&network_sandbox_policy) { + Ok(json) => json, + Err(err) => panic!("failed to serialize network sandbox policy: {err}"), + }; let mut inner = vec![ current_exe.to_string_lossy().to_string(), @@ -382,6 +489,10 @@ fn build_inner_seccomp_command( sandbox_policy_cwd.to_string_lossy().to_string(), "--sandbox-policy".to_string(), policy_json, + "--file-system-sandbox-policy".to_string(), + file_system_policy_json, + "--network-sandbox-policy".to_string(), + network_policy_json, ]; if use_bwrap_sandbox { inner.push("--use-bwrap-sandbox".to_string()); diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index cda509030..35c60f364 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -1,7 +1,13 @@ #[cfg(test)] use super::*; #[cfg(test)] +use codex_protocol::protocol::FileSystemSandboxPolicy; +#[cfg(test)] +use codex_protocol::protocol::NetworkSandboxPolicy; +#[cfg(test)] use codex_protocol::protocol::SandboxPolicy; +#[cfg(test)] +use pretty_assertions::assert_eq; #[test] fn detects_proc_mount_invalid_argument_failure() { @@ -91,42 +97,66 @@ fn inserts_unshare_net_when_proxy_only_network_mode_requested() { #[test] fn proxy_only_mode_takes_precedence_over_full_network_policy() { - let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true); + let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true); assert_eq!(mode, BwrapNetworkMode::ProxyOnly); } #[test] fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() { - let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true); + let mode = bwrap_network_mode(NetworkSandboxPolicy::Enabled, true); let argv = build_preflight_bwrap_argv(Path::new("/"), &SandboxPolicy::DangerFullAccess, mode); assert!(argv.iter().any(|arg| arg == "--")); } #[test] fn managed_proxy_inner_command_includes_route_spec() { - let args = build_inner_seccomp_command( - Path::new("/tmp"), - &SandboxPolicy::new_read_only_policy(), - true, - true, - Some("{\"routes\":[]}".to_string()), - vec!["/bin/true".to_string()], - ); + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let args = build_inner_seccomp_command(InnerSeccompCommandArgs { + sandbox_policy_cwd: Path::new("/tmp"), + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + use_bwrap_sandbox: true, + allow_network_for_proxy: true, + proxy_route_spec: Some("{\"routes\":[]}".to_string()), + command: vec!["/bin/true".to_string()], + }); assert!(args.iter().any(|arg| arg == "--proxy-route-spec")); assert!(args.iter().any(|arg| arg == "{\"routes\":[]}")); } +#[test] +fn inner_command_includes_split_policy_flags() { + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let args = build_inner_seccomp_command(InnerSeccompCommandArgs { + sandbox_policy_cwd: Path::new("/tmp"), + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + use_bwrap_sandbox: true, + allow_network_for_proxy: false, + proxy_route_spec: None, + command: vec!["/bin/true".to_string()], + }); + + assert!(args.iter().any(|arg| arg == "--file-system-sandbox-policy")); + assert!(args.iter().any(|arg| arg == "--network-sandbox-policy")); +} + #[test] fn non_managed_inner_command_omits_route_spec() { - let args = build_inner_seccomp_command( - Path::new("/tmp"), - &SandboxPolicy::new_read_only_policy(), - true, - false, - None, - vec!["/bin/true".to_string()], - ); + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let args = build_inner_seccomp_command(InnerSeccompCommandArgs { + sandbox_policy_cwd: Path::new("/tmp"), + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + use_bwrap_sandbox: true, + allow_network_for_proxy: false, + proxy_route_spec: None, + command: vec!["/bin/true".to_string()], + }); assert!(!args.iter().any(|arg| arg == "--proxy-route-spec")); } @@ -134,15 +164,71 @@ fn non_managed_inner_command_omits_route_spec() { #[test] fn managed_proxy_inner_command_requires_route_spec() { let result = std::panic::catch_unwind(|| { - build_inner_seccomp_command( + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + build_inner_seccomp_command(InnerSeccompCommandArgs { + sandbox_policy_cwd: Path::new("/tmp"), + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy), + network_sandbox_policy: NetworkSandboxPolicy::Restricted, + use_bwrap_sandbox: true, + allow_network_for_proxy: true, + proxy_route_spec: None, + command: vec!["/bin/true".to_string()], + }) + }); + assert!(result.is_err()); +} + +#[test] +fn resolve_sandbox_policies_derives_split_policies_from_legacy_policy() { + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + + let resolved = + resolve_sandbox_policies(Path::new("/tmp"), Some(sandbox_policy.clone()), None, None); + + assert_eq!(resolved.sandbox_policy, sandbox_policy); + assert_eq!( + resolved.file_system_sandbox_policy, + FileSystemSandboxPolicy::from(&sandbox_policy) + ); + assert_eq!( + resolved.network_sandbox_policy, + NetworkSandboxPolicy::from(&sandbox_policy) + ); +} + +#[test] +fn resolve_sandbox_policies_derives_legacy_policy_from_split_policies() { + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy); + let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); + + let resolved = resolve_sandbox_policies( + Path::new("/tmp"), + None, + Some(file_system_sandbox_policy.clone()), + Some(network_sandbox_policy), + ); + + assert_eq!(resolved.sandbox_policy, sandbox_policy); + assert_eq!( + resolved.file_system_sandbox_policy, + file_system_sandbox_policy + ); + assert_eq!(resolved.network_sandbox_policy, network_sandbox_policy); +} + +#[test] +fn resolve_sandbox_policies_rejects_partial_split_policies() { + let result = std::panic::catch_unwind(|| { + resolve_sandbox_policies( Path::new("/tmp"), - &SandboxPolicy::new_read_only_policy(), - true, - true, + Some(SandboxPolicy::new_read_only_policy()), + Some(FileSystemSandboxPolicy::default()), None, - vec!["/bin/true".to_string()], ) }); + assert!(result.is_err()); } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 420345c21..de899ab65 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -727,6 +727,22 @@ impl FromStr for SandboxPolicy { } } +impl FromStr for FileSystemSandboxPolicy { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +impl FromStr for NetworkSandboxPolicy { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + impl SandboxPolicy { /// Returns a policy with read-only disk access and no network. pub fn new_read_only_policy() -> Self { @@ -3177,6 +3193,7 @@ mod tests { use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::json; + use std::path::PathBuf; use tempfile::NamedTempFile; use tempfile::TempDir;