diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 0311482cf..7c060cadb 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -728,29 +728,23 @@ impl ConfigToml { active_project: Option<&ProjectConfig>, permission_profile_constraint: Option<&crate::Constrained>, ) -> PermissionProfile { - let sandbox_mode_was_explicit = - sandbox_mode_override.is_some() || self.sandbox_mode.is_some(); - let resolved_sandbox_mode = sandbox_mode_override - .or(self.sandbox_mode) - .or(if sandbox_mode_was_explicit { - None - } else { + let configured_sandbox_mode = sandbox_mode_override.or(self.sandbox_mode); + let resolved_sandbox_mode = configured_sandbox_mode + .or_else(|| { // If no sandbox_mode is set but this directory has a trust decision, // default to workspace-write except on unsandboxed Windows where we // default to read-only. - active_project.and_then(|p| { - if p.is_trusted() || p.is_untrusted() { + active_project + .filter(|project| project.is_trusted() || project.is_untrusted()) + .map(|_| { if cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled { - Some(SandboxMode::ReadOnly) + SandboxMode::ReadOnly } else { - Some(SandboxMode::WorkspaceWrite) + SandboxMode::WorkspaceWrite } - } else { - None - } - }) + }) }) .unwrap_or_default(); let effective_sandbox_mode = if cfg!(target_os = "windows") @@ -788,7 +782,7 @@ impl ConfigToml { }, SandboxMode::DangerFullAccess => PermissionProfile::Disabled, }; - if !sandbox_mode_was_explicit + if configured_sandbox_mode.is_none() && let Some(constraint) = permission_profile_constraint && let Err(err) = constraint.can_set(&permission_profile) { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 192e449ff..8bc33bbb4 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -2548,6 +2548,65 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std:: Ok(()) } +#[tokio::test] +async fn empty_config_defaults_to_builtin_profile_for_untrusted_project() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let project_key = cwd.path().to_string_lossy().to_string(); + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + projects: Some(HashMap::from([( + project_key, + ProjectConfig { + trust_level: Some(TrustLevel::Untrusted), + }, + )])), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + assert_eq!( + config + .permissions + .active_permission_profile() + .as_ref() + .map(|active| active.id.as_str()), + Some(if cfg!(target_os = "windows") { + BUILT_IN_PERMISSION_PROFILE_READ_ONLY + } else { + BUILT_IN_PERMISSION_PROFILE_WORKSPACE + }) + ); + assert!( + policy.can_read_path_with_cwd(cwd.path(), cwd.path()), + "expected untrusted project fallback to allow reads, policy: {policy:?}" + ); + if cfg!(target_os = "windows") { + assert!( + !policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected untrusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}" + ); + } else { + assert!( + policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected untrusted project fallback to use :workspace, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(&cwd.path().join(".codex"), cwd.path()), + "expected :workspace metadata carveouts, policy: {policy:?}" + ); + } + Ok(()) +} + #[tokio::test] async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_settings() -> std::io::Result<()> {