diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index e1a9cb3def..de508b953c 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -23,8 +23,8 @@ use codex_core::config::StartedNetworkProxy; use codex_core::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS; use codex_core::exec::ExecExpiration; use codex_core::exec::IO_DRAIN_TIMEOUT_MS; -use codex_core::exec::SandboxType; use codex_core::sandboxing::ExecRequest; +use codex_sandboxing::SandboxType; use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP; use codex_utils_pty::ProcessHandle; use codex_utils_pty::SpawnedProcess; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index cbbd023905..dc04d9bb14 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -477,7 +477,7 @@ pub struct Config { pub file_opener: UriBasedFileOpener, /// Path to the `codex-linux-sandbox` executable. This must be set if - /// [`crate::exec::SandboxType::LinuxSeccomp`] is used. Note that this + /// [`codex_sandboxing::SandboxType::LinuxSeccomp`] is used. Note that this /// cannot be set in the config file: it must be set in code via /// [`ConfigOverrides`]. /// diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 9be0518fa0..7f4664858a 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -24,20 +24,23 @@ use crate::protocol::EventMsg; use crate::protocol::ExecCommandOutputDeltaEvent; use crate::protocol::ExecOutputStream; use crate::protocol::SandboxPolicy; -use crate::sandboxing::CommandSpec; +use crate::sandboxing::ExecOptions; use crate::sandboxing::ExecRequest; -use crate::sandboxing::SandboxManager; use crate::sandboxing::SandboxPermissions; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; use crate::text_encoding::bytes_to_string_smart; -use crate::tools::sandboxing::SandboxablePreference; use codex_network_proxy::NetworkProxy; #[cfg(any(target_os = "windows", test))] use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_sandboxing::SandboxCommand; +use codex_sandboxing::SandboxManager; +use codex_sandboxing::SandboxTransformRequest; +use codex_sandboxing::SandboxType; +use codex_sandboxing::SandboxablePreference; use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP; use codex_utils_pty::process_group::kill_child_process_group; @@ -178,31 +181,6 @@ impl ExecCapturePolicy { } } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SandboxType { - None, - - /// Only available on macOS. - MacosSeatbelt, - - /// Only available on Linux. - LinuxSeccomp, - - /// Only available on Windows. - WindowsRestrictedToken, -} - -impl SandboxType { - pub(crate) fn as_metric_tag(self) -> &'static str { - match self { - SandboxType::None => "none", - SandboxType::MacosSeatbelt => "seatbelt", - SandboxType::LinuxSeccomp => "seccomp", - SandboxType::WindowsRestrictedToken => "windows_sandbox", - } - } -} - #[derive(Clone)] pub struct StdoutStream { pub sub_id: String, @@ -279,22 +257,23 @@ pub fn build_exec_request( )) })?; - let spec = CommandSpec { + let manager = SandboxManager::new(); + let command = SandboxCommand { program: program.clone(), args: args.to_vec(), cwd, env, + additional_permissions: None, + }; + let options = ExecOptions { expiration, capture_policy, sandbox_permissions, - additional_permissions: None, justification, }; - - let manager = SandboxManager::new(); let exec_req = manager - .transform(crate::sandboxing::SandboxTransformRequest { - spec, + .transform(SandboxTransformRequest { + command, policy: sandbox_policy, file_system_policy: file_system_sandbox_policy, network_policy: network_sandbox_policy, @@ -309,6 +288,7 @@ pub fn build_exec_request( windows_sandbox_level, windows_sandbox_private_desktop, }) + .map(|request| ExecRequest::from_sandbox_exec_request(request, options)) .map_err(CodexErr::from)?; Ok(exec_req) } @@ -620,7 +600,7 @@ fn finalize_exec_result( pub(crate) mod errors { use super::CodexErr; - use crate::sandboxing::SandboxTransformError; + use codex_sandboxing::SandboxTransformError; impl From for CodexErr { fn from(err: SandboxTransformError) -> Self { diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index fc312ec88e..6f452d6e8a 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -1,5 +1,6 @@ use super::*; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_sandboxing::SandboxType; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::time::Duration; @@ -542,7 +543,7 @@ fn windows_elevated_sandbox_allows_restricted_read_only_policies() { #[test] fn process_exec_tool_call_uses_platform_sandbox_for_network_only_restrictions() { - let expected = crate::get_platform_sandbox(false).unwrap_or(SandboxType::None); + let expected = codex_sandboxing::get_platform_sandbox(false).unwrap_or(SandboxType::None); assert_eq!( select_process_exec_tool_sandbox_type( diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index cd6b51b7e9..1c92080502 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -180,7 +180,6 @@ pub use exec_policy::check_execpolicy_for_warnings; pub use exec_policy::format_exec_policy_error_with_source; pub use exec_policy::load_exec_policy; pub use file_watcher::FileWatcherEvent; -pub use safety::get_platform_sandbox; pub use tools::spec::parse_tool_input_schema; pub use turn_metadata::build_turn_metadata_header; pub mod compact; diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 37bc9065d6..61fdc8f6e1 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -2,16 +2,15 @@ use std::path::Component; use std::path::Path; use std::path::PathBuf; -use codex_apply_patch::ApplyPatchAction; -use codex_apply_patch::ApplyPatchFileChange; - -use crate::exec::SandboxType; -use crate::util::resolve_path; - use crate::protocol::AskForApproval; use crate::protocol::FileSystemSandboxPolicy; use crate::protocol::SandboxPolicy; +use crate::util::resolve_path; +use codex_apply_patch::ApplyPatchAction; +use codex_apply_patch::ApplyPatchFileChange; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_sandboxing::SandboxType; +use codex_sandboxing::get_platform_sandbox; #[derive(Debug, PartialEq)] pub enum SafetyCheck { @@ -106,22 +105,6 @@ pub fn assess_patch_safety( } } -pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option { - if cfg!(target_os = "macos") { - Some(SandboxType::MacosSeatbelt) - } else if cfg!(target_os = "linux") { - Some(SandboxType::LinuxSeccomp) - } else if cfg!(target_os = "windows") { - if windows_sandbox_enabled { - Some(SandboxType::WindowsRestrictedToken) - } else { - None - } - } else { - None - } -} - fn is_write_patch_constrained_to_writable_paths( action: &ApplyPatchAction, file_system_sandbox_policy: &FileSystemSandboxPolicy, diff --git a/codex-rs/core/src/sandbox_tags.rs b/codex-rs/core/src/sandbox_tags.rs index 6a66d17dd0..57a99a8038 100644 --- a/codex-rs/core/src/sandbox_tags.rs +++ b/codex-rs/core/src/sandbox_tags.rs @@ -1,7 +1,7 @@ -use crate::exec::SandboxType; use crate::protocol::SandboxPolicy; -use crate::safety::get_platform_sandbox; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_sandboxing::SandboxType; +use codex_sandboxing::get_platform_sandbox; pub(crate) fn sandbox_tag( policy: &SandboxPolicy, diff --git a/codex-rs/core/src/sandbox_tags_tests.rs b/codex-rs/core/src/sandbox_tags_tests.rs index 7084d5ff92..40062e031b 100644 --- a/codex-rs/core/src/sandbox_tags_tests.rs +++ b/codex-rs/core/src/sandbox_tags_tests.rs @@ -1,9 +1,9 @@ use super::sandbox_tag; -use crate::exec::SandboxType; use crate::protocol::SandboxPolicy; -use crate::safety::get_platform_sandbox; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::protocol::NetworkAccess; +use codex_sandboxing::SandboxType; +use codex_sandboxing::get_platform_sandbox; use pretty_assertions::assert_eq; #[test] diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index fdce99d3e4..2b2f3c2364 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -1,55 +1,37 @@ /* Module: sandboxing -Build platform wrappers and produce ExecRequest for execution. Owns low-level -sandbox placement and transformation of portable CommandSpec into a -ready‑to‑spawn environment. +Core-owned adapter types for exec/runtime plumbing. Policy selection and +command transformation live in the codex-sandboxing crate; this module keeps +the exec-only metadata and translates transformed sandbox commands back into +ExecRequest for execution. */ use crate::exec::ExecCapturePolicy; use crate::exec::ExecExpiration; use crate::exec::ExecToolCallOutput; -use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::execute_exec_request; -use crate::protocol::SandboxPolicy; #[cfg(target_os = "macos")] use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; -use crate::tools::sandboxing::SandboxablePreference; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; -#[cfg(target_os = "macos")] -use codex_protocol::models::MacOsSeatbeltProfileExtensions; -use codex_protocol::models::PermissionProfile; pub use codex_protocol::models::SandboxPermissions; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_sandboxing::landlock::allow_network_for_proxy; -use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; -use codex_sandboxing::policy_transforms::EffectiveSandboxPermissions; -use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; -use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; -use codex_sandboxing::policy_transforms::should_require_platform_sandbox; -#[cfg(target_os = "macos")] -use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; -#[cfg(target_os = "macos")] -use codex_sandboxing::seatbelt::create_seatbelt_command_args_for_policies_with_extensions; +use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::SandboxExecRequest; +use codex_sandboxing::SandboxType; use std::collections::HashMap; -use std::path::Path; use std::path::PathBuf; #[derive(Debug)] -pub struct CommandSpec { - pub program: String, - pub args: Vec, - pub cwd: PathBuf, - pub env: HashMap, - pub expiration: ExecExpiration, - pub capture_policy: ExecCapturePolicy, - pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, - pub justification: Option, +pub(crate) struct ExecOptions { + pub(crate) expiration: ExecExpiration, + pub(crate) capture_policy: ExecCapturePolicy, + pub(crate) sandbox_permissions: SandboxPermissions, + pub(crate) justification: Option, } #[derive(Debug)] @@ -71,213 +53,57 @@ pub struct ExecRequest { pub arg0: Option, } -/// Bundled arguments for sandbox transformation. -/// -/// This keeps call sites self-documenting when several fields are optional. -pub(crate) struct SandboxTransformRequest<'a> { - pub spec: CommandSpec, - pub policy: &'a SandboxPolicy, - pub file_system_policy: &'a FileSystemSandboxPolicy, - pub network_policy: NetworkSandboxPolicy, - pub sandbox: SandboxType, - pub enforce_managed_network: bool, - // TODO(viyatb): Evaluate switching this to Option> - // to make shared ownership explicit across runtime/sandbox plumbing. - pub network: Option<&'a NetworkProxy>, - pub sandbox_policy_cwd: &'a Path, - #[cfg(target_os = "macos")] - pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, - pub codex_linux_sandbox_exe: Option<&'a PathBuf>, - pub use_legacy_landlock: bool, - pub windows_sandbox_level: WindowsSandboxLevel, - pub windows_sandbox_private_desktop: bool, -} - -pub enum SandboxPreference { - Auto, - Require, - Forbid, -} - -#[derive(Debug, thiserror::Error)] -pub(crate) enum SandboxTransformError { - #[error("missing codex-linux-sandbox executable path")] - MissingLinuxSandboxExecutable, - #[cfg(not(target_os = "macos"))] - #[error("seatbelt sandbox is only available on macOS")] - SeatbeltUnavailable, -} - -#[derive(Default)] -pub struct SandboxManager; - -impl SandboxManager { - pub fn new() -> Self { - Self - } - - pub(crate) fn select_initial( - &self, - file_system_policy: &FileSystemSandboxPolicy, - network_policy: NetworkSandboxPolicy, - pref: SandboxablePreference, - windows_sandbox_level: WindowsSandboxLevel, - has_managed_network_requirements: bool, - ) -> SandboxType { - match pref { - SandboxablePreference::Forbid => SandboxType::None, - SandboxablePreference::Require => { - // Require a platform sandbox when available; on Windows this - // respects the experimental_windows_sandbox feature. - crate::safety::get_platform_sandbox( - windows_sandbox_level != WindowsSandboxLevel::Disabled, - ) - .unwrap_or(SandboxType::None) - } - SandboxablePreference::Auto => { - if should_require_platform_sandbox( - file_system_policy, - network_policy, - has_managed_network_requirements, - ) { - crate::safety::get_platform_sandbox( - windows_sandbox_level != WindowsSandboxLevel::Disabled, - ) - .unwrap_or(SandboxType::None) - } else { - SandboxType::None - } - } - } - } - - pub(crate) fn transform( - &self, - request: SandboxTransformRequest<'_>, - ) -> Result { - let SandboxTransformRequest { - mut spec, - policy, - file_system_policy, - network_policy, - sandbox, - enforce_managed_network, +impl ExecRequest { + pub(crate) fn from_sandbox_exec_request( + request: SandboxExecRequest, + options: ExecOptions, + ) -> Self { + let SandboxExecRequest { + command, + cwd, + mut env, network, - sandbox_policy_cwd, - #[cfg(target_os = "macos")] - macos_seatbelt_profile_extensions, - codex_linux_sandbox_exe, - use_legacy_landlock, + sandbox, windows_sandbox_level, windows_sandbox_private_desktop, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + arg0, } = request; - #[cfg(not(target_os = "macos"))] - let macos_seatbelt_profile_extensions = None; - let additional_permissions = spec.additional_permissions.take(); - let EffectiveSandboxPermissions { - sandbox_policy: effective_policy, - macos_seatbelt_profile_extensions: _effective_macos_seatbelt_profile_extensions, - } = EffectiveSandboxPermissions::new( - policy, - macos_seatbelt_profile_extensions, - additional_permissions.as_ref(), - ); - let effective_file_system_policy = effective_file_system_sandbox_policy( - file_system_policy, - additional_permissions.as_ref(), - ); - let effective_network_policy = - effective_network_sandbox_policy(network_policy, additional_permissions.as_ref()); - let mut env = spec.env; - if !effective_network_policy.is_enabled() { + let ExecOptions { + expiration, + capture_policy, + sandbox_permissions, + justification, + } = options; + if !network_sandbox_policy.is_enabled() { env.insert( CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(), "1".to_string(), ); } - - let mut command = Vec::with_capacity(1 + spec.args.len()); - command.push(spec.program); - command.append(&mut spec.args); - - let (command, sandbox_env, arg0_override) = match sandbox { - SandboxType::None => (command, HashMap::new(), None), - #[cfg(target_os = "macos")] - SandboxType::MacosSeatbelt => { - let mut seatbelt_env = HashMap::new(); - seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); - let mut args = create_seatbelt_command_args_for_policies_with_extensions( - command.clone(), - &effective_file_system_policy, - effective_network_policy, - sandbox_policy_cwd, - enforce_managed_network, - network, - _effective_macos_seatbelt_profile_extensions.as_ref(), - ); - let mut full_command = Vec::with_capacity(1 + args.len()); - full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); - full_command.append(&mut args); - (full_command, seatbelt_env, None) - } - #[cfg(not(target_os = "macos"))] - SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable), - SandboxType::LinuxSeccomp => { - 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_for_policies( - command.clone(), - spec.cwd.as_path(), - &effective_policy, - &effective_file_system_policy, - effective_network_policy, - sandbox_policy_cwd, - use_legacy_landlock, - allow_proxy_network, - ); - let mut full_command = Vec::with_capacity(1 + args.len()); - full_command.push(exe.to_string_lossy().to_string()); - full_command.append(&mut args); - ( - full_command, - HashMap::new(), - Some("codex-linux-sandbox".to_string()), - ) - } - // On Windows, the restricted token sandbox executes in-process via the - // codex-windows-sandbox crate. We leave the command unchanged here and - // branch during execution based on the sandbox type. - #[cfg(target_os = "windows")] - SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None), - // When building for non-Windows targets, this variant is never constructed. - #[cfg(not(target_os = "windows"))] - SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None), - }; - - env.extend(sandbox_env); - - Ok(ExecRequest { + #[cfg(target_os = "macos")] + if sandbox == SandboxType::MacosSeatbelt { + env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); + } + Self { command, - cwd: spec.cwd, + cwd, env, - network: network.cloned(), - expiration: spec.expiration, - capture_policy: spec.capture_policy, + network, + expiration, + capture_policy, sandbox, windows_sandbox_level, windows_sandbox_private_desktop, - sandbox_permissions: spec.sandbox_permissions, - sandbox_policy: effective_policy, - file_system_sandbox_policy: effective_file_system_policy, - network_sandbox_policy: effective_network_policy, - justification: spec.justification, - arg0: arg0_override, - }) - } - - pub fn denied(&self, sandbox: SandboxType, out: &ExecToolCallOutput) -> bool { - crate::exec::is_likely_sandbox_denied(sandbox, out) + sandbox_permissions, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, + justification, + arg0, + } } } @@ -303,7 +129,3 @@ pub async fn execute_exec_request_with_after_spawn( let effective_policy = exec_request.sandbox_policy.clone(); execute_exec_request(exec_request, &effective_policy, stdout_stream, after_spawn).await } - -#[cfg(test)] -#[path = "mod_tests.rs"] -mod tests; diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 6b42be3cef..33a7c682b4 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -12,7 +12,6 @@ use uuid::Uuid; use crate::codex::TurnContext; use crate::exec::ExecCapturePolicy; use crate::exec::ExecToolCallOutput; -use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::StreamOutput; use crate::exec::execute_exec_request; @@ -31,6 +30,7 @@ use crate::state::TaskKind; use crate::tools::format_exec_output_str; use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot; use crate::user_shell_command::user_shell_command_record_item; +use codex_sandboxing::SandboxType; use super::SessionTask; use super::SessionTaskContext; diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 4f7c3d7436..73c0b34ead 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -39,14 +39,16 @@ use crate::exec::ExecExpiration; use crate::exec_env::create_env; use crate::function_tool::FunctionCallError; use crate::original_image_detail::normalize_output_image_detail; -use crate::sandboxing::CommandSpec; -use crate::sandboxing::SandboxManager; +use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; use crate::tools::ToolRouter; use crate::tools::context::SharedTurnDiffTracker; -use crate::tools::sandboxing::SandboxablePreference; use crate::truncate::TruncationPolicy; use crate::truncate::truncate_text; +use codex_sandboxing::SandboxCommand; +use codex_sandboxing::SandboxManager; +use codex_sandboxing::SandboxTransformRequest; +use codex_sandboxing::SandboxablePreference; pub(crate) const JS_REPL_PRAGMA_PREFIX: &str = "// codex-js-repl:"; const KERNEL_SOURCE: &str = include_str!("kernel.js"); @@ -1029,21 +1031,6 @@ impl JsReplManager { ); } - let spec = CommandSpec { - program: node_path.to_string_lossy().to_string(), - args: vec![ - "--experimental-vm-modules".to_string(), - kernel_path.to_string_lossy().to_string(), - ], - cwd: turn.cwd.clone(), - env, - expiration: ExecExpiration::DefaultTimeout, - capture_policy: ExecCapturePolicy::ShellTool, - sandbox_permissions: SandboxPermissions::UseDefault, - additional_permissions: None, - justification: None, - }; - let sandbox = SandboxManager::new(); let has_managed_network_requirements = turn .config @@ -1058,9 +1045,25 @@ impl JsReplManager { turn.windows_sandbox_level, has_managed_network_requirements, ); + let command = SandboxCommand { + program: node_path.to_string_lossy().to_string(), + args: vec![ + "--experimental-vm-modules".to_string(), + kernel_path.to_string_lossy().to_string(), + ], + cwd: turn.cwd.clone(), + env, + additional_permissions: None, + }; + let options = ExecOptions { + expiration: ExecExpiration::DefaultTimeout, + capture_policy: ExecCapturePolicy::ShellTool, + sandbox_permissions: SandboxPermissions::UseDefault, + justification: None, + }; let exec_env = sandbox - .transform(crate::sandboxing::SandboxTransformRequest { - spec, + .transform(SandboxTransformRequest { + command, policy: &turn.sandbox_policy, file_system_policy: &turn.file_system_sandbox_policy, network_policy: turn.network_sandbox_policy, @@ -1078,6 +1081,9 @@ impl JsReplManager { .permissions .windows_sandbox_private_desktop, }) + .map(|request| { + crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options) + }) .map_err(|err| format!("failed to configure sandbox for js_repl: {err}"))?; let mut cmd = diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 4b53ac156f..09f0dc5959 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -12,7 +12,6 @@ use crate::exec::ExecToolCallOutput; use crate::guardian::GUARDIAN_REJECTION_MESSAGE; use crate::guardian::routes_approval_to_guardian; use crate::network_policy_decision::network_approval_context_from_payload; -use crate::sandboxing::SandboxManager; use crate::tools::network_approval::DeferredNetworkApproval; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::begin_network_approval; @@ -30,6 +29,8 @@ use codex_otel::ToolDecisionSource; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::ReviewDecision; +use codex_sandboxing::SandboxManager; +use codex_sandboxing::SandboxType; pub(crate) struct ToolOrchestrator { sandbox: SandboxManager, @@ -178,7 +179,7 @@ impl ToolOrchestrator { .network .is_some(); let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { - SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None, + SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None, SandboxOverride::NoOverride => self.sandbox.select_initial( &turn_ctx.file_system_sandbox_policy, turn_ctx.network_sandbox_policy, @@ -188,8 +189,7 @@ impl ToolOrchestrator { ), }; - // Platform-specific flag gating is handled by SandboxManager::select_initial - // via crate::safety::get_platform_sandbox(..). + // Platform-specific flag gating is handled by SandboxManager::select_initial. let use_legacy_landlock = turn_ctx.features.use_legacy_landlock(); let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, @@ -323,7 +323,7 @@ impl ToolOrchestrator { } let escalated_attempt = SandboxAttempt { - sandbox: crate::exec::SandboxType::None, + sandbox: SandboxType::None, policy: &turn_ctx.sandbox_policy, file_system_policy: &turn_ctx.file_system_sandbox_policy, network_policy: turn_ctx.network_sandbox_policy, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index f1e9912bc5..d1a6efe001 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -9,7 +9,7 @@ use crate::exec::ExecToolCallOutput; use crate::guardian::GuardianApprovalRequest; use crate::guardian::review_approval_request; use crate::guardian::routes_approval_to_guardian; -use crate::sandboxing::CommandSpec; +use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; use crate::sandboxing::execute_env; use crate::tools::sandboxing::Approvable; @@ -17,7 +17,6 @@ use crate::tools::sandboxing::ApprovalCtx; use crate::tools::sandboxing::ExecApprovalRequirement; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::Sandboxable; -use crate::tools::sandboxing::SandboxablePreference; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; @@ -28,6 +27,8 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; +use codex_sandboxing::SandboxCommand; +use codex_sandboxing::SandboxablePreference; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::collections::HashMap; @@ -67,10 +68,10 @@ impl ApplyPatchRuntime { } } - fn build_command_spec( + fn build_sandbox_command( req: &ApplyPatchRequest, _codex_home: &std::path::Path, - ) -> Result { + ) -> Result { let exe = if let Some(path) = &req.codex_exe { path.clone() } else { @@ -85,21 +86,16 @@ impl ApplyPatchRuntime { })? } }; - let program = exe.to_string_lossy().to_string(); - Ok(CommandSpec { - program, + Ok(SandboxCommand { + program: exe.to_string_lossy().to_string(), args: vec![ CODEX_CORE_APPLY_PATCH_ARG1.to_string(), req.action.patch.clone(), ], cwd: req.action.cwd.clone(), - expiration: req.timeout_ms.into(), - capture_policy: ExecCapturePolicy::ShellTool, // Run apply_patch with a minimal environment for determinism and to avoid leaks. env: HashMap::new(), - sandbox_permissions: req.sandbox_permissions, additional_permissions: req.additional_permissions.clone(), - justification: None, }) } @@ -206,9 +202,15 @@ impl ToolRuntime for ApplyPatchRuntime { attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, ) -> Result { - let spec = Self::build_command_spec(req, &ctx.turn.config.codex_home)?; + let command = Self::build_sandbox_command(req, &ctx.turn.config.codex_home)?; + let options = ExecOptions { + expiration: req.timeout_ms.into(), + capture_policy: ExecCapturePolicy::ShellTool, + sandbox_permissions: req.sandbox_permissions, + justification: None, + }; let env = attempt - .env_for(spec, /*network*/ None) + .env_for(command, options, /*network*/ None) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env(env, Self::stdout_stream(ctx)) .await diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 2335a13ab7..bc2f56a430 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -4,15 +4,12 @@ Module: runtimes Concrete ToolRuntime implementations for specific tools. Each runtime stays small and focused and reuses the orchestrator for approvals + sandbox + retry. */ -use crate::exec::ExecCapturePolicy; -use crate::exec::ExecExpiration; use crate::path_utils; -use crate::sandboxing::CommandSpec; -use crate::sandboxing::SandboxPermissions; use crate::shell::Shell; use crate::skills::SkillMetadata; use crate::tools::sandboxing::ToolError; use codex_protocol::models::PermissionProfile; +use codex_sandboxing::SandboxCommand; use std::collections::HashMap; use std::path::Path; @@ -28,30 +25,23 @@ pub(crate) struct ExecveSessionApproval { pub skill: Option, } -/// Shared helper to construct a CommandSpec from a tokenized command line. +/// Shared helper to construct sandbox transform inputs from a tokenized command line. /// Validates that at least a program is present. -pub(crate) fn build_command_spec( +pub(crate) fn build_sandbox_command( command: &[String], cwd: &Path, env: &HashMap, - expiration: ExecExpiration, - sandbox_permissions: SandboxPermissions, additional_permissions: Option, - justification: Option, -) -> Result { +) -> Result { let (program, args) = command .split_first() .ok_or_else(|| ToolError::Rejected("command args are empty".to_string()))?; - Ok(CommandSpec { + Ok(SandboxCommand { program: program.clone(), args: args.to_vec(), cwd: cwd.to_path_buf(), env: env.clone(), - expiration, - capture_policy: ExecCapturePolicy::ShellTool, - sandbox_permissions, additional_permissions, - justification, }) } diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 6ff8349b53..6582557f85 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -2,24 +2,26 @@ Runtime: shell Executes shell requests under the orchestrator: asks for approval when needed, -builds a CommandSpec, and runs it under the current SandboxAttempt. +builds sandbox transform inputs, and runs them under the current SandboxAttempt. */ #[cfg(unix)] pub(crate) mod unix_escalation; pub(crate) mod zsh_fork_backend; use crate::command_canonicalization::canonicalize_command_for_approval; +use crate::exec::ExecCapturePolicy; use crate::exec::ExecToolCallOutput; use crate::guardian::GuardianApprovalRequest; use crate::guardian::review_approval_request; use crate::guardian::routes_approval_to_guardian; use crate::powershell::prefix_powershell_script_with_utf8; +use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; use crate::sandboxing::execute_env; use crate::shell::ShellType; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::NetworkApprovalSpec; -use crate::tools::runtimes::build_command_spec; +use crate::tools::runtimes::build_sandbox_command; use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot; use crate::tools::sandboxing::Approvable; use crate::tools::sandboxing::ApprovalCtx; @@ -27,7 +29,6 @@ use crate::tools::sandboxing::ExecApprovalRequirement; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::SandboxOverride; use crate::tools::sandboxing::Sandboxable; -use crate::tools::sandboxing::SandboxablePreference; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; @@ -36,6 +37,7 @@ use crate::tools::sandboxing::with_cached_approval; use codex_network_proxy::NetworkProxy; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReviewDecision; +use codex_sandboxing::SandboxablePreference; use futures::future::BoxFuture; use std::collections::HashMap; use std::path::PathBuf; @@ -243,17 +245,20 @@ impl ToolRuntime for ShellRuntime { } } - let spec = build_command_spec( + let command = build_sandbox_command( &command, &req.cwd, &req.env, - req.timeout_ms.into(), - req.sandbox_permissions, req.additional_permissions.clone(), - req.justification.clone(), )?; + let options = ExecOptions { + expiration: req.timeout_ms.into(), + capture_policy: ExecCapturePolicy::ShellTool, + sandbox_permissions: req.sandbox_permissions, + justification: req.justification.clone(), + }; let env = attempt - .env_for(spec, req.network.as_ref()) + .env_for(command, options, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env(env, Self::stdout_stream(ctx)) .await diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 948018dae6..a38a53ce0d 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -4,19 +4,18 @@ use crate::error::SandboxErr; use crate::exec::ExecCapturePolicy; use crate::exec::ExecExpiration; use crate::exec::ExecToolCallOutput; -use crate::exec::SandboxType; use crate::exec::is_likely_sandbox_denied; use crate::guardian::GuardianApprovalRequest; use crate::guardian::review_approval_request; use crate::guardian::routes_approval_to_guardian; +use crate::sandboxing::ExecOptions; use crate::sandboxing::ExecRequest; use crate::sandboxing::SandboxPermissions; use crate::shell::ShellType; use crate::skills::SkillMetadata; use crate::tools::runtimes::ExecveSessionApproval; -use crate::tools::runtimes::build_command_spec; +use crate::tools::runtimes::build_sandbox_command; use crate::tools::sandboxing::SandboxAttempt; -use crate::tools::sandboxing::SandboxablePreference; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use codex_execpolicy::Decision; @@ -35,6 +34,11 @@ use codex_protocol::protocol::ExecApprovalRequestSkillMetadata; use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::SandboxCommand; +use codex_sandboxing::SandboxManager; +use codex_sandboxing::SandboxTransformRequest; +use codex_sandboxing::SandboxType; +use codex_sandboxing::SandboxablePreference; use codex_shell_command::bash::parse_shell_lc_plain_commands; use codex_shell_command::bash::parse_shell_lc_single_command_prefix; use codex_shell_escalation::EscalateServer; @@ -107,17 +111,20 @@ pub(super) async fn try_run_zsh_fork( return Ok(None); } - let spec = build_command_spec( + let command = build_sandbox_command( command, &req.cwd, &req.env, - req.timeout_ms.into(), - req.sandbox_permissions, req.additional_permissions.clone(), - req.justification.clone(), )?; + let options = ExecOptions { + expiration: req.timeout_ms.into(), + capture_policy: ExecCapturePolicy::ShellTool, + sandbox_permissions: req.sandbox_permissions, + justification: req.justification.clone(), + }; let sandbox_exec_request = attempt - .env_for(spec, req.network.as_ref()) + .env_for(command, options, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; let crate::sandboxing::ExecRequest { command, @@ -1029,7 +1036,7 @@ impl CoreShellCommandExecutor { let (program, args) = command .split_first() .ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?; - let sandbox_manager = crate::sandboxing::SandboxManager::new(); + let sandbox_manager = SandboxManager::new(); let sandbox = sandbox_manager.select_initial( file_system_sandbox_policy, network_sandbox_policy, @@ -1037,37 +1044,42 @@ impl CoreShellCommandExecutor { self.windows_sandbox_level, self.network.is_some(), ); + let sandbox_permissions = if additional_permissions.is_some() { + SandboxPermissions::WithAdditionalPermissions + } else { + SandboxPermissions::UseDefault + }; + let command = SandboxCommand { + program: program.clone(), + args: args.to_vec(), + cwd: workdir.to_path_buf(), + env, + additional_permissions, + }; + let options = ExecOptions { + expiration: ExecExpiration::DefaultTimeout, + capture_policy: ExecCapturePolicy::ShellTool, + sandbox_permissions, + justification: self.justification.clone(), + }; + let exec_request = sandbox_manager.transform(SandboxTransformRequest { + command, + policy: sandbox_policy, + file_system_policy: file_system_sandbox_policy, + network_policy: network_sandbox_policy, + sandbox, + enforce_managed_network: self.network.is_some(), + network: self.network.as_ref(), + sandbox_policy_cwd: &self.sandbox_policy_cwd, + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions, + codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), + use_legacy_landlock: self.use_legacy_landlock, + windows_sandbox_level: self.windows_sandbox_level, + windows_sandbox_private_desktop: false, + })?; let mut exec_request = - sandbox_manager.transform(crate::sandboxing::SandboxTransformRequest { - spec: crate::sandboxing::CommandSpec { - program: program.clone(), - args: args.to_vec(), - cwd: workdir.to_path_buf(), - env, - expiration: ExecExpiration::DefaultTimeout, - capture_policy: ExecCapturePolicy::ShellTool, - sandbox_permissions: if additional_permissions.is_some() { - SandboxPermissions::WithAdditionalPermissions - } else { - SandboxPermissions::UseDefault - }, - additional_permissions, - justification: self.justification.clone(), - }, - policy: sandbox_policy, - file_system_policy: file_system_sandbox_policy, - network_policy: network_sandbox_policy, - sandbox, - enforce_managed_network: self.network.is_some(), - network: self.network.as_ref(), - sandbox_policy_cwd: &self.sandbox_policy_cwd, - #[cfg(target_os = "macos")] - macos_seatbelt_profile_extensions, - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), - use_legacy_landlock: self.use_legacy_landlock, - windows_sandbox_level: self.windows_sandbox_level, - windows_sandbox_private_desktop: false, - })?; + crate::sandboxing::ExecRequest::from_sandbox_exec_request(exec_request, options); if let Some(network) = exec_request.network.as_ref() { network.apply_to_env(&mut exec_request.env); } diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 3adee9c295..6373697c3b 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -14,7 +14,6 @@ use crate::config::Constrained; use crate::config::Permissions; #[cfg(target_os = "macos")] use crate::config::types::ShellEnvironmentPolicy; -use crate::exec::SandboxType; use crate::protocol::AskForApproval; use crate::protocol::GranularApprovalConfig; use crate::protocol::ReadOnlyAccess; @@ -38,6 +37,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SkillScope; +use codex_sandboxing::SandboxType; #[cfg(target_os = "macos")] use codex_sandboxing::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use codex_shell_escalation::EscalationExecution; diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 49ef0e6385..f59d38556b 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -7,16 +7,18 @@ the process manager to spawn PTYs once an ExecRequest is prepared. use crate::command_canonicalization::canonicalize_command_for_approval; use crate::error::CodexErr; use crate::error::SandboxErr; +use crate::exec::ExecCapturePolicy; use crate::exec::ExecExpiration; use crate::guardian::GuardianApprovalRequest; use crate::guardian::review_approval_request; use crate::guardian::routes_approval_to_guardian; use crate::powershell::prefix_powershell_script_with_utf8; +use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; use crate::shell::ShellType; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::NetworkApprovalSpec; -use crate::tools::runtimes::build_command_spec; +use crate::tools::runtimes::build_sandbox_command; use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot; use crate::tools::runtimes::shell::zsh_fork_backend; use crate::tools::sandboxing::Approvable; @@ -25,7 +27,6 @@ use crate::tools::sandboxing::ExecApprovalRequirement; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::SandboxOverride; use crate::tools::sandboxing::Sandboxable; -use crate::tools::sandboxing::SandboxablePreference; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; @@ -39,6 +40,7 @@ use crate::unified_exec::UnifiedExecProcessManager; use codex_network_proxy::NetworkProxy; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReviewDecision; +use codex_sandboxing::SandboxablePreference; use futures::future::BoxFuture; use std::collections::HashMap; use std::path::PathBuf; @@ -210,18 +212,17 @@ impl<'a> ToolRuntime for UnifiedExecRunt network.apply_to_env(&mut env); } if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode { - let spec = build_command_spec( - &command, - &req.cwd, - &env, - ExecExpiration::DefaultTimeout, - req.sandbox_permissions, - req.additional_permissions.clone(), - req.justification.clone(), - ) - .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; + let command = + build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone()) + .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; + let options = ExecOptions { + expiration: ExecExpiration::DefaultTimeout, + capture_policy: ExecCapturePolicy::ShellTool, + sandbox_permissions: req.sandbox_permissions, + justification: req.justification.clone(), + }; let exec_env = attempt - .env_for(spec, req.network.as_ref()) + .env_for(command, options, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; match zsh_fork_backend::maybe_prepare_unified_exec( req, @@ -258,18 +259,17 @@ impl<'a> ToolRuntime for UnifiedExecRunt } } } - let spec = build_command_spec( - &command, - &req.cwd, - &env, - ExecExpiration::DefaultTimeout, - req.sandbox_permissions, - req.additional_permissions.clone(), - req.justification.clone(), - ) - .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; + let command = + build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone()) + .map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?; + let options = ExecOptions { + expiration: ExecExpiration::DefaultTimeout, + capture_policy: ExecCapturePolicy::ShellTool, + sandbox_permissions: req.sandbox_permissions, + justification: req.justification.clone(), + }; let exec_env = attempt - .env_for(spec, req.network.as_ref()) + .env_for(command, options, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; self.manager .open_session_with_exec_env(&exec_env, req.tty, Box::new(NoopSpawnLifecycle)) diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index bf386a12d0..6cad5a3293 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -9,10 +9,8 @@ use crate::codex::TurnContext; use crate::error::CodexErr; #[cfg(test)] use crate::protocol::SandboxPolicy; -use crate::sandboxing::CommandSpec; -use crate::sandboxing::SandboxManager; +use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; -use crate::sandboxing::SandboxTransformError; use crate::state::SessionServices; use crate::tools::network_approval::NetworkApprovalSpec; use codex_network_proxy::NetworkProxy; @@ -23,6 +21,12 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; +use codex_sandboxing::SandboxCommand; +use codex_sandboxing::SandboxManager; +use codex_sandboxing::SandboxTransformError; +use codex_sandboxing::SandboxTransformRequest; +use codex_sandboxing::SandboxType; +use codex_sandboxing::SandboxablePreference; use futures::Future; use futures::future::BoxFuture; use serde::Serialize; @@ -280,15 +284,6 @@ pub(crate) trait Approvable { ) -> BoxFuture<'a, ReviewDecision>; } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum SandboxablePreference { - Auto, - #[allow(dead_code)] // Will be used by later tools. - Require, - #[allow(dead_code)] // Will be used by later tools. - Forbid, -} - pub(crate) trait Sandboxable { fn sandbox_preference(&self) -> SandboxablePreference; fn escalate_on_failure(&self) -> bool { @@ -323,7 +318,7 @@ pub(crate) trait ToolRuntime: Approvable + Sandboxable { } pub(crate) struct SandboxAttempt<'a> { - pub sandbox: crate::exec::SandboxType, + pub sandbox: SandboxType, pub policy: &'a crate::protocol::SandboxPolicy, pub file_system_policy: &'a FileSystemSandboxPolicy, pub network_policy: NetworkSandboxPolicy, @@ -339,12 +334,13 @@ pub(crate) struct SandboxAttempt<'a> { impl<'a> SandboxAttempt<'a> { pub fn env_for( &self, - spec: CommandSpec, + command: SandboxCommand, + options: ExecOptions, network: Option<&NetworkProxy>, ) -> Result { self.manager - .transform(crate::sandboxing::SandboxTransformRequest { - spec, + .transform(SandboxTransformRequest { + command, policy: self.policy, file_system_policy: self.file_system_policy, network_policy: self.network_policy, @@ -359,6 +355,9 @@ impl<'a> SandboxAttempt<'a> { windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, }) + .map(|request| { + crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options) + }) } } diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 3e69a71eea..44e926dbe6 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -12,7 +12,7 @@ //! Flow at a glance (open process) //! 1) Build a small request `{ command, cwd }`. //! 2) Orchestrator: approval (bypass/cache/prompt) → select sandbox → run. -//! 3) Runtime: transform `CommandSpec` -> `ExecRequest` -> spawn PTY. +//! 3) Runtime: transform `SandboxTransformRequest` -> `ExecRequest` -> spawn PTY. //! 4) If denial, orchestrator retries with `SandboxType::None`. //! 5) Process handle is returned with streaming output + metadata. //! diff --git a/codex-rs/core/src/unified_exec/process.rs b/codex-rs/core/src/unified_exec/process.rs index 6da7c739ec..58faed27de 100644 --- a/codex-rs/core/src/unified_exec/process.rs +++ b/codex-rs/core/src/unified_exec/process.rs @@ -13,11 +13,11 @@ use tokio::time::Duration; use tokio_util::sync::CancellationToken; use crate::exec::ExecToolCallOutput; -use crate::exec::SandboxType; use crate::exec::StreamOutput; use crate::exec::is_likely_sandbox_denied; use crate::truncate::TruncationPolicy; use crate::truncate::formatted_truncate_text; +use codex_sandboxing::SandboxType; use codex_utils_pty::ExecCommandSession; use codex_utils_pty::SpawnedPty; diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 069e824ee5..f297312827 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -6,7 +6,6 @@ use std::string::ToString; use codex_core::exec::ExecCapturePolicy; use codex_core::exec::ExecParams; use codex_core::exec::ExecToolCallOutput; -use codex_core::exec::SandboxType; use codex_core::exec::process_exec_tool_call; use codex_core::sandboxing::SandboxPermissions; use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; @@ -14,12 +13,12 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::SandboxType; +use codex_sandboxing::get_platform_sandbox; use tempfile::TempDir; use codex_core::error::Result; -use codex_core::get_platform_sandbox; - fn skip_test() -> bool { if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) { eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test."); diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index edd817e65e..5eca9d480c 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -1,7 +1,17 @@ pub mod landlock; pub mod macos_permissions; +mod manager; pub mod policy_transforms; #[cfg(target_os = "macos")] pub mod seatbelt; #[cfg(target_os = "macos")] mod seatbelt_permissions; + +pub use manager::SandboxCommand; +pub use manager::SandboxExecRequest; +pub use manager::SandboxManager; +pub use manager::SandboxTransformError; +pub use manager::SandboxTransformRequest; +pub use manager::SandboxType; +pub use manager::SandboxablePreference; +pub use manager::get_platform_sandbox; diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs new file mode 100644 index 0000000000..f55f53cb13 --- /dev/null +++ b/codex-rs/sandboxing/src/manager.rs @@ -0,0 +1,276 @@ +use crate::landlock::allow_network_for_proxy; +use crate::landlock::create_linux_sandbox_command_args_for_policies; +use crate::policy_transforms::EffectiveSandboxPermissions; +use crate::policy_transforms::effective_file_system_sandbox_policy; +use crate::policy_transforms::effective_network_sandbox_policy; +use crate::policy_transforms::should_require_platform_sandbox; +#[cfg(target_os = "macos")] +use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; +#[cfg(target_os = "macos")] +use crate::seatbelt::create_seatbelt_command_args_for_policies_with_extensions; +use codex_network_proxy::NetworkProxy; +use codex_protocol::config_types::WindowsSandboxLevel; +#[cfg(target_os = "macos")] +use codex_protocol::models::MacOsSeatbeltProfileExtensions; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::SandboxPolicy; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SandboxType { + None, + MacosSeatbelt, + LinuxSeccomp, + WindowsRestrictedToken, +} + +impl SandboxType { + pub fn as_metric_tag(self) -> &'static str { + match self { + SandboxType::None => "none", + SandboxType::MacosSeatbelt => "seatbelt", + SandboxType::LinuxSeccomp => "seccomp", + SandboxType::WindowsRestrictedToken => "windows_sandbox", + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SandboxablePreference { + Auto, + Require, + Forbid, +} + +pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option { + if cfg!(target_os = "macos") { + Some(SandboxType::MacosSeatbelt) + } else if cfg!(target_os = "linux") { + Some(SandboxType::LinuxSeccomp) + } else if cfg!(target_os = "windows") { + if windows_sandbox_enabled { + Some(SandboxType::WindowsRestrictedToken) + } else { + None + } + } else { + None + } +} + +#[derive(Debug)] +pub struct SandboxCommand { + pub program: String, + pub args: Vec, + pub cwd: PathBuf, + pub env: HashMap, + pub additional_permissions: Option, +} + +#[derive(Debug)] +pub struct SandboxExecRequest { + pub command: Vec, + pub cwd: PathBuf, + pub env: HashMap, + pub network: Option, + pub sandbox: SandboxType, + pub windows_sandbox_level: WindowsSandboxLevel, + pub windows_sandbox_private_desktop: bool, + pub sandbox_policy: SandboxPolicy, + pub file_system_sandbox_policy: FileSystemSandboxPolicy, + pub network_sandbox_policy: NetworkSandboxPolicy, + pub arg0: Option, +} + +/// Bundled arguments for sandbox transformation. +/// +/// This keeps call sites self-documenting when several fields are optional. +pub struct SandboxTransformRequest<'a> { + pub command: SandboxCommand, + pub policy: &'a SandboxPolicy, + pub file_system_policy: &'a FileSystemSandboxPolicy, + pub network_policy: NetworkSandboxPolicy, + pub sandbox: SandboxType, + pub enforce_managed_network: bool, + // TODO(viyatb): Evaluate switching this to Option> + // to make shared ownership explicit across runtime/sandbox plumbing. + pub network: Option<&'a NetworkProxy>, + pub sandbox_policy_cwd: &'a Path, + #[cfg(target_os = "macos")] + pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, + pub codex_linux_sandbox_exe: Option<&'a PathBuf>, + pub use_legacy_landlock: bool, + pub windows_sandbox_level: WindowsSandboxLevel, + pub windows_sandbox_private_desktop: bool, +} + +#[derive(Debug)] +pub enum SandboxTransformError { + MissingLinuxSandboxExecutable, + #[cfg(not(target_os = "macos"))] + SeatbeltUnavailable, +} + +impl std::fmt::Display for SandboxTransformError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MissingLinuxSandboxExecutable => { + write!(f, "missing codex-linux-sandbox executable path") + } + #[cfg(not(target_os = "macos"))] + Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"), + } + } +} + +impl std::error::Error for SandboxTransformError {} + +#[derive(Default)] +pub struct SandboxManager; + +impl SandboxManager { + pub fn new() -> Self { + Self + } + + pub fn select_initial( + &self, + file_system_policy: &FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, + pref: SandboxablePreference, + windows_sandbox_level: WindowsSandboxLevel, + has_managed_network_requirements: bool, + ) -> SandboxType { + match pref { + SandboxablePreference::Forbid => SandboxType::None, + SandboxablePreference::Require => { + get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) + .unwrap_or(SandboxType::None) + } + SandboxablePreference::Auto => { + if should_require_platform_sandbox( + file_system_policy, + network_policy, + has_managed_network_requirements, + ) { + get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) + .unwrap_or(SandboxType::None) + } else { + SandboxType::None + } + } + } + } + + pub fn transform( + &self, + request: SandboxTransformRequest<'_>, + ) -> Result { + let SandboxTransformRequest { + mut command, + policy, + file_system_policy, + network_policy, + sandbox, + enforce_managed_network, + network, + sandbox_policy_cwd, + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions, + codex_linux_sandbox_exe, + use_legacy_landlock, + windows_sandbox_level, + windows_sandbox_private_desktop, + } = request; + #[cfg(not(target_os = "macos"))] + let macos_seatbelt_profile_extensions = None; + let additional_permissions = command.additional_permissions.take(); + let EffectiveSandboxPermissions { + sandbox_policy: effective_policy, + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions: effective_macos_seatbelt_profile_extensions, + #[cfg(not(target_os = "macos"))] + macos_seatbelt_profile_extensions: _, + } = EffectiveSandboxPermissions::new( + policy, + macos_seatbelt_profile_extensions, + additional_permissions.as_ref(), + ); + let effective_file_system_policy = effective_file_system_sandbox_policy( + file_system_policy, + additional_permissions.as_ref(), + ); + let effective_network_policy = + effective_network_sandbox_policy(network_policy, additional_permissions.as_ref()); + let mut argv = Vec::with_capacity(1 + command.args.len()); + argv.push(command.program); + argv.append(&mut command.args); + + let (argv, arg0_override) = match sandbox { + SandboxType::None => (argv, None), + #[cfg(target_os = "macos")] + SandboxType::MacosSeatbelt => { + let mut args = create_seatbelt_command_args_for_policies_with_extensions( + argv.clone(), + &effective_file_system_policy, + effective_network_policy, + sandbox_policy_cwd, + enforce_managed_network, + network, + effective_macos_seatbelt_profile_extensions.as_ref(), + ); + let mut full_command = Vec::with_capacity(1 + args.len()); + full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); + full_command.append(&mut args); + (full_command, None) + } + #[cfg(not(target_os = "macos"))] + SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable), + SandboxType::LinuxSeccomp => { + 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_for_policies( + argv.clone(), + command.cwd.as_path(), + &effective_policy, + &effective_file_system_policy, + effective_network_policy, + sandbox_policy_cwd, + use_legacy_landlock, + allow_proxy_network, + ); + let mut full_command = Vec::with_capacity(1 + args.len()); + full_command.push(exe.to_string_lossy().to_string()); + full_command.append(&mut args); + (full_command, Some("codex-linux-sandbox".to_string())) + } + #[cfg(target_os = "windows")] + SandboxType::WindowsRestrictedToken => (argv, None), + #[cfg(not(target_os = "windows"))] + SandboxType::WindowsRestrictedToken => (argv, None), + }; + + Ok(SandboxExecRequest { + command: argv, + cwd: command.cwd, + env: command.env, + network: network.cloned(), + sandbox, + windows_sandbox_level, + windows_sandbox_private_desktop, + sandbox_policy: effective_policy, + file_system_sandbox_policy: effective_file_system_policy, + network_sandbox_policy: effective_network_policy, + arg0: arg0_override, + }) + } +} + +#[cfg(test)] +#[path = "manager_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs similarity index 84% rename from codex-rs/core/src/sandboxing/mod_tests.rs rename to codex-rs/sandboxing/src/manager_tests.rs index 62fa4e5bae..17921b1914 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -1,9 +1,9 @@ +use super::SandboxCommand; use super::SandboxManager; -use crate::exec::SandboxType; -use crate::protocol::NetworkAccess; -use crate::protocol::ReadOnlyAccess; -use crate::protocol::SandboxPolicy; -use crate::tools::sandboxing::SandboxablePreference; +use super::SandboxTransformRequest; +use super::SandboxType; +use super::SandboxablePreference; +use super::get_platform_sandbox; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; @@ -14,6 +14,9 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::NetworkAccess; +use codex_protocol::protocol::ReadOnlyAccess; +use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use dunce::canonicalize; use pretty_assertions::assert_eq; @@ -36,7 +39,7 @@ fn danger_full_access_defaults_to_no_sandbox_without_network_requirements() { #[test] fn danger_full_access_uses_platform_sandbox_with_network_requirements() { let manager = SandboxManager::new(); - let expected = crate::safety::get_platform_sandbox(false).unwrap_or(SandboxType::None); + let expected = get_platform_sandbox(false).unwrap_or(SandboxType::None); let sandbox = manager.select_initial( &FileSystemSandboxPolicy::unrestricted(), NetworkSandboxPolicy::Enabled, @@ -50,7 +53,7 @@ fn danger_full_access_uses_platform_sandbox_with_network_requirements() { #[test] fn restricted_file_system_uses_platform_sandbox_without_managed_network() { let manager = SandboxManager::new(); - let expected = crate::safety::get_platform_sandbox(false).unwrap_or(SandboxType::None); + let expected = get_platform_sandbox(false).unwrap_or(SandboxType::None); let sandbox = manager.select_initial( &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -71,20 +74,16 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() let manager = SandboxManager::new(); let cwd = std::env::current_dir().expect("current dir"); let exec_request = manager - .transform(super::SandboxTransformRequest { - spec: super::CommandSpec { + .transform(SandboxTransformRequest { + command: SandboxCommand { program: "true".to_string(), args: Vec::new(), cwd: cwd.clone(), env: HashMap::new(), - expiration: crate::exec::ExecExpiration::DefaultTimeout, - capture_policy: crate::exec::ExecCapturePolicy::ShellTool, - sandbox_permissions: super::SandboxPermissions::UseDefault, additional_permissions: None, - justification: None, }, policy: &SandboxPolicy::ExternalSandbox { - network_access: crate::protocol::NetworkAccess::Restricted, + network_access: NetworkAccess::Restricted, }, file_system_policy: &FileSystemSandboxPolicy::unrestricted(), network_policy: NetworkSandboxPolicy::Restricted, @@ -121,15 +120,12 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { ) .expect("absolute temp dir"); let exec_request = manager - .transform(super::SandboxTransformRequest { - spec: super::CommandSpec { + .transform(SandboxTransformRequest { + command: SandboxCommand { program: "true".to_string(), args: Vec::new(), cwd: cwd.clone(), env: HashMap::new(), - expiration: crate::exec::ExecExpiration::DefaultTimeout, - capture_policy: crate::exec::ExecCapturePolicy::ShellTool, - sandbox_permissions: super::SandboxPermissions::WithAdditionalPermissions, additional_permissions: Some(PermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), @@ -140,7 +136,6 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { }), ..Default::default() }), - justification: None, }, policy: &SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Restricted, @@ -184,15 +179,12 @@ fn transform_additional_permissions_preserves_denied_entries() { let allowed_path = workspace_root.join("allowed").expect("allowed path"); let denied_path = workspace_root.join("denied").expect("denied path"); let exec_request = manager - .transform(super::SandboxTransformRequest { - spec: super::CommandSpec { + .transform(SandboxTransformRequest { + command: SandboxCommand { program: "true".to_string(), args: Vec::new(), cwd: cwd.clone(), env: HashMap::new(), - expiration: crate::exec::ExecExpiration::DefaultTimeout, - capture_policy: crate::exec::ExecCapturePolicy::ShellTool, - sandbox_permissions: super::SandboxPermissions::WithAdditionalPermissions, additional_permissions: Some(PermissionProfile { file_system: Some(FileSystemPermissions { read: None, @@ -200,7 +192,6 @@ fn transform_additional_permissions_preserves_denied_entries() { }), ..Default::default() }), - justification: None, }, policy: &SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess,