From fefcaa13944ead005d4d9088bbfc60818629b3da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 28 Oct 2026 05:46:51 +0000 Subject: [PATCH] fix: expand leading ~ and $HOME paths in fs_write fs_write failed while printing a diff for paths that start with ~/ because the literal tilde path was opened. Expand a leading ~ or $HOME before file access, and cover that diff path with a unit test. Co-authored-by: Sharan S --- .../chat-cli/src/cli/chat/tools/fs_write.rs | 62 +++++++++++++++++++ crates/chat-cli/src/cli/chat/tools/mod.rs | 30 +++++++-- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/crates/chat-cli/src/cli/chat/tools/fs_write.rs b/crates/chat-cli/src/cli/chat/tools/fs_write.rs index 09a64058a1..9d202d6217 100644 --- a/crates/chat-cli/src/cli/chat/tools/fs_write.rs +++ b/crates/chat-cli/src/cli/chat/tools/fs_write.rs @@ -1314,6 +1314,68 @@ mod tests { assert_eq!(nested_content, "content in nested path\n"); } + // Issue 1932: printing the diff opened the literal "~/..." path and returned + // "No such file or directory", while fs_read already resolved that home path. + #[tokio::test] + async fn test_fs_write_tilde_path_queue_description() { + let os = Os::new().await.unwrap(); + let mut output = Vec::new(); + let mut line_tracker = HashMap::new(); + + let home = os.fs.chroot_path(os.env.home().unwrap_or_default()); + os.fs.create_dir_all(&home).await.unwrap(); + + serde_json::from_value::(serde_json::json!({ + "path": "~/karakuri-checkstyle.xml", + "command": "create", + "file_text": "\n\n\n" + })) + .unwrap() + .invoke(&os, &mut output, &mut line_tracker) + .await + .unwrap(); + + let replace = serde_json::from_value::(serde_json::json!({ + "path": "~/karakuri-checkstyle.xml", + "command": "str_replace", + "old_str": "", + "new_str": "" + })) + .unwrap(); + + replace + .queue_description(&os, &mut output) + .expect("fs_write should print a diff for paths starting with ~/"); + + replace + .invoke(&os, &mut output, &mut line_tracker) + .await + .expect("fs_write should update a file addressed with ~/"); + + assert_eq!( + os.fs + .read_to_string(home.join("karakuri-checkstyle.xml")) + .await + .unwrap(), + "\n\n\n" + ); + + serde_json::from_value::(serde_json::json!({ + "path": "$HOME/from-home-env.txt", + "command": "create", + "file_text": "expanded" + })) + .unwrap() + .invoke(&os, &mut output, &mut line_tracker) + .await + .expect("fs_write should create a file addressed with $HOME"); + + assert_eq!( + os.fs.read_to_string(home.join("from-home-env.txt")).await.unwrap(), + "expanded\n" + ); + } + #[tokio::test] async fn test_eval_perm() { const DENIED_PATH_ONE: &str = "/some/denied/path"; diff --git a/crates/chat-cli/src/cli/chat/tools/mod.rs b/crates/chat-cli/src/cli/chat/tools/mod.rs index 4859fbf210..d18aac5de0 100644 --- a/crates/chat-cli/src/cli/chat/tools/mod.rs +++ b/crates/chat-cli/src/cli/chat/tools/mod.rs @@ -375,17 +375,18 @@ impl Default for OutputKind { } } -/// Performs tilde expansion and other required sanitization modifications for handling tool use -/// path arguments. +/// Performs tilde / `$HOME` expansion and other required sanitization modifications for handling +/// tool use path arguments. /// -/// Required since path arguments are defined by the model. +/// Required since path arguments are defined by the model. Only a leading `~` or `$HOME` +/// component is expanded. #[allow(dead_code)] pub fn sanitize_path_tool_arg(os: &Os, path: impl AsRef) -> PathBuf { let mut res = PathBuf::new(); - // Expand `~` only if it is the first part. + // Expand `~` and `$HOME` only if either is the first part. let mut path = path.as_ref().components(); match path.next() { - Some(p) if p.as_os_str() == "~" => { + Some(p) if is_home_path_alias(p.as_os_str()) => { res.push(os.env.home().unwrap_or_default()); }, Some(p) => res.push(p), @@ -399,6 +400,11 @@ pub fn sanitize_path_tool_arg(os: &Os, path: impl AsRef) -> PathBuf { os.fs.chroot_path(res) } +/// `~` and `$HOME` are aliases for the user home directory when they are the first path component. +fn is_home_path_alias(component: &std::ffi::OsStr) -> bool { + component == "~" || component == "$HOME" +} + /// Converts `path` to a relative path according to the current working directory `cwd`. fn absolute_to_relative(cwd: impl AsRef, path: impl AsRef) -> Result { let cwd = cwd.as_ref().canonicalize()?; @@ -562,12 +568,26 @@ mod tests { os.fs.chroot_path(expected_home.join("hello")), "tilde should expand" ); + let actual = sanitize_path_tool_arg(&os, "$HOME/hello"); + assert_eq!( + actual, + os.fs.chroot_path(expected_home.join("hello")), + "$HOME should expand" + ); + let actual = sanitize_path_tool_arg(&os, "$HOME"); + assert_eq!(actual, os.fs.chroot_path(&expected_home), "$HOME should expand"); let actual = sanitize_path_tool_arg(&os, "/~"); assert_eq!( actual, os.fs.chroot_path("/~"), "tilde should not expand when not the first component" ); + let actual = sanitize_path_tool_arg(&os, "dir/$HOME"); + assert_eq!( + actual, + os.fs.chroot_path("dir/$HOME"), + "$HOME should not expand when not the first component" + ); } #[tokio::test]