Skip to main content

openclaw_agents/tools/
mod.rs

1//! Tool registry and execution.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use openclaw_providers::traits::Tool as ToolDefinition;
11
12/// Tool execution errors.
13#[derive(Error, Debug)]
14pub enum ToolError {
15    /// Tool not found.
16    #[error("Tool not found: {0}")]
17    NotFound(String),
18
19    /// Invalid parameters.
20    #[error("Invalid parameters: {0}")]
21    InvalidParams(String),
22
23    /// Execution failed.
24    #[error("Execution failed: {0}")]
25    ExecutionFailed(String),
26
27    /// Tool timed out.
28    #[error("Tool timed out")]
29    Timeout,
30}
31
32/// Tool execution result.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ToolResult {
35    /// Whether execution succeeded.
36    pub success: bool,
37    /// Result content.
38    pub content: String,
39    /// Error message if failed.
40    pub error: Option<String>,
41}
42
43impl ToolResult {
44    /// Create a successful result.
45    #[must_use]
46    pub fn success(content: impl Into<String>) -> Self {
47        Self {
48            success: true,
49            content: content.into(),
50            error: None,
51        }
52    }
53
54    /// Create an error result.
55    #[must_use]
56    pub fn error(error: impl Into<String>) -> Self {
57        Self {
58            success: false,
59            content: String::new(),
60            error: Some(error.into()),
61        }
62    }
63}
64
65/// Tool trait for implementing custom tools.
66#[async_trait]
67pub trait Tool: Send + Sync {
68    /// Tool name.
69    fn name(&self) -> &str;
70
71    /// Tool description.
72    fn description(&self) -> &str;
73
74    /// Input schema (JSON Schema).
75    fn input_schema(&self) -> serde_json::Value;
76
77    /// Execute the tool.
78    async fn execute(&self, params: serde_json::Value) -> Result<ToolResult, ToolError>;
79}
80
81/// Registry of available tools.
82pub struct ToolRegistry {
83    tools: HashMap<String, Arc<dyn Tool>>,
84}
85
86impl ToolRegistry {
87    /// Create a new empty tool registry.
88    #[must_use]
89    pub fn new() -> Self {
90        Self {
91            tools: HashMap::new(),
92        }
93    }
94
95    /// Register a tool.
96    pub fn register(&mut self, tool: Arc<dyn Tool>) {
97        self.tools.insert(tool.name().to_string(), tool);
98    }
99
100    /// Get a tool by name.
101    #[must_use]
102    pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
103        self.tools.get(name)
104    }
105
106    /// List all tool names.
107    #[must_use]
108    pub fn list(&self) -> Vec<&str> {
109        self.tools.keys().map(String::as_str).collect()
110    }
111
112    /// Execute a tool by name.
113    ///
114    /// # Errors
115    ///
116    /// Returns error if tool not found or execution fails.
117    pub async fn execute(
118        &self,
119        name: &str,
120        params: serde_json::Value,
121    ) -> Result<ToolResult, ToolError> {
122        let tool = self
123            .tools
124            .get(name)
125            .ok_or_else(|| ToolError::NotFound(name.to_string()))?;
126        tool.execute(params).await
127    }
128
129    /// Get tool definitions for provider API.
130    #[must_use]
131    pub fn as_tool_definitions(&self) -> Vec<ToolDefinition> {
132        self.tools
133            .values()
134            .map(|tool| ToolDefinition {
135                name: tool.name().to_string(),
136                description: tool.description().to_string(),
137                input_schema: tool.input_schema(),
138            })
139            .collect()
140    }
141}
142
143impl Default for ToolRegistry {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149/// Built-in bash tool for command execution.
150pub struct BashTool {
151    sandbox_config: crate::sandbox::SandboxConfig,
152}
153
154impl BashTool {
155    /// Create a new bash tool.
156    #[must_use]
157    pub fn new() -> Self {
158        Self {
159            sandbox_config: crate::sandbox::SandboxConfig::default(),
160        }
161    }
162
163    /// Create with custom sandbox config.
164    #[must_use]
165    pub const fn with_sandbox_config(config: crate::sandbox::SandboxConfig) -> Self {
166        Self {
167            sandbox_config: config,
168        }
169    }
170}
171
172impl Default for BashTool {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178#[async_trait]
179impl Tool for BashTool {
180    fn name(&self) -> &'static str {
181        "bash"
182    }
183
184    fn description(&self) -> &'static str {
185        "Execute a bash command in a sandboxed environment"
186    }
187
188    fn input_schema(&self) -> serde_json::Value {
189        serde_json::json!({
190            "type": "object",
191            "properties": {
192                "command": {
193                    "type": "string",
194                    "description": "The bash command to execute"
195                }
196            },
197            "required": ["command"]
198        })
199    }
200
201    async fn execute(&self, params: serde_json::Value) -> Result<ToolResult, ToolError> {
202        let command = params["command"]
203            .as_str()
204            .ok_or_else(|| ToolError::InvalidParams("Missing 'command' parameter".to_string()))?;
205
206        // Execute in sandbox
207        let output =
208            crate::sandbox::execute_sandboxed("bash", &["-c", command], &self.sandbox_config)
209                .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
210
211        if output.exit_code == 0 {
212            Ok(ToolResult::success(output.stdout))
213        } else {
214            let error_msg = if output.stderr.is_empty() {
215                format!("Command failed with exit code {}", output.exit_code)
216            } else {
217                output.stderr
218            };
219            Ok(ToolResult::error(error_msg))
220        }
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_tool_registry() {
230        let mut registry = ToolRegistry::new();
231        registry.register(Arc::new(BashTool::new()));
232
233        assert!(registry.get("bash").is_some());
234        assert!(registry.get("nonexistent").is_none());
235    }
236
237    #[test]
238    fn test_tool_definitions() {
239        let mut registry = ToolRegistry::new();
240        registry.register(Arc::new(BashTool::new()));
241
242        let defs = registry.as_tool_definitions();
243        assert_eq!(defs.len(), 1);
244        assert_eq!(defs[0].name, "bash");
245    }
246}