From 4d7ce3447d1cc7851e746297c96071f94b15501b Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sun, 26 Apr 2026 13:29:54 -0700 Subject: [PATCH] permissions: make runtime config profile-backed (#19606) ## Why This supersedes #19391. During stack repair, GitHub marked #19391 as merged into a temporary stack branch rather than into `main`, so the runtime-config change needed a fresh PR. `PermissionProfile` is now the canonical permissions shape after #19231 because it can distinguish `Managed`, `Disabled`, and `External` enforcement while also carrying filesystem rules that legacy `SandboxPolicy` cannot represent cleanly. Core config and session state still needed to accept profile-backed permissions without forcing every profile through the strict legacy bridge, which rejected valid runtime profiles such as direct write roots. The unrelated CI/test hardening that previously rode along with this PR has been split into #19683 so this PR stays focused on the permissions model migration. ## What Changed - Adds `Permissions.permission_profile` and `SessionConfiguration.permission_profile` as constrained runtime state, while keeping `sandbox_policy` as a legacy compatibility projection. - Introduces profile setters that keep `PermissionProfile`, split filesystem/network policies, and legacy `SandboxPolicy` projections synchronized. - Uses a compatibility projection for requirement checks and legacy consumers instead of rejecting profiles that cannot round-trip through `SandboxPolicy` exactly. - Updates config loading, config overrides, session updates, turn context plumbing, prompt permission text, sandbox tags, and exec request construction to carry profile-backed runtime permissions. - Preserves configured deny-read entries and `glob_scan_max_depth` when command/session profiles are narrowed. - Adds `PermissionProfile::read_only()` and `PermissionProfile::workspace_write()` presets that match legacy defaults. ## Verification - `cargo test -p codex-core direct_write_roots` - `cargo test -p codex-core runtime_roots_to_legacy_projection` - `cargo test -p codex-app-server requested_permissions_trust_project_uses_permission_profile_intent` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19606). * #19395 * #19394 * #19393 * #19392 * __->__ #19606 --- .../analytics/src/analytics_client_tests.rs | 2 +- .../app-server/src/codex_message_processor.rs | 144 ++++++------ codex-rs/app-server/src/command_exec.rs | 24 +- .../tests/suite/v2/marketplace_upgrade.rs | 21 +- .../app-server/tests/suite/v2/turn_start.rs | 16 +- codex-rs/core/src/config/config_tests.rs | 213 +++++++++++++++++- codex-rs/core/src/config/mod.rs | 181 +++++++++++++-- codex-rs/core/src/config/permissions.rs | 10 + codex-rs/core/src/config/permissions_tests.rs | 12 + .../src/context/permissions_instructions.rs | 97 +++++--- .../context/permissions_instructions_tests.rs | 36 +++ codex-rs/core/src/context_manager/updates.rs | 4 +- codex-rs/core/src/exec.rs | 22 +- codex-rs/core/src/exec_tests.rs | 11 +- codex-rs/core/src/guardian/review_session.rs | 14 +- codex-rs/core/src/mcp_tool_exposure_test.rs | 4 +- codex-rs/core/src/memories/phase2.rs | 15 +- codex-rs/core/src/sandbox_tags.rs | 61 ++++- codex-rs/core/src/sandbox_tags_tests.rs | 121 ++++++++++ codex-rs/core/src/sandboxing/mod.rs | 18 +- codex-rs/core/src/session/mod.rs | 6 +- codex-rs/core/src/session/review.rs | 6 +- codex-rs/core/src/session/session.rs | 94 ++++++-- codex-rs/core/src/session/tests.rs | 59 +++++ codex-rs/core/src/session/turn_context.rs | 26 ++- codex-rs/core/src/tasks/user_shell.rs | 12 +- .../src/tools/handlers/multi_agents_common.rs | 7 +- .../src/tools/handlers/multi_agents_tests.rs | 25 +- codex-rs/core/src/tools/orchestrator.rs | 8 +- codex-rs/core/src/tools/registry.rs | 37 +-- .../core/src/tools/runtimes/apply_patch.rs | 20 +- .../src/tools/runtimes/apply_patch_tests.rs | 15 +- codex-rs/core/src/tools/runtimes/mod_tests.rs | 11 +- .../tools/runtimes/shell/unix_escalation.rs | 35 ++- codex-rs/core/src/tools/sandboxing.rs | 9 +- codex-rs/core/src/tools/spec_tests.rs | 36 +-- codex-rs/core/src/turn_metadata.rs | 16 +- codex-rs/core/src/turn_metadata_tests.rs | 17 +- codex-rs/core/src/unified_exec/mod_tests.rs | 8 +- .../src/unified_exec/process_manager_tests.rs | 19 +- codex-rs/core/tests/suite/approvals.rs | 6 +- codex-rs/core/tests/suite/exec.rs | 8 +- codex-rs/core/tests/suite/pending_input.rs | 14 +- .../core/tests/suite/permissions_messages.rs | 5 +- codex-rs/exec-server/src/fs_sandbox.rs | 67 +----- codex-rs/exec/tests/suite/sandbox.rs | 7 +- .../linux-sandbox/tests/suite/landlock.rs | 16 +- codex-rs/protocol/src/models.rs | 84 +++++++ codex-rs/sandboxing/src/lib.rs | 1 + codex-rs/sandboxing/src/manager.rs | 76 +++++-- codex-rs/sandboxing/src/manager_tests.rs | 68 +++--- codex-rs/sandboxing/src/policy_transforms.rs | 17 ++ codex-rs/tools/src/tool_config.rs | 22 +- codex-rs/tools/src/tool_config_tests.rs | 41 +++- .../tools/src/tool_registry_plan_tests.rs | 88 ++++---- codex-rs/tui/src/app/config_persistence.rs | 35 ++- codex-rs/tui/src/app/tests.rs | 28 +-- codex-rs/tui/src/app/thread_events.rs | 4 +- codex-rs/tui/src/app_server_session.rs | 8 +- codex-rs/tui/src/chatwidget.rs | 79 ++++--- .../src/chatwidget/tests/history_replay.rs | 7 +- codex-rs/utils/absolute-path/src/lib.rs | 99 +++++++- 62 files changed, 1601 insertions(+), 671 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index ed1731463..c0465ca7d 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -161,7 +161,7 @@ fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) - } fn sample_permission_profile() -> AppServerPermissionProfile { - CorePermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::DangerFullAccess).into() + CorePermissionProfile::Disabled.into() } fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index cddc5d585..5c7500f5a 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -359,6 +359,7 @@ use codex_rmcp_client::perform_oauth_login_return_url; use codex_rollout::state_db::StateDbHandle; use codex_rollout::state_db::get_state_db; use codex_rollout::state_db::reconcile_rollout; +use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile; use codex_state::StateRuntime; use codex_state::ThreadMetadata; use codex_state::ThreadMetadataBuilder; @@ -2272,44 +2273,34 @@ impl CodexMessageProcessor { arg0: None, }; - let ( - effective_policy, - effective_file_system_sandbox_policy, - effective_network_sandbox_policy, - ) = if let Some(permission_profile) = permission_profile { + let effective_permission_profile = if let Some(permission_profile) = permission_profile { let permission_profile = codex_protocol::models::PermissionProfile::from(permission_profile); - let sandbox_policy = match permission_profile.to_legacy_sandbox_policy(&sandbox_cwd) { - Ok(sandbox_policy) => sandbox_policy, - Err(err) => { - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("invalid permission profile: {err}"), - data: None, - }; - self.outgoing.send_error(request, error).await; - return; - } - }; + let (mut file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + Self::preserve_configured_deny_read_restrictions( + &mut file_system_sandbox_policy, + &self.config.permissions.file_system_sandbox_policy, + ); + let effective_permission_profile = + codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( + permission_profile.enforcement(), + &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, + sandbox_cwd.as_path(), + ); match self .config .permissions .sandbox_policy .can_set(&sandbox_policy) { - Ok(()) => { - let (mut file_system_sandbox_policy, network_sandbox_policy) = - permission_profile.to_runtime_permissions(); - Self::preserve_configured_deny_read_restrictions( - &mut file_system_sandbox_policy, - &self.config.permissions.file_system_sandbox_policy, - ); - ( - sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, - ) - } + Ok(()) => effective_permission_profile, Err(err) => { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, @@ -2327,7 +2318,13 @@ impl CodexMessageProcessor { codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd); let network_sandbox_policy = codex_protocol::permissions::NetworkSandboxPolicy::from(&policy); - (policy, file_system_sandbox_policy, network_sandbox_policy) + codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( + codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy( + &policy, + ), + &file_system_sandbox_policy, + network_sandbox_policy, + ) } Err(err) => { let error = JSONRPCErrorError { @@ -2340,11 +2337,7 @@ impl CodexMessageProcessor { } } } else { - ( - self.config.permissions.sandbox_policy.get().clone(), - self.config.permissions.file_system_sandbox_policy.clone(), - self.config.permissions.network_sandbox_policy, - ) + self.config.permissions.permission_profile() }; let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone(); @@ -2363,9 +2356,7 @@ impl CodexMessageProcessor { match codex_core::exec::build_exec_request( exec_params, - &effective_policy, - &effective_file_system_sandbox_policy, - effective_network_sandbox_policy, + &effective_permission_profile, &sandbox_cwd, &codex_linux_sandbox_exe, use_legacy_landlock, @@ -10161,16 +10152,20 @@ fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path) .permission_profile .as_ref() .is_some_and(|profile| { - profile - .to_legacy_sandbox_policy(cwd) - .is_ok_and(|sandbox_policy| { - matches!( - sandbox_policy, - codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } - | codex_protocol::protocol::SandboxPolicy::DangerFullAccess - | codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } - ) - }) + let (file_system_sandbox_policy, network_sandbox_policy) = + profile.to_runtime_permissions(); + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + profile, + &file_system_sandbox_policy, + network_sandbox_policy, + cwd, + ); + matches!( + sandbox_policy, + codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } + | codex_protocol::protocol::SandboxPolicy::DangerFullAccess + | codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } + ) }) } @@ -10672,16 +10667,10 @@ mod tests { #[test] fn thread_response_permission_profile_preserves_enforcement() { - let full_access_profile = - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::DangerFullAccess, - ); - let external_profile = - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::ExternalSandbox { - network_access: codex_protocol::protocol::NetworkAccess::Restricted, - }, - ); + let full_access_profile = codex_protocol::models::PermissionProfile::Disabled; + let external_profile = codex_protocol::models::PermissionProfile::External { + network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted, + }; assert_eq!( thread_response_permission_profile(external_profile.clone()), @@ -10696,17 +10685,20 @@ mod tests { #[test] fn requested_permissions_trust_project_uses_permission_profile_intent() { let cwd = test_path_buf("/tmp/project").abs(); - let full_access_profile = - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::DangerFullAccess, - ); - let workspace_write_profile = - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_workspace_write_policy(), - ); - let read_only_profile = - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_read_only_policy(), + let full_access_profile = codex_protocol::models::PermissionProfile::Disabled; + let workspace_write_profile = codex_protocol::models::PermissionProfile::workspace_write(); + let read_only_profile = codex_protocol::models::PermissionProfile::read_only(); + let direct_write_profile = + codex_protocol::models::PermissionProfile::from_runtime_permissions( + &codex_protocol::permissions::FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: test_path_buf("/tmp/other").abs(), + }, + access: FileSystemAccessMode::Write, + }, + ]), + codex_protocol::permissions::NetworkSandboxPolicy::Restricted, ); assert!(requested_permissions_trust_project( @@ -10723,6 +10715,13 @@ mod tests { }, cwd.as_path() )); + assert!(requested_permissions_trust_project( + &ConfigOverrides { + permission_profile: Some(direct_write_profile), + ..Default::default() + }, + cwd.as_path() + )); assert!(!requested_permissions_trust_project( &ConfigOverrides { permission_profile: Some(read_only_profile), @@ -10915,10 +10914,7 @@ mod tests { approval_policy: codex_protocol::protocol::AskForApproval::OnRequest, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - permission_profile: - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - ), + permission_profile: codex_protocol::models::PermissionProfile::Disabled, cwd, ephemeral: false, reasoning_effort: None, diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index 8004e282e..ab8618996 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -710,9 +710,7 @@ mod tests { use std::collections::HashMap; use codex_protocol::config_types::WindowsSandboxLevel; - use codex_protocol::permissions::FileSystemSandboxPolicy; - use codex_protocol::permissions::NetworkSandboxPolicy; - use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; #[cfg(not(target_os = "windows"))] @@ -729,12 +727,10 @@ mod tests { use crate::outgoing_message::OutgoingMessage; fn windows_sandbox_exec_request() -> ExecRequest { - let sandbox_policy = SandboxPolicy::ReadOnly { - network_access: false, - }; + let cwd = AbsolutePathBuf::current_dir().expect("current dir"); ExecRequest::new( vec!["cmd".to_string()], - AbsolutePathBuf::current_dir().expect("current dir"), + cwd, HashMap::new(), /*network*/ None, ExecExpiration::DefaultTimeout, @@ -742,9 +738,7 @@ mod tests { SandboxType::WindowsRestrictedToken, WindowsSandboxLevel::Disabled, /*windows_sandbox_private_desktop*/ false, - sandbox_policy.clone(), - FileSystemSandboxPolicy::from(&sandbox_policy), - NetworkSandboxPolicy::from(&sandbox_policy), + PermissionProfile::read_only(), /*arg0*/ None, ) } @@ -834,9 +828,7 @@ mod tests { connection_id: ConnectionId(8), request_id: codex_app_server_protocol::RequestId::Integer(100), }; - let sandbox_policy = SandboxPolicy::ReadOnly { - network_access: false, - }; + let cwd = AbsolutePathBuf::current_dir().expect("current dir"); manager .start(StartCommandExecParams { @@ -845,7 +837,7 @@ mod tests { process_id: Some("proc-100".to_string()), exec_request: ExecRequest::new( vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], - AbsolutePathBuf::current_dir().expect("current dir"), + cwd.clone(), HashMap::new(), /*network*/ None, ExecExpiration::Cancellation(CancellationToken::new()), @@ -853,9 +845,7 @@ mod tests { SandboxType::None, WindowsSandboxLevel::Disabled, /*windows_sandbox_private_desktop*/ false, - sandbox_policy.clone(), - FileSystemSandboxPolicy::from(&sandbox_policy), - NetworkSandboxPolicy::from(&sandbox_policy), + PermissionProfile::read_only(), /*arg0*/ None, ), started_network_proxy: None, diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs b/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs index c10bb5cae..8660497da 100644 --- a/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs +++ b/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs @@ -17,6 +17,9 @@ use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::timeout; +#[cfg(windows)] +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(25); +#[cfg(not(windows))] const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces"; @@ -63,13 +66,14 @@ fn commit_marketplace_marker(root: &Path, marker: &str) -> Result { fn configured_git_marketplace_update<'a>( source: &'a str, last_revision: Option<&'a str>, + ref_name: Option<&'a str>, ) -> MarketplaceConfigUpdate<'a> { MarketplaceConfigUpdate { last_updated: "2026-04-13T00:00:00Z", last_revision, source_type: "git", source, - ref_name: None, + ref_name, sparse_paths: &[], } } @@ -90,12 +94,13 @@ fn record_git_marketplace( marketplace_name: &str, source: &Path, last_revision: &str, + ref_name: Option<&str>, ) -> Result<()> { let source = source.display().to_string(); record_user_marketplace( codex_home, marketplace_name, - &configured_git_marketplace_update(&source, Some(last_revision)), + &configured_git_marketplace_update(&source, Some(last_revision), ref_name), )?; Ok(()) } @@ -153,12 +158,14 @@ async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> { "debug", debug_source.path(), &debug_old_revision, + Some(&debug_new_revision), )?; record_git_marketplace( codex_home.path(), "tools", tools_source.path(), &tools_old_revision, + Some(&tools_new_revision), )?; disable_plugin_startup_tasks(codex_home.path())?; @@ -205,12 +212,14 @@ async fn marketplace_upgrade_named_marketplace_only() -> Result<()> { "debug", debug_source.path(), &debug_old_revision, + /*ref_name*/ None, )?; record_git_marketplace( codex_home.path(), "tools", tools_source.path(), &tools_old_revision, + /*ref_name*/ None, )?; disable_plugin_startup_tasks(codex_home.path())?; @@ -246,7 +255,13 @@ async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Re let source = TempDir::new()?; let old_revision = init_marketplace_repo(source.path(), "debug", "debug old")?; commit_marketplace_marker(source.path(), "debug new")?; - record_git_marketplace(codex_home.path(), "debug", source.path(), &old_revision)?; + record_git_marketplace( + codex_home.path(), + "debug", + source.path(), + &old_revision, + /*ref_name*/ None, + )?; disable_plugin_startup_tasks(codex_home.path())?; let mut mcp = McpProcess::new(codex_home.path()).await?; diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index d41ca2610..6d66edd3d 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -749,13 +749,17 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> { #[tokio::test] async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> Result<()> { let codex_home = TempDir::new()?; - let unsupported_write_root = TempDir::new()?; + let disallowed_write_root = TempDir::new()?; create_config_toml( codex_home.path(), "http://localhost/unused", "never", &BTreeMap::from([(Feature::Personality, true)]), )?; + std::fs::write( + codex_home.path().join("managed_config.toml"), + "sandbox_mode = \"read-only\"\n", + )?; let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -772,7 +776,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> ) .await??; let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; - let unsupported_write_root = AbsolutePathBuf::from_absolute_path(unsupported_write_root.path()) + let disallowed_write_root = AbsolutePathBuf::from_absolute_path(disallowed_write_root.path()) .expect("tempdir path should be absolute"); let turn_req = mcp @@ -787,7 +791,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> file_system: PermissionProfileFileSystemPermissions::Restricted { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::Path { - path: unsupported_write_root, + path: disallowed_write_root, }, access: FileSystemAccessMode::Write, }], @@ -806,9 +810,9 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> assert_eq!(err.error.code, INVALID_REQUEST_ERROR_CODE); assert!(err.error.message.contains("invalid turn context override")); assert!( - err.error - .message - .contains("filesystem writes outside the workspace root") + err.error.message.contains("allowed set [ReadOnly]"), + "unexpected error message: {}", + err.error.message ); let turn_started = tokio::time::timeout( std::time::Duration::from_millis(250), diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 8462c0470..dd73cb5e5 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -56,6 +56,7 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; +use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; @@ -63,6 +64,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::RealtimeVoice; use codex_protocol::protocol::SandboxPolicy; use serde::Deserialize; @@ -843,6 +845,145 @@ async fn permission_profile_override_populates_runtime_permissions() -> std::io: Ok(()) } +#[tokio::test] +async fn permission_profile_override_preserves_managed_unrestricted_filesystem() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let permission_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Restricted, + }; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + permission_profile: Some(permission_profile.clone()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert_eq!(config.permissions.permission_profile(), permission_profile); + assert_eq!( + config.permissions.sandbox_policy.get(), + &SandboxPolicy::ExternalSandbox { + network_access: NetworkAccess::Restricted, + } + ); + Ok(()) +} + +#[tokio::test] +async fn managed_unrestricted_permission_profile_still_enables_network_requirements() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let permission_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Enabled, + }; + + let mut config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + permission_profile: Some(permission_profile), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + assert_eq!( + config.permissions.sandbox_policy.get(), + &SandboxPolicy::DangerFullAccess, + "the legacy projection is intentionally lossy for managed unrestricted profiles" + ); + + let layers = config + .config_layer_stack + .get_layers( + ConfigLayerStackOrdering::LowestPrecedenceFirst, + /*include_disabled*/ true, + ) + .into_iter() + .cloned() + .collect(); + let mut requirements = config.config_layer_stack.requirements().clone(); + requirements.network = Some(Sourced::new( + crate::config_loader::NetworkConstraints { + enabled: Some(true), + ..Default::default() + }, + RequirementSource::CloudRequirements, + )); + let mut requirements_toml = config.config_layer_stack.requirements_toml().clone(); + requirements_toml.network = Some(crate::config_loader::NetworkRequirementsToml { + enabled: Some(true), + ..Default::default() + }); + config.config_layer_stack = ConfigLayerStack::new(layers, requirements, requirements_toml) + .expect("config layer stack with network requirements"); + + assert!(config.managed_network_requirements_enabled()); + Ok(()) +} + +#[tokio::test] +async fn permission_profile_override_applies_runtime_roots_to_legacy_projection() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let permission_profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }, + ]), + NetworkSandboxPolicy::Restricted, + ); + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + permission_profile: Some(permission_profile), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let memories_root = codex_home.path().join("memories").abs(); + assert!( + config + .permissions + .file_system_sandbox_policy() + .can_write_path_with_cwd(memories_root.as_path(), cwd.path()) + ); + assert_eq!( + config.permissions.sandbox_policy.get(), + &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![memories_root], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + } + ); + Ok(()) +} + #[tokio::test] async fn permission_profile_override_preserves_configured_network_proxy() -> std::io::Result<()> { let codex_home = TempDir::new()?; @@ -1020,13 +1161,16 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<( } #[tokio::test] -async fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Result<()> { +async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() +-> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; - let external_write_path = if cfg!(windows) { r"C:\temp" } else { "/tmp" }; + let external_write_dir = TempDir::new()?; + let external_write_path = + AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(external_write_dir.path())?)?; - let err = Config::load_from_base_config_with_overrides( + let config = Config::load_from_base_config_with_overrides( ConfigToml { default_permissions: Some("workspace".to_string()), permissions: Some(PermissionsToml { @@ -1036,7 +1180,7 @@ async fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io: filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, entries: BTreeMap::from([( - external_write_path.to_string(), + external_write_path.to_string_lossy().into_owned(), FilesystemPermissionToml::Access(FileSystemAccessMode::Write), )]), }), @@ -1052,14 +1196,25 @@ async fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io: }, codex_home.abs(), ) - .await - .expect_err("writes outside the workspace root should be rejected"); + .await?; - assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + let memories_root = AbsolutePathBuf::from_absolute_path(std::fs::canonicalize( + codex_home.path().join("memories"), + )?)?; assert!( - err.to_string() - .contains("filesystem writes outside the workspace root"), - "{err}" + config + .permissions + .file_system_sandbox_policy() + .can_write_path_with_cwd(external_write_path.as_path(), cwd.path()) + ); + assert_eq!( + config.permissions.sandbox_policy.get(), + &SandboxPolicy::WorkspaceWrite { + writable_roots: vec![external_write_path, memories_root], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + } ); Ok(()) } @@ -5292,6 +5447,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { model_provider: fixture.openai_provider.clone(), permissions: Permissions { 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(), @@ -5489,6 +5645,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { model_provider: fixture.openai_custom_provider.clone(), permissions: Permissions { 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(), @@ -5640,6 +5797,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { model_provider: fixture.openai_provider.clone(), permissions: Permissions { 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(), @@ -5776,6 +5934,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { model_provider: fixture.openai_provider.clone(), permissions: Permissions { 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(), @@ -6602,6 +6761,40 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s Ok(()) } +#[tokio::test] +async fn permission_profile_override_falls_back_when_disallowed_by_requirements() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let requirements = crate::config_loader::ConfigRequirementsToml { + allowed_sandbox_modes: Some(vec![crate::config_loader::SandboxModeRequirement::ReadOnly]), + ..Default::default() + }; + + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .harness_overrides(ConfigOverrides { + permission_profile: Some(PermissionProfile::Disabled), + ..Default::default() + }) + .cloud_requirements(CloudRequirementsLoader::new(async move { + Ok(Some(requirements)) + })) + .build() + .await?; + + let expected_sandbox_policy = SandboxPolicy::new_read_only_policy(); + assert_eq!( + *config.permissions.sandbox_policy.get(), + expected_sandbox_policy + ); + assert_eq!( + config.permissions.permission_profile(), + PermissionProfile::read_only() + ); + Ok(()) +} + #[tokio::test] async fn requirements_web_search_mode_overrides_danger_full_access_default() -> std::io::Result<()> { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 70a4e4eef..7f8be38f1 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -115,6 +115,7 @@ pub use codex_config::Constrained; pub use codex_config::ConstraintError; pub use codex_config::ConstraintResult; pub use codex_network_proxy::NetworkProxyAuditMetadata; +use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile; pub use codex_sandboxing::system_bwrap_warning; pub use managed_features::ManagedFeatures; pub use network_proxy_spec::NetworkProxySpec; @@ -191,13 +192,25 @@ pub(crate) async fn test_config() -> Config { pub struct Permissions { /// Approval policy for executing commands. pub approval_policy: Constrained, + /// Canonical effective runtime permissions after config requirements and + /// runtime readable-root additions have been applied. + pub permission_profile: Constrained, /// Effective sandbox policy used for shell/unified exec. + /// + /// 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, @@ -223,12 +236,87 @@ impl Permissions { /// Effective runtime permissions after config requirements and runtime /// readable-root additions have been applied. pub fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(self.sandbox_policy.get()), - &self.file_system_sandbox_policy, - self.network_sandbox_policy, - ) + self.permission_profile.get().clone() } + + /// Effective filesystem sandbox policy projection. + pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { + self.file_system_sandbox_policy.clone() + } + + /// Effective network sandbox policy projection. + pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy { + self.network_sandbox_policy + } + + /// Replace permissions from a legacy sandbox policy and keep every + /// permission projection in sync. + pub fn set_legacy_sandbox_policy( + &mut self, + sandbox_policy: SandboxPolicy, + cwd: &Path, + ) -> ConstraintResult<()> { + self.sandbox_policy.can_set(&sandbox_policy)?; + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, cwd); + let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, + ); + self.permission_profile.can_set(&permission_profile)?; + + 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(()) + } + + /// Replace permissions from the canonical profile and update compatibility + /// projections for legacy consumers. + pub fn set_permission_profile( + &mut self, + permission_profile: PermissionProfile, + cwd: &Path, + ) -> ConstraintResult<()> { + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + &permission_profile, + &file_system_sandbox_policy, + network_sandbox_policy, + cwd, + ); + self.permission_profile.can_set(&permission_profile)?; + self.sandbox_policy.can_set(&sandbox_policy)?; + + 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(()) + } +} + +fn constrained_permission_profile_from_sandbox_projection( + initial_value: PermissionProfile, + sandbox_constraint: Constrained, + cwd: AbsolutePathBuf, +) -> std::io::Result> { + Constrained::new(initial_value, move |candidate| { + let (file_system_sandbox_policy, network_sandbox_policy) = + candidate.to_runtime_permissions(); + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + candidate, + &file_system_sandbox_policy, + network_sandbox_policy, + cwd.as_path(), + ); + sandbox_constraint.can_set(&sandbox_policy) + }) + .map_err(std::io::Error::from) } /// Configured thread persistence backend. @@ -1807,10 +1895,11 @@ impl Config { && has_permission_profiles); let ( configured_network_proxy_config, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, - ) = if let Some(permission_profile) = permission_profile { + ) = if let Some(mut permission_profile) = permission_profile { let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); let configured_network_proxy_config = @@ -1836,25 +1925,33 @@ impl Config { } else { NetworkProxyConfig::default() }; - let mut sandbox_policy = permission_profile - .to_legacy_sandbox_policy(resolved_cwd.as_path()) - .map_err(|err| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("invalid permission_profile override: {err}"), - ) - })?; + let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + &permission_profile, + &file_system_sandbox_policy, + network_sandbox_policy, + resolved_cwd.as_path(), + ); if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { file_system_sandbox_policy = file_system_sandbox_policy .with_additional_writable_roots( resolved_cwd.as_path(), &additional_writable_roots, ); - sandbox_policy = file_system_sandbox_policy - .to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?; + permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + permission_profile.enforcement(), + &file_system_sandbox_policy, + network_sandbox_policy, + ); + sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + &permission_profile, + &file_system_sandbox_policy, + network_sandbox_policy, + resolved_cwd.as_path(), + ); } ( configured_network_proxy_config, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, @@ -1882,19 +1979,36 @@ impl Config { resolved_cwd.as_path(), &mut startup_warnings, )?; - let mut sandbox_policy = file_system_sandbox_policy - .to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?; + let mut permission_profile = PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + network_sandbox_policy, + ); + let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + &permission_profile, + &file_system_sandbox_policy, + network_sandbox_policy, + resolved_cwd.as_path(), + ); if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { file_system_sandbox_policy = file_system_sandbox_policy .with_additional_writable_roots( resolved_cwd.as_path(), &additional_writable_roots, ); - sandbox_policy = file_system_sandbox_policy - .to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?; + permission_profile = PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + network_sandbox_policy, + ); + sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + &permission_profile, + &file_system_sandbox_policy, + network_sandbox_policy, + resolved_cwd.as_path(), + ); } ( configured_network_proxy_config, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, @@ -1923,8 +2037,14 @@ impl Config { resolved_cwd.as_path(), ); let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, + ); ( configured_network_proxy_config, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, @@ -2343,6 +2463,22 @@ impl Config { } else { NetworkSandboxPolicy::from(&effective_sandbox_policy) }; + let effective_enforcement = if effective_sandbox_policy == original_sandbox_policy { + permission_profile.enforcement() + } else { + SandboxEnforcement::from_legacy_sandbox_policy(&effective_sandbox_policy) + }; + let effective_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + effective_enforcement, + &effective_file_system_sandbox_policy, + effective_network_sandbox_policy, + ); + let constrained_permission_profile = + constrained_permission_profile_from_sandbox_projection( + effective_permission_profile, + constrained_sandbox_policy.value.clone(), + resolved_cwd.clone(), + )?; let config = Self { model, service_tier, @@ -2355,6 +2491,7 @@ impl Config { startup_warnings, permissions: Permissions { 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, @@ -2610,8 +2747,8 @@ impl Config { pub fn managed_network_requirements_enabled(&self) -> bool { !matches!( - self.permissions.sandbox_policy.get(), - SandboxPolicy::DangerFullAccess + self.permissions.permission_profile.get(), + PermissionProfile::Disabled ) && self .config_layer_stack .requirements_toml() diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 943c82c0e..6d938e918 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -383,6 +383,16 @@ fn validate_glob_scan_max_depth(max_depth: Option) -> io::Result bool { + contains_glob_chars_for_platform(path, cfg!(windows)) +} + +fn contains_glob_chars_for_platform(path: &str, is_windows: bool) -> bool { + let normalized_windows_path = if is_windows { + normalize_windows_device_path(path) + } else { + None + }; + let path = normalized_windows_path.as_deref().unwrap_or(path); path.chars().any(|ch| matches!(ch, '*' | '?' | '[' | ']')) } diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index e22376b21..63d73c47a 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -30,6 +30,18 @@ fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() { assert_eq!(parsed, PathBuf::from(r"D:\c\x\worktrees\2508\swift-base")); } +#[test] +fn windows_verbatim_path_prefix_does_not_count_as_glob_syntax() { + assert!(!contains_glob_chars_for_platform( + r"\\?\D:\c\x\worktrees\2508\swift-base", + /*is_windows*/ true, + )); + assert!(contains_glob_chars_for_platform( + r"\\?\D:\c\x\worktrees\2508\**\*.env", + /*is_windows*/ true, + )); +} + #[tokio::test] async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> { let temp_dir = TempDir::new()?; diff --git a/codex-rs/core/src/context/permissions_instructions.rs b/codex-rs/core/src/context/permissions_instructions.rs index 6ba4e7c15..0ccd6c33a 100644 --- a/codex-rs/core/src/context/permissions_instructions.rs +++ b/codex-rs/core/src/context/permissions_instructions.rs @@ -2,7 +2,9 @@ use super::ContextualUserFragment; use codex_execpolicy::Policy; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::SandboxMode; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::format_allow_prefixes; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::NetworkAccess; @@ -57,7 +59,33 @@ pub struct PermissionsInstructions { } impl PermissionsInstructions { - /// Builds permissions instructions from the effective sandbox and approval policy. + /// Builds permissions instructions from the effective permission profile and approval policy. + pub fn from_permission_profile( + permission_profile: &PermissionProfile, + approval_policy: AskForApproval, + approvals_reviewer: ApprovalsReviewer, + exec_policy: &Policy, + cwd: &Path, + exec_permission_approvals_enabled: bool, + request_permissions_tool_enabled: bool, + ) -> Self { + let (sandbox_mode, writable_roots) = sandbox_prompt_from_profile(permission_profile, cwd); + + Self::from_permissions_with_network( + sandbox_mode, + network_access_from_policy(permission_profile.network_sandbox_policy()), + PermissionsPromptConfig { + approval_policy, + approvals_reviewer, + exec_policy, + exec_permission_approvals_enabled, + request_permissions_tool_enabled, + }, + writable_roots, + ) + } + + /// Builds permissions instructions from a legacy sandbox policy. pub fn from_policy( sandbox_policy: &SandboxPolicy, approval_policy: AskForApproval, @@ -67,33 +95,14 @@ impl PermissionsInstructions { exec_permission_approvals_enabled: bool, request_permissions_tool_enabled: bool, ) -> Self { - let network_access = if sandbox_policy.has_full_network_access() { - NetworkAccess::Enabled - } else { - NetworkAccess::Restricted - }; - - let (sandbox_mode, writable_roots) = match sandbox_policy { - SandboxPolicy::DangerFullAccess => (SandboxMode::DangerFullAccess, None), - SandboxPolicy::ReadOnly { .. } => (SandboxMode::ReadOnly, None), - SandboxPolicy::ExternalSandbox { .. } => (SandboxMode::DangerFullAccess, None), - SandboxPolicy::WorkspaceWrite { .. } => { - let roots = sandbox_policy.get_writable_roots_with_cwd(cwd); - (SandboxMode::WorkspaceWrite, Some(roots)) - } - }; - - Self::from_permissions_with_network( - sandbox_mode, - network_access, - PermissionsPromptConfig { - approval_policy, - approvals_reviewer, - exec_policy, - exec_permission_approvals_enabled, - request_permissions_tool_enabled, - }, - writable_roots, + Self::from_permission_profile( + &PermissionProfile::from_legacy_sandbox_policy(sandbox_policy), + approval_policy, + approvals_reviewer, + exec_policy, + cwd, + exec_permission_approvals_enabled, + request_permissions_tool_enabled, ) } @@ -125,6 +134,38 @@ impl PermissionsInstructions { } } +fn sandbox_prompt_from_profile( + permission_profile: &PermissionProfile, + cwd: &Path, +) -> (SandboxMode, Option>) { + match permission_profile { + PermissionProfile::Disabled | PermissionProfile::External { .. } => { + (SandboxMode::DangerFullAccess, None) + } + PermissionProfile::Managed { .. } => { + let file_system_policy = permission_profile.file_system_sandbox_policy(); + if file_system_policy.has_full_disk_write_access() { + return (SandboxMode::DangerFullAccess, None); + } + + let writable_roots = file_system_policy.get_writable_roots_with_cwd(cwd); + if writable_roots.is_empty() { + (SandboxMode::ReadOnly, None) + } else { + (SandboxMode::WorkspaceWrite, Some(writable_roots)) + } + } + } +} + +fn network_access_from_policy(network_policy: NetworkSandboxPolicy) -> NetworkAccess { + if network_policy.is_enabled() { + NetworkAccess::Enabled + } else { + NetworkAccess::Restricted + } +} + impl ContextualUserFragment for PermissionsInstructions { const ROLE: &'static str = "developer"; const START_MARKER: &'static str = ""; diff --git a/codex-rs/core/src/context/permissions_instructions_tests.rs b/codex-rs/core/src/context/permissions_instructions_tests.rs index c8d4607ba..16d5dc631 100644 --- a/codex-rs/core/src/context/permissions_instructions_tests.rs +++ b/codex-rs/core/src/context/permissions_instructions_tests.rs @@ -1,5 +1,11 @@ use super::*; use codex_execpolicy::Decision; +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_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::path::PathBuf; @@ -70,6 +76,36 @@ fn builds_permissions_from_policy() { assert!(text.contains("`approval_policy` is `unless-trusted`")); } +#[test] +fn builds_permissions_from_profile() { + let cwd = PathBuf::from("/tmp"); + let writable_root = + AbsolutePathBuf::from_absolute_path(cwd.join("repo")).expect("absolute path"); + let permission_profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: writable_root.clone(), + }, + access: FileSystemAccessMode::Write, + }]), + NetworkSandboxPolicy::Enabled, + ); + + let instructions = PermissionsInstructions::from_permission_profile( + &permission_profile, + AskForApproval::UnlessTrusted, + ApprovalsReviewer::User, + &Policy::empty(), + &cwd, + /*exec_permission_approvals_enabled*/ false, + /*request_permissions_tool_enabled*/ false, + ); + let text = instructions.body(); + assert!(text.contains("`sandbox_mode` is `workspace-write`")); + assert!(text.contains("Network access is enabled.")); + assert!(text.contains(writable_root.to_string_lossy().as_ref())); +} + #[test] fn includes_request_rule_instructions_for_on_request() { let mut exec_policy = Policy::empty(); diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 862b2698d..4277f0b7e 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -56,8 +56,8 @@ fn build_permissions_update_item( } Some( - PermissionsInstructions::from_policy( - next.sandbox_policy.get(), + PermissionsInstructions::from_permission_profile( + &next.permission_profile, next.approval_policy.value(), next.config.approvals_reviewer, exec_policy, diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index ec5292d36..aee6b14c7 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -30,6 +30,7 @@ use codex_protocol::error::Result; use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -220,9 +221,7 @@ pub struct StdoutStream { #[allow(clippy::too_many_arguments)] pub async fn process_exec_tool_call( params: ExecParams, - sandbox_policy: &SandboxPolicy, - file_system_sandbox_policy: &FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, + permission_profile: &PermissionProfile, sandbox_cwd: &AbsolutePathBuf, codex_linux_sandbox_exe: &Option, use_legacy_landlock: bool, @@ -230,9 +229,7 @@ pub async fn process_exec_tool_call( ) -> Result { let exec_req = build_exec_request( params, - sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, + permission_profile, sandbox_cwd, codex_linux_sandbox_exe, use_legacy_landlock, @@ -246,9 +243,7 @@ pub async fn process_exec_tool_call( /// spawned under the requested sandbox policy. pub fn build_exec_request( params: ExecParams, - sandbox_policy: &SandboxPolicy, - file_system_sandbox_policy: &FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, + permission_profile: &PermissionProfile, sandbox_cwd: &AbsolutePathBuf, codex_linux_sandbox_exe: &Option, use_legacy_landlock: bool, @@ -271,8 +266,10 @@ pub fn build_exec_request( } = params; let enforce_managed_network = network.is_some(); + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); let sandbox_type = select_process_exec_tool_sandbox_type( - file_system_sandbox_policy, + &file_system_sandbox_policy, network_sandbox_policy, windows_sandbox_level, enforce_managed_network, @@ -304,9 +301,7 @@ pub fn build_exec_request( let mut exec_req = manager .transform(SandboxTransformRequest { command, - policy: sandbox_policy, - file_system_policy: file_system_sandbox_policy, - network_policy: network_sandbox_policy, + permissions: permission_profile, sandbox: sandbox_type, enforce_managed_network, network: network.as_ref(), @@ -366,6 +361,7 @@ pub(crate) async fn execute_exec_request( windows_sandbox_policy_cwd: _, windows_sandbox_level, windows_sandbox_private_desktop, + permission_profile: _, sandbox_policy, // TODO(mbolin): Use file_system_sandbox_policy instead of sandbox_policy. file_system_sandbox_policy: _, diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index c09d4b48d..4e8ba10c2 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -1,5 +1,6 @@ use super::*; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_sandboxing::SandboxType; use core_test_support::PathBufExt; use core_test_support::PathExt; @@ -346,6 +347,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result let cwd = codex_utils_absolute_path::AbsolutePathBuf::current_dir()?; let sandbox_policy = SandboxPolicy::DangerFullAccess; + let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy); let output = process_exec_tool_call( ExecParams { command, @@ -360,9 +362,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result justification: None, arg0: None, }, - &sandbox_policy, - &FileSystemSandboxPolicy::from(&sandbox_policy), - NetworkSandboxPolicy::Enabled, + &permission_profile, &cwd, &None, /*use_legacy_landlock*/ false, @@ -1021,11 +1021,10 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { tokio::time::sleep(Duration::from_millis(1_000)).await; cancel_tx.cancel(); }); + let permission_profile = PermissionProfile::Disabled; let result = process_exec_tool_call( params, - &SandboxPolicy::DangerFullAccess, - &FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess), - NetworkSandboxPolicy::Enabled, + &permission_profile, &cwd, &None, /*use_legacy_landlock*/ false, diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 429bdce5e..754cb43af 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -9,6 +9,7 @@ use codex_analytics::GuardianReviewAnalyticsResult; use codex_analytics::GuardianReviewSessionKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::AskForApproval; @@ -843,8 +844,17 @@ pub(crate) fn build_guardian_review_session_config( ); guardian_config.developer_instructions = None; guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); - guardian_config.permissions.sandbox_policy = - Constrained::allow_only(SandboxPolicy::new_read_only_policy()); + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + guardian_config.permissions.permission_profile = Constrained::allow_only( + PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy), + ); + guardian_config.permissions.sandbox_policy = Constrained::allow_only(sandbox_policy.clone()); + guardian_config + .permissions + .set_legacy_sandbox_policy(sandbox_policy, guardian_config.cwd.as_path()) + .map_err(|err| { + anyhow::anyhow!("guardian review session could not set sandbox policy: {err}") + })?; guardian_config.include_apps_instructions = false; guardian_config .mcp_servers diff --git a/codex-rs/core/src/mcp_tool_exposure_test.rs b/codex-rs/core/src/mcp_tool_exposure_test.rs index 18bb97642..cbd4d3b29 100644 --- a/codex-rs/core/src/mcp_tool_exposure_test.rs +++ b/codex-rs/core/src/mcp_tool_exposure_test.rs @@ -9,7 +9,7 @@ use codex_mcp::ToolInfo; use codex_models_manager::test_support::construct_model_info_offline_for_tests; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::SessionSource; use codex_tools::ToolsConfig; use codex_tools::ToolsConfigParams; @@ -104,7 +104,7 @@ async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); tools_config.search_tool = search_tool; diff --git a/codex-rs/core/src/memories/phase2.rs b/codex-rs/core/src/memories/phase2.rs index f780c0dc8..248e61dba 100644 --- a/codex-rs/core/src/memories/phase2.rs +++ b/codex-rs/core/src/memories/phase2.rs @@ -16,8 +16,6 @@ use crate::session::session::Session; use codex_config::Constrained; use codex_features::Feature; use codex_protocol::ThreadId; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; @@ -327,21 +325,10 @@ mod agent { exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; - let consolidation_file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - &consolidation_sandbox_policy, - agent_config.cwd.as_path(), - ); - let consolidation_network_sandbox_policy = - NetworkSandboxPolicy::from(&consolidation_sandbox_policy); agent_config .permissions - .sandbox_policy - .set(consolidation_sandbox_policy) + .set_legacy_sandbox_policy(consolidation_sandbox_policy, agent_config.cwd.as_path()) .ok()?; - agent_config.permissions.file_system_sandbox_policy = - consolidation_file_system_sandbox_policy; - agent_config.permissions.network_sandbox_policy = consolidation_network_sandbox_policy; agent_config.model = Some( config diff --git a/codex-rs/core/src/sandbox_tags.rs b/codex-rs/core/src/sandbox_tags.rs index b3b474909..f6db4da91 100644 --- a/codex-rs/core/src/sandbox_tags.rs +++ b/codex-rs/core/src/sandbox_tags.rs @@ -1,17 +1,45 @@ use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; +#[cfg(test)] use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; +use codex_sandboxing::policy_transforms::should_require_platform_sandbox; +use std::path::Path; +#[cfg(test)] pub(crate) fn sandbox_tag( policy: &SandboxPolicy, windows_sandbox_level: WindowsSandboxLevel, ) -> &'static str { - if matches!(policy, SandboxPolicy::DangerFullAccess) { - return "none"; - } - if matches!(policy, SandboxPolicy::ExternalSandbox { .. }) { - return "external"; + permission_profile_sandbox_tag( + &PermissionProfile::from_legacy_sandbox_policy(policy), + windows_sandbox_level, + /*enforce_managed_network*/ false, + ) +} + +pub(crate) fn permission_profile_sandbox_tag( + profile: &PermissionProfile, + windows_sandbox_level: WindowsSandboxLevel, + enforce_managed_network: bool, +) -> &'static str { + match profile { + PermissionProfile::Disabled => return "none", + PermissionProfile::External { .. } => return "external", + PermissionProfile::Managed { + file_system, + network, + } => { + let file_system_policy = file_system.to_sandbox_policy(); + if !should_require_platform_sandbox( + &file_system_policy, + *network, + enforce_managed_network, + ) { + return "none"; + } + } } if cfg!(target_os = "windows") && matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated) { @@ -23,6 +51,29 @@ pub(crate) fn sandbox_tag( .unwrap_or("none") } +pub(crate) fn permission_profile_policy_tag( + profile: &PermissionProfile, + cwd: &Path, +) -> &'static str { + match profile { + PermissionProfile::Disabled => "danger-full-access", + PermissionProfile::External { .. } => "external-sandbox", + PermissionProfile::Managed { .. } => { + let file_system_policy = profile.file_system_sandbox_policy(); + if file_system_policy.has_full_disk_write_access() { + "danger-full-access" + } else if file_system_policy + .get_writable_roots_with_cwd(cwd) + .is_empty() + { + "read-only" + } else { + "workspace-write" + } + } + } +} + #[cfg(test)] #[path = "sandbox_tags_tests.rs"] mod tests; diff --git a/codex-rs/core/src/sandbox_tags_tests.rs b/codex-rs/core/src/sandbox_tags_tests.rs index 6ff54f6eb..8b00de9cc 100644 --- a/codex-rs/core/src/sandbox_tags_tests.rs +++ b/codex-rs/core/src/sandbox_tags_tests.rs @@ -1,10 +1,22 @@ +use super::permission_profile_policy_tag; +use super::permission_profile_sandbox_tag; use super::sandbox_tag; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::ManagedFileSystemPermissions; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxKind; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; +use std::path::Path; #[test] fn danger_full_access_is_untagged_even_when_linux_sandbox_defaults_apply() { @@ -37,3 +49,112 @@ fn default_linux_sandbox_uses_platform_sandbox_tag() { .unwrap_or("none"); assert_eq!(actual, expected); } + +#[test] +fn profile_sandbox_tag_distinguishes_disabled_from_external() { + assert_eq!( + permission_profile_sandbox_tag( + &PermissionProfile::Disabled, + WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, + ), + "none" + ); + assert_eq!( + permission_profile_sandbox_tag( + &PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }, + WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, + ), + "external" + ); +} + +#[test] +fn unrestricted_managed_profile_with_enabled_network_is_untagged() { + let profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Enabled, + }; + + assert_eq!( + permission_profile_sandbox_tag( + &profile, + WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, + ), + "none" + ); +} + +#[test] +fn root_write_managed_profile_with_enabled_network_is_untagged() { + let profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Enabled, + }; + + assert_eq!( + permission_profile_sandbox_tag( + &profile, + WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, + ), + "none" + ); +} + +#[test] +fn managed_network_enforcement_tags_unrestricted_profiles_as_sandboxed() { + let profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Enabled, + }; + let expected = get_platform_sandbox(/*windows_sandbox_enabled*/ false) + .map(SandboxType::as_metric_tag) + .unwrap_or("none"); + + assert_eq!( + permission_profile_sandbox_tag( + &profile, + WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ true, + ), + expected + ); +} + +#[test] +fn profile_policy_tag_reports_closest_legacy_mode() { + let cwd = AbsolutePathBuf::from_absolute_path(Path::new("/tmp/codex")).expect("absolute cwd"); + let writable_root = AbsolutePathBuf::from_absolute_path(Path::new("/tmp/codex/work")) + .expect("absolute writable root"); + let profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy { + kind: FileSystemSandboxKind::Restricted, + glob_scan_max_depth: None, + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: writable_root, + }, + access: FileSystemAccessMode::Write, + }], + }, + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!( + permission_profile_policy_tag(&profile, cwd.as_path()), + "workspace-write" + ); +} diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 09e31274e..e7b992519 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -18,12 +18,14 @@ use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::exec_output::ExecToolCallOutput; +use codex_protocol::models::PermissionProfile; pub use codex_protocol::models::SandboxPermissions; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxExecRequest; use codex_sandboxing::SandboxType; +use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; @@ -52,6 +54,7 @@ pub struct ExecRequest { pub windows_sandbox_policy_cwd: AbsolutePathBuf, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, + pub permission_profile: PermissionProfile, pub sandbox_policy: SandboxPolicy, pub file_system_sandbox_policy: FileSystemSandboxPolicy, pub network_sandbox_policy: NetworkSandboxPolicy, @@ -71,12 +74,18 @@ impl ExecRequest { sandbox: SandboxType, windows_sandbox_level: WindowsSandboxLevel, windows_sandbox_private_desktop: bool, - sandbox_policy: SandboxPolicy, - file_system_sandbox_policy: FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, + permission_profile: PermissionProfile, arg0: Option, ) -> Self { let windows_sandbox_policy_cwd = cwd.clone(); + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + &permission_profile, + &file_system_sandbox_policy, + network_sandbox_policy, + cwd.as_path(), + ); Self { command, cwd, @@ -89,6 +98,7 @@ impl ExecRequest { windows_sandbox_policy_cwd, windows_sandbox_level, windows_sandbox_private_desktop, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, @@ -110,6 +120,7 @@ impl ExecRequest { sandbox, windows_sandbox_level, windows_sandbox_private_desktop, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, @@ -141,6 +152,7 @@ impl ExecRequest { windows_sandbox_policy_cwd, windows_sandbox_level, windows_sandbox_private_desktop, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 5bb734da3..459d98549 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -125,6 +125,7 @@ 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; @@ -604,6 +605,7 @@ impl Codex { compact_prompt: config.compact_prompt.clone(), 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, @@ -2518,8 +2520,8 @@ impl Session { } if turn_context.config.include_permissions_instructions { developer_sections.push( - PermissionsInstructions::from_policy( - turn_context.sandbox_policy.get(), + PermissionsInstructions::from_permission_profile( + &turn_context.permission_profile, turn_context.approval_policy.value(), turn_context.config.approvals_reviewer, self.services.exec_policy.current().as_ref(), diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 799af791e..8df62ecc8 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -38,7 +38,7 @@ pub(super) async fn spawn_review_thread( )), web_search_mode: Some(review_web_search_mode), session_source: parent_turn_context.session_source.clone(), - sandbox_policy: parent_turn_context.sandbox_policy.get(), + permission_profile: &parent_turn_context.permission_profile, windows_sandbox_level: parent_turn_context.windows_sandbox_level, }) .with_unified_exec_shell_mode_for_session( @@ -97,8 +97,9 @@ pub(super) async fn spawn_review_thread( &session_source, review_turn_id.clone(), parent_turn_context.cwd.clone(), - parent_turn_context.sandbox_policy.get(), + &parent_turn_context.permission_profile, parent_turn_context.windows_sandbox_level, + parent_turn_context.network.is_some(), )); let review_turn_context = TurnContext { @@ -127,6 +128,7 @@ pub(super) async fn spawn_review_thread( collaboration_mode: parent_turn_context.collaboration_mode.clone(), 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, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 9520485a5..2226cc04c 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1,5 +1,4 @@ use super::*; -use crate::config::ConstraintError; use crate::goals::GoalRuntimeState; use tokio::sync::Semaphore; @@ -57,9 +56,13 @@ pub(crate) struct SessionConfiguration { /// When to escalate for approval for execution pub(super) approval_policy: Constrained, pub(super) approvals_reviewer: ApprovalsReviewer, - /// How to sandbox commands executed in the system + /// 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, @@ -95,11 +98,16 @@ impl SessionConfiguration { } pub(super) fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(self.sandbox_policy.get()), - &self.file_system_sandbox_policy, - self.network_sandbox_policy, - ) + self.permission_profile.get().clone() + } + + pub(super) fn sandbox_policy(&self) -> SandboxPolicy { + self.sandbox_policy.get().clone() + } + + #[cfg(test)] + pub(super) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { + self.file_system_sandbox_policy.clone() } pub(super) fn thread_config_snapshot(&self) -> ThreadConfigSnapshot { @@ -109,7 +117,7 @@ impl SessionConfiguration { service_tier: self.service_tier, approval_policy: self.approval_policy.value(), approvals_reviewer: self.approvals_reviewer, - sandbox_policy: self.sandbox_policy.get().clone(), + sandbox_policy: self.sandbox_policy(), permission_profile: self.permission_profile(), cwd: self.cwd.clone(), ephemeral: self.original_config_do_not_use.ephemeral, @@ -171,23 +179,10 @@ impl SessionConfiguration { } if let Some(permission_profile) = updates.permission_profile.clone() { - let sandbox_policy = permission_profile - .to_legacy_sandbox_policy(&next_configuration.cwd) - .map_err(|err| ConstraintError::InvalidValue { - field_name: "permission_profile", - candidate: format!("{permission_profile:?}"), - allowed: format!( - "permission profiles that can be represented by the active sandbox constraints: {err}" - ), - requirement_source: codex_config::RequirementSource::Unknown, - })?; - next_configuration.sandbox_policy.set(sandbox_policy)?; - let (mut file_system_sandbox_policy, network_sandbox_policy) = - permission_profile.to_runtime_permissions(); - file_system_sandbox_policy - .preserve_deny_read_restrictions_from(&self.file_system_sandbox_policy); - next_configuration.file_system_sandbox_policy = file_system_sandbox_policy; - next_configuration.network_sandbox_policy = network_sandbox_policy; + next_configuration.set_permission_profile_projection( + permission_profile, + Some(&self.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 = @@ -198,6 +193,15 @@ impl SessionConfiguration { ); next_configuration.network_sandbox_policy = NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get()); + 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, + ), + )?; } else if cwd_changed && file_system_policy_matches_legacy { // Preserve richer split policies across cwd-only updates; only // rederive when the session is already using the legacy bridge. @@ -206,6 +210,15 @@ impl SessionConfiguration { next_configuration.sandbox_policy.get(), &next_configuration.cwd, ); + 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, + ), + )?; } if let Some(app_server_client_name) = updates.app_server_client_name.clone() { next_configuration.app_server_client_name = Some(app_server_client_name); @@ -215,6 +228,37 @@ impl SessionConfiguration { } Ok(next_configuration) } + + fn set_permission_profile_projection( + &mut self, + permission_profile: PermissionProfile, + preserve_deny_reads_from: Option<&FileSystemSandboxPolicy>, + ) -> ConstraintResult<()> { + let enforcement = permission_profile.enforcement(); + let (mut file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + if let Some(existing_file_system_policy) = preserve_deny_reads_from { + file_system_sandbox_policy + .preserve_deny_read_restrictions_from(existing_file_system_policy); + } + let effective_permission_profile = + PermissionProfile::from_runtime_permissions_with_enforcement( + enforcement, + &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(()) + } } #[derive(Default, Clone)] diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index ec08f646d..109c80666 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -37,6 +37,7 @@ use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -1493,6 +1494,11 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() -> 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, + )); }); let test = builder.build(&server).await?; @@ -2246,6 +2252,7 @@ async fn set_rate_limits_retains_previous_credits() { compact_prompt: config.compact_prompt.clone(), 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, @@ -2350,6 +2357,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { compact_prompt: config.compact_prompt.clone(), 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, @@ -2799,6 +2807,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati compact_prompt: config.compact_prompt.clone(), 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, @@ -2922,6 +2931,52 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ ); } +#[tokio::test] +async fn session_configuration_apply_permission_profile_accepts_direct_write_roots() { + let mut session_configuration = make_session_configuration_for_tests().await; + let cwd = tempfile::tempdir().expect("create cwd"); + session_configuration.cwd = cwd.path().abs(); + let external_write_dir = tempfile::tempdir().expect("create external write root"); + let external_write_path = AbsolutePathBuf::from_absolute_path( + codex_utils_absolute_path::canonicalize_preserving_symlinks(external_write_dir.path()) + .expect("canonical temp dir"), + ) + .expect("canonical temp dir should be absolute"); + let file_system_sandbox_policy = + FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: external_write_path.clone(), + }, + access: FileSystemAccessMode::Write, + }]); + let permission_profile = PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + ); + + let updated = session_configuration + .apply(&SessionSettingsUpdate { + permission_profile: Some(permission_profile.clone()), + ..Default::default() + }) + .expect("permission profile update should accept direct runtime permissions"); + + assert_eq!(updated.permission_profile(), permission_profile); + assert_eq!( + updated.file_system_sandbox_policy(), + file_system_sandbox_policy + ); + assert_eq!( + updated.sandbox_policy(), + SandboxPolicy::WorkspaceWrite { + writable_roots: vec![external_write_path], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + } + ); +} + #[cfg_attr(windows, ignore)] #[tokio::test] async fn new_default_turn_uses_config_aware_skills_for_role_overrides() { @@ -3114,6 +3169,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { compact_prompt: config.compact_prompt.clone(), 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, @@ -3220,6 +3276,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { compact_prompt: config.compact_prompt.clone(), 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, @@ -3434,6 +3491,7 @@ async fn make_session_with_config_and_rx( compact_prompt: config.compact_prompt.clone(), 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, @@ -4583,6 +4641,7 @@ where compact_prompt: config.compact_prompt.clone(), 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, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 11292c81c..e5916a935 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -2,7 +2,6 @@ use super::*; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; use codex_protocol::models::AdditionalPermissionProfile; -use codex_protocol::models::SandboxEnforcement; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; @@ -73,6 +72,7 @@ pub(crate) struct TurnContext { pub(crate) collaboration_mode: CollaborationMode, 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, @@ -96,11 +96,7 @@ pub(crate) struct TurnContext { } impl TurnContext { pub(crate) fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy), - &self.file_system_sandbox_policy, - self.network_sandbox_policy, - ) + self.permission_profile.clone() } pub(crate) fn model_context_window(&self) -> Option { @@ -170,7 +166,7 @@ impl TurnContext { ), web_search_mode: self.tools_config.web_search_mode, session_source: self.session_source.clone(), - sandbox_policy: self.sandbox_policy.get(), + permission_profile: &self.permission_profile, windows_sandbox_level: self.windows_sandbox_level, }) .with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone()) @@ -213,6 +209,7 @@ impl TurnContext { collaboration_mode, 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, @@ -258,7 +255,7 @@ impl TurnContext { additional_permissions.as_ref(), ); let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy), + self.permission_profile.enforcement(), &file_system_sandbox_policy, network_sandbox_policy, ); @@ -367,6 +364,13 @@ impl Session { per_turn_config.service_tier = session_configuration.service_tier; per_turn_config.personality = session_configuration.personality; 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(), @@ -429,7 +433,7 @@ impl Session { image_generation_tool_auth_allowed, web_search_mode: Some(per_turn_config.web_search_mode.value()), session_source: session_source.clone(), - sandbox_policy: session_configuration.sandbox_policy.get(), + permission_profile: &session_configuration.permission_profile(), windows_sandbox_level: session_configuration.windows_sandbox_level, }) .with_unified_exec_shell_mode_for_session( @@ -455,8 +459,9 @@ impl Session { &session_source, sub_id.clone(), cwd.clone(), - session_configuration.sandbox_policy.get(), + &session_configuration.permission_profile(), session_configuration.windows_sandbox_level, + network.is_some(), )); let (current_date, timezone) = local_time_context(); TurnContext { @@ -483,6 +488,7 @@ impl Session { collaboration_mode: session_configuration.collaboration_mode.clone(), 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, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 5587dd46f..f12200e54 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -33,10 +33,9 @@ use codex_shell_command::parse_command::parse_command; use super::SessionTask; use super::SessionTaskContext; use crate::session::session::Session; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; const USER_SHELL_TIMEOUT_MS: u64 = 60 * 60 * 1000; // 1 hour @@ -157,7 +156,7 @@ pub(crate) async fn execute_user_shell_command( ) .await; - let sandbox_policy = SandboxPolicy::DangerFullAccess; + let permission_profile = PermissionProfile::Disabled; let exec_env = ExecRequest { command: exec_command.clone(), cwd: cwd.clone(), @@ -177,9 +176,10 @@ pub(crate) async fn execute_user_shell_command( .config .permissions .windows_sandbox_private_desktop, - sandbox_policy: sandbox_policy.clone(), - file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), - network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy), + permission_profile: permission_profile.clone(), + sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: permission_profile.file_system_sandbox_policy(), + network_sandbox_policy: permission_profile.network_sandbox_policy(), windows_sandbox_filesystem_overrides: None, arg0: None, }; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 5efe4e22c..266666022 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -269,13 +269,10 @@ pub(crate) fn apply_spawn_agent_runtime_overrides( config.cwd = turn.cwd.clone(); config .permissions - .sandbox_policy - .set(turn.sandbox_policy.get().clone()) + .set_permission_profile(turn.permission_profile(), turn.cwd.as_path()) .map_err(|err| { - FunctionCallError::RespondToModel(format!("sandbox_policy is invalid: {err}")) + FunctionCallError::RespondToModel(format!("permission_profile is invalid: {err}")) })?; - config.permissions.file_system_sandbox_policy = turn.file_system_sandbox_policy.clone(); - config.permissions.network_sandbox_policy = turn.network_sandbox_policy; Ok(()) } 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 baa88ccaa..5f2c6b096 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -29,8 +29,10 @@ use codex_protocol::ThreadId; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::AskForApproval; @@ -2103,6 +2105,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { let expected_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected_sandbox, &turn.cwd); 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), + &expected_file_system_sandbox_policy, + expected_network_sandbox_policy, + ); turn.approval_policy .set(AskForApproval::OnRequest) .expect("approval policy should be set"); @@ -2111,6 +2118,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { .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(), @@ -2149,6 +2157,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { .await; assert_eq!(snapshot.sandbox_policy, expected_sandbox); assert_eq!(snapshot.approval_policy, AskForApproval::OnRequest); + assert_eq!(snapshot.permission_profile, expected_permission_profile); let child_thread = manager .get_thread(agent_id) .await @@ -2162,6 +2171,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { child_turn.network_sandbox_policy, expected_network_sandbox_policy ); + assert_eq!(child_turn.permission_profile(), expected_permission_profile); } #[tokio::test] @@ -3622,11 +3632,17 @@ async fn build_agent_spawn_config_uses_turn_context_values() { let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, &turn.cwd); let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &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.clone(); + 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) .expect("approval policy set"); @@ -3650,11 +3666,8 @@ async fn build_agent_spawn_config_uses_turn_context_values() { .expect("approval policy set"); expected .permissions - .sandbox_policy - .set(turn.sandbox_policy.get().clone()) - .expect("sandbox policy set"); - expected.permissions.file_system_sandbox_policy = file_system_sandbox_policy; - expected.permissions.network_sandbox_policy = network_sandbox_policy; + .set_permission_profile(permission_profile, turn.cwd.as_path()) + .expect("permission profile set"); assert_eq!(config, expected); } diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 2e0f8072a..6621756c1 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -206,9 +206,7 @@ impl ToolOrchestrator { let use_legacy_landlock = turn_ctx.features.use_legacy_landlock(); let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, - policy: &turn_ctx.sandbox_policy, - file_system_policy: &turn_ctx.file_system_sandbox_policy, - network_policy: turn_ctx.network_sandbox_policy, + permissions: &turn_ctx.permission_profile, enforce_managed_network: managed_network_active, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, @@ -325,9 +323,7 @@ impl ToolOrchestrator { let escalated_attempt = SandboxAttempt { sandbox: SandboxType::None, - policy: &turn_ctx.sandbox_policy, - file_system_policy: &turn_ctx.file_system_sandbox_policy, - network_policy: turn_ctx.network_sandbox_policy, + permissions: &turn_ctx.permission_profile, enforce_managed_network: managed_network_active, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 0b0d48a46..acc1eacbf 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -9,7 +9,8 @@ use crate::hook_runtime::record_additional_contexts; use crate::hook_runtime::run_post_tool_use_hooks; use crate::hook_runtime::run_pre_tool_use_hooks; use crate::memories::usage::emit_metric_for_tool_read; -use crate::sandbox_tags::sandbox_tag; +use crate::sandbox_tags::permission_profile_policy_tag; +use crate::sandbox_tags::permission_profile_sandbox_tag; use crate::session::turn_context::TurnContext; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; @@ -26,7 +27,6 @@ use codex_hooks::HookToolInputLocalShell; use codex_hooks::HookToolKind; use codex_protocol::models::ResponseInputItem; use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::SandboxPolicy; use codex_tools::ConfiguredToolSpec; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -275,14 +275,18 @@ impl ToolRegistry { let metric_tags = [ ( "sandbox", - sandbox_tag( - &invocation.turn.sandbox_policy, + permission_profile_sandbox_tag( + &invocation.turn.permission_profile, invocation.turn.windows_sandbox_level, + invocation.turn.network.is_some(), ), ), ( "sandbox_policy", - sandbox_policy_tag(&invocation.turn.sandbox_policy), + permission_profile_policy_tag( + &invocation.turn.permission_profile, + invocation.turn.cwd.as_path(), + ), ), ]; let (mcp_server, mcp_server_origin) = match &invocation.payload { @@ -580,15 +584,6 @@ fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &ToolName) -> } } -fn sandbox_policy_tag(policy: &SandboxPolicy) -> &'static str { - match policy { - SandboxPolicy::ReadOnly { .. } => "read-only", - SandboxPolicy::WorkspaceWrite { .. } => "workspace-write", - SandboxPolicy::DangerFullAccess => "danger-full-access", - SandboxPolicy::ExternalSandbox { .. } => "external-sandbox", - } -} - // Hooks use a separate wire-facing input type so hook payload JSON stays stable // and decoupled from core's internal tool runtime representation. impl From<&ToolPayload> for HookToolInput { @@ -673,9 +668,17 @@ async fn dispatch_after_tool_use_hook( success: dispatch.success, duration_ms: u64::try_from(dispatch.duration.as_millis()).unwrap_or(u64::MAX), mutating: dispatch.mutating, - sandbox: sandbox_tag(&turn.sandbox_policy, turn.windows_sandbox_level) - .to_string(), - sandbox_policy: sandbox_policy_tag(&turn.sandbox_policy).to_string(), + sandbox: permission_profile_sandbox_tag( + &turn.permission_profile, + turn.windows_sandbox_level, + turn.network.is_some(), + ) + .to_string(), + sandbox_policy: permission_profile_policy_tag( + &turn.permission_profile, + turn.cwd.as_path(), + ) + .to_string(), output_preview: dispatch.output_preview.clone(), }, }, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index ecf3ccdc0..7f218a629 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -24,8 +24,6 @@ use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; use codex_protocol::models::AdditionalPermissionProfile; -use codex_protocol::models::PermissionProfile; -use codex_protocol::models::SandboxEnforcement; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -35,8 +33,7 @@ use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; -use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; -use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_permission_profile; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::path::PathBuf; @@ -80,19 +77,8 @@ impl ApplyPatchRuntime { return None; } - let file_system_policy = effective_file_system_sandbox_policy( - attempt.file_system_policy, - req.additional_permissions.as_ref(), - ); - let network_policy = effective_network_sandbox_policy( - attempt.network_policy, - req.additional_permissions.as_ref(), - ); - let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(attempt.policy), - &file_system_policy, - network_policy, - ); + let permissions = + effective_permission_profile(attempt.permissions, req.additional_permissions.as_ref()); Some(FileSystemSandboxContext { permissions, cwd: Some(attempt.sandbox_cwd.clone()), diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 0bc4d2e6f..c4f9b44ae 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -138,12 +138,14 @@ fn file_system_sandbox_context_uses_active_attempt() { }; let sandbox_policy = SandboxPolicy::new_read_only_policy(); let file_system_policy = FileSystemSandboxPolicy::from(&sandbox_policy); + let permissions = PermissionProfile::from_runtime_permissions( + &file_system_policy, + NetworkSandboxPolicy::Restricted, + ); let manager = SandboxManager::new(); let attempt = SandboxAttempt { sandbox: SandboxType::MacosSeatbelt, - policy: &sandbox_policy, - file_system_policy: &file_system_policy, - network_policy: NetworkSandboxPolicy::Restricted, + permissions: &permissions, enforce_managed_network: false, manager: &manager, sandbox_cwd: &path, @@ -190,14 +192,11 @@ fn no_sandbox_attempt_has_no_file_system_context() { additional_permissions: None, permissions_preapproved: false, }; - let sandbox_policy = SandboxPolicy::DangerFullAccess; - let file_system_policy = FileSystemSandboxPolicy::from(&sandbox_policy); + let permissions = PermissionProfile::Disabled; let manager = SandboxManager::new(); let attempt = SandboxAttempt { sandbox: SandboxType::None, - policy: &sandbox_policy, - file_system_policy: &file_system_policy, - network_policy: NetworkSandboxPolicy::Enabled, + permissions: &permissions, enforce_managed_network: false, manager: &manager, sandbox_cwd: &path, diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index e4753533a..6aa64d1e3 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -19,9 +19,7 @@ use codex_network_proxy::PROXY_ENV_KEYS; #[cfg(target_os = "macos")] use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxType; use codex_utils_absolute_path::AbsolutePathBuf; @@ -105,14 +103,11 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow:: expiration: ExecExpiration::DefaultTimeout, capture_policy: ExecCapturePolicy::ShellTool, }; - let sandbox_policy = SandboxPolicy::DangerFullAccess; - let file_system_policy = FileSystemSandboxPolicy::from(&sandbox_policy); + let permissions = PermissionProfile::Disabled; let manager = SandboxManager::new(); let attempt = SandboxAttempt { sandbox: SandboxType::None, - policy: &sandbox_policy, - file_system_policy: &file_system_policy, - network_policy: NetworkSandboxPolicy::Enabled, + permissions: &permissions, enforce_managed_network: false, manager: &manager, sandbox_cwd: &cwd, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 2484e914f..b850a36b5 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -142,6 +142,7 @@ pub(super) async fn try_run_zsh_fork( windows_sandbox_policy_cwd: sandbox_policy_cwd, windows_sandbox_level, windows_sandbox_private_desktop: _windows_sandbox_private_desktop, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, @@ -159,6 +160,7 @@ pub(super) async fn try_run_zsh_fork( let command_executor = CoreShellCommandExecutor { command, cwd: sandbox_cwd, + permission_profile, sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, @@ -257,6 +259,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( let command_executor = CoreShellCommandExecutor { command: exec_request.command.clone(), cwd: exec_request.cwd.clone(), + permission_profile: exec_request.permission_profile.clone(), sandbox_policy: exec_request.sandbox_policy.clone(), file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(), network_sandbox_policy: exec_request.network_sandbox_policy, @@ -746,6 +749,7 @@ fn commands_for_intercepted_exec_policy( struct CoreShellCommandExecutor { command: Vec, cwd: AbsolutePathBuf, + permission_profile: PermissionProfile, sandbox_policy: SandboxPolicy, file_system_sandbox_policy: FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, @@ -763,9 +767,7 @@ struct PrepareSandboxedExecParams<'a> { command: Vec, workdir: &'a AbsolutePathBuf, env: HashMap, - sandbox_policy: &'a SandboxPolicy, - file_system_sandbox_policy: &'a FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, + permission_profile: &'a PermissionProfile, additional_permissions: Option, } @@ -801,6 +803,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { windows_sandbox_policy_cwd: self.sandbox_policy_cwd.clone(), windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: false, + 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, @@ -849,9 +852,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { command, workdir, env, - sandbox_policy: &self.sandbox_policy, - file_system_sandbox_policy: &self.file_system_sandbox_policy, - network_sandbox_policy: self.network_sandbox_policy, + permission_profile: &self.permission_profile, additional_permissions: None, })? } @@ -863,9 +864,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { command, workdir, env, - sandbox_policy: &self.sandbox_policy, - file_system_sandbox_policy: &self.file_system_sandbox_policy, - network_sandbox_policy: self.network_sandbox_policy, + permission_profile: &self.permission_profile, additional_permissions: Some(permission_profile), })? } @@ -873,15 +872,11 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { permissions, )) => { // Use a fully specified permission profile instead of merging into the turn policy. - let (file_system_sandbox_policy, network_sandbox_policy) = - permissions.permission_profile.to_runtime_permissions(); self.prepare_sandboxed_exec(PrepareSandboxedExecParams { command, workdir, env, - sandbox_policy: &permissions.sandbox_policy, - file_system_sandbox_policy: &file_system_sandbox_policy, - network_sandbox_policy, + permission_profile: &permissions.permission_profile, additional_permissions: None, })? } @@ -901,17 +896,17 @@ impl CoreShellCommandExecutor { command, workdir, env, - sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, + permission_profile, additional_permissions, } = params; + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); let (program, args) = command .split_first() .ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?; let sandbox_manager = SandboxManager::new(); let sandbox = sandbox_manager.select_initial( - file_system_sandbox_policy, + &file_system_sandbox_policy, network_sandbox_policy, SandboxablePreference::Auto, self.windows_sandbox_level, @@ -930,9 +925,7 @@ impl CoreShellCommandExecutor { }; let exec_request = sandbox_manager.transform(SandboxTransformRequest { command, - policy: sandbox_policy, - file_system_policy: file_system_sandbox_policy, - network_policy: network_sandbox_policy, + permissions: permission_profile, sandbox, enforce_managed_network: self.network.is_some(), network: self.network.as_ref(), diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index e8e17464a..f6b960ee1 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -17,7 +17,6 @@ use codex_protocol::approvals::NetworkApprovalContext; use codex_protocol::error::CodexErr; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; #[cfg(test)] @@ -368,9 +367,7 @@ pub(crate) trait ToolRuntime: Approvable + Sandboxable { pub(crate) struct SandboxAttempt<'a> { pub sandbox: SandboxType, - pub policy: &'a codex_protocol::protocol::SandboxPolicy, - pub file_system_policy: &'a FileSystemSandboxPolicy, - pub network_policy: NetworkSandboxPolicy, + pub permissions: &'a codex_protocol::models::PermissionProfile, pub enforce_managed_network: bool, pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a AbsolutePathBuf, @@ -390,9 +387,7 @@ impl<'a> SandboxAttempt<'a> { self.manager .transform(SandboxTransformRequest { command, - policy: self.policy, - file_system_policy: self.file_system_policy, - network_policy: self.network_policy, + permissions: self.permissions, sandbox: self.sandbox, enforce_managed_network: self.enforce_managed_network, network, diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 1c27c3ca0..f88bb04f3 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -12,9 +12,9 @@ use codex_models_manager::bundled_models_response; use codex_models_manager::model_info::with_config_overrides; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_tools::AdditionalProperties; use codex_tools::ConfiguredToolSpec; @@ -230,7 +230,7 @@ async fn multi_agent_v2_tools_config() -> ToolsConfig { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }) .with_max_concurrent_threads_per_session(Some(4)) @@ -309,7 +309,7 @@ async fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::new_workspace_write_policy(), + permission_profile: &PermissionProfile::workspace_write(), windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, }); @@ -335,7 +335,7 @@ async fn get_memory_requires_feature_flag() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -367,7 +367,7 @@ async fn assert_model_tools( image_generation_tool_auth_allowed: true, web_search_mode, session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let router = ToolRouter::from_config( @@ -650,7 +650,7 @@ async fn test_build_specs_default_shell_present() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -685,7 +685,7 @@ async fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let user_shell = Shell { @@ -809,7 +809,7 @@ async fn tool_suggest_requires_apps_and_plugins_features() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs_with_discoverable_tools( @@ -845,7 +845,7 @@ async fn search_tool_description_handles_no_enabled_mcp_tools() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -879,7 +879,7 @@ async fn search_tool_description_falls_back_to_connector_name_without_descriptio image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -930,7 +930,7 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1015,7 +1015,7 @@ async fn direct_mcp_tools_register_namespaced_handlers() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1052,7 +1052,7 @@ async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1101,7 +1101,7 @@ async fn test_mcp_tool_property_missing_type_defaults_to_string() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1164,7 +1164,7 @@ async fn test_mcp_tool_preserves_integer_schema() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1226,7 +1226,7 @@ async fn test_mcp_tool_array_without_items_gets_default_string_items() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1290,7 +1290,7 @@ async fn test_mcp_tool_anyof_defaults_to_string() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1359,7 +1359,7 @@ async fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 59b146408..095fb8e21 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -8,13 +8,13 @@ use serde::Serialize; use serde_json::Value; use tokio::task::JoinHandle; -use crate::sandbox_tags::sandbox_tag; +use crate::sandbox_tags::permission_profile_sandbox_tag; use codex_git_utils::get_git_remote_urls_assume_git_repo; use codex_git_utils::get_git_repo_root; use codex_git_utils::get_has_changes; use codex_git_utils::get_head_commit_hash; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::SessionSource; use codex_utils_absolute_path::AbsolutePathBuf; @@ -163,11 +163,19 @@ impl TurnMetadataState { session_source: &SessionSource, turn_id: String, cwd: AbsolutePathBuf, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, windows_sandbox_level: WindowsSandboxLevel, + enforce_managed_network: bool, ) -> Self { let repo_root = get_git_repo_root(&cwd).map(|root| root.to_string_lossy().into_owned()); - let sandbox = Some(sandbox_tag(sandbox_policy, windows_sandbox_level).to_string()); + let sandbox = Some( + permission_profile_sandbox_tag( + permission_profile, + windows_sandbox_level, + enforce_managed_network, + ) + .to_string(), + ); let base_metadata = build_turn_metadata_bag( Some(session_id), session_source.thread_source_name(), diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index 998aa8174..0004633c4 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -1,5 +1,8 @@ use super::*; +use crate::sandbox_tags::sandbox_tag; +use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use core_test_support::PathBufExt; @@ -70,14 +73,16 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( "session-a".to_string(), &SessionSource::Exec, "turn-a".to_string(), cwd, - &sandbox_policy, + &permission_profile, WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, ); let header = state.current_header_value().expect("header"); @@ -97,7 +102,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { fn turn_metadata_state_classifies_subagent_thread_source() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); - let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let permission_profile = PermissionProfile::read_only(); let session_source = SessionSource::SubAgent(SubAgentSource::Review); let state = TurnMetadataState::new( @@ -105,8 +110,9 @@ fn turn_metadata_state_classifies_subagent_thread_source() { &session_source, "turn-a".to_string(), cwd, - &sandbox_policy, + &permission_profile, WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, ); let header = state.current_header_value().expect("header"); @@ -120,15 +126,16 @@ fn turn_metadata_state_classifies_subagent_thread_source() { fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields() { let temp_dir = TempDir::new().expect("temp dir"); let cwd = temp_dir.path().abs(); - let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let permission_profile = PermissionProfile::read_only(); let state = TurnMetadataState::new( "session-a".to_string(), &SessionSource::Exec, "turn-a".to_string(), cwd, - &sandbox_policy, + &permission_profile, WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, ); state.set_responsesapi_client_metadata(HashMap::from([ ("fiber_run_id".to_string(), "fiber-123".to_string()), diff --git a/codex-rs/core/src/unified_exec/mod_tests.rs b/codex-rs/core/src/unified_exec/mod_tests.rs index f96498b5a..0730335c8 100644 --- a/codex-rs/core/src/unified_exec/mod_tests.rs +++ b/codex-rs/core/src/unified_exec/mod_tests.rs @@ -55,9 +55,7 @@ fn test_exec_request( env: HashMap, ) -> ExecRequest { let windows_sandbox_private_desktop = false; - let sandbox_policy = turn.sandbox_policy.get().clone(); - let file_system_sandbox_policy = turn.file_system_sandbox_policy.clone(); - let network_sandbox_policy = turn.network_sandbox_policy; + let permission_profile = turn.permission_profile(); let network = None; let arg0 = None; ExecRequest::new( @@ -70,9 +68,7 @@ fn test_exec_request( SandboxType::None, turn.windows_sandbox_level, windows_sandbox_private_desktop, - sandbox_policy, - file_system_sandbox_policy, - network_sandbox_policy, + permission_profile, arg0, ) } diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index fac05f2b4..955c37bd5 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -71,6 +71,16 @@ fn exec_server_params_use_env_policy_overlay_contract() { .expect("current dir") .try_into() .expect("absolute path"); + let sandbox_policy = codex_protocol::protocol::SandboxPolicy::DangerFullAccess; + let file_system_sandbox_policy = + codex_protocol::permissions::FileSystemSandboxPolicy::from(&sandbox_policy); + let network_sandbox_policy = codex_protocol::permissions::NetworkSandboxPolicy::Restricted; + let permission_profile = + codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( + codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, + ); let request = ExecRequest { command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()], cwd: cwd.clone(), @@ -99,11 +109,10 @@ fn exec_server_params_use_env_policy_overlay_contract() { windows_sandbox_policy_cwd: cwd, windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, - sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - file_system_sandbox_policy: codex_protocol::permissions::FileSystemSandboxPolicy::from( - &codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - ), - network_sandbox_policy: codex_protocol::permissions::NetworkSandboxPolicy::Restricted, + permission_profile, + sandbox_policy, + file_system_sandbox_policy, + network_sandbox_policy, windows_sandbox_filesystem_overrides: None, arg0: None, }; diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index a8396dd57..9358506a2 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -2880,7 +2880,11 @@ allow_local_binding = true }; let mut builder = test_codex().with_home(home).with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(SandboxPolicy::DangerFullAccess); + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path()) + .expect("test setup should allow sandbox policy"); let layers = config .config_layer_stack .get_layers( diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index c80c8a334..a0dd7f8e2 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -8,8 +8,7 @@ use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::error::Result; use codex_protocol::exec_output::ExecToolCallOutput; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; @@ -52,12 +51,11 @@ where }; let policy = SandboxPolicy::new_read_only_policy(); + let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&policy); process_exec_tool_call( params, - &policy, - &FileSystemSandboxPolicy::from(&policy), - NetworkSandboxPolicy::from(&policy), + &permission_profile, &cwd, &None, /*use_legacy_landlock*/ false, diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 25048d5c8..cfe9ba8fc 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -672,12 +672,24 @@ async fn steered_user_input_follows_compact_when_only_the_steer_needs_follow_up( async fn steered_user_input_waits_when_tool_output_triggers_compact_before_next_request() { let (gate_first_completed_tx, gate_first_completed_rx) = oneshot::channel(); + let large_output_command = if cfg!(windows) { + "[Console]::Out.Write([string]::new([char]'0', 4000))" + } else { + "printf '%04000d' 0" + }; + let large_output_args = json!({ + "command": large_output_command, + "login": false, + "timeout_ms": 2000, + }) + .to_string(); + let first_chunks = vec![ chunk(ev_response_created("resp-1")), chunk(ev_function_call( "call-1", "shell_command", - r#"{"command":"printf '%04000d' 0","login":false,"timeout_ms":2000}"#, + &large_output_args, )), gated_chunk( gate_first_completed_rx, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index fea228375..e3c04361b 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -550,7 +550,10 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .permissions + .set_legacy_sandbox_policy(sandbox_policy_for_config, config.cwd.as_path()) + .expect("test sandbox policy should be allowed"); config.config_layer_stack = ConfigLayerStack::default(); }); let test = builder.build(&server).await?; diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index a1c77fb88..be3ea457b 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -1,13 +1,13 @@ use std::collections::HashMap; use codex_app_server_protocol::JSONRPCErrorError; +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::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxExecRequest; use codex_sandboxing::SandboxManager; @@ -60,31 +60,27 @@ impl FileSystemSandboxRunner { add_helper_runtime_permissions(&mut file_system_policy, &helper_read_roots, cwd.as_path()); normalize_file_system_policy_root_aliases(&mut file_system_policy); let network_policy = NetworkSandboxPolicy::Restricted; - let sandbox_policy = - compatibility_sandbox_policy(&file_system_policy, network_policy, cwd.as_path()); - let command = self.sandbox_exec_request( - &sandbox_policy, + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + sandbox.permissions.enforcement(), &file_system_policy, network_policy, - &cwd, - sandbox, - )?; + ); + let command = self.sandbox_exec_request(&permission_profile, &cwd, sandbox)?; let request_json = serde_json::to_vec(&request).map_err(json_error)?; run_command(command, request_json).await } fn sandbox_exec_request( &self, - sandbox_policy: &SandboxPolicy, - file_system_policy: &FileSystemSandboxPolicy, - network_policy: NetworkSandboxPolicy, + permission_profile: &PermissionProfile, cwd: &AbsolutePathBuf, sandbox_context: &FileSystemSandboxContext, ) -> Result { let helper = &self.runtime_paths.codex_self_exe; let sandbox_manager = SandboxManager::new(); + let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions(); let sandbox = sandbox_manager.select_initial( - file_system_policy, + &file_system_policy, network_policy, SandboxablePreference::Auto, sandbox_context.windows_sandbox_level, @@ -100,9 +96,7 @@ impl FileSystemSandboxRunner { sandbox_manager .transform(SandboxTransformRequest { command, - policy: sandbox_policy, - file_system_policy, - network_policy, + permissions: permission_profile, sandbox, enforce_managed_network: false, network: None, @@ -179,36 +173,6 @@ fn add_helper_runtime_permissions( } } -fn compatibility_sandbox_policy( - file_system_policy: &FileSystemSandboxPolicy, - network_policy: NetworkSandboxPolicy, - cwd: &std::path::Path, -) -> SandboxPolicy { - file_system_policy - .to_legacy_sandbox_policy(network_policy, cwd) - .unwrap_or_else(|_| compatibility_workspace_write_policy(file_system_policy, cwd)) -} - -fn compatibility_workspace_write_policy( - file_system_policy: &FileSystemSandboxPolicy, - cwd: &std::path::Path, -) -> SandboxPolicy { - let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok(); - let writable_roots = file_system_policy - .get_writable_roots_with_cwd(cwd) - .into_iter() - .map(|root| root.root) - .filter(|root| cwd_abs.as_ref() != Some(root)) - .collect(); - - SandboxPolicy::WorkspaceWrite { - writable_roots, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - } -} - fn normalize_file_system_policy_root_aliases(file_system_policy: &mut FileSystemSandboxPolicy) { for entry in &mut file_system_policy.entries { if let FileSystemPath::Path { path } = &mut entry.path { @@ -347,7 +311,6 @@ mod tests { use super::FileSystemSandboxRunner; use super::add_helper_runtime_permissions; - use super::compatibility_sandbox_policy; use super::helper_env; use super::helper_env_from_vars; use super::helper_env_key_is_allowed; @@ -488,18 +451,12 @@ mod tests { let file_system_policy = restricted_policy(vec![path_entry(cwd.clone(), FileSystemAccessMode::Write)]); let network_policy = NetworkSandboxPolicy::Restricted; - let sandbox_policy = - compatibility_sandbox_policy(&file_system_policy, network_policy, cwd.as_path()); + let permission_profile = + PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy); let sandbox_context = sandbox_context_with_cwd(&file_system_policy, cwd.clone()); let request = runner - .sandbox_exec_request( - &sandbox_policy, - &file_system_policy, - network_policy, - &cwd, - &sandbox_context, - ) + .sandbox_exec_request(&permission_profile, &cwd, &sandbox_context) .expect("sandbox exec request"); assert_eq!(request.env.get(&path_key), Some(&path)); diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index aa41464ec..84d4a6beb 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -24,8 +24,7 @@ async fn spawn_command_under_sandbox( use codex_core::exec::build_exec_request; use codex_core::sandboxing::SandboxPermissions; use codex_protocol::config_types::WindowsSandboxLevel; - use codex_protocol::permissions::FileSystemSandboxPolicy; - use codex_protocol::permissions::NetworkSandboxPolicy; + use codex_protocol::models::PermissionProfile; use std::process::Stdio; let codex_linux_sandbox_exe = None; @@ -43,9 +42,7 @@ async fn spawn_command_under_sandbox( justification: None, arg0: None, }, - sandbox_policy, - &FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, sandbox_cwd), - NetworkSandboxPolicy::from(sandbox_policy), + &PermissionProfile::from_legacy_sandbox_policy(sandbox_policy), sandbox_cwd, &codex_linux_sandbox_exe, /*use_legacy_landlock*/ false, diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 17ee7dd8a..38478e11f 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -10,6 +10,8 @@ use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::error::CodexErr; use codex_protocol::error::Result; use codex_protocol::error::SandboxErr; +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; @@ -132,12 +134,15 @@ async fn run_cmd_result_with_policies( }; let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox"); let codex_linux_sandbox_exe = Some(PathBuf::from(sandbox_program)); + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, + ); process_exec_tool_call( params, - &sandbox_policy, - &file_system_sandbox_policy, - network_sandbox_policy, + &permission_profile, &sandbox_cwd, &codex_linux_sandbox_exe, use_legacy_landlock, @@ -394,11 +399,10 @@ async fn assert_network_blocked(cmd: &[&str]) { let sandbox_policy = SandboxPolicy::new_read_only_policy(); let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox"); let codex_linux_sandbox_exe: Option = Some(PathBuf::from(sandbox_program)); + let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy); let result = process_exec_tool_call( params, - &sandbox_policy, - &FileSystemSandboxPolicy::from(&sandbox_policy), - NetworkSandboxPolicy::from(&sandbox_policy), + &permission_profile, &sandbox_cwd, &codex_linux_sandbox_exe, /*use_legacy_landlock*/ false, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index f26a48f7e..2511dfecf 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -386,6 +386,76 @@ impl Default for PermissionProfile { } impl PermissionProfile { + /// Managed read-only filesystem access with restricted network access. + pub fn read_only() -> Self { + Self::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + } + } + + /// Managed workspace-write filesystem access with restricted network access. + pub fn workspace_write() -> Self { + Self::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::SlashTmp, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Tmpdir, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(".git".into())), + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(".agents".into())), + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(".codex".into())), + }, + access: FileSystemAccessMode::Read, + }, + ], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + } + } + pub fn from_runtime_permissions( file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, @@ -1762,6 +1832,20 @@ mod tests { Ok(()) } + #[test] + fn permission_profile_presets_match_legacy_defaults() { + assert_eq!( + PermissionProfile::read_only(), + PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_read_only_policy()) + ); + assert_eq!( + PermissionProfile::workspace_write(), + PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy() + ) + ); + } + #[test] fn permission_profile_round_trip_preserves_disabled_sandbox() -> Result<()> { let cwd = tempdir()?; diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index 38aed6ce2..f4263fdfd 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -17,6 +17,7 @@ pub use manager::SandboxTransformError; pub use manager::SandboxTransformRequest; pub use manager::SandboxType; pub use manager::SandboxablePreference; +pub use manager::compatibility_sandbox_policy_for_permission_profile; pub use manager::get_platform_sandbox; use codex_protocol::error::CodexErr; diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index a13f828fd..5115edb6d 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -5,13 +5,12 @@ use crate::bwrap::is_wsl1; use crate::landlock::CODEX_LINUX_SANDBOX_ARG0; use crate::landlock::allow_network_for_proxy; use crate::landlock::create_linux_sandbox_command_args_for_policies; -use crate::policy_transforms::EffectiveSandboxPermissions; -use crate::policy_transforms::effective_file_system_sandbox_policy; -use crate::policy_transforms::effective_network_sandbox_policy; +use crate::policy_transforms::effective_permission_profile; use crate::policy_transforms::should_require_platform_sandbox; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::AdditionalPermissionProfile; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; @@ -80,6 +79,7 @@ pub struct SandboxExecRequest { pub sandbox: SandboxType, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, + pub permission_profile: PermissionProfile, pub sandbox_policy: SandboxPolicy, pub file_system_sandbox_policy: FileSystemSandboxPolicy, pub network_sandbox_policy: NetworkSandboxPolicy, @@ -91,9 +91,7 @@ pub struct SandboxExecRequest { /// This keeps call sites self-documenting when several fields are optional. pub struct SandboxTransformRequest<'a> { pub command: SandboxCommand, - pub policy: &'a SandboxPolicy, - pub file_system_policy: &'a FileSystemSandboxPolicy, - pub network_policy: NetworkSandboxPolicy, + pub permissions: &'a PermissionProfile, pub sandbox: SandboxType, pub enforce_managed_network: bool, // TODO(viyatb): Evaluate switching this to Option> @@ -174,9 +172,7 @@ impl SandboxManager { ) -> Result { let SandboxTransformRequest { mut command, - policy, - file_system_policy, - network_policy, + permissions, sandbox, enforce_managed_network, network, @@ -187,15 +183,16 @@ impl SandboxManager { windows_sandbox_private_desktop, } = request; let additional_permissions = command.additional_permissions.take(); - let EffectiveSandboxPermissions { - sandbox_policy: effective_policy, - } = EffectiveSandboxPermissions::new(policy, additional_permissions.as_ref()); - let effective_file_system_policy = effective_file_system_sandbox_policy( - file_system_policy, - additional_permissions.as_ref(), + let effective_permission_profile = + effective_permission_profile(permissions, additional_permissions.as_ref()); + let (effective_file_system_policy, effective_network_policy) = + effective_permission_profile.to_runtime_permissions(); + let effective_policy = compatibility_sandbox_policy_for_permission_profile( + &effective_permission_profile, + &effective_file_system_policy, + effective_network_policy, + sandbox_policy_cwd, ); - let effective_network_policy = - effective_network_sandbox_policy(network_policy, additional_permissions.as_ref()); let mut argv = Vec::with_capacity(1 + command.args.len()); argv.push(command.program); argv.extend(command.args.into_iter().map(OsString::from)); @@ -264,6 +261,7 @@ impl SandboxManager { sandbox, windows_sandbox_level, windows_sandbox_private_desktop, + permission_profile: effective_permission_profile, sandbox_policy: effective_policy, file_system_sandbox_policy: effective_file_system_policy, network_sandbox_policy: effective_network_policy, @@ -272,6 +270,50 @@ impl SandboxManager { } } +pub fn compatibility_sandbox_policy_for_permission_profile( + permissions: &PermissionProfile, + file_system_policy: &FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, + cwd: &Path, +) -> SandboxPolicy { + permissions + .to_legacy_sandbox_policy(cwd) + .unwrap_or_else(|_| { + compatibility_workspace_write_policy(file_system_policy, network_policy, cwd) + }) +} + +fn compatibility_workspace_write_policy( + file_system_policy: &FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, + cwd: &Path, +) -> SandboxPolicy { + let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok(); + let writable_roots = file_system_policy + .get_writable_roots_with_cwd(cwd) + .into_iter() + .map(|root| root.root) + .filter(|root| cwd_abs.as_ref() != Some(root)) + .collect(); + let tmpdir_writable = std::env::var_os("TMPDIR") + .filter(|tmpdir| !tmpdir.is_empty()) + .and_then(|tmpdir| { + AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok() + }) + .is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd)); + let slash_tmp = Path::new("/tmp"); + let slash_tmp_writable = slash_tmp.is_absolute() + && slash_tmp.is_dir() + && file_system_policy.can_write_path_with_cwd(slash_tmp, cwd); + + SandboxPolicy::WorkspaceWrite { + writable_roots, + network_access: network_policy.is_enabled(), + exclude_tmpdir_env_var: !tmpdir_writable, + exclude_slash_tmp: !slash_tmp_writable, + } +} + #[cfg(target_os = "linux")] fn ensure_linux_bubblewrap_is_supported( file_system_sandbox_policy: &FileSystemSandboxPolicy, diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index d9c3e194f..7b8bc8579 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -5,9 +5,10 @@ use super::SandboxType; use super::SandboxablePreference; use super::get_platform_sandbox; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -74,6 +75,10 @@ fn restricted_file_system_uses_platform_sandbox_without_managed_network() { fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() { let manager = SandboxManager::new(); let cwd = AbsolutePathBuf::current_dir().expect("current dir"); + let permissions = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Restricted, + ); let exec_request = manager .transform(SandboxTransformRequest { command: SandboxCommand { @@ -83,11 +88,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() env: HashMap::new(), additional_permissions: None, }, - policy: &SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Restricted, - }, - file_system_policy: &FileSystemSandboxPolicy::unrestricted(), - network_policy: NetworkSandboxPolicy::Restricted, + permissions: &permissions, sandbox: SandboxType::None, enforce_managed_network: false, network: None, @@ -113,6 +114,9 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() fn transform_additional_permissions_enable_network_for_external_sandbox() { let manager = SandboxManager::new(); let cwd = AbsolutePathBuf::current_dir().expect("current dir"); + let permissions = PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }; let temp_dir = TempDir::new().expect("create temp dir"); let path = AbsolutePathBuf::from_absolute_path( canonicalize(temp_dir.path()).expect("canonicalize temp dir"), @@ -125,7 +129,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { args: Vec::new(), cwd: cwd.clone(), env: HashMap::new(), - additional_permissions: Some(PermissionProfile { + additional_permissions: Some(AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), @@ -135,11 +139,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { )), }), }, - policy: &SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Restricted, - }, - file_system_policy: &FileSystemSandboxPolicy::unrestricted(), - network_policy: NetworkSandboxPolicy::Restricted, + permissions: &permissions, sandbox: SandboxType::None, enforce_managed_network: false, network: None, @@ -174,6 +174,24 @@ fn transform_additional_permissions_preserves_denied_entries() { .expect("absolute temp dir"); let allowed_path = workspace_root.join("allowed"); let denied_path = workspace_root.join("denied"); + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: denied_path.clone(), + }, + access: FileSystemAccessMode::None, + }, + ]); + let permissions = PermissionProfile::from_runtime_permissions( + &file_system_policy, + NetworkSandboxPolicy::Restricted, + ); let exec_request = manager .transform(SandboxTransformRequest { command: SandboxCommand { @@ -181,7 +199,7 @@ fn transform_additional_permissions_preserves_denied_entries() { args: Vec::new(), cwd: cwd.clone(), env: HashMap::new(), - additional_permissions: Some(PermissionProfile { + additional_permissions: Some(AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![allowed_path.clone()]), @@ -189,24 +207,7 @@ fn transform_additional_permissions_preserves_denied_entries() { ..Default::default() }), }, - policy: &SandboxPolicy::ReadOnly { - network_access: false, - }, - file_system_policy: &FileSystemSandboxPolicy::restricted(vec![ - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Path { - path: denied_path.clone(), - }, - access: FileSystemAccessMode::None, - }, - ]), - network_policy: NetworkSandboxPolicy::Restricted, + permissions: &permissions, sandbox: SandboxType::None, enforce_managed_network: false, network: None, @@ -249,6 +250,7 @@ fn transform_linux_seccomp_request( ) -> super::SandboxExecRequest { let manager = SandboxManager::new(); let cwd = AbsolutePathBuf::current_dir().expect("current dir"); + let permissions = PermissionProfile::Disabled; manager .transform(SandboxTransformRequest { command: SandboxCommand { @@ -258,9 +260,7 @@ fn transform_linux_seccomp_request( env: HashMap::new(), additional_permissions: None, }, - policy: &SandboxPolicy::DangerFullAccess, - file_system_policy: &FileSystemSandboxPolicy::unrestricted(), - network_policy: NetworkSandboxPolicy::Enabled, + permissions: &permissions, sandbox: SandboxType::LinuxSeccomp, enforce_managed_network: false, network: None, diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 20a026d00..20efb4b90 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -1,6 +1,7 @@ use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -561,6 +562,22 @@ pub fn effective_network_sandbox_policy( } } +pub fn effective_permission_profile( + permission_profile: &PermissionProfile, + additional_permissions: Option<&AdditionalPermissionProfile>, +) -> PermissionProfile { + let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions(); + let effective_file_system_policy = + effective_file_system_sandbox_policy(&file_system_policy, additional_permissions); + let effective_network_policy = + effective_network_sandbox_policy(network_policy, additional_permissions); + PermissionProfile::from_runtime_permissions_with_enforcement( + permission_profile.enforcement(), + &effective_file_system_policy, + effective_network_policy, + ) +} + fn sandbox_policy_with_additional_permissions( sandbox_policy: &SandboxPolicy, additional_permissions: &AdditionalPermissionProfile, diff --git a/codex-rs/tools/src/tool_config.rs b/codex-rs/tools/src/tool_config.rs index 7520beeae..3ddcf481a 100644 --- a/codex-rs/tools/src/tool_config.rs +++ b/codex-rs/tools/src/tool_config.rs @@ -4,13 +4,13 @@ use codex_features::Features; use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::WebSearchToolType; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_utils_absolute_path::AbsolutePathBuf; @@ -121,7 +121,7 @@ pub struct ToolsConfigParams<'a> { pub image_generation_tool_auth_allowed: bool, pub web_search_mode: Option, pub session_source: SessionSource, - pub sandbox_policy: &'a SandboxPolicy, + pub permission_profile: &'a PermissionProfile, pub windows_sandbox_level: WindowsSandboxLevel, } @@ -134,7 +134,7 @@ impl ToolsConfig { image_generation_tool_auth_allowed, web_search_mode, session_source, - sandbox_policy, + permission_profile, windows_sandbox_level, } = params; let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform); @@ -167,7 +167,7 @@ impl ToolsConfig { }; let unified_exec_allowed = unified_exec_allowed_in_environment( cfg!(target_os = "windows"), - sandbox_policy, + permission_profile, *windows_sandbox_level, ); let shell_type = if !features.enabled(Feature::ShellTool) { @@ -322,15 +322,19 @@ fn supports_image_generation(model_info: &ModelInfo) -> bool { fn unified_exec_allowed_in_environment( is_windows: bool, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, windows_sandbox_level: WindowsSandboxLevel, ) -> bool { + let managed_sandbox_required = match permission_profile { + PermissionProfile::Managed { + file_system, + network, + } => !file_system.to_sandbox_policy().has_full_disk_write_access() || !network.is_enabled(), + PermissionProfile::Disabled | PermissionProfile::External { .. } => false, + }; !(is_windows && windows_sandbox_level != WindowsSandboxLevel::Disabled - && !matches!( - sandbox_policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - )) + && managed_sandbox_required) } #[cfg(test)] diff --git a/codex-rs/tools/src/tool_config_tests.rs b/codex-rs/tools/src/tool_config_tests.rs index 25c78a053..ab82b0bdf 100644 --- a/codex-rs/tools/src/tool_config_tests.rs +++ b/codex-rs/tools/src/tool_config_tests.rs @@ -3,10 +3,12 @@ use codex_features::Feature; use codex_features::Features; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::ManagedFileSystemPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_utils_absolute_path::AbsolutePathBuf; @@ -50,25 +52,40 @@ fn model_info() -> ModelInfo { } #[test] -fn unified_exec_is_blocked_for_windows_sandboxed_policies_only() { +fn unified_exec_is_blocked_for_windows_managed_profiles_only() { assert!(!unified_exec_allowed_in_environment( /*is_windows*/ true, - &SandboxPolicy::new_read_only_policy(), + &PermissionProfile::read_only(), WindowsSandboxLevel::RestrictedToken, )); assert!(!unified_exec_allowed_in_environment( /*is_windows*/ true, - &SandboxPolicy::new_workspace_write_policy(), + &PermissionProfile::workspace_write(), WindowsSandboxLevel::RestrictedToken, )); assert!(unified_exec_allowed_in_environment( /*is_windows*/ true, - &SandboxPolicy::DangerFullAccess, + &PermissionProfile::Disabled, WindowsSandboxLevel::RestrictedToken, )); assert!(unified_exec_allowed_in_environment( /*is_windows*/ true, - &SandboxPolicy::DangerFullAccess, + &PermissionProfile::External { + network: Default::default(), + }, + WindowsSandboxLevel::RestrictedToken, + )); + assert!(unified_exec_allowed_in_environment( + /*is_windows*/ true, + &PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Enabled, + }, + WindowsSandboxLevel::RestrictedToken, + )); + assert!(unified_exec_allowed_in_environment( + /*is_windows*/ true, + &PermissionProfile::Disabled, WindowsSandboxLevel::Disabled, )); } @@ -88,7 +105,7 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -148,7 +165,7 @@ fn subagents_keep_request_user_input_mode_config_and_agent_jobs_workers_opt_in_b session_source: SessionSource::SubAgent(SubAgentSource::Other( "agent_job:test".to_string(), )), - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -176,7 +193,7 @@ fn image_generation_requires_feature_and_supported_model() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let supported_tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -186,7 +203,7 @@ fn image_generation_requires_feature_and_supported_model() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let auth_disallowed_tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -196,7 +213,7 @@ fn image_generation_requires_feature_and_supported_model() { image_generation_tool_auth_allowed: false, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let unsupported_tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -206,7 +223,7 @@ fn image_generation_requires_feature_and_supported_model() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); assert!(!default_tools_config.image_gen_tool); diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs index 24a1ea9ce..8f64349c2 100644 --- a/codex-rs/tools/src/tool_registry_plan_tests.rs +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -26,11 +26,11 @@ use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::VIEW_IMAGE_TOOL_NAME; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::WebSearchToolType; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use pretty_assertions::assert_eq; @@ -57,7 +57,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -169,7 +169,7 @@ fn test_build_specs_collab_tools_enabled() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -207,7 +207,7 @@ fn goal_tools_require_goals_feature() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -228,7 +228,7 @@ fn goal_tools_require_goals_feature() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -254,7 +254,7 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -397,7 +397,7 @@ fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -432,7 +432,7 @@ fn view_image_tool_omits_detail_without_original_detail_support() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -462,7 +462,7 @@ fn view_image_tool_includes_detail_with_original_detail_support() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -499,7 +499,7 @@ fn disabled_environment_omits_environment_backed_tools() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }) .with_has_environment(/*has_environment*/ false); @@ -537,7 +537,7 @@ fn test_build_specs_agent_job_worker_tools_enabled() { session_source: SessionSource::SubAgent(SubAgentSource::Other( "agent_job:test".to_string(), )), - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -574,7 +574,7 @@ fn request_user_input_description_reflects_default_mode_feature_flag() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -597,7 +597,7 @@ fn request_user_input_description_reflects_default_mode_feature_flag() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -625,7 +625,7 @@ fn request_permissions_requires_feature_flag() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -645,7 +645,7 @@ fn request_permissions_requires_feature_flag() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -674,7 +674,7 @@ fn request_permissions_tool_is_independent_from_additional_permissions() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -705,7 +705,7 @@ fn image_generation_tools_require_feature_and_supported_model() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (default_tools, _) = build_specs( @@ -728,7 +728,7 @@ fn image_generation_tools_require_feature_and_supported_model() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (supported_tools, _) = build_specs( @@ -754,7 +754,7 @@ fn image_generation_tools_require_feature_and_supported_model() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -784,7 +784,7 @@ fn web_search_mode_cached_sets_external_web_access_false() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -820,7 +820,7 @@ fn web_search_mode_live_sets_external_web_access_true() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -869,7 +869,7 @@ fn web_search_config_is_forwarded_to_tool_spec() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }) .with_web_search_config(Some(web_search_config.clone())); @@ -911,7 +911,7 @@ fn web_search_tool_type_text_and_image_sets_search_content_types() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -946,7 +946,7 @@ fn mcp_resource_tools_are_hidden_without_mcp_servers() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -977,7 +977,7 @@ fn mcp_resource_tools_are_included_when_mcp_servers_are_present() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -1011,7 +1011,7 @@ fn test_parallel_support_flags() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -1038,7 +1038,7 @@ fn test_test_model_info_includes_sync_tool() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -1064,7 +1064,7 @@ fn test_build_specs_mcp_tools_converted() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Live), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -1156,7 +1156,7 @@ fn test_build_specs_mcp_namespace_description_falls_back_when_missing() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -1196,7 +1196,7 @@ fn test_build_specs_mcp_tools_sorted_by_name() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1246,7 +1246,7 @@ fn search_tool_description_lists_each_mcp_source_once() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1348,7 +1348,7 @@ fn search_tool_requires_model_capability_and_enabled_feature() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -1368,7 +1368,7 @@ fn search_tool_requires_model_capability_and_enabled_feature() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -1386,7 +1386,7 @@ fn search_tool_requires_model_capability_and_enabled_feature() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs( @@ -1411,7 +1411,7 @@ fn search_tool_registers_for_deferred_dynamic_tools() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let dynamic_tools = vec![ @@ -1500,7 +1500,7 @@ fn tool_suggest_is_not_registered_without_feature_flag() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs_with_discoverable_tools( @@ -1540,7 +1540,7 @@ fn tool_suggest_can_be_registered_without_search_tool() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); let (tools, _) = build_specs_with_discoverable_tools( @@ -1586,7 +1586,7 @@ fn tool_suggest_description_lists_discoverable_tools() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1688,7 +1688,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1741,7 +1741,7 @@ fn code_mode_preserves_nullable_and_literal_mcp_input_shapes() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1824,7 +1824,7 @@ fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1860,7 +1860,7 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -1897,7 +1897,7 @@ fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only( image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); @@ -2054,7 +2054,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_structured_output_sample() { image_generation_tool_auth_allowed: true, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: &PermissionProfile::Disabled, windows_sandbox_level: WindowsSandboxLevel::Disabled, }); diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index abf90bdaf..44ef5f664 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -72,15 +72,15 @@ impl App { "Failed to carry forward approval policy override: {err}" )); } - if let Some(policy) = self.runtime_sandbox_policy_override.as_ref() { - if let Err(err) = config.permissions.sandbox_policy.set(policy.clone()) { - tracing::warn!(%err, "failed to carry forward sandbox policy override"); - self.chat_widget.add_error_message(format!( - "Failed to carry forward sandbox policy override: {err}" - )); - } else { - sync_runtime_permissions_from_legacy_sandbox_policy(config); - } + if let Some(policy) = self.runtime_sandbox_policy_override.as_ref() + && let Err(err) = config + .permissions + .set_legacy_sandbox_policy(policy.clone(), config.cwd.as_path()) + { + tracing::warn!(%err, "failed to carry forward sandbox policy override"); + self.chat_widget.add_error_message(format!( + "Failed to carry forward sandbox policy override: {err}" + )); } } @@ -113,13 +113,15 @@ impl App { user_message_prefix: &str, log_message: &str, ) -> bool { - if let Err(err) = config.permissions.sandbox_policy.set(policy) { + if let Err(err) = config + .permissions + .set_legacy_sandbox_policy(policy, config.cwd.as_path()) + { tracing::warn!(error = %err, "{log_message}"); self.chat_widget .add_error_message(format!("{user_message_prefix}: {err}")); return false; } - sync_runtime_permissions_from_legacy_sandbox_policy(config); true } @@ -543,17 +545,6 @@ impl App { } } -fn sync_runtime_permissions_from_legacy_sandbox_policy(config: &mut Config) { - let sandbox_policy = config.permissions.sandbox_policy.get(); - config.permissions.file_system_sandbox_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - sandbox_policy, - &config.cwd, - ); - config.permissions.network_sandbox_policy = - codex_protocol::permissions::NetworkSandboxPolicy::from(sandbox_policy); -} - #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 550dcff80..da83a72c4 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2218,9 +2218,7 @@ async fn inactive_thread_approval_bubbles_into_active_view() -> Result<()> { ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_workspace_write_policy(), - )), + permission_profile: Some(PermissionProfile::workspace_write()), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2380,9 +2378,7 @@ async fn side_defers_subagent_approval_overlay_until_side_exits() -> Result<()> ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_workspace_write_policy(), - )), + permission_profile: Some(PermissionProfile::workspace_write()), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2605,9 +2601,7 @@ async fn inactive_thread_approval_badge_clears_after_turn_completion_notificatio ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_workspace_write_policy(), - )), + permission_profile: Some(PermissionProfile::workspace_write()), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) }, @@ -2661,9 +2655,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_workspace_write_policy(), - )), + permission_profile: Some(PermissionProfile::workspace_write()), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2776,9 +2768,7 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_ let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_workspace_write_policy(), - )), + permission_profile: Some(PermissionProfile::workspace_write()), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2847,9 +2837,7 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { let primary_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), - permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_workspace_write_policy(), - )), + permission_profile: Some(PermissionProfile::workspace_write()), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; app.primary_session_configured = Some(primary_session); @@ -3752,9 +3740,7 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_read_only_policy(), - )), + permission_profile: Some(PermissionProfile::read_only()), cwd: cwd.abs(), instruction_source_paths: Vec::new(), reasoning_effort: None, diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index 4de0b33f1..daf743b46 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -303,9 +303,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), - permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_read_only_policy(), - )), + permission_profile: Some(PermissionProfile::read_only()), cwd: cwd.abs(), instruction_source_paths: Vec::new(), reasoning_effort: None, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 44e745e16..8fa763021 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1776,9 +1776,7 @@ mod tests { AskForApproval::Never, codex_protocol::config_types::ApprovalsReviewer::User, SandboxPolicy::new_read_only_policy(), - Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_read_only_policy(), - )), + Some(PermissionProfile::read_only()), test_path_buf("/tmp/project").abs(), Vec::new(), /*reasoning_effort*/ None, @@ -1809,9 +1807,7 @@ mod tests { AskForApproval::Never, codex_protocol::config_types::ApprovalsReviewer::User, SandboxPolicy::new_read_only_policy(), - Some(PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::new_read_only_policy(), - )), + Some(PermissionProfile::read_only()), test_path_buf("/tmp/project").abs(), Vec::new(), /*reasoning_effort*/ None, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6d2450ece..400a59712 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2333,20 +2333,6 @@ impl ChatWidget { display: SessionConfiguredDisplay, fork_parent_title: Option, ) { - let (file_system_sandbox_policy, network_sandbox_policy) = match event - .permission_profile - .as_ref() - { - Some(permission_profile) => permission_profile.to_runtime_permissions(), - None => ( - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - &event.sandbox_policy, - &event.cwd, - ), - codex_protocol::permissions::NetworkSandboxPolicy::from(&event.sandbox_policy), - ), - }; - self.last_agent_markdown = None; self.agent_turn_markdowns.clear(); self.visible_user_turn_count = 0; @@ -2379,18 +2365,52 @@ impl ChatWidget { self.config.permissions.approval_policy = Constrained::allow_only(event.approval_policy); } - if let Err(err) = self - .config - .permissions - .sandbox_policy - .set(event.sandbox_policy.clone()) - { - tracing::warn!(%err, "failed to sync sandbox_policy from SessionConfigured"); + let permission_sync = match event.permission_profile.clone() { + Some(permission_profile) => self + .config + .permissions + .set_permission_profile(permission_profile, event.cwd.as_path()), + None => self + .config + .permissions + .set_legacy_sandbox_policy(event.sandbox_policy.clone(), event.cwd.as_path()), + }; + if let Err(err) = permission_sync { + 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); + } + } } - self.config.permissions.file_system_sandbox_policy = file_system_sandbox_policy; - self.config.permissions.network_sandbox_policy = network_sandbox_policy; self.config.approvals_reviewer = event.approvals_reviewer; self.status_line_project_root_name_cache = None; let forked_from_id = event.forked_from_id; @@ -10284,16 +10304,9 @@ impl ChatWidget { /// Set the sandbox policy in the widget's config copy. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> { - self.config.permissions.sandbox_policy.set(policy)?; - let sandbox_policy = self.config.permissions.sandbox_policy.get(); - self.config.permissions.file_system_sandbox_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - sandbox_policy, - &self.config.cwd, - ); - self.config.permissions.network_sandbox_policy = - codex_protocol::permissions::NetworkSandboxPolicy::from(sandbox_policy); - Ok(()) + self.config + .permissions + .set_legacy_sandbox_policy(policy, self.config.cwd.as_path()) } #[cfg_attr(not(target_os = "windows"), allow(dead_code))] diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index cc684d0a8..83cfa7557 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -258,7 +258,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { .expect("set sandbox policy"); chat.config.cwd = test_path_buf("/home/user/main").abs(); - let expected_sandbox = SandboxPolicy::new_read_only_policy(); + let legacy_fallback_sandbox = SandboxPolicy::new_read_only_policy(); let expected_cwd = test_path_buf("/home/user/sub-agent").abs(); let expected_file_system_policy = FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { @@ -279,6 +279,9 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { &expected_file_system_policy, NetworkSandboxPolicy::Restricted, ); + let expected_sandbox = expected_permission_profile + .to_legacy_sandbox_policy(expected_cwd.as_path()) + .expect("permission profile should project to legacy sandbox policy"); let configured = codex_protocol::protocol::SessionConfiguredEvent { session_id: ThreadId::new(), forked_from_id: None, @@ -288,7 +291,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - sandbox_policy: expected_sandbox.clone(), + sandbox_policy: legacy_fallback_sandbox, permission_profile: Some(expected_permission_profile.clone()), cwd: expected_cwd.clone(), reasoning_effort: Some(ReasoningEffortConfig::default()), diff --git a/codex-rs/utils/absolute-path/src/lib.rs b/codex-rs/utils/absolute-path/src/lib.rs index 86161d23f..6c37e0866 100644 --- a/codex-rs/utils/absolute-path/src/lib.rs +++ b/codex-rs/utils/absolute-path/src/lib.rs @@ -4,6 +4,7 @@ use serde::Deserialize; use serde::Deserializer; use serde::Serialize; use serde::de::Error as SerdeError; +use std::borrow::Cow; use std::cell::RefCell; use std::path::Display; use std::path::Path; @@ -46,16 +47,23 @@ impl AbsolutePathBuf { base_path: B, ) -> Self { let expanded = Self::maybe_expand_home_directory(path.as_ref()); - Self(absolutize::absolutize_from(&expanded, base_path.as_ref())) + let expanded = normalize_path_for_platform(&expanded); + let base_path = normalize_path_for_platform(base_path.as_ref()); + Self(absolutize::absolutize_from( + expanded.as_ref(), + base_path.as_ref(), + )) } pub fn from_absolute_path>(path: P) -> std::io::Result { let expanded = Self::maybe_expand_home_directory(path.as_ref()); - Ok(Self(absolutize::absolutize(&expanded)?)) + let expanded = normalize_path_for_platform(&expanded); + Ok(Self(absolutize::absolutize(expanded.as_ref())?)) } pub fn from_absolute_path_checked>(path: P) -> std::io::Result { let expanded = Self::maybe_expand_home_directory(path.as_ref()); + let expanded = normalize_path_for_platform(&expanded); if !expanded.is_absolute() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -63,15 +71,14 @@ impl AbsolutePathBuf { )); } - Ok(Self(absolutize::absolutize_from(&expanded, Path::new("/")))) + Ok(Self(absolutize::absolutize_from( + expanded.as_ref(), + Path::new("/"), + ))) } pub fn current_dir() -> std::io::Result { - let current_dir = std::env::current_dir()?; - Ok(Self(absolutize::absolutize_from( - ¤t_dir, - ¤t_dir, - ))) + Self::from_absolute_path(std::env::current_dir()?) } /// Construct an absolute path from `path`, resolving relative paths against @@ -132,6 +139,45 @@ impl AbsolutePathBuf { } } +fn normalize_path_for_platform(path: &Path) -> Cow<'_, Path> { + if cfg!(windows) + && let Some(path) = path.to_str() + && let Some(normalized) = normalize_windows_device_path(path) + { + return Cow::Owned(PathBuf::from(normalized)); + } + + Cow::Borrowed(path) +} + +fn normalize_windows_device_path(path: &str) -> Option { + if let Some(unc) = path.strip_prefix(r"\\?\UNC\") { + return Some(format!(r"\\{unc}")); + } + if let Some(unc) = path.strip_prefix(r"\\.\UNC\") { + return Some(format!(r"\\{unc}")); + } + if let Some(path) = path.strip_prefix(r"\\?\") + && is_windows_drive_absolute_path(path) + { + return Some(path.to_string()); + } + if let Some(path) = path.strip_prefix(r"\\.\") + && is_windows_drive_absolute_path(path) + { + return Some(path.to_string()); + } + None +} + +fn is_windows_drive_absolute_path(path: &str) -> bool { + let bytes = path.as_bytes(); + bytes.len() >= 3 + && bytes[0].is_ascii_alphabetic() + && bytes[1] == b':' + && matches!(bytes[2], b'\\' | b'/') +} + /// Canonicalize a path when possible, but preserve the logical absolute path /// whenever canonicalization would rewrite it through a nested symlink. /// @@ -391,6 +437,43 @@ mod tests { assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); } + #[test] + fn normalize_windows_device_path_strips_supported_verbatim_prefixes() { + assert_eq!( + normalize_windows_device_path(r"\\?\D:\c\x\worktrees\2508\swift-base"), + Some(r"D:\c\x\worktrees\2508\swift-base".to_string()) + ); + assert_eq!( + normalize_windows_device_path(r"\\.\D:\c\x\worktrees\2508\swift-base"), + Some(r"D:\c\x\worktrees\2508\swift-base".to_string()) + ); + assert_eq!( + normalize_windows_device_path(r"\\?\UNC\server\share\workspace"), + Some(r"\\server\share\workspace".to_string()) + ); + assert_eq!( + normalize_windows_device_path(r"\\.\UNC\server\share\workspace"), + Some(r"\\server\share\workspace".to_string()) + ); + assert_eq!( + normalize_windows_device_path(r"\\?\GLOBALROOT\Device"), + None + ); + } + + #[cfg(target_os = "windows")] + #[test] + fn from_absolute_path_strips_windows_verbatim_prefix() { + let path = + AbsolutePathBuf::from_absolute_path_checked(r"\\?\D:\c\x\worktrees\2508\swift-base") + .expect("verbatim drive path should be absolute"); + + assert_eq!( + path.as_path(), + Path::new(r"D:\c\x\worktrees\2508\swift-base") + ); + } + #[test] fn relative_path_is_resolved_against_base_path() { let temp_dir = tempdir().expect("base dir");