opi_coding_agent/tool/
mod.rs1mod bash;
2mod edit;
3mod glob;
4mod grep;
5mod read;
6mod write;
7
8pub use bash::BashTool;
9pub use edit::EditTool;
10pub use glob::GlobTool;
11pub use grep::GrepTool;
12pub use read::ReadTool;
13pub use write::WriteTool;
14
15use std::path::{Path, PathBuf};
16
17pub fn validate_workspace_path(workspace_root: &Path, user_path: &str) -> Result<PathBuf, String> {
26 let resolved = workspace_root.join(user_path);
27 let canonical_root = std::fs::canonicalize(workspace_root)
28 .map_err(|e| format!("cannot canonicalize workspace root: {e}"))?;
29
30 if let Ok(canonical) = std::fs::canonicalize(&resolved) {
32 return if canonical.starts_with(&canonical_root) {
33 Ok(canonical)
34 } else {
35 Err(format!(
36 "path '{}' resolves outside the workspace",
37 user_path
38 ))
39 };
40 }
41
42 let mut ancestor = resolved.as_path();
46 let mut suffix_components: Vec<std::ffi::OsString> = Vec::new();
47 while let Some(parent) = ancestor.parent() {
48 if let Some(name) = ancestor.file_name() {
49 suffix_components.push(name.to_os_string());
50 }
51 if let Ok(canonical_ancestor) = std::fs::canonicalize(parent) {
52 suffix_components.reverse();
53 let suffix: PathBuf = suffix_components.iter().collect();
54 let canonical = canonical_ancestor.join(suffix);
55 return if canonical.starts_with(&canonical_root) {
56 Ok(canonical)
57 } else {
58 Err(format!(
59 "path '{}' resolves outside the workspace",
60 user_path
61 ))
62 };
63 }
64 ancestor = parent;
65 }
66
67 let normalized = normalize_path_components(&resolved);
70 if normalized.starts_with(&canonical_root) {
71 Ok(normalized)
72 } else {
73 Err(format!(
74 "path '{}' resolves outside the workspace",
75 user_path
76 ))
77 }
78}
79
80fn normalize_path_components(path: &Path) -> PathBuf {
82 let mut stack = Vec::new();
83 for component in path.components() {
84 match component {
85 std::path::Component::ParentDir => {
86 stack.pop();
87 }
88 std::path::Component::CurDir => {}
89 c => stack.push(c.as_os_str()),
90 }
91 }
92 stack.iter().collect()
93}