From d9845f56cae73841e3a4a5ea64be29ddf95d4f2c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 6 Mar 2026 14:41:46 -0800 Subject: [PATCH] linux-sandbox: plumb split sandbox policies through helper --- codex-rs/core/src/landlock.rs | 113 +++++++++++++---- codex-rs/core/src/sandboxing/mod.rs | 6 +- codex-rs/linux-sandbox/src/landlock.rs | 12 +- codex-rs/linux-sandbox/src/linux_run_main.rs | 115 ++++++++++++++++-- .../linux-sandbox/src/linux_run_main_tests.rs | 98 ++++++++++++++- codex-rs/protocol/src/protocol.rs | 16 +++ 6 files changed, 313 insertions(+), 47 deletions(-) diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 7e49191067..9a55badcdf 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -1,3 +1,4 @@ +use crate::protocol::FileSystemSandboxPolicy; use crate::protocol::NetworkSandboxPolicy; use crate::protocol::SandboxPolicy; use crate::spawn::SpawnChildRequest; @@ -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,13 +65,55 @@ 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( +#[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).expect("Failed to serialize SandboxPolicy to JSON"); + #[expect(clippy::expect_used)] + let file_system_policy_json = serde_json::to_string(file_system_sandbox_policy) + .expect("Failed to serialize FileSystemSandboxPolicy to JSON"); + #[expect(clippy::expect_used)] + let network_policy_json = serde_json::to_string(&network_sandbox_policy) + .expect("Failed to serialize NetworkSandboxPolicy to JSON"); + + let mut linux_cmd = create_linux_sandbox_command_args( + command, + sandbox_policy_cwd, + use_bwrap_sandbox, + allow_network_for_proxy, + ); + linux_cmd.splice( + 2..2, + [ + "--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, + ], + ); + linux_cmd +} + +/// Converts the sandbox cwd and execution options into the CLI invocation for +/// `codex-linux-sandbox`. +pub(crate) fn create_linux_sandbox_command_args( + command: Vec, sandbox_policy_cwd: &Path, use_bwrap_sandbox: bool, allow_network_for_proxy: bool, @@ -77,16 +124,7 @@ pub(crate) fn create_linux_sandbox_command_args( .expect("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, - ]; + 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()); } @@ -113,16 +151,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 +169,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 eab5d0430e..eae5554848 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::FileSystemAccessMode; use crate::protocol::FileSystemPath; use crate::protocol::FileSystemSandboxEntry; @@ -514,9 +514,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 f9477a1264..a120ab7054 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) diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index c9fdd06a32..6a624ac1dd 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, @@ -142,6 +169,8 @@ pub fn run_main() -> ! { let inner = build_inner_seccomp_command( &sandbox_policy_cwd, &sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, use_bwrap_sandbox, allow_network_for_proxy, proxy_route_spec, @@ -150,6 +179,7 @@ pub fn run_main() -> ! { 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,53 @@ 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 { + match ( + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + ) { + (Some(sandbox_policy), Some(file_system_sandbox_policy), Some(network_sandbox_policy)) => { + EffectiveSandboxPolicies { + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + } + } + (Some(sandbox_policy), None, 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), Some(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, None) => panic!("missing sandbox policy configuration"), + _ => panic!("file-system and network sandbox policies must be provided together"), + } +} + 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 +255,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 +279,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 +293,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 +316,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 +327,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()]; @@ -361,7 +440,9 @@ fn is_proc_mount_failure(stderr: &str) -> bool { /// Build the inner command that applies seccomp after bubblewrap. fn build_inner_seccomp_command( sandbox_policy_cwd: &Path, - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, + sandbox_policy: &SandboxPolicy, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, use_bwrap_sandbox: bool, allow_network_for_proxy: bool, proxy_route_spec: Option, @@ -375,6 +456,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 +471,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 cda5090306..c466de5e27 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,22 +97,25 @@ 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 sandbox_policy = SandboxPolicy::new_read_only_policy(); let args = build_inner_seccomp_command( Path::new("/tmp"), - &SandboxPolicy::new_read_only_policy(), + &sandbox_policy, + &FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::Restricted, true, true, Some("{\"routes\":[]}".to_string()), @@ -118,10 +127,31 @@ fn managed_proxy_inner_command_includes_route_spec() { } #[test] -fn non_managed_inner_command_omits_route_spec() { +fn inner_command_includes_split_policy_flags() { + let sandbox_policy = SandboxPolicy::new_read_only_policy(); let args = build_inner_seccomp_command( Path::new("/tmp"), - &SandboxPolicy::new_read_only_policy(), + &sandbox_policy, + &FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::Restricted, + true, + false, + None, + 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 sandbox_policy = SandboxPolicy::new_read_only_policy(); + let args = build_inner_seccomp_command( + Path::new("/tmp"), + &sandbox_policy, + &FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::Restricted, true, false, None, @@ -134,9 +164,12 @@ fn non_managed_inner_command_omits_route_spec() { #[test] fn managed_proxy_inner_command_requires_route_spec() { let result = std::panic::catch_unwind(|| { + let sandbox_policy = SandboxPolicy::new_read_only_policy(); build_inner_seccomp_command( Path::new("/tmp"), - &SandboxPolicy::new_read_only_policy(), + &sandbox_policy, + &FileSystemSandboxPolicy::from(&sandbox_policy), + NetworkSandboxPolicy::Restricted, true, true, None, @@ -146,6 +179,59 @@ fn managed_proxy_inner_command_requires_route_spec() { 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.clone()); + 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"), + Some(SandboxPolicy::new_read_only_policy()), + Some(FileSystemSandboxPolicy::default()), + None, + ) + }); + + assert!(result.is_err()); +} + #[test] fn apply_seccomp_then_exec_without_bwrap_panics() { let result = std::panic::catch_unwind(|| ensure_inner_stage_mode_is_valid(true, false)); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f4ec554dbb..b482d5d3c9 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -728,6 +728,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 {