diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 5c7500f5a..d479de353 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2278,9 +2278,11 @@ impl CodexMessageProcessor { codex_protocol::models::PermissionProfile::from(permission_profile); let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); + let configured_file_system_sandbox_policy = + self.config.permissions.file_system_sandbox_policy(); Self::preserve_configured_deny_read_restrictions( &mut file_system_sandbox_policy, - &self.config.permissions.file_system_sandbox_policy, + &configured_file_system_sandbox_policy, ); let effective_permission_profile = codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index e173f6573..a59ce31d5 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -189,22 +189,23 @@ async fn run_command_under_sandbox( let mut child = match sandbox_type { #[cfg(target_os = "macos")] SandboxType::Seatbelt => { + let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy(); + let network_sandbox_policy = config.permissions.network_sandbox_policy(); let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command, - file_system_sandbox_policy: &config.permissions.file_system_sandbox_policy, - network_sandbox_policy: config.permissions.network_sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, + network_sandbox_policy, sandbox_policy_cwd: sandbox_policy_cwd.as_path(), enforce_managed_network: false, network: network.as_ref(), extra_allow_unix_sockets: allow_unix_sockets, }); - let network_policy = config.permissions.network_sandbox_policy; spawn_debug_sandbox_child( PathBuf::from("/usr/bin/sandbox-exec"), args, /*arg0*/ None, cwd.to_path_buf(), - network_policy, + network_sandbox_policy, env, |env_map| { env_map.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); @@ -221,23 +222,24 @@ async fn run_command_under_sandbox( .codex_linux_sandbox_exe .expect("codex-linux-sandbox executable not found"); let use_legacy_landlock = config.features.use_legacy_landlock(); + let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy(); + let network_sandbox_policy = config.permissions.network_sandbox_policy(); let args = create_linux_sandbox_command_args_for_policies( command, cwd.as_path(), config.permissions.sandbox_policy.get(), - &config.permissions.file_system_sandbox_policy, - config.permissions.network_sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, sandbox_policy_cwd.as_path(), use_legacy_landlock, /*allow_network_for_proxy*/ false, ); - let network_policy = config.permissions.network_sandbox_policy; spawn_debug_sandbox_child( codex_linux_sandbox_exe, args, Some("codex-linux-sandbox"), cwd.to_path_buf(), - network_policy, + network_sandbox_policy, env, |env_map| { if let Some(network) = network.as_ref() { @@ -715,17 +717,17 @@ mod tests { assert!(config_uses_permission_profiles(&config)); assert!( - profile_config.permissions.file_system_sandbox_policy - != legacy_config.permissions.file_system_sandbox_policy, + profile_config.permissions.file_system_sandbox_policy() + != legacy_config.permissions.file_system_sandbox_policy(), "test fixture should distinguish profile syntax from legacy sandbox_mode" ); assert_eq!( - config.permissions.file_system_sandbox_policy, - profile_config.permissions.file_system_sandbox_policy, + config.permissions.file_system_sandbox_policy(), + profile_config.permissions.file_system_sandbox_policy(), ); assert_ne!( - config.permissions.file_system_sandbox_policy, - legacy_config.permissions.file_system_sandbox_policy, + config.permissions.file_system_sandbox_policy(), + legacy_config.permissions.file_system_sandbox_policy(), ); Ok(()) diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 3c2a97108..e928621b5 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -26,10 +26,10 @@ use codex_plugin::PluginCapabilitySummary; use codex_protocol::mcp::Resource; use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::Tool; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpAuthStatus; use codex_protocol::protocol::McpListToolsResponseEvent; -use codex_protocol::protocol::SandboxPolicy; use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceResult; use serde_json::Value; @@ -66,13 +66,18 @@ pub fn qualified_mcp_tool_name_prefix(server_name: &str) -> String { /// of being shown to the user. pub fn mcp_permission_prompt_is_auto_approved( approval_policy: AskForApproval, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, ) -> bool { - approval_policy == AskForApproval::Never - && matches!( - sandbox_policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) + if approval_policy != AskForApproval::Never { + return false; + } + + match permission_profile { + PermissionProfile::Disabled | PermissionProfile::External { .. } => true, + PermissionProfile::Managed { file_system, .. } => { + file_system.to_sandbox_policy().has_full_disk_write_access() + } + } } /// MCP runtime settings derived from `codex_core::config::Config`. @@ -229,7 +234,7 @@ pub async fn read_mcp_resource( &config.approval_policy, String::new(), tx_event, - SandboxPolicy::new_read_only_policy(), + PermissionProfile::default(), runtime_environment, config.codex_home.clone(), codex_apps_tools_cache_key(auth), @@ -294,7 +299,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( &config.approval_policy, submit_id, tx_event, - SandboxPolicy::new_read_only_policy(), + PermissionProfile::default(), runtime_environment, config.codex_home.clone(), codex_apps_tools_cache_key(auth), diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 01a977077..885dcc890 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -3,6 +3,9 @@ use codex_config::Constrained; use codex_login::CodexAuth; use codex_plugin::AppConnectorId; use codex_plugin::PluginCapabilitySummary; +use codex_protocol::models::ManagedFileSystemPermissions; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use pretty_assertions::assert_eq; use std::collections::HashMap; @@ -33,6 +36,34 @@ fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() { ); } +#[test] +fn mcp_prompt_auto_approval_honors_unrestricted_managed_profiles() { + assert!(mcp_permission_prompt_is_auto_approved( + AskForApproval::Never, + &PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Enabled, + }, + )); + assert!(mcp_permission_prompt_is_auto_approved( + AskForApproval::Never, + &PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Restricted, + }, + )); + assert!(!mcp_permission_prompt_is_auto_approved( + AskForApproval::Never, + &PermissionProfile::read_only(), + )); + assert!(!mcp_permission_prompt_is_auto_approved( + AskForApproval::OnRequest, + &PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Enabled, + }, + )); +} #[test] fn tool_plugin_provenance_collects_app_and_mcp_sources() { let provenance = ToolPluginProvenance::from_capability_summaries(&[ diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager.rs b/codex-rs/codex-mcp/src/mcp_connection_manager.rs index 3b2dffc90..d7e345fb1 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager.rs @@ -334,15 +334,15 @@ fn can_auto_accept_elicitation(elicitation: &CreateElicitationRequestParams) -> struct ElicitationRequestManager { requests: Arc>, approval_policy: Arc>, - sandbox_policy: Arc>, + permission_profile: Arc>, } impl ElicitationRequestManager { - fn new(approval_policy: AskForApproval, sandbox_policy: SandboxPolicy) -> Self { + fn new(approval_policy: AskForApproval, permission_profile: PermissionProfile) -> Self { Self { requests: Arc::new(Mutex::new(HashMap::new())), approval_policy: Arc::new(StdMutex::new(approval_policy)), - sandbox_policy: Arc::new(StdMutex::new(sandbox_policy)), + permission_profile: Arc::new(StdMutex::new(permission_profile)), } } @@ -364,23 +364,23 @@ impl ElicitationRequestManager { fn make_sender(&self, server_name: String, tx_event: Sender) -> SendElicitation { let elicitation_requests = self.requests.clone(); let approval_policy = self.approval_policy.clone(); - let sandbox_policy = self.sandbox_policy.clone(); + let permission_profile = self.permission_profile.clone(); Box::new(move |id, elicitation| { let elicitation_requests = elicitation_requests.clone(); let tx_event = tx_event.clone(); let server_name = server_name.clone(); let approval_policy = approval_policy.clone(); - let sandbox_policy = sandbox_policy.clone(); + let permission_profile = permission_profile.clone(); async move { let approval_policy = approval_policy .lock() .map(|policy| *policy) .unwrap_or(AskForApproval::Never); - let sandbox_policy = sandbox_policy + let permission_profile = permission_profile .lock() - .map(|policy| policy.clone()) - .unwrap_or_else(|_| SandboxPolicy::new_read_only_policy()); - if mcp_permission_prompt_is_auto_approved(approval_policy, &sandbox_policy) + .map(|profile| profile.clone()) + .unwrap_or_default(); + if mcp_permission_prompt_is_auto_approved(approval_policy, &permission_profile) && can_auto_accept_elicitation(&elicitation) { return Ok(ElicitationResponse { @@ -666,14 +666,14 @@ impl AsyncManagedClient { impl McpConnectionManager { pub fn new_uninitialized( approval_policy: &Constrained, - sandbox_policy: &Constrained, + permission_profile: &Constrained, ) -> Self { Self { clients: HashMap::new(), server_origins: HashMap::new(), elicitation_requests: ElicitationRequestManager::new( approval_policy.value(), - sandbox_policy.get().clone(), + permission_profile.get().clone(), ), } } @@ -692,9 +692,9 @@ impl McpConnectionManager { } } - pub fn set_sandbox_policy(&self, sandbox_policy: &SandboxPolicy) { - if let Ok(mut policy) = self.elicitation_requests.sandbox_policy.lock() { - *policy = sandbox_policy.clone(); + pub fn set_permission_profile(&self, permission_profile: PermissionProfile) { + if let Ok(mut profile) = self.elicitation_requests.permission_profile.lock() { + *profile = permission_profile; } } @@ -706,7 +706,7 @@ impl McpConnectionManager { approval_policy: &Constrained, submit_id: String, tx_event: Sender, - initial_sandbox_policy: SandboxPolicy, + initial_permission_profile: PermissionProfile, runtime_environment: McpRuntimeEnvironment, codex_home: PathBuf, codex_apps_tools_cache_key: CodexAppsToolsCacheKey, @@ -718,7 +718,7 @@ impl McpConnectionManager { let mut server_origins = HashMap::new(); let mut join_set = JoinSet::new(); let elicitation_requests = - ElicitationRequestManager::new(approval_policy.value(), initial_sandbox_policy); + ElicitationRequestManager::new(approval_policy.value(), initial_permission_profile); let tool_plugin_provenance = Arc::new(tool_plugin_provenance); let startup_submit_id = submit_id.clone(); let codex_apps_auth_provider = auth diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs b/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs index cf2889ccd..0b9c1f3b6 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager_tests.rs @@ -1,5 +1,6 @@ use super::*; use codex_protocol::ToolName; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::McpAuthStatus; use pretty_assertions::assert_eq; @@ -179,9 +180,9 @@ fn elicitation_granular_policy_respects_never_and_config() { } #[tokio::test] -async fn full_access_auto_accepts_elicitation_with_empty_form_schema() { +async fn disabled_permissions_auto_accept_elicitation_with_empty_form_schema() { let manager = - ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess); + ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled); let (tx_event, _rx_event) = async_channel::bounded(1); let sender = manager.make_sender("server".to_string(), tx_event); @@ -209,9 +210,9 @@ async fn full_access_auto_accepts_elicitation_with_empty_form_schema() { } #[tokio::test] -async fn full_access_does_not_auto_accept_elicitation_with_requested_fields() { +async fn disabled_permissions_do_not_auto_accept_elicitation_with_requested_fields() { let manager = - ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess); + ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled); let (tx_event, _rx_event) = async_channel::bounded(1); let sender = manager.make_sender("server".to_string(), tx_event); @@ -627,8 +628,9 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() { .boxed() .shared(); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); - let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); - let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); + let permission_profile = Constrained::allow_any(PermissionProfile::default()); + let mut manager = + McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile); manager.clients.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), AsyncManagedClient { @@ -654,8 +656,9 @@ async fn resolve_tool_info_accepts_canonical_namespaced_tool_names() { .boxed() .shared(); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); - let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); - let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); + let permission_profile = Constrained::allow_any(PermissionProfile::default()); + let mut manager = + McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile); manager.clients.insert( "rmcp".to_string(), AsyncManagedClient { @@ -689,8 +692,9 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot( .boxed() .shared(); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); - let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); - let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); + let permission_profile = Constrained::allow_any(PermissionProfile::default()); + let mut manager = + McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile); manager.clients.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), AsyncManagedClient { @@ -712,8 +716,9 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty( .boxed() .shared(); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); - let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); - let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); + let permission_profile = Constrained::allow_any(PermissionProfile::default()); + let mut manager = + McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile); manager.clients.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), AsyncManagedClient { @@ -744,8 +749,9 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() { .boxed() .shared(); let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); - let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); - let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy); + let permission_profile = Constrained::allow_any(PermissionProfile::default()); + let mut manager = + McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile); let startup_complete = Arc::new(std::sync::atomic::AtomicBool::new(true)); manager.clients.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), diff --git a/codex-rs/core/src/apply_patch.rs b/codex-rs/core/src/apply_patch.rs index c05a45904..d31b4f034 100644 --- a/codex-rs/core/src/apply_patch.rs +++ b/codex-rs/core/src/apply_patch.rs @@ -35,10 +35,11 @@ pub(crate) async fn apply_patch( file_system_sandbox_policy: &FileSystemSandboxPolicy, action: ApplyPatchAction, ) -> InternalApplyPatchInvocation { + let sandbox_policy = turn_context.sandbox_policy(); match assess_patch_safety( &action, turn_context.approval_policy.value(), - turn_context.sandbox_policy.get(), + &sandbox_policy, file_system_sandbox_policy, &turn_context.cwd, turn_context.windows_sandbox_level, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index dd73cb5e5..3b0dd3359 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -776,7 +776,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: let memories_root = codex_home.path().join("memories").abs(); assert_eq!( - config.permissions.file_system_sandbox_policy, + config.permissions.file_system_sandbox_policy(), FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -814,7 +814,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: } ); assert_eq!( - config.permissions.network_sandbox_policy, + config.permissions.network_sandbox_policy(), NetworkSandboxPolicy::Restricted ); Ok(()) @@ -1082,7 +1082,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i assert_eq!( config .permissions - .file_system_sandbox_policy + .file_system_sandbox_policy() .glob_scan_max_depth, Some(2) ); @@ -1092,7 +1092,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i assert!( config .permissions - .file_system_sandbox_policy + .file_system_sandbox_policy() .entries .contains(&FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { @@ -1104,7 +1104,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i assert!( !config .permissions - .file_system_sandbox_policy + .file_system_sandbox_policy() .entries .iter() .any(|entry| matches!( @@ -1304,7 +1304,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<( .await?; assert_eq!( - config.permissions.file_system_sandbox_policy, + config.permissions.file_system_sandbox_policy(), FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::unknown( @@ -1350,7 +1350,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() .await?; assert_eq!( - config.permissions.file_system_sandbox_policy, + config.permissions.file_system_sandbox_policy(), FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::unknown(":future_special_path", Some("docs".into())), @@ -1377,7 +1377,7 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io .await?; assert_eq!( - config.permissions.file_system_sandbox_policy, + config.permissions.file_system_sandbox_policy(), FileSystemSandboxPolicy::restricted(Vec::new()) ); assert_eq!( @@ -1408,7 +1408,7 @@ async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io:: .await?; assert_eq!( - config.permissions.file_system_sandbox_policy, + config.permissions.file_system_sandbox_policy(), FileSystemSandboxPolicy::restricted(Vec::new()) ); assert!( @@ -1505,7 +1505,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()> .await?; assert!( - config.permissions.network_sandbox_policy.is_enabled(), + config.permissions.network_sandbox_policy().is_enabled(), "expected network sandbox policy to be enabled", ); assert!( @@ -1800,20 +1800,20 @@ exclude_slash_tmp = true let sandbox_policy = config.permissions.sandbox_policy.get(); assert_eq!( - config.permissions.file_system_sandbox_policy, + config.permissions.file_system_sandbox_policy(), FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.path()), "case `{name}` should preserve filesystem semantics from legacy config" ); assert_eq!( - config.permissions.network_sandbox_policy, + config.permissions.network_sandbox_policy(), NetworkSandboxPolicy::from(sandbox_policy), "case `{name}` should preserve network semantics from legacy config" ); assert_eq!( config .permissions - .file_system_sandbox_policy - .to_legacy_sandbox_policy(config.permissions.network_sandbox_policy, cwd.path()) + .file_system_sandbox_policy() + .to_legacy_sandbox_policy(config.permissions.network_sandbox_policy(), cwd.path()) .unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")), sandbox_policy.clone(), "case `{name}` should round-trip through split policies without drift" @@ -5449,10 +5449,6 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { approval_policy: Constrained::allow_any(AskForApproval::Never), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), - file_system_sandbox_policy: FileSystemSandboxPolicy::from( - &SandboxPolicy::new_read_only_policy(), - ), - network_sandbox_policy: NetworkSandboxPolicy::Restricted, network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -5647,10 +5643,6 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), - file_system_sandbox_policy: FileSystemSandboxPolicy::from( - &SandboxPolicy::new_read_only_policy(), - ), - network_sandbox_policy: NetworkSandboxPolicy::Restricted, network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -5799,10 +5791,6 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { approval_policy: Constrained::allow_any(AskForApproval::OnFailure), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), - file_system_sandbox_policy: FileSystemSandboxPolicy::from( - &SandboxPolicy::new_read_only_policy(), - ), - network_sandbox_policy: NetworkSandboxPolicy::Restricted, network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -5936,10 +5924,6 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { approval_policy: Constrained::allow_any(AskForApproval::OnFailure), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), - file_system_sandbox_policy: FileSystemSandboxPolicy::from( - &SandboxPolicy::new_read_only_policy(), - ), - network_sandbox_policy: NetworkSandboxPolicy::Restricted, network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 7f8be38f1..c7f13c63d 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -200,18 +200,6 @@ pub struct Permissions { /// Legacy projection retained while runtime call sites migrate to /// `permission_profile`. pub sandbox_policy: Constrained, - /// Effective filesystem sandbox policy, including entries that cannot yet - /// be fully represented by the legacy [`SandboxPolicy`] projection. - /// - /// Runtime projection retained while callers migrate to - /// `permission_profile`. - pub file_system_sandbox_policy: FileSystemSandboxPolicy, - /// Effective network sandbox policy split out from the legacy - /// [`SandboxPolicy`] projection. - /// - /// Runtime projection retained while callers migrate to - /// `permission_profile`. - pub network_sandbox_policy: NetworkSandboxPolicy, /// Effective network configuration applied to all spawned processes. pub network: Option, /// Whether the model may request a login shell for shell-based tools. @@ -239,14 +227,14 @@ impl Permissions { self.permission_profile.get().clone() } - /// Effective filesystem sandbox policy projection. + /// Effective filesystem sandbox policy derived from the canonical profile. pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { - self.file_system_sandbox_policy.clone() + self.permission_profile.get().file_system_sandbox_policy() } - /// Effective network sandbox policy projection. + /// Effective network sandbox policy derived from the canonical profile. pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy { - self.network_sandbox_policy + self.permission_profile.get().network_sandbox_policy() } /// Replace permissions from a legacy sandbox policy and keep every @@ -269,8 +257,6 @@ impl Permissions { self.sandbox_policy.set(sandbox_policy)?; self.permission_profile.set(permission_profile)?; - self.file_system_sandbox_policy = file_system_sandbox_policy; - self.network_sandbox_policy = network_sandbox_policy; Ok(()) } @@ -294,8 +280,6 @@ impl Permissions { self.permission_profile.set(permission_profile)?; self.sandbox_policy.set(sandbox_policy)?; - self.file_system_sandbox_policy = file_system_sandbox_policy; - self.network_sandbox_policy = network_sandbox_policy; Ok(()) } } @@ -2493,8 +2477,6 @@ impl Config { approval_policy: constrained_approval_policy.value, permission_profile: constrained_permission_profile, sandbox_policy: constrained_sandbox_policy.value, - file_system_sandbox_policy: effective_file_system_sandbox_policy, - network_sandbox_policy: effective_network_sandbox_policy, network, allow_login_shell, shell_environment_policy, diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index 63d73c47a..e021db3d2 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -89,7 +89,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?; let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?; let expected_sibling_arg0_dir = AbsolutePathBuf::try_from(sibling_arg0_dir)?; - let policy = &config.permissions.file_system_sandbox_policy; + let policy = config.permissions.file_system_sandbox_policy(); assert!( policy.can_read_path_with_cwd(expected_zsh.as_path(), &cwd), diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 968b93214..4c710e3a3 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -17,7 +17,7 @@ use codex_connectors::DirectoryListResponse; use codex_exec_server::EnvironmentManager; use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use codex_tools::DiscoverableTool; use rmcp::model::ToolAnnotations; use serde::Deserialize; @@ -274,7 +274,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( &config.permissions.approval_policy, INITIAL_SUBMIT_ID.to_owned(), tx_event, - SandboxPolicy::new_read_only_policy(), + PermissionProfile::default(), McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()), config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), diff --git a/codex-rs/core/src/mcp_skill_dependencies.rs b/codex-rs/core/src/mcp_skill_dependencies.rs index c711d1a15..a97424f13 100644 --- a/codex-rs/core/src/mcp_skill_dependencies.rs +++ b/codex-rs/core/src/mcp_skill_dependencies.rs @@ -221,7 +221,7 @@ async fn should_install_mcp_dependencies( ) -> bool { if mcp_permission_prompt_is_auto_approved( turn_context.approval_policy.value(), - turn_context.sandbox_policy.get(), + &turn_context.permission_profile(), ) { return true; } diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 7a76db9e4..b6646a338 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -524,7 +524,7 @@ async fn augment_mcp_tool_request_meta_with_sandbox_state( let sandbox_state = serde_json::to_value(SandboxState { permission_profile: Some(turn_context.permission_profile()), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(), sandbox_cwd: turn_context.cwd.to_path_buf(), use_legacy_landlock: turn_context.features.use_legacy_landlock(), @@ -830,7 +830,7 @@ async fn maybe_request_mcp_tool_approval( ) -> Option { if mcp_permission_prompt_is_auto_approved( turn_context.approval_policy.value(), - turn_context.sandbox_policy.get(), + &turn_context.permission_profile(), ) { return None; } diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index da0c54900..8b3f22770 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -16,8 +16,8 @@ use codex_config::types::McpServerToolConfig; use codex_hooks::Hooks; use codex_hooks::HooksConfig; use codex_model_provider::create_model_provider; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use core_test_support::PathExt; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -2162,10 +2162,7 @@ async fn full_access_mode_skips_arc_monitor_for_all_approval_modes() { .approval_policy .set(AskForApproval::Never) .expect("test setup should allow updating approval policy"); - turn_context - .sandbox_policy - .set(SandboxPolicy::DangerFullAccess) - .expect("test setup should allow updating sandbox policy"); + turn_context.permission_profile = PermissionProfile::Disabled; let mut config = (*turn_context.config).clone(); config.chatgpt_base_url = server.uri(); turn_context.config = Arc::new(config); diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index d56ceb1e5..f718c309a 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -432,6 +432,7 @@ mod phase2 { use codex_login::CodexAuth; use codex_protocol::AgentPath; use codex_protocol::ThreadId; + use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; @@ -482,8 +483,14 @@ mod phase2 { codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path()) .expect("codex home is absolute"); config.cwd = config.codex_home.clone(); - config.permissions.file_system_sandbox_policy = FileSystemSandboxPolicy::unrestricted(); - config.permissions.network_sandbox_policy = NetworkSandboxPolicy::Enabled; + let permission_profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Enabled, + ); + config + .permissions + .set_permission_profile(permission_profile, config.cwd.as_path()) + .expect("permissions are configurable"); configure(&mut config); let config = Arc::new(config); @@ -712,14 +719,16 @@ mod phase2 { memory_root(&harness.config.codex_home).as_path() ); match &config_snapshot.sandbox_policy { - SandboxPolicy::WorkspaceWrite { - writable_roots, - network_access, - .. - } => { + SandboxPolicy::WorkspaceWrite { network_access, .. } => { assert!(!*network_access); + let effective_writable_roots: Vec<_> = config_snapshot + .sandbox_policy + .get_writable_roots_with_cwd(config_snapshot.cwd.as_path()) + .into_iter() + .map(|root| root.root) + .collect(); pretty_assertions::assert_eq!( - writable_roots.as_slice(), + effective_writable_roots.as_slice(), [memory_root(&harness.config.codex_home)], "consolidation subagent should only be able to write the memory root" ); @@ -740,34 +749,35 @@ mod phase2 { "memory consolidation should not be registered in the root collab agent registry" ); let turn_context = subagent.codex.session.new_default_turn().await; - pretty_assertions::assert_eq!( - turn_context.file_system_sandbox_policy, + let file_system_sandbox_policy = turn_context.file_system_sandbox_policy(); + let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &config_snapshot.sandbox_policy, config_snapshot.cwd.as_path(), + ); + assert!( + file_system_sandbox_policy.is_semantically_equivalent_to( + &legacy_file_system_sandbox_policy, + config_snapshot.cwd.as_path(), ), "consolidation subagent split filesystem policy should match the memory-root legacy policy" ); assert!( - turn_context - .file_system_sandbox_policy - .can_write_path_with_cwd( - memory_root(&harness.config.codex_home).as_path(), - config_snapshot.cwd.as_path(), - ), + file_system_sandbox_policy.can_write_path_with_cwd( + memory_root(&harness.config.codex_home).as_path(), + config_snapshot.cwd.as_path(), + ), "consolidation subagent should be able to write the memory root" ); assert!( - !turn_context - .file_system_sandbox_policy - .can_write_path_with_cwd( - harness.config.codex_home.join("config.toml").as_path(), - config_snapshot.cwd.as_path(), - ), + !file_system_sandbox_policy.can_write_path_with_cwd( + harness.config.codex_home.join("config.toml").as_path(), + config_snapshot.cwd.as_path(), + ), "consolidation subagent should not inherit codex_home write access" ); pretty_assertions::assert_eq!( - turn_context.network_sandbox_policy, + turn_context.network_sandbox_policy(), NetworkSandboxPolicy::Restricted, "consolidation subagent split network policy should preserve no-network sandboxing" ); diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 99cdae53e..eae4608c4 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -233,7 +233,7 @@ impl Session { &turn_context.approval_policy, turn_context.sub_id.clone(), self.get_tx_event(), - turn_context.sandbox_policy.get().clone(), + turn_context.permission_profile(), McpRuntimeEnvironment::new( turn_context .environment diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 459d98549..3eb6fdddf 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -125,7 +125,6 @@ use codex_rollout::state_db; use codex_rollout_trace::AgentResultTracePayload; use codex_rollout_trace::ThreadStartedTraceMetadata; use codex_rollout_trace::ThreadTraceContext; -use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile; use codex_sandboxing::policy_transforms::intersect_permission_profiles; use codex_shell_command::parse_command::parse_command; use codex_terminal_detection::user_agent; @@ -606,9 +605,6 @@ impl Codex { approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile: config.permissions.permission_profile.clone(), - sandbox_policy: config.permissions.sandbox_policy.clone(), - file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), - network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -939,8 +935,7 @@ impl Session { return; }; - let spec = match spec - .recompute_for_sandbox_policy(session_configuration.sandbox_policy.get()) + let spec = match spec.recompute_for_sandbox_policy(&session_configuration.sandbox_policy()) { Ok(spec) => spec, Err(err) => { @@ -1301,8 +1296,9 @@ impl Session { }; let previous_cwd = state.session_configuration.cwd.clone(); - let sandbox_policy_changed = - state.session_configuration.sandbox_policy != updated.sandbox_policy; + let previous_sandbox_policy = state.session_configuration.sandbox_policy(); + let updated_sandbox_policy = updated.sandbox_policy(); + let sandbox_policy_changed = previous_sandbox_policy != updated_sandbox_policy; let next_cwd = updated.cwd.clone(); let codex_home = updated.codex_home.clone(); let session_source = updated.session_source.clone(); diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 8df62ecc8..9401c2d0b 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -129,9 +129,6 @@ pub(super) async fn spawn_review_thread( personality: parent_turn_context.personality, approval_policy: parent_turn_context.approval_policy.clone(), permission_profile: parent_turn_context.permission_profile(), - sandbox_policy: parent_turn_context.sandbox_policy.clone(), - file_system_sandbox_policy: parent_turn_context.file_system_sandbox_policy.clone(), - network_sandbox_policy: parent_turn_context.network_sandbox_policy, network: parent_turn_context.network.clone(), windows_sandbox_level: parent_turn_context.windows_sandbox_level, shell_environment_policy: parent_turn_context.shell_environment_policy.clone(), diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index 7b5674816..89345e2d3 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -67,7 +67,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -108,7 +108,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -918,7 +918,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -996,7 +996,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -1027,7 +1027,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -1142,7 +1142,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -1256,7 +1256,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -1408,7 +1408,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 2226cc04c..1c725745f 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1,5 +1,7 @@ use super::*; use crate::goals::GoalRuntimeState; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSpecialPath; use tokio::sync::Semaphore; /// Context for an initialized model agent @@ -58,12 +60,6 @@ pub(crate) struct SessionConfiguration { pub(super) approvals_reviewer: ApprovalsReviewer, /// Canonical permission profile for the session. pub(super) permission_profile: Constrained, - /// Legacy sandbox projection retained while lower-level callers migrate. - pub(super) sandbox_policy: Constrained, - /// Filesystem sandbox projection of `permission_profile`. - pub(super) file_system_sandbox_policy: FileSystemSandboxPolicy, - /// Network sandbox projection of `permission_profile`. - pub(super) network_sandbox_policy: NetworkSandboxPolicy, pub(super) windows_sandbox_level: WindowsSandboxLevel, /// Absolute working directory that should be treated as the *root* of the @@ -102,12 +98,25 @@ impl SessionConfiguration { } pub(super) fn sandbox_policy(&self) -> SandboxPolicy { - self.sandbox_policy.get().clone() + self.permission_profile() + .to_legacy_sandbox_policy(&self.cwd) + .unwrap_or_else(|_| { + let file_system_sandbox_policy = self.file_system_sandbox_policy(); + codex_sandboxing::compatibility_sandbox_policy_for_permission_profile( + self.permission_profile.get(), + &file_system_sandbox_policy, + self.network_sandbox_policy(), + &self.cwd, + ) + }) } - #[cfg(test)] pub(super) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { - self.file_system_sandbox_policy.clone() + self.permission_profile.get().file_system_sandbox_policy() + } + + pub(super) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy { + self.permission_profile.get().network_sandbox_policy() } pub(super) fn thread_config_snapshot(&self) -> ThreadConfigSnapshot { @@ -129,11 +138,30 @@ impl SessionConfiguration { pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult { let mut next_configuration = self.clone(); - let file_system_policy_matches_legacy = self.file_system_sandbox_policy - == FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - self.sandbox_policy.get(), + let current_sandbox_policy = self.sandbox_policy(); + let current_file_system_sandbox_policy = self.file_system_sandbox_policy(); + let current_network_sandbox_policy = self.network_sandbox_policy(); + let legacy_file_system_projection = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( + ¤t_sandbox_policy, &self.cwd, + ¤t_file_system_sandbox_policy, ); + let file_system_policy_matches_legacy = current_file_system_sandbox_policy + .is_semantically_equivalent_to(&legacy_file_system_projection, &self.cwd); + let file_system_policy_has_rebindable_cwd_write = current_file_system_sandbox_policy + .entries + .iter() + .any(|entry| { + entry.access.can_write() + && matches!( + &entry.path, + FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory + | FileSystemSpecialPath::ProjectRoots { subpath: None }, + } + ) + }); if let Some(collaboration_mode) = updates.collaboration_mode.clone() { next_configuration.collaboration_mode = collaboration_mode; } @@ -181,42 +209,41 @@ impl SessionConfiguration { if let Some(permission_profile) = updates.permission_profile.clone() { next_configuration.set_permission_profile_projection( permission_profile, - Some(&self.file_system_sandbox_policy), + Some(¤t_file_system_sandbox_policy), )?; } else if let Some(sandbox_policy) = updates.sandbox_policy.clone() { - next_configuration.sandbox_policy.set(sandbox_policy)?; - next_configuration.file_system_sandbox_policy = + let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( - next_configuration.sandbox_policy.get(), + &sandbox_policy, &next_configuration.cwd, - &self.file_system_sandbox_policy, + ¤t_file_system_sandbox_policy, ); - next_configuration.network_sandbox_policy = - NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get()); + let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); next_configuration.permission_profile.set( PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy( - next_configuration.sandbox_policy.get(), - ), - &next_configuration.file_system_sandbox_policy, - next_configuration.network_sandbox_policy, + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, ), )?; - } else if cwd_changed && file_system_policy_matches_legacy { + } else if cwd_changed + && file_system_policy_matches_legacy + && file_system_policy_has_rebindable_cwd_write + { // Preserve richer split policies across cwd-only updates; only - // rederive when the session is already using the legacy bridge. - next_configuration.file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - next_configuration.sandbox_policy.get(), + // rederive when the session is already using a structurally + // cwd-bound legacy bridge. + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( + ¤t_sandbox_policy, &next_configuration.cwd, + ¤t_file_system_sandbox_policy, ); next_configuration.permission_profile.set( PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy( - next_configuration.sandbox_policy.get(), - ), - &next_configuration.file_system_sandbox_policy, - next_configuration.network_sandbox_policy, + SandboxEnforcement::from_legacy_sandbox_policy(¤t_sandbox_policy), + &file_system_sandbox_policy, + current_network_sandbox_policy, ), )?; } @@ -247,16 +274,7 @@ impl SessionConfiguration { &file_system_sandbox_policy, network_sandbox_policy, ); - let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - &effective_permission_profile, - &file_system_sandbox_policy, - network_sandbox_policy, - self.cwd.as_path(), - ); self.permission_profile.set(effective_permission_profile)?; - self.sandbox_policy.set(sandbox_policy)?; - self.file_system_sandbox_policy = file_system_sandbox_policy; - self.network_sandbox_policy = network_sandbox_policy; Ok(()) } } @@ -487,7 +505,7 @@ impl Session { model: session_configuration.collaboration_mode.model().to_string(), provider_name: config.model_provider_id.clone(), approval_policy: session_configuration.approval_policy.value().to_string(), - sandbox_policy: format!("{:?}", session_configuration.sandbox_policy.get()), + sandbox_policy: format!("{:?}", session_configuration.sandbox_policy()), }; let rollout_thread_trace = if matches!( session_configuration.session_source, @@ -768,7 +786,7 @@ impl Session { // setup is straightforward enough and performs well. mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( &config.permissions.approval_policy, - &config.permissions.sandbox_policy, + &config.permissions.permission_profile, ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( @@ -847,6 +865,7 @@ impl Session { // Dispatch the SessionConfiguredEvent first and then report any errors. // If resuming, include converted initial messages in the payload so UIs can render them immediately. let initial_messages = initial_history.get_event_msgs(); + let session_sandbox_policy = session_configuration.sandbox_policy(); let events = std::iter::once(Event { id: INITIAL_SUBMIT_ID.to_owned(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { @@ -858,7 +877,7 @@ impl Session { service_tier: session_configuration.service_tier, approval_policy: session_configuration.approval_policy.value(), approvals_reviewer: session_configuration.approvals_reviewer, - sandbox_policy: session_configuration.sandbox_policy.get().clone(), + sandbox_policy: session_sandbox_policy.clone(), permission_profile: Some(session_configuration.permission_profile()), cwd: session_configuration.cwd.clone(), reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(), @@ -867,7 +886,7 @@ impl Session { initial_messages, network_proxy: session_network_proxy.filter(|_| { Self::managed_network_proxy_active_for_sandbox_policy( - session_configuration.sandbox_policy.get(), + &session_sandbox_policy, ) }), rollout_path, @@ -901,7 +920,7 @@ impl Session { &session_configuration.approval_policy, INITIAL_SUBMIT_ID.to_owned(), tx_event.clone(), - session_configuration.sandbox_policy.get().clone(), + session_configuration.permission_profile(), McpRuntimeEnvironment::new( sess.services .environment_manager diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 109c80666..0206ee460 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -38,6 +38,7 @@ use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -779,11 +780,19 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow let mut state = session.state.lock().await; let mut config = (*state.session_configuration.original_config_do_not_use).clone(); config.permissions.network = Some(spec); - config.permissions.sandbox_policy = - codex_config::Constrained::allow_any(initial_policy.clone()); + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(initial_policy.clone(), cwd.as_path()) + .expect("test setup should allow sandbox policy"); state.session_configuration.original_config_do_not_use = Arc::new(config); - state.session_configuration.sandbox_policy = - codex_config::Constrained::allow_any(initial_policy); + state + .session_configuration + .permission_profile + .set(PermissionProfile::from_legacy_sandbox_policy( + &initial_policy, + )) + .expect("test setup should allow permission profile"); } session.services.network_proxy = Some(started_proxy); @@ -829,6 +838,8 @@ async fn danger_full_access_turns_do_not_expose_managed_network_proxy() -> anyho let session = make_session_with_config(move |config| { config.permissions.sandbox_policy = codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess); + config.permissions.permission_profile = + codex_config::Constrained::allow_any(PermissionProfile::Disabled); config.permissions.network = Some(network_spec); }) .await?; @@ -892,6 +903,8 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an let session = make_session_with_config(move |config| { config.permissions.sandbox_policy = codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess); + config.permissions.permission_profile = + codex_config::Constrained::allow_any(PermissionProfile::Disabled); config.permissions.network = Some(network_spec); let layers = config @@ -1492,12 +1505,10 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() -> let expected_sandbox_policy = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy); - config.permissions.file_system_sandbox_policy = FileSystemSandboxPolicy::external_sandbox(); - config.permissions.network_sandbox_policy = NetworkSandboxPolicy::Restricted; config.permissions.permission_profile = codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions( - &config.permissions.file_system_sandbox_policy, - config.permissions.network_sandbox_policy, + &FileSystemSandboxPolicy::external_sandbox(), + NetworkSandboxPolicy::Restricted, )); }); @@ -1654,7 +1665,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -2253,9 +2264,6 @@ async fn set_rate_limits_retains_previous_credits() { approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile: config.permissions.permission_profile.clone(), - sandbox_policy: config.permissions.sandbox_policy.clone(), - file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), - network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -2358,9 +2366,6 @@ async fn set_rate_limits_updates_plan_type_when_present() { approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile: config.permissions.permission_profile.clone(), - sandbox_policy: config.permissions.sandbox_policy.clone(), - file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), - network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -2808,9 +2813,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile: config.permissions.permission_profile.clone(), - sandbox_policy: config.permissions.sandbox_policy.clone(), - file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), - network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -2840,7 +2842,7 @@ fn turn_environments_for_tests( } #[tokio::test] -async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_only_update() { +async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd_only_update() { let mut session_configuration = make_session_configuration_for_tests().await; let workspace = tempfile::tempdir().expect("create temp dir"); let project_root = workspace.path().join("project"); @@ -2850,14 +2852,13 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o let docs_dir = docs_dir.abs(); session_configuration.cwd = original_cwd.abs(); - session_configuration.sandbox_policy = - codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }); - session_configuration.file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ + let sandbox_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::CurrentWorkingDirectory, @@ -2869,6 +2870,14 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o access: FileSystemAccessMode::Read, }, ]); + let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); + session_configuration.permission_profile = codex_config::Constrained::allow_any( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, + ), + ); let updated = session_configuration .apply(&SessionSettingsUpdate { @@ -2878,8 +2887,8 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o .expect("cwd-only update should succeed"); assert_eq!( - updated.file_system_sandbox_policy, - session_configuration.file_system_sandbox_policy + updated.file_system_sandbox_policy(), + file_system_sandbox_policy ); } @@ -2890,8 +2899,6 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ session_configuration.cwd = cwd.path().abs(); let workspace_policy = SandboxPolicy::new_workspace_write_policy(); - session_configuration.sandbox_policy = - codex_config::Constrained::allow_any(workspace_policy.clone()); let deny_entry = FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), @@ -2905,7 +2912,13 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ ); existing_file_system_policy.glob_scan_max_depth = Some(2); existing_file_system_policy.entries.push(deny_entry.clone()); - session_configuration.file_system_sandbox_policy = existing_file_system_policy; + session_configuration.permission_profile = codex_config::Constrained::allow_any( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&workspace_policy), + &existing_file_system_policy, + NetworkSandboxPolicy::Restricted, + ), + ); let requested_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &workspace_policy, @@ -2926,7 +2939,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ expected_file_system_policy.glob_scan_max_depth = Some(2); expected_file_system_policy.entries.push(deny_entry); assert_eq!( - updated.file_system_sandbox_policy, + updated.file_system_sandbox_policy(), expected_file_system_policy ); } @@ -3070,18 +3083,23 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_ let project_root = workspace.path().join("project"); let original_cwd = project_root.join("subdir"); session_configuration.cwd = original_cwd.abs(); - session_configuration.sandbox_policy = - codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }); - session_configuration.file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - session_configuration.sandbox_policy.get(), - &session_configuration.cwd, - ); + let sandbox_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + &sandbox_policy, + &session_configuration.cwd, + ); + session_configuration.permission_profile = codex_config::Constrained::allow_any( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + NetworkSandboxPolicy::from(&sandbox_policy), + ), + ); let updated = session_configuration .apply(&SessionSettingsUpdate { @@ -3090,12 +3108,73 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_ }) .expect("cwd-only update should succeed"); + let expected_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + &updated.sandbox_policy(), + &project_root, + ); + assert!( + updated + .file_system_sandbox_policy() + .is_semantically_equivalent_to(&expected_file_system_policy, &project_root), + "cwd-only update should rederive the legacy filesystem policy for the new cwd" + ); +} + +#[tokio::test] +async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_update() { + let mut session_configuration = make_session_configuration_for_tests().await; + let workspace = tempfile::tempdir().expect("create temp dir"); + let original_cwd = workspace.path().join("repo-a"); + let next_cwd = workspace.path().join("repo-b"); + std::fs::create_dir_all(&original_cwd).expect("create original cwd"); + std::fs::create_dir_all(&next_cwd).expect("create next cwd"); + let original_cwd = original_cwd.abs(); + + session_configuration.cwd = original_cwd.clone(); + let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: original_cwd.clone(), + }, + access: FileSystemAccessMode::Write, + }, + ]); + session_configuration.permission_profile = codex_config::Constrained::allow_any( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::Managed, + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + ), + ); + + let updated = session_configuration + .apply(&SessionSettingsUpdate { + cwd: Some(next_cwd.clone()), + ..Default::default() + }) + .expect("cwd-only update should succeed"); + assert_eq!( - updated.file_system_sandbox_policy, - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - updated.sandbox_policy.get(), - &project_root, - ) + updated.file_system_sandbox_policy(), + file_system_sandbox_policy + ); + assert!( + updated + .file_system_sandbox_policy() + .can_write_path_with_cwd(original_cwd.as_path(), updated.cwd.as_path()), + "absolute grant to the old cwd must remain writable" + ); + assert!( + !updated + .file_system_sandbox_policy() + .can_write_path_with_cwd(next_cwd.as_path(), updated.cwd.as_path()), + "cwd-only update must not reinterpret an absolute old-cwd grant as :cwd" ); } @@ -3170,9 +3249,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile: config.permissions.permission_profile.clone(), - sandbox_policy: config.permissions.sandbox_policy.clone(), - file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), - network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -3277,9 +3353,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile: config.permissions.permission_profile.clone(), - sandbox_policy: config.permissions.sandbox_policy.clone(), - file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), - network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -3325,7 +3398,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( &config.permissions.approval_policy, - &config.permissions.sandbox_policy, + &config.permissions.permission_profile, ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( @@ -3492,9 +3565,6 @@ async fn make_session_with_config_and_rx( approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile: config.permissions.permission_profile.clone(), - sandbox_policy: config.permissions.sandbox_policy.clone(), - file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), - network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -4642,9 +4712,6 @@ where approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile: config.permissions.permission_profile.clone(), - sandbox_policy: config.permissions.sandbox_policy.clone(), - file_system_sandbox_policy: config.permissions.file_system_sandbox_policy.clone(), - network_sandbox_policy: config.permissions.network_sandbox_policy, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), @@ -4690,7 +4757,7 @@ where let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( &config.permissions.approval_policy, - &config.permissions.sandbox_policy, + &config.permissions.permission_profile, ))), mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()), unified_exec_manager: UnifiedExecProcessManager::new( @@ -5572,7 +5639,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSystemSandboxPolicy { let mut policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - turn_context.sandbox_policy.get(), + &turn_context.sandbox_policy(), &turn_context.cwd, ); policy.entries.push(FileSystemSandboxEntry { @@ -5586,12 +5653,7 @@ fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSy #[tokio::test] async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() { - let (_session, mut turn_context) = make_session_and_context().await; - turn_context.file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - turn_context.sandbox_policy.get(), - &turn_context.cwd, - ); + let (_session, turn_context) = make_session_and_context().await; let item = turn_context.to_turn_context_item(); @@ -5606,7 +5668,11 @@ async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() async fn turn_context_item_stores_split_file_system_sandbox_policy_when_different() { let (_session, mut turn_context) = make_session_and_context().await; let file_system_sandbox_policy = file_system_policy_with_unreadable_glob(&turn_context); - turn_context.file_system_sandbox_policy = file_system_sandbox_policy.clone(); + turn_context.permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + turn_context.permission_profile.enforcement(), + &file_system_sandbox_policy, + turn_context.network_sandbox_policy(), + ); let item = turn_context.to_turn_context_item(); @@ -5743,7 +5809,11 @@ async fn record_context_updates_and_set_reference_context_item_persists_split_fi { let (mut session, mut turn_context) = make_session_and_context().await; let file_system_sandbox_policy = file_system_policy_with_unreadable_glob(&turn_context); - turn_context.file_system_sandbox_policy = file_system_sandbox_policy.clone(); + turn_context.permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + turn_context.permission_profile.enforcement(), + &file_system_sandbox_policy, + turn_context.network_sandbox_policy(), + ); let rollout_path = attach_thread_persistence(&mut session).await; session @@ -7674,7 +7744,6 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { use crate::tools::sandboxing::ExecApprovalRequirement; use crate::turn_diff_tracker::TurnDiffTracker; use codex_protocol::protocol::AskForApproval; - use codex_protocol::protocol::SandboxPolicy; use std::collections::HashMap; let (session, mut turn_context_raw) = make_session_and_context().await; @@ -7761,23 +7830,18 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { // command. Force DangerFullAccess so this check stays focused on approval // policy rather than platform-specific sandbox behavior. let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique turn context Arc"); - turn_context_mut - .sandbox_policy - .set(SandboxPolicy::DangerFullAccess) - .expect("test setup should allow updating sandbox policy"); - turn_context_mut.file_system_sandbox_policy = - FileSystemSandboxPolicy::from(turn_context_mut.sandbox_policy.get()); - turn_context_mut.network_sandbox_policy = - NetworkSandboxPolicy::from(turn_context_mut.sandbox_policy.get()); + turn_context_mut.permission_profile = PermissionProfile::Disabled; + let file_system_sandbox_policy = turn_context.file_system_sandbox_policy(); + let sandbox_policy = turn_context.sandbox_policy(); let exec_approval_requirement = session .services .exec_policy .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: ¶ms.command, approval_policy: turn_context.approval_policy.value(), - sandbox_policy: turn_context.sandbox_policy.get(), - file_system_sandbox_policy: &turn_context.file_system_sandbox_policy, + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index f527182fd..d76660b29 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -25,8 +25,6 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::NetworkPermissions; use codex_protocol::models::ResponseItem; use codex_protocol::models::function_call_output_content_items_to_text; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; @@ -274,17 +272,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid .features .enable(Feature::ExecPermissionApprovals) .expect("test setup should allow enabling request permissions"); - turn_context_raw - .sandbox_policy - .set(SandboxPolicy::DangerFullAccess) - .expect("test setup should allow updating sandbox policy"); - // This test is about request-permissions validation, not managed sandbox - // policy enforcement. Widen the derived sandbox policies directly so the - // command runs without depending on a platform sandbox binary. - turn_context_raw.file_system_sandbox_policy = - FileSystemSandboxPolicy::from(turn_context_raw.sandbox_policy.get()); - turn_context_raw.network_sandbox_policy = - NetworkSandboxPolicy::from(turn_context_raw.sandbox_policy.get()); + turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled; let mut config = (*turn_context_raw.config).clone(); config.model_provider.base_url = Some(format!("{}/v1", server.uri())); let config = Arc::new(config); @@ -429,14 +417,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_policy_skip() { .approval_policy .set(AskForApproval::OnFailure) .expect("test setup should allow updating approval policy"); - turn_context_raw - .sandbox_policy - .set(SandboxPolicy::DangerFullAccess) - .expect("test setup should allow updating sandbox policy"); - turn_context_raw.file_system_sandbox_policy = - FileSystemSandboxPolicy::from(turn_context_raw.sandbox_policy.get()); - turn_context_raw.network_sandbox_policy = - NetworkSandboxPolicy::from(turn_context_raw.sandbox_policy.get()); + turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled; let mut config = (*turn_context_raw.config).clone(); config.approvals_reviewer = ApprovalsReviewer::User; config.model_provider.base_url = Some(format!("{}/v1", server.uri())); diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 2577ec47d..827053f0f 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -692,13 +692,13 @@ async fn track_turn_resolved_config_analytics( session_source: thread_config.session_source, model: turn_context.model_info.slug.clone(), model_provider: turn_context.config.model_provider_id.clone(), - sandbox_policy: turn_context.sandbox_policy.get().clone(), + sandbox_policy: turn_context.sandbox_policy(), reasoning_effort: turn_context.reasoning_effort, reasoning_summary: Some(turn_context.reasoning_summary), service_tier: turn_context.config.service_tier, approval_policy: turn_context.approval_policy.value(), approvals_reviewer: turn_context.config.approvals_reviewer, - sandbox_network_access: turn_context.network_sandbox_policy.is_enabled(), + sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(), collaboration_mode: turn_context.collaboration_mode.mode, personality: turn_context.personality, is_first_turn, @@ -1871,7 +1871,7 @@ async fn try_run_sampling_request( feedback_tags!( model = turn_context.model_info.slug.clone(), approval_policy = turn_context.approval_policy.value(), - sandbox_policy = turn_context.sandbox_policy.get(), + sandbox_policy = &turn_context.sandbox_policy(), effort = turn_context.reasoning_effort, auth_mode = sess.services.auth_manager.auth_mode(), features = sess.features.enabled_features(), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index e5916a935..3cdaad2b4 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -3,6 +3,7 @@ use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile; use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use std::sync::atomic::AtomicBool; @@ -73,9 +74,6 @@ pub(crate) struct TurnContext { pub(crate) personality: Option, pub(crate) approval_policy: Constrained, pub(crate) permission_profile: PermissionProfile, - pub(crate) sandbox_policy: Constrained, - pub(crate) file_system_sandbox_policy: FileSystemSandboxPolicy, - pub(crate) network_sandbox_policy: NetworkSandboxPolicy, pub(crate) network: Option, pub(crate) windows_sandbox_level: WindowsSandboxLevel, pub(crate) shell_environment_policy: ShellEnvironmentPolicy, @@ -99,6 +97,25 @@ impl TurnContext { self.permission_profile.clone() } + pub(crate) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { + self.permission_profile.file_system_sandbox_policy() + } + + pub(crate) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy { + self.permission_profile.network_sandbox_policy() + } + + pub(crate) fn sandbox_policy(&self) -> SandboxPolicy { + let file_system_sandbox_policy = self.file_system_sandbox_policy(); + let network_sandbox_policy = self.network_sandbox_policy(); + compatibility_sandbox_policy_for_permission_profile( + &self.permission_profile, + &file_system_sandbox_policy, + network_sandbox_policy, + &self.cwd, + ) + } + pub(crate) fn model_context_window(&self) -> Option { let effective_context_window_percent = self.model_info.effective_context_window_percent; self.model_info @@ -210,9 +227,6 @@ impl TurnContext { personality: self.personality, approval_policy: self.approval_policy.clone(), permission_profile: self.permission_profile.clone(), - sandbox_policy: self.sandbox_policy.clone(), - file_system_sandbox_policy: self.file_system_sandbox_policy.clone(), - network_sandbox_policy: self.network_sandbox_policy, network: self.network.clone(), windows_sandbox_level: self.windows_sandbox_level, shell_environment_policy: self.shell_environment_policy.clone(), @@ -246,12 +260,14 @@ impl TurnContext { &self, additional_permissions: Option, ) -> FileSystemSandboxContext { + let (base_file_system_sandbox_policy, base_network_sandbox_policy) = + self.permission_profile.to_runtime_permissions(); let file_system_sandbox_policy = effective_file_system_sandbox_policy( - &self.file_system_sandbox_policy, + &base_file_system_sandbox_policy, additional_permissions.as_ref(), ); let network_sandbox_policy = effective_network_sandbox_policy( - self.network_sandbox_policy, + base_network_sandbox_policy, additional_permissions.as_ref(), ); let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( @@ -278,11 +294,12 @@ impl TurnContext { // this comparison and the legacy projection should go away. let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - self.sandbox_policy.get(), + &self.sandbox_policy(), &self.cwd, ); - (self.file_system_sandbox_policy != legacy_file_system_sandbox_policy) - .then(|| self.file_system_sandbox_policy.clone()) + let file_system_sandbox_policy = self.file_system_sandbox_policy(); + (file_system_sandbox_policy != legacy_file_system_sandbox_policy) + .then_some(file_system_sandbox_policy) } pub(crate) fn compact_prompt(&self) -> &str { @@ -299,7 +316,7 @@ impl TurnContext { current_date: self.current_date.clone(), timezone: self.timezone.clone(), approval_policy: self.approval_policy.value(), - sandbox_policy: self.sandbox_policy.get().clone(), + sandbox_policy: self.sandbox_policy(), permission_profile: Some(self.permission_profile()), network: self.turn_context_network_item(), file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(), @@ -366,15 +383,11 @@ impl Session { per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer; per_turn_config.permissions.permission_profile = session_configuration.permission_profile.clone(); - per_turn_config.permissions.sandbox_policy = session_configuration.sandbox_policy.clone(); - per_turn_config.permissions.file_system_sandbox_policy = - session_configuration.file_system_sandbox_policy.clone(); - per_turn_config.permissions.network_sandbox_policy = - session_configuration.network_sandbox_policy; - let resolved_web_search_mode = resolve_web_search_mode_for_turn( - &per_turn_config.web_search_mode, - session_configuration.sandbox_policy.get(), - ); + let sandbox_policy = session_configuration.sandbox_policy(); + per_turn_config.permissions.sandbox_policy = + Constrained::allow_only(sandbox_policy.clone()); + let resolved_web_search_mode = + resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &sandbox_policy); if let Err(err) = per_turn_config .web_search_mode .set(resolved_web_search_mode) @@ -489,9 +502,6 @@ impl Session { personality: session_configuration.personality, approval_policy: session_configuration.approval_policy.clone(), permission_profile: session_configuration.permission_profile(), - sandbox_policy: session_configuration.sandbox_policy.clone(), - file_system_sandbox_policy: session_configuration.file_system_sandbox_policy.clone(), - network_sandbox_policy: session_configuration.network_sandbox_policy, network, windows_sandbox_level: session_configuration.windows_sandbox_level, shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(), @@ -528,8 +538,9 @@ impl Session { let turn_environments = self.resolve_turn_environments(&effective_environments)?; let previous_cwd = state.session_configuration.cwd.clone(); - let sandbox_policy_changed = - state.session_configuration.sandbox_policy != next.sandbox_policy; + let previous_sandbox_policy = state.session_configuration.sandbox_policy(); + let next_sandbox_policy = next.sandbox_policy(); + let sandbox_policy_changed = previous_sandbox_policy != next_sandbox_policy; let codex_home = next.codex_home.clone(); let session_source = next.session_source.clone(); state.session_configuration = next.clone(); @@ -635,7 +646,8 @@ impl Session { { let mcp_connection_manager = self.services.mcp_connection_manager.read().await; mcp_connection_manager.set_approval_policy(&session_configuration.approval_policy); - mcp_connection_manager.set_sandbox_policy(session_configuration.sandbox_policy.get()); + mcp_connection_manager + .set_permission_profile(session_configuration.permission_profile()); } let model_info = self @@ -680,7 +692,7 @@ impl Session { .as_ref() .and_then(|started_proxy| { Self::managed_network_proxy_active_for_sandbox_policy( - session_configuration.sandbox_policy.get(), + &session_configuration.sandbox_policy(), ) .then(|| started_proxy.proxy()) }), diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 9d63ad0a8..7c1b7a92f 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -272,8 +272,9 @@ async fn effective_patch_permissions( session.granted_session_permissions().await.as_ref(), session.granted_turn_permissions().await.as_ref(), ); + let base_file_system_sandbox_policy = turn.file_system_sandbox_policy(); let file_system_sandbox_policy = effective_file_system_sandbox_policy( - &turn.file_system_sandbox_policy, + &base_file_system_sandbox_policy, granted_permissions.as_ref(), ); let effective_additional_permissions = apply_granted_turn_permissions( diff --git a/codex-rs/core/src/tools/handlers/list_dir.rs b/codex-rs/core/src/tools/handlers/list_dir.rs index 22909c1d1..047906003 100644 --- a/codex-rs/core/src/tools/handlers/list_dir.rs +++ b/codex-rs/core/src/tools/handlers/list_dir.rs @@ -99,7 +99,8 @@ impl ToolHandler for ListDirHandler { "dir_path must be an absolute path".to_string(), )); } - let read_deny_matcher = ReadDenyMatcher::new(&turn.file_system_sandbox_policy, &turn.cwd); + let file_system_sandbox_policy = turn.file_system_sandbox_policy(); + let read_deny_matcher = ReadDenyMatcher::new(&file_system_sandbox_policy, &turn.cwd); if read_deny_matcher .as_ref() .is_some_and(|matcher| matcher.is_read_denied(&path)) diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 5f2c6b096..705a9ecb4 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -37,6 +37,9 @@ use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::FileSystemAccessMode; +use codex_protocol::protocol::FileSystemPath; +use codex_protocol::protocol::FileSystemSandboxEntry; use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::InterAgentCommunication; @@ -2074,21 +2077,6 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() { #[tokio::test] async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { - fn pick_allowed_sandbox_policy( - constraint: &crate::config::Constrained, - base: SandboxPolicy, - ) -> SandboxPolicy { - let candidates = [ - SandboxPolicy::DangerFullAccess, - SandboxPolicy::new_workspace_write_policy(), - SandboxPolicy::new_read_only_policy(), - ]; - candidates - .into_iter() - .find(|candidate| *candidate != base && constraint.can_set(candidate).is_ok()) - .unwrap_or(base) - } - #[derive(Debug, Deserialize)] struct SpawnAgentResult { agent_id: String, @@ -2098,12 +2086,17 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { let (mut session, mut turn) = make_session_and_context().await; let manager = thread_manager(); session.services.agent_control = manager.agent_control(); - let expected_sandbox = pick_allowed_sandbox_policy( - &turn.config.permissions.sandbox_policy, - turn.config.permissions.sandbox_policy.get().clone(), - ); - let expected_file_system_sandbox_policy = + let expected_sandbox = turn.config.permissions.sandbox_policy.get().clone(); + let mut expected_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected_sandbox, &turn.cwd); + expected_file_system_sandbox_policy + .entries + .push(FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/.env".to_string(), + }, + access: FileSystemAccessMode::None, + }); let expected_network_sandbox_policy = NetworkSandboxPolicy::from(&expected_sandbox); let expected_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( SandboxEnforcement::from_legacy_sandbox_policy(&expected_sandbox), @@ -2113,16 +2106,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { turn.approval_policy .set(AskForApproval::OnRequest) .expect("approval policy should be set"); - turn.sandbox_policy - .set(expected_sandbox.clone()) - .expect("sandbox policy should be set"); - turn.file_system_sandbox_policy = expected_file_system_sandbox_policy.clone(); - turn.network_sandbox_policy = expected_network_sandbox_policy; turn.permission_profile = expected_permission_profile.clone(); assert_ne!( - expected_sandbox, - turn.config.permissions.sandbox_policy.get().clone(), - "test requires a runtime sandbox override that differs from base config" + expected_permission_profile, + turn.config.permissions.permission_profile(), + "test requires a runtime profile override that differs from base config" ); let invocation = invocation( @@ -2164,11 +2152,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { .expect("spawned agent thread should exist"); let child_turn = child_thread.codex.session.new_default_turn().await; assert_eq!( - child_turn.file_system_sandbox_policy, + child_turn.file_system_sandbox_policy(), expected_file_system_sandbox_policy ); assert_eq!( - child_turn.network_sandbox_policy, + child_turn.network_sandbox_policy(), expected_network_sandbox_policy ); assert_eq!(child_turn.permission_profile(), expected_permission_profile); @@ -3637,11 +3625,6 @@ async fn build_agent_spawn_config_uses_turn_context_values() { &file_system_sandbox_policy, network_sandbox_policy, ); - turn.sandbox_policy - .set(sandbox_policy) - .expect("sandbox policy set"); - turn.file_system_sandbox_policy = file_system_sandbox_policy; - turn.network_sandbox_policy = network_sandbox_policy; turn.permission_profile = permission_profile.clone(); turn.approval_policy .set(AskForApproval::OnRequest) @@ -3718,7 +3701,7 @@ async fn build_agent_resume_config_clears_base_instructions() { expected .permissions .sandbox_policy - .set(turn.sandbox_policy.get().clone()) + .set(turn.sandbox_policy()) .expect("sandbox policy set"); assert_eq!(config, expected); } diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 17daaa738..b43fab30b 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -513,14 +513,16 @@ impl ShellHandler { ); emitter.begin(event_ctx).await; + let file_system_sandbox_policy = turn.file_system_sandbox_policy(); + let sandbox_policy = turn.sandbox_policy(); let exec_approval_requirement = session .services .exec_policy .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &exec_params.command, approval_policy: turn.approval_policy.value(), - sandbox_policy: turn.sandbox_policy.get(), - file_system_sandbox_policy: &turn.file_system_sandbox_policy, + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions: if effective_additional_permissions.permissions_preapproved { codex_protocol::models::SandboxPermissions::UseDefault } else { diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 888add09f..af0331700 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -359,7 +359,8 @@ impl NetworkApprovalService { .await; return NetworkDecision::deny(REASON_NOT_ALLOWED); }; - if !sandbox_policy_allows_network_approval_flow(turn_context.sandbox_policy.get()) { + let sandbox_policy = turn_context.sandbox_policy(); + if !sandbox_policy_allows_network_approval_flow(&sandbox_policy) { pending.set_decision(PendingApprovalDecision::Deny).await; self.pending_host_approvals.lock().await.remove(&key); self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 6621756c1..0b59215f2 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -122,8 +122,10 @@ impl ToolOrchestrator { // 1) Approval let mut already_approved = false; + let file_system_sandbox_policy = turn_ctx.file_system_sandbox_policy(); + let network_sandbox_policy = turn_ctx.network_sandbox_policy(); let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| { - default_exec_approval_requirement(approval_policy, &turn_ctx.file_system_sandbox_policy) + default_exec_approval_requirement(approval_policy, &file_system_sandbox_policy) }); match requirement { ExecApprovalRequirement::Skip { .. } => { @@ -194,8 +196,8 @@ impl ToolOrchestrator { let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None, SandboxOverride::NoOverride => self.sandbox.select_initial( - &turn_ctx.file_system_sandbox_policy, - turn_ctx.network_sandbox_policy, + &file_system_sandbox_policy, + network_sandbox_policy, tool.sandbox_preference(), turn_ctx.windows_sandbox_level, managed_network_active, @@ -268,7 +270,7 @@ impl ToolOrchestrator { && matches!( default_exec_approval_requirement( approval_policy, - &turn_ctx.file_system_sandbox_policy + &file_system_sandbox_policy ), ExecApprovalRequirement::NeedsApproval { .. } ); 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 927d1b1ce..b02ad0877 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 @@ -390,11 +390,12 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul ..HooksConfig::default() }); - let sandbox_policy = SandboxPolicy::new_read_only_policy(); turn_context.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - turn_context.sandbox_policy = Constrained::allow_any(sandbox_policy.clone()); - turn_context.file_system_sandbox_policy = read_only_file_system_sandbox_policy(); - turn_context.network_sandbox_policy = NetworkSandboxPolicy::Restricted; + turn_context.permission_profile = PermissionProfile::from_runtime_permissions( + &read_only_file_system_sandbox_policy(), + NetworkSandboxPolicy::Restricted, + ); + let sandbox_policy = SandboxPolicy::new_read_only_policy(); let workdir = AbsolutePathBuf::try_from(std::env::current_dir()?)?; let target = std::env::temp_dir().join("execve-hook-short-circuit.txt"); diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index ec0b00cf5..bd4452ce1 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -788,6 +788,8 @@ impl UnifiedExecProcessManager { self, context.turn.tools_config.unified_exec_shell_mode.clone(), ); + let file_system_sandbox_policy = context.turn.file_system_sandbox_policy(); + let sandbox_policy = context.turn.sandbox_policy(); let exec_approval_requirement = context .session .services @@ -795,8 +797,8 @@ impl UnifiedExecProcessManager { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &request.command, approval_policy: context.turn.approval_policy.value(), - sandbox_policy: context.turn.sandbox_policy.get(), - file_system_sandbox_policy: &context.turn.file_system_sandbox_policy, + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions: if request.additional_permissions_preapproved { crate::sandboxing::SandboxPermissions::UseDefault } else { diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index 2391e35fd..46bedff36 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -14,10 +14,12 @@ use codex_config::types::McpServerConfig; use codex_config::types::McpServerTransportConfig; use codex_core::sandboxing::SandboxPermissions; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnEnvironmentSelection; @@ -560,7 +562,11 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> { }, access: FileSystemAccessMode::None, }); - config.permissions.file_system_sandbox_policy = file_system_sandbox_policy; + config.permissions.permission_profile = + Constrained::allow_any(PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + )); }); let fixture = builder.build(&server).await?; diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index ab7011039..67226ef20 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -2527,10 +2527,12 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> { use codex_config::Constrained; + use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; + use codex_protocol::permissions::NetworkSandboxPolicy; skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); @@ -2553,7 +2555,11 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> { }, access: FileSystemAccessMode::None, }); - config.permissions.file_system_sandbox_policy = file_system_sandbox_policy; + config.permissions.permission_profile = + Constrained::allow_any(PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + )); }); let TestCodex { codex, diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 01521dab0..552635669 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -1,5 +1,6 @@ use anyhow::Context; use codex_features::Feature; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -338,7 +339,12 @@ async fn user_shell_command_history_is_persisted_and_shared_with_model() -> anyh async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Result<()> { let server = responses::start_mock_server().await; let mut builder = core_test_support::test_codex::test_codex().with_config(|config| { - config.permissions.network_sandbox_policy = NetworkSandboxPolicy::Restricted; + let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy(); + config.permissions.permission_profile = + codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + )); }); let test = builder.build(&server).await?; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 2511dfecf..87091a16e 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -482,10 +482,7 @@ impl PermissionProfile { FileSystemSandboxKind::ExternalSandbox => Self::External { network: network_sandbox_policy, }, - FileSystemSandboxKind::Unrestricted - if enforcement == SandboxEnforcement::Disabled - && network_sandbox_policy.is_enabled() => - { + FileSystemSandboxKind::Unrestricted if enforcement == SandboxEnforcement::Disabled => { Self::Disabled } FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => { @@ -1867,6 +1864,17 @@ mod tests { Ok(()) } + #[test] + fn disabled_permission_profile_ignores_runtime_network_policy() { + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::Disabled, + &FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!(permission_profile, PermissionProfile::Disabled); + } + #[test] fn permission_profile_from_runtime_permissions_preserves_external_sandbox() { let permission_profile = PermissionProfile::from_runtime_permissions( diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index 450fb3997..63c2e3c4f 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -631,6 +631,12 @@ impl FileSystemSandboxPolicy { .semantic_signature(cwd) } + /// Returns true when two policies resolve to the same filesystem access + /// model for `cwd`, ignoring incidental entry ordering. + pub fn is_semantically_equivalent_to(&self, other: &Self, cwd: &Path) -> bool { + self.semantic_signature(cwd) == other.semantic_signature(cwd) + } + /// Returns the explicit readable roots resolved against the provided cwd. pub fn get_readable_roots_with_cwd(&self, cwd: &Path) -> Vec { if self.has_full_disk_read_access() { @@ -949,9 +955,9 @@ impl FileSystemSandboxPolicy { has_full_disk_read_access: self.has_full_disk_read_access(), has_full_disk_write_access: self.has_full_disk_write_access(), include_platform_defaults: self.include_platform_defaults(), - readable_roots: self.get_readable_roots_with_cwd(cwd), - writable_roots: self.get_writable_roots_with_cwd(cwd), - unreadable_roots: self.get_unreadable_roots_with_cwd(cwd), + readable_roots: sorted_absolute_paths(self.get_readable_roots_with_cwd(cwd)), + writable_roots: sorted_writable_roots(self.get_writable_roots_with_cwd(cwd)), + unreadable_roots: sorted_absolute_paths(self.get_unreadable_roots_with_cwd(cwd)), unreadable_globs: self.get_unreadable_globs_with_cwd(cwd), } } @@ -1257,6 +1263,20 @@ fn dedup_absolute_paths( deduped } +fn sorted_absolute_paths(mut paths: Vec) -> Vec { + paths.sort_by(|left, right| left.as_path().cmp(right.as_path())); + paths +} + +fn sorted_writable_roots(mut roots: Vec) -> Vec { + for root in &mut roots { + root.read_only_subpaths = + sorted_absolute_paths(std::mem::take(&mut root.read_only_subpaths)); + } + roots.sort_by(|left, right| left.root.as_path().cmp(right.root.as_path())); + roots +} + fn normalize_effective_absolute_path(path: AbsolutePathBuf) -> AbsolutePathBuf { let raw_path = path.to_path_buf(); for ancestor in raw_path.ancestors() { @@ -2145,6 +2165,31 @@ mod tests { ); } + #[test] + fn legacy_projection_runtime_enforcement_ignores_entry_order() { + let cwd = TempDir::new().expect("tempdir"); + let legacy_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + let legacy_order = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&legacy_policy, cwd.path()); + let mut reordered_entries = legacy_order.entries.clone(); + reordered_entries.reverse(); + let reordered = FileSystemSandboxPolicy::restricted(reordered_entries); + + assert!( + legacy_order.is_semantically_equivalent_to(&reordered, cwd.path()), + "entry order should not affect filesystem semantics" + ); + assert!( + !reordered + .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path()) + ); + } + #[test] fn root_write_with_read_only_child_is_not_full_disk_write() { let cwd = TempDir::new().expect("tempdir"); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 400a59712..e27e82865 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2379,37 +2379,13 @@ impl ChatWidget { tracing::warn!(%err, "failed to sync permissions from SessionConfigured"); self.config.permissions.sandbox_policy = Constrained::allow_only(event.sandbox_policy.clone()); - match event.permission_profile.clone() { - Some(permission_profile) => { - let (file_system_sandbox_policy, network_sandbox_policy) = - permission_profile.to_runtime_permissions(); - self.config.permissions.permission_profile = - Constrained::allow_only(permission_profile); - self.config.permissions.file_system_sandbox_policy = file_system_sandbox_policy; - self.config.permissions.network_sandbox_policy = network_sandbox_policy; - } - None => { - self.config.permissions.file_system_sandbox_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - &event.sandbox_policy, - &event.cwd, - ); - self.config.permissions.network_sandbox_policy = - codex_protocol::permissions::NetworkSandboxPolicy::from( - &event.sandbox_policy, - ); - let permission_profile = - codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( - codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy( - &event.sandbox_policy, - ), - &self.config.permissions.file_system_sandbox_policy, - self.config.permissions.network_sandbox_policy, - ); - self.config.permissions.permission_profile = - Constrained::allow_only(permission_profile); - } - } + let permission_profile = event.permission_profile.clone().unwrap_or_else(|| { + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &event.sandbox_policy, + ) + }); + self.config.permissions.permission_profile = + Constrained::allow_only(permission_profile); } self.config.approvals_reviewer = event.approvals_reviewer; self.status_line_project_root_name_cache = None; diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 83cfa7557..be0fd03a1 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -380,12 +380,12 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { assert_eq!( chat.config_ref() .permissions - .file_system_sandbox_policy + .file_system_sandbox_policy() .kind, FileSystemSandboxKind::ExternalSandbox, ); assert_eq!( - chat.config_ref().permissions.network_sandbox_policy, + chat.config_ref().permissions.network_sandbox_policy(), NetworkSandboxPolicy::Restricted, ); }