diff --git a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs index a6976c9c07..b0656f5f9b 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs @@ -312,11 +312,13 @@ pub enum FileSystemPath { impl From for FileSystemPath { fn from(value: CoreFileSystemPath) -> Self { match value { - CoreFileSystemPath::Path { path } => Self::Path { + CoreFileSystemPath::Path { path } + | CoreFileSystemPath::GeneratedDefaultPath { path } => Self::Path { path: LegacyAppPathString::from_abs_path(&path), }, CoreFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, - CoreFileSystemPath::Special { value } => Self::Special { + CoreFileSystemPath::Special { value } + | CoreFileSystemPath::GeneratedDefaultSpecial { value } => Self::Special { value: value.into(), }, } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 11ec23fc52..7752df2f78 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4263,7 +4263,7 @@ exclude_slash_tmp = true file_system_policy .entries .contains(&FileSystemSandboxEntry { - path: FileSystemPath::Path { + path: FileSystemPath::GeneratedDefaultPath { path: AbsolutePathBuf::resolve_path_against_base( subpath, cwd.path() diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 47fa863619..1e26725a79 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -249,6 +249,8 @@ fn insert_filesystem_permission_toml( FileSystemPath::Special { value } => { insert_special_filesystem_permission_toml(entries, value, entry.access); } + FileSystemPath::GeneratedDefaultPath { .. } + | FileSystemPath::GeneratedDefaultSpecial { .. } => {} } } diff --git a/codex-rs/core/src/context/environment_context.rs b/codex-rs/core/src/context/environment_context.rs index a54f7d4330..f18725d888 100644 --- a/codex-rs/core/src/context/environment_context.rs +++ b/codex-rs/core/src/context/environment_context.rs @@ -153,13 +153,13 @@ fn render_file_system_entry(rendered: &mut String, entry: &FileSystemSandboxEntr } rendered.push_str("\">"); match &entry.path { - FileSystemPath::Path { path } => { + FileSystemPath::Path { path } | FileSystemPath::GeneratedDefaultPath { path } => { push_text_element(rendered, "path", path.to_string_lossy().as_ref()); } FileSystemPath::GlobPattern { pattern } => { push_text_element(rendered, "glob", pattern); } - FileSystemPath::Special { value } => { + FileSystemPath::Special { value } | FileSystemPath::GeneratedDefaultSpecial { value } => { let value = render_special_path(value); push_text_element(rendered, "special", &value); } diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 9987b7fb79..14ee334eb5 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -797,6 +797,51 @@ fn windows_elevated_supports_split_write_read_carveouts() { ); } +#[cfg(target_os = "windows")] +#[test] +fn windows_workspace_defaults_do_not_hide_explicit_metadata_carveouts() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let cwd = temp_dir.path().canonicalize().expect("canonical cwd").abs(); + + let default_profile = PermissionProfile::workspace_write(); + let default_overrides = resolve_windows_elevated_filesystem_overrides( + SandboxType::WindowsRestrictedToken, + &default_profile, + &cwd, + /*use_windows_elevated_backend*/ true, + ) + .expect("resolve workspace defaults"); + assert!( + default_overrides.is_none_or(|overrides| overrides.additional_deny_write_paths.is_empty()) + ); + + for name in codex_protocol::permissions::PROTECTED_METADATA_PATH_NAMES { + let (mut explicit_policy, network_policy) = default_profile.to_runtime_permissions(); + explicit_policy + .entries + .push(codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::project_roots(Some( + (*name).into(), + )), + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }); + let explicit_profile = + PermissionProfile::from_runtime_permissions(&explicit_policy, network_policy); + + let overrides = resolve_windows_elevated_filesystem_overrides( + SandboxType::WindowsRestrictedToken, + &explicit_profile, + &cwd, + /*use_windows_elevated_backend*/ true, + ) + .expect("resolve explicit metadata carveout") + .expect("explicit metadata carveout needs an override"); + assert_eq!(overrides.additional_deny_write_paths, vec![cwd.join(name)]); + } +} + #[test] fn windows_elevated_supports_unreadable_split_carveouts() { let temp_dir = tempfile::TempDir::new().expect("tempdir"); diff --git a/codex-rs/core/tests/suite/windows_sandbox.rs b/codex-rs/core/tests/suite/windows_sandbox.rs index b8895f49e4..4392250e56 100644 --- a/codex-rs/core/tests/suite/windows_sandbox.rs +++ b/codex-rs/core/tests/suite/windows_sandbox.rs @@ -197,6 +197,59 @@ async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> a Ok(()) } +#[tokio::test] +#[serial(codex_home)] +async fn windows_elevated_does_not_create_missing_workspace_metadata() -> anyhow::Result<()> { + let codex_home = + codex_home_for_windows_sandbox_test("windows-elevated-missing-metadata-codex-home")?; + let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); + stage_windows_sandbox_helpers()?; + let workspace = TempDir::new()?; + let cwd = dunce::canonicalize(workspace.path())?.abs(); + let permission_profile = PermissionProfile::workspace_write() + .materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd)); + + let output = process_exec_tool_call( + ExecParams { + command: vec![ + "cmd.exe".to_string(), + "/D".to_string(), + "/C".to_string(), + "echo sandbox-ok".to_string(), + ], + cwd: cwd.clone(), + expiration: 10_000.into(), + capture_policy: ExecCapturePolicy::ShellTool, + env: HashMap::new(), + network: None, + network_environment_id: None, + sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Elevated, + windows_sandbox_private_desktop: false, + justification: None, + arg0: None, + }, + &permission_profile, + &cwd, + std::slice::from_ref(&cwd), + &None, + /*use_legacy_landlock*/ false, + /*stdout_stream*/ None, + ) + .await?; + + assert_eq!(output.exit_code, 0, "sandboxed command should complete"); + for name in codex_protocol::permissions::PROTECTED_METADATA_PATH_NAMES { + let path = cwd.join(name); + assert!( + !path.exists(), + "elevated setup should not create missing workspace metadata: {}", + path.display() + ); + } + Ok(()) +} + #[tokio::test] #[serial(codex_home)] async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyhow::Result<()> { diff --git a/codex-rs/file-system/src/lib.rs b/codex-rs/file-system/src/lib.rs index c9effdc20c..5224b6f101 100644 --- a/codex-rs/file-system/src/lib.rs +++ b/codex-rs/file-system/src/lib.rs @@ -126,8 +126,10 @@ pub struct WalkOutcome { #[serde(tag = "type", rename_all = "snake_case")] pub enum ExecFileSystemPath { Path { path: PathUri }, + GeneratedDefaultPath { path: PathUri }, GlobPattern { pattern: String }, Special { value: FileSystemSpecialPath }, + GeneratedDefaultSpecial { value: FileSystemSpecialPath }, } impl From for ExecFileSystemPath { @@ -136,8 +138,14 @@ impl From for ExecFileSystemPath { FileSystemPath::Path { path } => Self::Path { path: PathUri::from_abs_path(&path), }, + FileSystemPath::GeneratedDefaultPath { path } => Self::GeneratedDefaultPath { + path: PathUri::from_abs_path(&path), + }, FileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, FileSystemPath::Special { value } => Self::Special { value }, + FileSystemPath::GeneratedDefaultSpecial { value } => { + Self::GeneratedDefaultSpecial { value } + } } } } @@ -150,8 +158,14 @@ impl TryFrom for FileSystemPath { ExecFileSystemPath::Path { path } => Self::Path { path: path.to_abs_path()?, }, + ExecFileSystemPath::GeneratedDefaultPath { path } => Self::GeneratedDefaultPath { + path: path.to_abs_path()?, + }, ExecFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, ExecFileSystemPath::Special { value } => Self::Special { value }, + ExecFileSystemPath::GeneratedDefaultSpecial { value } => { + Self::GeneratedDefaultSpecial { value } + } }) } } @@ -350,8 +364,14 @@ impl FileSystemSandboxContext { ExecFileSystemPath::GlobPattern { pattern } => !Path::new(pattern).is_absolute(), ExecFileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { .. }, + } + | ExecFileSystemPath::GeneratedDefaultSpecial { + value: FileSystemSpecialPath::ProjectRoots { .. }, } => true, - ExecFileSystemPath::Path { .. } | ExecFileSystemPath::Special { .. } => false, + ExecFileSystemPath::Path { .. } + | ExecFileSystemPath::GeneratedDefaultPath { .. } + | ExecFileSystemPath::Special { .. } + | ExecFileSystemPath::GeneratedDefaultSpecial { .. } => false, }), ExecPermissionProfile::Managed { file_system: ExecManagedFileSystemPermissions::Unrestricted, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 287780e5b5..39094331a4 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -111,7 +111,10 @@ impl FileSystemPermissions { ) -> impl Iterator { self.entries.iter().filter_map(|entry| match &entry.path { FileSystemPath::Path { path } => Some((path, entry.access)), - FileSystemPath::GlobPattern { .. } | FileSystemPath::Special { .. } => None, + FileSystemPath::GeneratedDefaultPath { .. } + | FileSystemPath::GlobPattern { .. } + | FileSystemPath::Special { .. } + | FileSystemPath::GeneratedDefaultSpecial { .. } => None, }) } diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index ec37292e18..bbd8b8290c 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -343,6 +343,11 @@ pub enum FileSystemPath { // TODO(anp): Use PathUri once permission paths no longer require native-path rollout serialization. path: AbsolutePathBuf, }, + /// A concrete path synthesized by a built-in permissions profile rather + /// than explicitly configured by the user. + GeneratedDefaultPath { + path: AbsolutePathBuf, + }, /// A git-style glob pattern. Pattern entries currently support /// FileSystemAccessMode::Deny only. GlobPattern { @@ -351,6 +356,20 @@ pub enum FileSystemPath { Special { value: FileSystemSpecialPath, }, + /// A symbolic path synthesized by a built-in permissions profile rather + /// than explicitly configured by the user. + GeneratedDefaultSpecial { + value: FileSystemSpecialPath, + }, +} + +impl FileSystemPath { + pub fn is_generated_default(&self) -> bool { + matches!( + self, + Self::GeneratedDefaultPath { .. } | Self::GeneratedDefaultSpecial { .. } + ) + } } const PROJECT_ROOTS_GLOB_PATTERN_PREFIX: &str = "codex-project-roots://"; @@ -403,6 +422,25 @@ impl FileSystemSandboxPolicy { } } + /// Removes entries synthesized by built-in profiles while preserving + /// explicitly configured entries targeting the same paths. + pub fn remove_generated_defaults(&mut self) { + self.entries + .retain(|entry| !entry.path.is_generated_default()); + } + + pub fn has_explicit_non_write_entry_for_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool { + let Some(path) = resolve_candidate_path(path, cwd) else { + return false; + }; + let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok(); + self.entries.iter().any(|entry| { + !entry.path.is_generated_default() + && !entry.access.can_write() + && resolve_entry_path(&entry.path, cwd.as_ref()).as_ref() == Some(&path) + }) + } + fn has_root_access(&self, predicate: impl Fn(FileSystemAccessMode) -> bool) -> bool { matches!(self.kind, FileSystemSandboxKind::Restricted) && self.entries.iter().any(|entry| { @@ -496,9 +534,12 @@ impl FileSystemSandboxPolicy { } match &entry.path { - FileSystemPath::Path { .. } => !self.has_same_target_write_override(entry), + FileSystemPath::Path { .. } | FileSystemPath::GeneratedDefaultPath { .. } => { + !self.has_same_target_write_override(entry) + } FileSystemPath::GlobPattern { .. } => true, - FileSystemPath::Special { value } => match value { + FileSystemPath::Special { value } + | FileSystemPath::GeneratedDefaultSpecial { value } => match value { FileSystemSpecialPath::Root => entry.access == FileSystemAccessMode::Deny, FileSystemSpecialPath::Minimal | FileSystemSpecialPath::Unknown { .. } => { false @@ -717,6 +758,13 @@ impl FileSystemSandboxPolicy { let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok(); for entry in &mut self.entries { match &entry.path { + FileSystemPath::GeneratedDefaultSpecial { + value: FileSystemSpecialPath::ProjectRoots { .. }, + } => { + if let Some(path) = resolve_file_system_path(&entry.path, cwd.as_ref()) { + entry.path = FileSystemPath::GeneratedDefaultPath { path }; + } + } FileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { .. }, } => { @@ -734,7 +782,9 @@ impl FileSystemSandboxPolicy { } } FileSystemPath::Special { value: _ } => {} + FileSystemPath::GeneratedDefaultSpecial { value: _ } => {} FileSystemPath::Path { .. } => {} + FileSystemPath::GeneratedDefaultPath { .. } => {} } } self @@ -749,6 +799,22 @@ impl FileSystemSandboxPolicy { let mut entries = Vec::with_capacity(self.entries.len()); for entry in self.entries { match entry.path { + FileSystemPath::GeneratedDefaultSpecial { + value: FileSystemSpecialPath::ProjectRoots { subpath }, + } => { + entries.extend(workspace_roots.iter().map(|root| FileSystemSandboxEntry { + path: FileSystemPath::GeneratedDefaultPath { + path: match subpath.as_ref() { + Some(subpath) => AbsolutePathBuf::resolve_path_against_base( + subpath, + root.as_path(), + ), + None => root.clone(), + }, + }, + access: entry.access, + })); + } FileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { subpath }, } => { @@ -786,12 +852,24 @@ impl FileSystemSandboxPolicy { access: entry.access, }); } + FileSystemPath::GeneratedDefaultPath { path } => { + entries.push(FileSystemSandboxEntry { + path: FileSystemPath::GeneratedDefaultPath { path }, + access: entry.access, + }); + } FileSystemPath::Special { value } => { entries.push(FileSystemSandboxEntry { path: FileSystemPath::Special { value }, access: entry.access, }); } + FileSystemPath::GeneratedDefaultSpecial { value } => { + entries.push(FileSystemSandboxEntry { + path: FileSystemPath::GeneratedDefaultSpecial { value }, + access: entry.access, + }); + } } } self.entries = entries; @@ -1095,7 +1173,10 @@ impl FileSystemSandboxPolicy { FileSystemPath::GlobPattern { pattern } => { Some(AbsolutePathBuf::resolve_path_against_base(pattern, cwd)) } - FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => None, + FileSystemPath::Path { .. } + | FileSystemPath::GeneratedDefaultPath { .. } + | FileSystemPath::Special { .. } + | FileSystemPath::GeneratedDefaultSpecial { .. } => None, }) .map(|pattern| pattern.to_string_lossy().into_owned()) .collect::>(); @@ -1138,7 +1219,8 @@ impl FileSystemSandboxPolicy { for entry in &self.entries { match &entry.path { FileSystemPath::GlobPattern { .. } => {} - FileSystemPath::Path { path } => { + FileSystemPath::Path { path } + | FileSystemPath::GeneratedDefaultPath { path } => { if entry.access.can_write() { if cwd_absolute.as_ref().is_some_and(|cwd| cwd == path) { workspace_root_writable = true; @@ -1147,7 +1229,8 @@ impl FileSystemSandboxPolicy { } } } - FileSystemPath::Special { value } => match value { + FileSystemPath::Special { value } + | FileSystemPath::GeneratedDefaultSpecial { value } => match value { FileSystemSpecialPath::Root => match entry.access { FileSystemAccessMode::Deny => {} FileSystemAccessMode::Read => {} @@ -1289,9 +1372,13 @@ fn resolve_file_system_path( cwd: Option<&AbsolutePathBuf>, ) -> Option { match path { - FileSystemPath::Path { path } => Some(path.clone()), + FileSystemPath::Path { path } | FileSystemPath::GeneratedDefaultPath { path } => { + Some(path.clone()) + } FileSystemPath::GlobPattern { .. } => None, - FileSystemPath::Special { value } => resolve_file_system_special_path(value, cwd), + FileSystemPath::Special { value } | FileSystemPath::GeneratedDefaultSpecial { value } => { + resolve_file_system_special_path(value, cwd) + } } } @@ -1335,16 +1422,32 @@ fn resolve_candidate_path(path: &Path, cwd: &Path) -> Option { /// by `has_write_narrowing_entries`. fn file_system_paths_share_target(left: &FileSystemPath, right: &FileSystemPath) -> bool { match (left, right) { - (FileSystemPath::Path { path: left }, FileSystemPath::Path { path: right }) => { - left == right - } - (FileSystemPath::Special { value: left }, FileSystemPath::Special { value: right }) => { - special_paths_share_target(left, right) - } + ( + FileSystemPath::Path { path: left } + | FileSystemPath::GeneratedDefaultPath { path: left }, + FileSystemPath::Path { path: right } + | FileSystemPath::GeneratedDefaultPath { path: right }, + ) => left == right, + ( + FileSystemPath::Special { value: left } + | FileSystemPath::GeneratedDefaultSpecial { value: left }, + FileSystemPath::Special { value: right } + | FileSystemPath::GeneratedDefaultSpecial { value: right }, + ) => special_paths_share_target(left, right), (FileSystemPath::Path { path }, FileSystemPath::Special { value }) - | (FileSystemPath::Special { value }, FileSystemPath::Path { path }) => { - special_path_matches_absolute_path(value, path) - } + | (FileSystemPath::Path { path }, FileSystemPath::GeneratedDefaultSpecial { value }) + | (FileSystemPath::GeneratedDefaultPath { path }, FileSystemPath::Special { value }) + | ( + FileSystemPath::GeneratedDefaultPath { path }, + FileSystemPath::GeneratedDefaultSpecial { value }, + ) + | (FileSystemPath::Special { value }, FileSystemPath::Path { path }) + | (FileSystemPath::Special { value }, FileSystemPath::GeneratedDefaultPath { path }) + | (FileSystemPath::GeneratedDefaultSpecial { value }, FileSystemPath::Path { path }) + | ( + FileSystemPath::GeneratedDefaultSpecial { value }, + FileSystemPath::GeneratedDefaultPath { path }, + ) => special_path_matches_absolute_path(value, path), ( FileSystemPath::GlobPattern { pattern: left }, FileSystemPath::GlobPattern { pattern: right }, @@ -1670,7 +1773,7 @@ fn append_default_read_only_project_root_subpath_if_no_explicit_rule( ) { append_default_read_only_entry_if_no_explicit_rule( entries, - FileSystemPath::Special { + FileSystemPath::GeneratedDefaultSpecial { value: FileSystemSpecialPath::project_roots(Some(subpath.into())), }, ); @@ -1970,19 +2073,19 @@ mod tests { access: FileSystemAccessMode::Write, }, FileSystemSandboxEntry { - path: FileSystemPath::Special { + path: FileSystemPath::GeneratedDefaultSpecial { value: FileSystemSpecialPath::project_roots(Some(".git".into())), }, access: FileSystemAccessMode::Read, }, FileSystemSandboxEntry { - path: FileSystemPath::Special { + path: FileSystemPath::GeneratedDefaultSpecial { value: FileSystemSpecialPath::project_roots(Some(".agents".into())), }, access: FileSystemAccessMode::Read, }, FileSystemSandboxEntry { - path: FileSystemPath::Special { + path: FileSystemPath::GeneratedDefaultSpecial { value: FileSystemSpecialPath::project_roots(Some(".codex".into())), }, access: FileSystemAccessMode::Read, @@ -2129,7 +2232,7 @@ mod tests { ]; expected_entries.extend(PROTECTED_METADATA_PATH_NAMES.iter().map(|name| { FileSystemSandboxEntry { - path: FileSystemPath::Special { + path: FileSystemPath::GeneratedDefaultSpecial { value: FileSystemSpecialPath::project_roots(Some((*name).into())), }, access: FileSystemAccessMode::Read, diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 958a259182..9166a804e0 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -41,10 +41,21 @@ pub fn normalize_additional_permissions( .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()) .unwrap_or(path), }, + FileSystemPath::GeneratedDefaultPath { path } => { + FileSystemPath::GeneratedDefaultPath { + path: canonicalize_preserving_symlinks(path.as_path()) + .ok() + .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()) + .unwrap_or(path), + } + } FileSystemPath::GlobPattern { pattern } => { FileSystemPath::GlobPattern { pattern } } FileSystemPath::Special { value } => FileSystemPath::Special { value }, + FileSystemPath::GeneratedDefaultSpecial { value } => { + FileSystemPath::GeneratedDefaultSpecial { value } + } }; let normalized_entry = FileSystemSandboxEntry { path, @@ -302,7 +313,10 @@ fn deny_entry_constrains_accepted_grant( match &deny_entry.path { FileSystemPath::GlobPattern { pattern } => glob_static_prefix_path(pattern, cwd) .is_some_and(|prefix| paths_overlap(prefix.as_path(), grant_path.as_path())), - FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => { + FileSystemPath::Path { .. } + | FileSystemPath::GeneratedDefaultPath { .. } + | FileSystemPath::Special { .. } + | FileSystemPath::GeneratedDefaultSpecial { .. } => { resolve_permission_path(&deny_entry.path, cwd).is_some_and(|deny_path| { paths_overlap(deny_path.as_path(), grant_path.as_path()) }) @@ -349,6 +363,14 @@ fn materialize_cwd_dependent_entry( cwd: &Path, ) -> FileSystemSandboxEntry { match &entry.path { + FileSystemPath::GeneratedDefaultSpecial { + value: FileSystemSpecialPath::ProjectRoots { .. }, + } => resolve_permission_path(&entry.path, cwd) + .map(|path| FileSystemSandboxEntry { + path: FileSystemPath::GeneratedDefaultPath { path }, + access: entry.access, + }) + .unwrap_or_else(|| entry.clone()), FileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { .. }, } => resolve_permission_path(&entry.path, cwd) @@ -365,41 +387,48 @@ fn materialize_cwd_dependent_entry( }, access: entry.access, }, - FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => entry.clone(), + FileSystemPath::Path { .. } + | FileSystemPath::GeneratedDefaultPath { .. } + | FileSystemPath::Special { .. } + | FileSystemPath::GeneratedDefaultSpecial { .. } => entry.clone(), } } fn resolve_permission_path(path: &FileSystemPath, cwd: &Path) -> Option { match path { - FileSystemPath::Path { path } => Some(path.clone()), + FileSystemPath::Path { path } | FileSystemPath::GeneratedDefaultPath { path } => { + Some(path.clone()) + } FileSystemPath::GlobPattern { .. } => None, - FileSystemPath::Special { value } => match value { - FileSystemSpecialPath::Root => { - let root = cwd.ancestors().last()?; - AbsolutePathBuf::from_absolute_path(root).ok() - } - FileSystemSpecialPath::ProjectRoots { subpath } => { - let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok()?; - Some(match subpath { - Some(subpath) => { - AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path()) - } - None => cwd, - }) - } - FileSystemSpecialPath::Tmpdir => { - let tmpdir = std::env::var_os("TMPDIR")?; - if tmpdir.is_empty() { - None - } else { - AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok() + FileSystemPath::Special { value } | FileSystemPath::GeneratedDefaultSpecial { value } => { + match value { + FileSystemSpecialPath::Root => { + let root = cwd.ancestors().last()?; + AbsolutePathBuf::from_absolute_path(root).ok() } + FileSystemSpecialPath::ProjectRoots { subpath } => { + let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok()?; + Some(match subpath { + Some(subpath) => { + AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path()) + } + None => cwd, + }) + } + FileSystemSpecialPath::Tmpdir => { + let tmpdir = std::env::var_os("TMPDIR")?; + if tmpdir.is_empty() { + None + } else { + AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok() + } + } + FileSystemSpecialPath::SlashTmp => AbsolutePathBuf::from_absolute_path("/tmp") + .ok() + .filter(|path| path.as_path().is_dir()), + FileSystemSpecialPath::Minimal | FileSystemSpecialPath::Unknown { .. } => None, } - FileSystemSpecialPath::SlashTmp => AbsolutePathBuf::from_absolute_path("/tmp") - .ok() - .filter(|path| path.as_path().is_dir()), - FileSystemSpecialPath::Minimal | FileSystemSpecialPath::Unknown { .. } => None, - }, + } } } diff --git a/codex-rs/sandboxing/src/windows.rs b/codex-rs/sandboxing/src/windows.rs index eec3213668..065b8d5fee 100644 --- a/codex-rs/sandboxing/src/windows.rs +++ b/codex-rs/sandboxing/src/windows.rs @@ -81,7 +81,7 @@ pub fn resolve_windows_restricted_token_filesystem_overrides( return Ok(None); } - let (file_system_sandbox_policy, network_sandbox_policy) = + let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); let needs_direct_runtime_enforcement = file_system_sandbox_policy @@ -101,6 +101,11 @@ pub fn resolve_windows_restricted_token_filesystem_overrides( )); } + // Windows protects built-in metadata defaults through the legacy writable + // root projection when those paths exist. Do not turn missing generated + // defaults into explicit deny-write sentinels. + file_system_sandbox_policy.remove_generated_defaults(); + // The restricted-token backend can still enforce split write restrictions, // but its WRITE_RESTRICTED token does not make capability SID deny-read ACEs // participate in read access checks. Read restrictions therefore require the @@ -215,7 +220,7 @@ pub fn resolve_windows_elevated_filesystem_overrides( return Ok(None); } - let (file_system_sandbox_policy, network_sandbox_policy) = + let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); if !permission_profile_supports_windows_restricted_token_sandbox(permission_profile) { @@ -226,6 +231,11 @@ pub fn resolve_windows_elevated_filesystem_overrides( )); } + // Windows protects built-in metadata defaults through the legacy writable + // root projection when those paths exist. Do not turn missing generated + // defaults into explicit deny-write sentinels. + file_system_sandbox_policy.remove_generated_defaults(); + let additional_deny_read_paths = codex_windows_sandbox::resolve_windows_deny_read_paths( &file_system_sandbox_policy, sandbox_policy_cwd, @@ -242,6 +252,12 @@ pub fn resolve_windows_elevated_filesystem_overrides( let needs_direct_runtime_enforcement = file_system_sandbox_policy .needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd); + let has_explicit_write_carveouts = split_writable_roots.iter().any(|writable_root| { + writable_root.read_only_subpaths.iter().any(|path| { + file_system_sandbox_policy + .has_explicit_non_write_entry_for_path_with_cwd(path.as_path(), sandbox_policy_cwd) + }) + }); let normalize_path = |path: PathBuf| dunce::canonicalize(&path).unwrap_or(path); let legacy_projection = compatibility_sandbox_policy_for_permission_profile( permission_profile, @@ -282,7 +298,9 @@ pub fn resolve_windows_elevated_filesystem_overrides( Some(split_root_paths) }; - let additional_deny_write_paths = if needs_direct_runtime_enforcement { + let additional_deny_write_paths = if needs_direct_runtime_enforcement + || has_explicit_write_carveouts + { let mut deny_paths = BTreeSet::new(); for writable_root in &split_writable_roots { let writable_root_path = normalize_path(writable_root.root.to_path_buf()); @@ -303,7 +321,12 @@ pub fn resolve_windows_elevated_filesystem_overrides( == read_only_subpath_suffix }) }); - if !already_denied_by_legacy { + let explicitly_configured = file_system_sandbox_policy + .has_explicit_non_write_entry_for_path_with_cwd( + read_only_subpath.as_path(), + sandbox_policy_cwd, + ); + if explicitly_configured || !already_denied_by_legacy { deny_paths.insert(normalize_path(read_only_subpath.to_path_buf())); } }