From 97b7ec75349446b8ca10ef29dbe8e6899fe23eb4 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 2 Jun 2026 14:37:06 -0700 Subject: [PATCH] config: default untrusted projects to read-only permissions --- codex-rs/config/src/config_toml.rs | 6 +- .../core/src/config/config_loader_tests.rs | 6 +- codex-rs/core/src/config/config_tests.rs | 87 ++++++++++++------- codex-rs/core/src/config/permissions.rs | 2 +- 4 files changed, 61 insertions(+), 40 deletions(-) diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 0311482cf2..aee259e6fd 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -735,11 +735,11 @@ impl ConfigToml { .or(if sandbox_mode_was_explicit { None } else { - // If no sandbox_mode is set but this directory has a trust decision, - // default to workspace-write except on unsandboxed Windows where we + // If no sandbox_mode is set for a trusted directory, 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() { + if p.is_trusted() { if cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled { diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 5edecebec0..01c23f5408 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1423,11 +1423,7 @@ async fn system_allowed_permissions_keep_builtin_permission_fallbacks() -> anyho ), ( Some(TrustLevel::Untrusted), - if cfg!(target_os = "windows") { - BUILT_IN_PERMISSION_PROFILE_READ_ONLY - } else { - BUILT_IN_PERMISSION_PROFILE_WORKSPACE - }, + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, ), (None, BUILT_IN_PERMISSION_PROFILE_READ_ONLY), ] { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index a8531f06d2..3e10f8d7ab 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -2549,6 +2549,50 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std:: Ok(()) } +#[tokio::test] +async fn empty_config_defaults_to_builtin_read_only_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(BUILT_IN_PERMISSION_PROFILE_READ_ONLY) + ); + assert!( + policy.can_read_path_with_cwd(cwd.path(), cwd.path()), + "expected untrusted project fallback to allow reads, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected untrusted project fallback to use :read-only, policy: {policy:?}" + ); + Ok(()) +} + #[tokio::test] async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_settings() -> std::io::Result<()> { @@ -8566,7 +8610,7 @@ async fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error } #[tokio::test] -async fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> { +async fn test_untrusted_project_gets_read_only_sandbox() -> anyhow::Result<()> { let config_with_untrusted = r#" [projects."/tmp/test"] trust_level = "untrusted" @@ -8587,18 +8631,10 @@ trust_level = "untrusted" ) .await; - // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) - if cfg!(target_os = "windows") { - assert!( - matches!(resolution, SandboxPolicy::ReadOnly { .. }), - "Expected ReadOnly on Windows, got {resolution:?}" - ); - } else { - assert!( - matches!(resolution, SandboxPolicy::WorkspaceWrite { .. }), - "Expected WorkspaceWrite for untrusted project, got {resolution:?}" - ); - } + assert!( + matches!(resolution, SandboxPolicy::ReadOnly { .. }), + "Expected ReadOnly for untrusted project, got {resolution:?}" + ); Ok(()) } @@ -8955,24 +8991,13 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow: "Expected UnlessTrusted approval policy for untrusted project" ); - // Verify that untrusted projects still get WorkspaceWrite sandbox (or ReadOnly on Windows) - if cfg!(target_os = "windows") { - assert!( - matches!( - &config.legacy_sandbox_policy(), - SandboxPolicy::ReadOnly { .. } - ), - "Expected ReadOnly on Windows" - ); - } else { - assert!( - matches!( - &config.legacy_sandbox_policy(), - SandboxPolicy::WorkspaceWrite { .. } - ), - "Expected WorkspaceWrite sandbox for untrusted project" - ); - } + assert!( + matches!( + &config.legacy_sandbox_policy(), + SandboxPolicy::ReadOnly { .. } + ), + "Expected ReadOnly sandbox for untrusted project" + ); Ok(()) } diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index f683d9c7eb..f338d2d61d 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -49,7 +49,7 @@ pub(crate) fn default_builtin_permission_profile_name( active_project: &ProjectConfig, windows_sandbox_level: WindowsSandboxLevel, ) -> &'static str { - if (active_project.is_trusted() || active_project.is_untrusted()) + if active_project.is_trusted() && !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled) { BUILT_IN_WORKSPACE_PROFILE