From 2c3843728c11c69b3f3e7d422485e748a69ae674 Mon Sep 17 00:00:00 2001 From: Tiffany Citra Date: Tue, 20 Jan 2026 10:55:01 -0800 Subject: [PATCH] fix: `writable_roots` doesn't recognize home directory symbol in non-windows OS (#9193) Fixes: ``` [sandbox_workspace_write] writable_roots = ["~/code/"] ``` translates to ``` /Users/ccunningham/.codex/~/code ``` (i.e. the home dir symbol isn't recognized) --- codex-rs/Cargo.lock | 2 + codex-rs/utils/absolute-path/Cargo.toml | 2 + codex-rs/utils/absolute-path/src/lib.rs | 68 ++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ba2400ba9..3a81d2b7d 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1888,7 +1888,9 @@ dependencies = [ name = "codex-utils-absolute-path" version = "0.0.0" dependencies = [ + "dirs", "path-absolutize", + "pretty_assertions", "schemars 0.8.22", "serde", "serde_json", diff --git a/codex-rs/utils/absolute-path/Cargo.toml b/codex-rs/utils/absolute-path/Cargo.toml index 5af9dc23a..f4fa8bf78 100644 --- a/codex-rs/utils/absolute-path/Cargo.toml +++ b/codex-rs/utils/absolute-path/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true workspace = true [dependencies] +dirs = { workspace = true } path-absolutize = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } @@ -18,5 +19,6 @@ ts-rs = { workspace = true, features = [ ] } [dev-dependencies] +pretty_assertions = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/utils/absolute-path/src/lib.rs b/codex-rs/utils/absolute-path/src/lib.rs index 7054103e3..966b061bf 100644 --- a/codex-rs/utils/absolute-path/src/lib.rs +++ b/codex-rs/utils/absolute-path/src/lib.rs @@ -1,3 +1,4 @@ +use dirs::home_dir; use path_absolutize::Absolutize; use schemars::JsonSchema; use serde::Deserialize; @@ -21,16 +22,35 @@ use ts_rs::TS; pub struct AbsolutePathBuf(PathBuf); impl AbsolutePathBuf { + fn maybe_expand_home_directory(path: &Path) -> PathBuf { + let Some(path_str) = path.to_str() else { + return path.to_path_buf(); + }; + if cfg!(not(target_os = "windows")) + && let Some(home) = home_dir() + { + if path_str == "~" { + return home; + } + if let Some(rest) = path_str.strip_prefix("~/") { + return home.join(rest); + } + } + path.to_path_buf() + } + pub fn resolve_path_against_base, B: AsRef>( path: P, base_path: B, ) -> std::io::Result { - let absolute_path = path.as_ref().absolutize_from(base_path.as_ref())?; + let expanded = Self::maybe_expand_home_directory(path.as_ref()); + let absolute_path = expanded.absolutize_from(base_path.as_ref())?; Ok(Self(absolute_path.into_owned())) } pub fn from_absolute_path>(path: P) -> std::io::Result { - let absolute_path = path.as_ref().absolutize()?; + let expanded = Self::maybe_expand_home_directory(path.as_ref()); + let absolute_path = expanded.absolutize()?; Ok(Self(absolute_path.into_owned())) } @@ -165,6 +185,7 @@ impl<'de> Deserialize<'de> for AbsolutePathBuf { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; use tempfile::tempdir; #[test] @@ -203,4 +224,47 @@ mod tests { base_dir.join(relative_path).as_path() ); } + + #[cfg(not(target_os = "windows"))] + #[test] + fn home_directory_root_on_non_windows_is_expanded_in_deserialization() { + let Some(home) = home_dir() else { + return; + }; + let temp_dir = tempdir().expect("base dir"); + let abs_path_buf = { + let _guard = AbsolutePathBufGuard::new(temp_dir.path()); + serde_json::from_str::("\"~\"").expect("failed to deserialize") + }; + assert_eq!(abs_path_buf.as_path(), home.as_path()); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn home_directory_subpath_on_non_windows_is_expanded_in_deserialization() { + let Some(home) = home_dir() else { + return; + }; + let temp_dir = tempdir().expect("base dir"); + let abs_path_buf = { + let _guard = AbsolutePathBufGuard::new(temp_dir.path()); + serde_json::from_str::("\"~/code\"").expect("failed to deserialize") + }; + assert_eq!(abs_path_buf.as_path(), home.join("code").as_path()); + } + + #[cfg(target_os = "windows")] + #[test] + fn home_directory_on_windows_is_not_expanded_in_deserialization() { + let temp_dir = tempdir().expect("base dir"); + let base_dir = temp_dir.path(); + let abs_path_buf = { + let _guard = AbsolutePathBufGuard::new(base_dir); + serde_json::from_str::("\"~/code\"").expect("failed to deserialize") + }; + assert_eq!( + abs_path_buf.as_path(), + base_dir.join("~").join("code").as_path() + ); + } }