From 772e88c8ae987445b81d932ea0f38fa0cc8fb5dc Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Sun, 16 Aug 2026 23:52:30 +0000 Subject: [PATCH] Honor legacy `:project_roots` permission entries (#38916) ## Why Permission profiles written before the rename to `:workspace_roots` can still contain `:project_roots`. Treating that token as unknown ignores its entries and can drop filesystem restrictions. ## What changed - Parse `:project_roots` as an alias for `:workspace_roots`. - Cover deny rules and read-only subpath carveouts in legacy profiles. GitOrigin-RevId: 10aafc9e15372d1a8bcc2dbcf3ad7cc987f4e67e --- codex-rs/core/src/config/permissions.rs | 5 +- codex-rs/core/src/config/permissions_tests.rs | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index df56ac4594..c14d0b7dc3 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -779,7 +779,10 @@ fn parse_special_path(path: &str) -> Option { match path { ":root" => Some(FileSystemSpecialPath::Root), ":minimal" => Some(FileSystemSpecialPath::Minimal), - ":workspace_roots" => Some(FileSystemSpecialPath::project_roots(/*subpath*/ None)), + // `:project_roots` shipped before the canonical rename; keep it as an alias. + ":project_roots" | ":workspace_roots" => { + Some(FileSystemSpecialPath::project_roots(/*subpath*/ None)) + } ":tmpdir" => Some(FileSystemSpecialPath::Tmpdir), ":slash_tmp" => Some(FileSystemSpecialPath::SlashTmp), _ if path.starts_with(':') => { diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index 130e9d7f06..c87f5fea32 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -466,6 +466,56 @@ fn compile_permission_profile_workspace_roots_resolves_enabled_entries() -> std: Ok(()) } +#[test] +fn legacy_project_roots_restrictions_do_not_fail_open() -> std::io::Result<()> { + let permissions = toml::from_str::( + r#" +[read_deny.filesystem] +":root" = "read" +":project_roots" = "none" + +[write_deny.filesystem] +":root" = "write" +":project_roots" = "none" + +[write_read.filesystem] +":root" = "write" + +[write_read.filesystem.":project_roots"] +docs = "read" +"#, + ) + .expect("legacy project roots profiles should deserialize"); + let cwd = TempDir::new()?; + let docs = cwd.path().join("docs"); + let mut startup_warnings = Vec::new(); + + let (read_deny_policy, _) = + compile_permission_profile(&permissions, "read_deny", &mut startup_warnings)?; + assert_eq!( + read_deny_policy.resolve_access_with_cwd(cwd.path(), cwd.path()), + FileSystemAccessMode::Deny + ); + + let (write_deny_policy, _) = + compile_permission_profile(&permissions, "write_deny", &mut startup_warnings)?; + assert!(!write_deny_policy.has_full_disk_write_access()); + assert_eq!( + write_deny_policy.resolve_access_with_cwd(cwd.path(), cwd.path()), + FileSystemAccessMode::Deny + ); + + let (write_read_policy, _) = + compile_permission_profile(&permissions, "write_read", &mut startup_warnings)?; + assert!(!write_read_policy.has_full_disk_write_access()); + assert_eq!( + write_read_policy.resolve_access_with_cwd(&docs, cwd.path()), + FileSystemAccessMode::Read + ); + + Ok(()) +} + #[test] fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths() { let filesystem = FilesystemPermissionsToml {