diff --git a/src/command/handlers/list_todos.rs b/src/command/handlers/list_todos.rs index 07e3dda..3b837ca 100644 --- a/src/command/handlers/list_todos.rs +++ b/src/command/handlers/list_todos.rs @@ -76,6 +76,9 @@ impl CommandHandler for ListTodosCommandHandler { id: r.id, content: r.content, status: r.status, + priority: r.priority, + created_at: r.created_at, + updated_at: r.updated_at, created_by_message_id: r.created_by_message_id, }) .collect(); diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index c6073c5..a15c496 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -88,6 +88,9 @@ pub struct TodoItemSummary { pub id: String, pub content: String, pub status: String, + pub priority: String, + pub created_at: i64, + pub updated_at: i64, pub created_by_message_id: Option, } diff --git a/src/tools/file_edit.rs b/src/tools/file_edit.rs index b9f2e52..c751235 100644 --- a/src/tools/file_edit.rs +++ b/src/tools/file_edit.rs @@ -33,11 +33,31 @@ impl FileEditTool { // Check directory restriction if let Some(ref allowed) = self.allowed_dir { - let allowed_path = Path::new(allowed); - if !resolved.starts_with(allowed_path) { + // canonicalize both paths to resolve symlinks and prevent path traversal + // via symlinks inside allowed_dir pointing outside. + // For edit tool the target file may not exist yet; fall back to + // canonicalizing the parent directory. + let canonical_allowed = std::fs::canonicalize(allowed) + .map_err(|e| format!("Failed to canonicalize allowed dir '{}': {}", allowed, e))?; + let canonical_resolved = match std::fs::canonicalize(&resolved) { + Ok(c) => c, + Err(_) => { + // File doesn't exist yet; canonicalize parent directory + let parent = resolved.parent().ok_or_else(|| { + format!("Path '{}' has no parent directory", path) + })?; + let canonical_parent = std::fs::canonicalize(parent).map_err(|e| { + format!("Failed to canonicalize parent directory of '{}': {}", path, e) + })?; + canonical_parent.join(resolved.file_name().ok_or_else(|| { + format!("Path '{}' has no file name component", path) + })?) + } + }; + if !canonical_resolved.starts_with(&canonical_allowed) { return Err(format!( - "Path '{}' is outside allowed directory '{}'", - path, allowed + "Path '{}' (resolves to '{}') is outside allowed directory '{}'", + path, canonical_resolved.display(), canonical_allowed.display() )); } } diff --git a/src/tools/file_read.rs b/src/tools/file_read.rs index 6a8a8a8..5ba4074 100644 --- a/src/tools/file_read.rs +++ b/src/tools/file_read.rs @@ -37,11 +37,18 @@ impl FileReadTool { // Check directory restriction if let Some(ref allowed) = self.allowed_dir { - let allowed_path = Path::new(allowed); - if !resolved.starts_with(allowed_path) { + // canonicalize both paths to resolve symlinks and prevent path traversal + // via symlinks inside allowed_dir pointing outside + let canonical_allowed = std::fs::canonicalize(allowed) + .map_err(|e| format!("Failed to canonicalize allowed dir '{}': {}", allowed, e))?; + // For read tool, file must exist; canonicalize will fail for non-existent paths + // which is acceptable (returns error) + let canonical_resolved = std::fs::canonicalize(&resolved) + .map_err(|e| format!("Failed to canonicalize path '{}': {}", path, e))?; + if !canonical_resolved.starts_with(&canonical_allowed) { return Err(format!( - "Path '{}' is outside allowed directory '{}'", - path, allowed + "Path '{}' (resolves to '{}') is outside allowed directory '{}'", + path, canonical_resolved.display(), canonical_allowed.display() )); } } diff --git a/src/tools/file_write.rs b/src/tools/file_write.rs index 15ca360..ef07ea5 100644 --- a/src/tools/file_write.rs +++ b/src/tools/file_write.rs @@ -32,11 +32,31 @@ impl FileWriteTool { // Check directory restriction if let Some(ref allowed) = self.allowed_dir { - let allowed_path = Path::new(allowed); - if !resolved.starts_with(allowed_path) { + // canonicalize both paths to resolve symlinks and prevent path traversal + // via symlinks inside allowed_dir pointing outside. + // For write tool the target file may not exist yet; fall back to + // canonicalizing the parent directory. + let canonical_allowed = std::fs::canonicalize(allowed) + .map_err(|e| format!("Failed to canonicalize allowed dir '{}': {}", allowed, e))?; + let canonical_resolved = match std::fs::canonicalize(&resolved) { + Ok(c) => c, + Err(_) => { + // File doesn't exist yet; canonicalize parent directory + let parent = resolved.parent().ok_or_else(|| { + format!("Path '{}' has no parent directory", path) + })?; + let canonical_parent = std::fs::canonicalize(parent).map_err(|e| { + format!("Failed to canonicalize parent directory of '{}': {}", path, e) + })?; + canonical_parent.join(resolved.file_name().ok_or_else(|| { + format!("Path '{}' has no file name component", path) + })?) + } + }; + if !canonical_resolved.starts_with(&canonical_allowed) { return Err(format!( - "Path '{}' is outside allowed directory '{}'", - path, allowed + "Path '{}' (resolves to '{}') is outside allowed directory '{}'", + path, canonical_resolved.display(), canonical_allowed.display() )); } } diff --git a/src/tools/http_request.rs b/src/tools/http_request.rs index be6f9ce..f484b8f 100644 --- a/src/tools/http_request.rs +++ b/src/tools/http_request.rs @@ -311,6 +311,7 @@ impl Tool for HttpRequestTool { let client = match reqwest::Client::builder() .timeout(Duration::from_secs(self.timeout_secs)) + .redirect(reqwest::redirect::Policy::none()) .build() { Ok(c) => c,