ai_agent/memdir/
team_mem_paths.rs1use crate::memdir::paths::{get_auto_mem_path, is_auto_memory_enabled};
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
12pub struct PathTraversalError {
13 message: String,
14}
15
16impl std::fmt::Display for PathTraversalError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 write!(f, "{}", self.message)
19 }
20}
21
22impl std::error::Error for PathTraversalError {}
23
24impl PathTraversalError {
25 pub fn new(message: &str) -> Self {
26 Self {
27 message: message.to_string(),
28 }
29 }
30}
31
32fn sanitize_path_key(key: &str) -> Result<String, PathTraversalError> {
36 if key.contains('\0') {
38 return Err(PathTraversalError::new(&format!(
39 "Null byte in path key: \"{}\"",
40 key
41 )));
42 }
43
44 let mut has_url_encoded_traversal = false;
47 let key_lower = key.to_lowercase();
48 if key_lower.contains("%2e%2e") || key_lower.contains("%2e/") || key_lower.contains("/%2e%2e") {
50 has_url_encoded_traversal = true;
51 }
52
53 if has_url_encoded_traversal {
54 return Err(PathTraversalError::new(&format!(
55 "URL-encoded traversal in path key: \"{}\"",
56 key
57 )));
58 }
59
60 if key.contains('\\') {
62 return Err(PathTraversalError::new(&format!(
63 "Backslash in path key: \"{}\"",
64 key
65 )));
66 }
67
68 if key.starts_with('/') {
70 return Err(PathTraversalError::new(&format!(
71 "Absolute path key: \"{}\"",
72 key
73 )));
74 }
75
76 Ok(key.to_string())
77}
78
79pub fn is_team_memory_enabled() -> bool {
85 if !is_auto_memory_enabled() {
86 return false;
87 }
88
89 false
92}
93
94pub fn get_team_mem_path() -> PathBuf {
97 let auto_mem = get_auto_mem_path();
98 let team_path = auto_mem.join("team");
99 let path_str = team_path.to_string_lossy().to_string();
100
101 let sep = std::path::MAIN_SEPARATOR;
103 if !path_str.ends_with(sep) {
104 format!("{}{}", path_str, sep).into()
105 } else {
106 team_path
107 }
108}
109
110pub fn get_team_mem_entypoint() -> PathBuf {
113 get_team_mem_path().join("MEMORY.md")
114}
115
116pub fn is_team_mem_path(file_path: &Path) -> bool {
121 let resolved_path = std::fs::canonicalize(file_path).unwrap_or_else(|_| {
124 if file_path.is_absolute() {
126 file_path.to_path_buf()
127 } else {
128 std::env::current_dir()
129 .map(|c| c.join(file_path))
130 .unwrap_or_else(|_| file_path.to_path_buf())
131 }
132 });
133
134 let team_dir = get_team_mem_path();
135 let team_dir_str = team_dir.to_string_lossy();
136
137 resolved_path.to_string_lossy().starts_with(&*team_dir_str)
138}
139
140pub fn validate_team_mem_write_path(file_path: &Path) -> Result<PathBuf, PathTraversalError> {
145 let path_str = file_path.to_string_lossy();
146 if path_str.contains('\0') {
147 return Err(PathTraversalError::new(&format!(
148 "Null byte in path: \"{}\"",
149 file_path.display()
150 )));
151 }
152
153 let resolved_path = std::fs::canonicalize(file_path).unwrap_or_else(|_| {
157 if file_path.is_absolute() {
158 file_path.to_path_buf()
159 } else {
160 std::env::current_dir()
161 .map(|c| c.join(file_path))
162 .unwrap_or_else(|_| file_path.to_path_buf())
163 }
164 });
165
166 let team_dir = get_team_mem_path();
167 let team_dir_str = team_dir.to_string_lossy();
168
169 if !resolved_path.to_string_lossy().starts_with(&*team_dir_str) {
172 return Err(PathTraversalError::new(&format!(
173 "Path escapes team memory directory: \"{}\"",
174 file_path.display()
175 )));
176 }
177
178 Ok(resolved_path)
182}
183
184pub fn validate_team_mem_key(relative_key: &str) -> Result<PathBuf, PathTraversalError> {
190 sanitize_path_key(relative_key)?;
191
192 let team_dir = get_team_mem_path();
193 let full_path = team_dir.join(relative_key);
194
195 let resolved_path = std::fs::canonicalize(&full_path).unwrap_or_else(|_| full_path.clone());
197
198 let team_dir_str = team_dir.to_string_lossy();
199 if !resolved_path.to_string_lossy().starts_with(&*team_dir_str) {
200 return Err(PathTraversalError::new(&format!(
201 "Key escapes team memory directory: \"{}\"",
202 relative_key
203 )));
204 }
205
206 Ok(resolved_path)
209}
210
211pub fn is_team_mem_file(file_path: &Path) -> bool {
214 is_team_memory_enabled() && is_team_mem_path(file_path)
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_sanitize_path_key_valid() {
223 assert!(sanitize_path_key("valid_name.md").is_ok());
224 assert!(sanitize_path_key("subdir/test.md").is_ok());
225 }
226
227 #[test]
228 fn test_sanitize_path_key_null_byte() {
229 let result = sanitize_path_key("test\0.md");
230 assert!(result.is_err());
231 }
232
233 #[test]
234 fn test_sanitize_path_key_absolute() {
235 let result = sanitize_path_key("/etc/passwd");
236 assert!(result.is_err());
237 }
238
239 #[test]
240 fn test_sanitize_path_key_backslash() {
241 let result = sanitize_path_key("test\\md");
242 assert!(result.is_err());
243 }
244
245 #[test]
246 fn test_is_team_memory_enabled_when_auto_disabled() {
247 let _ = is_team_memory_enabled();
249 }
250}