1use async_trait::async_trait;
7use serde_json::Value;
8use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use crate::error::{Error, Result};
12use crate::llm::ToolSpec;
13
14pub mod apply_patch;
15pub mod count_lines;
16pub mod fs;
17pub mod shell;
18
19pub use apply_patch::ApplyPatch;
20pub use count_lines::CountLines;
21pub use fs::{ListDir, ReadFile, WriteFile};
22pub use shell::RunShell;
23
24#[async_trait]
25pub trait Tool: Send + Sync {
26 fn spec(&self) -> ToolSpec;
27 async fn execute(&self, arguments: Value) -> Result<String>;
28}
29
30#[derive(Default, Clone)]
31pub struct ToolRegistry {
32 tools: BTreeMap<String, Arc<dyn Tool>>,
33}
34
35impl ToolRegistry {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn register(mut self, tool: Arc<dyn Tool>) -> Self {
41 let name = tool.spec().name;
42 self.tools.insert(name, tool);
43 self
44 }
45
46 pub fn specs(&self) -> Vec<ToolSpec> {
47 self.tools.values().map(|t| t.spec()).collect()
48 }
49
50 pub fn names(&self) -> Vec<String> {
51 self.tools.keys().cloned().collect()
52 }
53
54 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
55 self.tools.get(name).cloned()
56 }
57
58 pub async fn invoke(&self, name: &str, arguments: Value) -> Result<String> {
59 let tool = self
60 .get(name)
61 .ok_or_else(|| Error::UnknownTool(name.into()))?;
62 tool.execute(arguments).await.map_err(|e| match e {
63 Error::Tool { .. } | Error::BadToolArgs { .. } | Error::UnknownTool(_) => e,
64 other => Error::Tool {
65 name: name.into(),
66 message: other.to_string(),
67 },
68 })
69 }
70}
71
72pub(crate) fn resolve_within(root: &std::path::Path, path: &str) -> Result<std::path::PathBuf> {
78 let candidate = std::path::Path::new(path);
79 let joined = if candidate.is_absolute() {
80 candidate.to_path_buf()
81 } else {
82 root.join(candidate)
83 };
84 let abs_root = absolutise(root);
85 let abs_joined = absolutise(&joined);
86 if !abs_joined.starts_with(&abs_root) {
87 return Err(Error::BadToolArgs {
88 name: "<fs>".into(),
89 message: format!(
90 "path `{}` escapes workspace root `{}`",
91 path,
92 abs_root.display()
93 ),
94 });
95 }
96 Ok(abs_joined)
97}
98
99fn absolutise(p: &std::path::Path) -> std::path::PathBuf {
102 let abs = if p.is_absolute() {
103 p.to_path_buf()
104 } else {
105 std::env::current_dir()
106 .unwrap_or_else(|_| std::path::PathBuf::from("."))
107 .join(p)
108 };
109 normalise(&abs)
110}
111
112fn normalise(p: &std::path::Path) -> std::path::PathBuf {
113 let mut out = std::path::PathBuf::new();
114 for c in p.components() {
115 use std::path::Component::*;
116 match c {
117 ParentDir => {
118 out.pop();
119 }
120 CurDir => {}
121 other => out.push(other.as_os_str()),
122 }
123 }
124 out
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use async_trait::async_trait;
131
132 struct Echo;
133
134 #[async_trait]
135 impl Tool for Echo {
136 fn spec(&self) -> ToolSpec {
137 ToolSpec {
138 name: "echo".into(),
139 description: "echo".into(),
140 parameters: serde_json::json!({"type":"object","properties":{"msg":{"type":"string"}}}),
141 }
142 }
143 async fn execute(&self, args: Value) -> Result<String> {
144 Ok(args["msg"].as_str().unwrap_or("").into())
145 }
146 }
147
148 #[tokio::test]
149 async fn registry_dispatches_and_errors_on_unknown() {
150 let reg = ToolRegistry::new().register(Arc::new(Echo));
151 let out = reg
152 .invoke("echo", serde_json::json!({"msg":"hi"}))
153 .await
154 .unwrap();
155 assert_eq!(out, "hi");
156 let err = reg.invoke("nope", serde_json::json!({})).await.unwrap_err();
157 assert!(matches!(err, Error::UnknownTool(_)));
158 }
159
160 #[test]
161 fn resolve_within_rejects_escape() {
162 let root = std::path::Path::new("/work");
163 assert!(resolve_within(root, "../etc/passwd").is_err());
164 assert!(resolve_within(root, "/elsewhere").is_err());
165 assert!(resolve_within(root, "src/lib.rs").is_ok());
166 }
167
168 #[test]
169 fn resolve_within_handles_relative_root() {
170 let cwd = std::env::current_dir().unwrap();
172 let resolved = resolve_within(std::path::Path::new("."), "src/lib.rs").unwrap();
173 assert!(resolved.starts_with(&cwd));
174 assert!(resolved.ends_with("src/lib.rs"));
175 }
176}