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)
This commit is contained in:
Tiffany Citra
2026-01-20 10:55:01 -08:00
committed by GitHub
parent 5ae6e70801
commit 2c3843728c
3 changed files with 70 additions and 2 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -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",

View File

@@ -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 }

View File

@@ -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<P: AsRef<Path>, B: AsRef<Path>>(
path: P,
base_path: B,
) -> std::io::Result<Self> {
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<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
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::<AbsolutePathBuf>("\"~\"").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::<AbsolutePathBuf>("\"~/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::<AbsolutePathBuf>("\"~/code\"").expect("failed to deserialize")
};
assert_eq!(
abs_path_buf.as_path(),
base_dir.join("~").join("code").as_path()
);
}
}