Skip to main content

clawft_core/tools/
registry.rs

1//! Tool registry and [`Tool`] trait definition.
2//!
3//! Defines the interface that all tool implementations must satisfy
4//! ([`Tool`]) and provides a [`ToolRegistry`] that stores registered
5//! tools and dispatches execution requests by name.
6//!
7//! Tool implementations live in the `clawft-tools` crate; this module
8//! only defines the contract and registry infrastructure.
9
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15use tracing::debug;
16
17use clawft_types::routing::UserPermissions;
18
19/// Error type for tool execution.
20///
21/// Covers the common failure modes: unknown tool, bad arguments,
22/// runtime failures, permission issues, and timeouts.
23#[non_exhaustive]
24#[derive(Debug, thiserror::Error)]
25pub enum ToolError {
26    /// The requested tool was not found in the registry.
27    #[error("tool not found: {0}")]
28    NotFound(String),
29
30    /// The arguments provided to the tool are invalid.
31    #[error("invalid arguments: {0}")]
32    InvalidArgs(String),
33
34    /// The tool execution failed at runtime.
35    #[error("execution failed: {0}")]
36    ExecutionFailed(String),
37
38    /// The caller lacks permission to invoke this tool.
39    ///
40    /// `tool` is the name of the tool that was denied.
41    /// `reason` explains why (not in allowlist, explicitly denied,
42    /// insufficient level, missing custom permission).
43    #[error("permission denied for tool '{tool}': {reason}")]
44    PermissionDenied { tool: String, reason: String },
45
46    /// A file or resource the tool needs was not found.
47    #[error("not found: {0}")]
48    FileNotFound(String),
49
50    /// A filesystem path is invalid or traverses outside allowed boundaries.
51    #[error("invalid path: {0}")]
52    InvalidPath(String),
53
54    /// The tool execution exceeded the allowed time limit.
55    #[error("timeout after {0}s")]
56    Timeout(u64),
57}
58
59// ---------------------------------------------------------------------------
60// ToolMetadata
61// ---------------------------------------------------------------------------
62
63/// Permission metadata that a tool can declare.
64///
65/// Built-in tools define this via the `Tool::metadata()` trait method.
66/// MCP tools derive this from their server's tool declaration JSON.
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct ToolMetadata {
69    /// Minimum permission level required to invoke this tool.
70    /// `None` means no level requirement beyond the allowlist check.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub required_permission_level: Option<u8>,
73
74    /// Custom permission keys and values that must match.
75    /// Example: `{"exec_enabled": true}` requires the user to have
76    /// `custom_permissions["exec_enabled"] == true`.
77    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
78    pub required_custom_permissions: HashMap<String, serde_json::Value>,
79}
80
81// ---------------------------------------------------------------------------
82// Glob matching
83// ---------------------------------------------------------------------------
84
85/// Match a tool name against a glob pattern.
86///
87/// Supports `*` (matches zero or more characters) and `?` (matches exactly
88/// one character). This is a minimal implementation sufficient for tool
89/// access patterns without pulling in a full glob crate.
90///
91/// Examples:
92///   `glob_matches("file_*", "file_read")` -> true
93///   `glob_matches("file_*", "web_search")` -> false
94///   `glob_matches("*", "anything")` -> true
95///   `glob_matches("read_?", "read_a")` -> true
96///   `glob_matches("read_?", "read_file")` -> false
97fn glob_matches(pattern: &str, text: &str) -> bool {
98    let pattern: Vec<char> = pattern.chars().collect();
99    let text: Vec<char> = text.chars().collect();
100    let (plen, tlen) = (pattern.len(), text.len());
101
102    let mut pi = 0;
103    let mut ti = 0;
104    let mut star_pi = None;
105    let mut star_ti = 0;
106
107    while ti < tlen {
108        if pi < plen && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
109            pi += 1;
110            ti += 1;
111        } else if pi < plen && pattern[pi] == '*' {
112            star_pi = Some(pi);
113            star_ti = ti;
114            pi += 1;
115        } else if let Some(spi) = star_pi {
116            pi = spi + 1;
117            star_ti += 1;
118            ti = star_ti;
119        } else {
120            return false;
121        }
122    }
123
124    // Consume trailing stars in pattern.
125    while pi < plen && pattern[pi] == '*' {
126        pi += 1;
127    }
128
129    pi == plen
130}
131
132/// Check whether a tool name matches any pattern in the given list.
133/// Each entry in `patterns` is either an exact name or a glob pattern.
134fn matches_any_pattern(tool_name: &str, patterns: &[String]) -> bool {
135    patterns.iter().any(|pattern| {
136        if pattern.contains('*') || pattern.contains('?') {
137            glob_matches(pattern, tool_name)
138        } else {
139            pattern == tool_name
140        }
141    })
142}
143
144// ---------------------------------------------------------------------------
145// Permission checking
146// ---------------------------------------------------------------------------
147
148/// Check whether the given permissions allow invoking the named tool.
149///
150/// This is a pure function with no side effects -- suitable for
151/// unit testing without async or I/O.
152///
153/// Evaluation order:
154///   1. Denylist (checked first, deny overrides everything)
155///   2. Allowlist (empty = deny all, `["*"]` = allow all, else pattern match)
156///   3. Tool metadata `required_permission_level`
157///   4. Tool metadata `required_custom_permissions`
158///
159/// Returns `Ok(())` if allowed, `Err(ToolError::PermissionDenied)` if not.
160pub fn check_tool_permission(
161    tool_name: &str,
162    permissions: &UserPermissions,
163    tool_metadata: Option<&ToolMetadata>,
164) -> Result<(), ToolError> {
165    // Step 1: Denylist check (deny overrides everything, even ["*"] allowlist).
166    if matches_any_pattern(tool_name, &permissions.tool_denylist) {
167        return Err(ToolError::PermissionDenied {
168            tool: tool_name.to_string(),
169            reason: "tool is explicitly denied for this user".to_string(),
170        });
171    }
172
173    // Step 2: Allowlist check.
174    let allowed = if permissions.tool_access.is_empty() {
175        // Empty allowlist = no tools allowed (zero_trust default).
176        false
177    } else if permissions.tool_access.iter().any(|s| s == "*") {
178        // Wildcard entry = all tools allowed (admin default).
179        true
180    } else {
181        // Check each pattern in tool_access for a match.
182        matches_any_pattern(tool_name, &permissions.tool_access)
183    };
184
185    if !allowed {
186        return Err(ToolError::PermissionDenied {
187            tool: tool_name.to_string(),
188            reason: format!(
189                "tool is not in the allowed tools for permission level {}",
190                permissions.level,
191            ),
192        });
193    }
194
195    // Step 3: Tool metadata -- required permission level.
196    if let Some(meta) = tool_metadata {
197        if let Some(required_level) = meta.required_permission_level
198            && permissions.level < required_level
199        {
200            return Err(ToolError::PermissionDenied {
201                tool: tool_name.to_string(),
202                reason: format!(
203                    "tool requires permission level {} but user has level {}",
204                    required_level, permissions.level,
205                ),
206            });
207        }
208
209        // Step 4: Tool metadata -- required custom permissions.
210        for (key, required_value) in &meta.required_custom_permissions {
211            match permissions.custom_permissions.get(key) {
212                None => {
213                    return Err(ToolError::PermissionDenied {
214                        tool: tool_name.to_string(),
215                        reason: format!(
216                            "tool requires custom permission '{}' which is not set",
217                            key,
218                        ),
219                    });
220                }
221                Some(actual) if actual != required_value => {
222                    return Err(ToolError::PermissionDenied {
223                        tool: tool_name.to_string(),
224                        reason: format!(
225                            "tool requires {}={} but user has {}={}",
226                            key, required_value, key, actual,
227                        ),
228                    });
229                }
230                Some(_) => {} // Value matches, continue.
231            }
232        }
233    }
234
235    Ok(())
236}
237
238/// Extract permission metadata from an MCP tool declaration JSON.
239///
240/// MCP tool declarations may include:
241///   `"required_permission_level": 2`
242///   `"required_custom_permissions": {"exec_enabled": true}`
243pub fn extract_mcp_metadata(tool_decl: &serde_json::Value) -> ToolMetadata {
244    let required_level = tool_decl
245        .get("required_permission_level")
246        .and_then(|v| v.as_u64())
247        .map(|v| v as u8);
248
249    let required_custom = tool_decl
250        .get("required_custom_permissions")
251        .and_then(|v| v.as_object())
252        .map(|obj| {
253            obj.iter()
254                .map(|(k, v)| (k.clone(), v.clone()))
255                .collect()
256        })
257        .unwrap_or_default();
258
259    ToolMetadata {
260        required_permission_level: required_level,
261        required_custom_permissions: required_custom,
262    }
263}
264
265// ---------------------------------------------------------------------------
266// Tool trait
267// ---------------------------------------------------------------------------
268
269/// A tool that can be invoked by the agent pipeline.
270///
271/// Implementations provide a name, description, JSON Schema for parameters,
272/// and an async `execute` method. Tools are registered in a [`ToolRegistry`]
273/// and dispatched by the agent loop when the LLM emits a tool-use request.
274///
275/// # Implementing a tool
276///
277/// ```rust,ignore
278/// use async_trait::async_trait;
279/// use clawft_core::tools::registry::{Tool, ToolError};
280///
281/// struct EchoTool;
282///
283/// #[async_trait]
284/// impl Tool for EchoTool {
285///     fn name(&self) -> &str { "echo" }
286///     fn description(&self) -> &str { "Echo back the input" }
287///     fn parameters(&self) -> serde_json::Value {
288///         serde_json::json!({
289///             "type": "object",
290///             "properties": {
291///                 "text": { "type": "string", "description": "Text to echo" }
292///             },
293///             "required": ["text"]
294///         })
295///     }
296///     async fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
297///         let text = args.get("text")
298///             .and_then(|v| v.as_str())
299///             .ok_or_else(|| ToolError::InvalidArgs("missing 'text'".into()))?;
300///         Ok(serde_json::json!({ "output": text }))
301///     }
302/// }
303/// ```
304#[cfg_attr(not(feature = "browser"), async_trait)]
305#[cfg_attr(feature = "browser", async_trait(?Send))]
306pub trait Tool: Send + Sync {
307    /// The unique name of this tool (used in LLM function calling).
308    fn name(&self) -> &str;
309
310    /// A human-readable description of what this tool does.
311    fn description(&self) -> &str;
312
313    /// JSON Schema describing the tool's parameters.
314    ///
315    /// Should return a valid JSON Schema object (type: "object" with
316    /// properties, required, etc.) suitable for OpenAI function calling.
317    fn parameters(&self) -> serde_json::Value;
318
319    /// Execute the tool with the given arguments.
320    ///
321    /// Arguments are a JSON object matching the schema from [`parameters`].
322    /// Returns a JSON value representing the tool's output, or a
323    /// [`ToolError`] on failure.
324    async fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError>;
325
326    /// Optional permission metadata for this tool.
327    ///
328    /// Override to declare minimum permission levels or required
329    /// custom permissions. Default: no requirements (returns `None`).
330    fn metadata(&self) -> Option<ToolMetadata> {
331        None
332    }
333}
334
335/// Registry of available tools, indexed by name.
336///
337/// Provides lookup, listing, schema generation in OpenAI function calling
338/// format, and dispatch-by-name execution.
339pub struct ToolRegistry {
340    tools: HashMap<String, Arc<dyn Tool>>,
341    metadata: HashMap<String, ToolMetadata>,
342}
343
344impl ToolRegistry {
345    /// Create an empty tool registry.
346    pub fn new() -> Self {
347        Self {
348            tools: HashMap::new(),
349            metadata: HashMap::new(),
350        }
351    }
352
353    /// Register a tool in the registry.
354    ///
355    /// If a tool with the same name already exists, it is replaced.
356    /// If the tool provides metadata via [`Tool::metadata()`], it is stored
357    /// in the metadata map automatically.
358    pub fn register(&mut self, tool: Arc<dyn Tool>) {
359        let name = tool.name().to_string();
360        debug!(tool = %name, "registering tool");
361        // Extract metadata from the tool if provided.
362        if let Some(meta) = tool.metadata() {
363            self.metadata.insert(name.clone(), meta);
364        }
365        self.tools.insert(name, tool);
366    }
367
368    /// Register a tool with explicit metadata (used for MCP tools whose
369    /// metadata comes from JSON declarations rather than the Rust trait).
370    pub fn register_with_metadata(&mut self, tool: Arc<dyn Tool>, metadata: ToolMetadata) {
371        let name = tool.name().to_string();
372        debug!(tool = %name, "registering tool with metadata");
373        self.metadata.insert(name.clone(), metadata);
374        self.tools.insert(name, tool);
375    }
376
377    /// Check if a tool with the given name is registered.
378    pub fn has(&self, name: &str) -> bool {
379        self.tools.contains_key(name)
380    }
381
382    /// Look up a tool by name.
383    pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
384        self.tools.get(name).cloned()
385    }
386
387    /// Look up metadata for a tool by name.
388    pub fn get_metadata(&self, name: &str) -> Option<&ToolMetadata> {
389        self.metadata.get(name)
390    }
391
392    /// List all registered tool names (sorted alphabetically).
393    pub fn list(&self) -> Vec<String> {
394        let mut names: Vec<String> = self.tools.keys().cloned().collect();
395        names.sort();
396        names
397    }
398
399    /// Generate tool schemas in OpenAI function calling format.
400    ///
401    /// Returns one schema object per registered tool:
402    /// ```json
403    /// {
404    ///   "type": "function",
405    ///   "function": {
406    ///     "name": "tool_name",
407    ///     "description": "tool description",
408    ///     "parameters": { ... json schema ... }
409    ///   }
410    /// }
411    /// ```
412    ///
413    /// The returned vector is sorted by tool name for deterministic output.
414    pub fn schemas(&self) -> Vec<serde_json::Value> {
415        let mut schemas: Vec<(String, serde_json::Value)> = self
416            .tools
417            .iter()
418            .map(|(name, tool)| {
419                let schema = serde_json::json!({
420                    "type": "function",
421                    "function": {
422                        "name": name,
423                        "description": tool.description(),
424                        "parameters": tool.parameters(),
425                    }
426                });
427                (name.clone(), schema)
428            })
429            .collect();
430
431        schemas.sort_by(|a, b| a.0.cmp(&b.0));
432        schemas.into_iter().map(|(_, v)| v).collect()
433    }
434
435    /// Generate tool schemas for a specified subset of tools.
436    ///
437    /// Only includes tools whose names match at least one pattern in
438    /// `allowed`. Patterns support glob syntax (`*` and `?`).
439    ///
440    /// This enables per-turn tool filtering: skills declare which tools
441    /// they need, and only those schemas are sent to the LLM.
442    pub fn schemas_for_tools(&self, allowed: &[String]) -> Vec<serde_json::Value> {
443        let mut schemas: Vec<(String, serde_json::Value)> = self
444            .tools
445            .iter()
446            .filter(|(name, _)| matches_any_pattern(name, allowed))
447            .map(|(name, tool)| {
448                let schema = serde_json::json!({
449                    "type": "function",
450                    "function": {
451                        "name": name,
452                        "description": tool.description(),
453                        "parameters": tool.parameters(),
454                    }
455                });
456                (name.clone(), schema)
457            })
458            .collect();
459
460        schemas.sort_by(|a, b| a.0.cmp(&b.0));
461        schemas.into_iter().map(|(_, v)| v).collect()
462    }
463
464    /// Execute a tool by name with optional permission enforcement.
465    ///
466    /// When `permissions` is `None`, all tools are allowed (backward
467    /// compatibility for StaticRouter mode and unit tests).
468    /// When `Some`, permissions are checked before the tool runs.
469    ///
470    /// Returns [`ToolError::NotFound`] if no tool with that name is registered.
471    /// Returns [`ToolError::PermissionDenied`] if the caller lacks permission.
472    pub async fn execute(
473        &self,
474        name: &str,
475        args: serde_json::Value,
476        permissions: Option<&UserPermissions>,
477    ) -> Result<serde_json::Value, ToolError> {
478        // Look up the tool first (NotFound fires before PermissionDenied).
479        let tool = self
480            .tools
481            .get(name)
482            .ok_or_else(|| ToolError::NotFound(name.to_string()))?;
483
484        // Permission check (only when permissions are provided).
485        if let Some(perms) = permissions {
486            let meta = self.metadata.get(name);
487            check_tool_permission(name, perms, meta)?;
488        }
489
490        debug!(tool = %name, "executing tool");
491        tool.execute(args).await
492    }
493
494    /// Return the number of registered tools.
495    pub fn len(&self) -> usize {
496        self.tools.len()
497    }
498
499    /// Return true if no tools are registered.
500    pub fn is_empty(&self) -> bool {
501        self.tools.is_empty()
502    }
503
504    /// Create a filtered snapshot of this registry containing only tools
505    /// whose names are in the given allow list.
506    ///
507    /// If `allow` is empty, all tools are included (no filtering).
508    /// If `deny` is non-empty, matching tools are excluded even if
509    /// they appear in the allow list (deny overrides allow).
510    ///
511    /// This is used by the kernel supervisor to create per-agent
512    /// tool registries that respect capability restrictions.
513    pub fn filtered_tools(&self, allow: &[String], deny: &[String]) -> Self {
514        let mut filtered = Self::new();
515        for (name, tool) in &self.tools {
516            // Check deny list first
517            if deny.iter().any(|d| d == name) {
518                continue;
519            }
520            // Check allow list (empty = all allowed)
521            if !allow.is_empty() && !allow.iter().any(|a| a == name) {
522                continue;
523            }
524            filtered.tools.insert(name.clone(), tool.clone());
525            if let Some(meta) = self.metadata.get(name) {
526                filtered.metadata.insert(name.clone(), meta.clone());
527            }
528        }
529        filtered
530    }
531
532    /// Create a snapshot of this registry as a new `ToolRegistry`.
533    ///
534    /// The returned registry contains clones of all `Arc<dyn Tool>`
535    /// handles currently registered. This is useful for passing a
536    /// frozen copy of the tool set to components that need shared
537    /// access (e.g., wrapped in `Arc<ToolRegistry>`) without requiring
538    /// the original registry to be `Arc`-wrapped itself.
539    pub fn snapshot(&self) -> Self {
540        Self {
541            tools: self.tools.clone(),
542            metadata: self.metadata.clone(),
543        }
544    }
545}
546
547impl Default for ToolRegistry {
548    fn default() -> Self {
549        Self::new()
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556
557    /// A simple test tool that echoes its input.
558    struct EchoTool;
559
560    #[async_trait]
561    impl Tool for EchoTool {
562        fn name(&self) -> &str {
563            "echo"
564        }
565
566        fn description(&self) -> &str {
567            "Echo back the input text"
568        }
569
570        fn parameters(&self) -> serde_json::Value {
571            serde_json::json!({
572                "type": "object",
573                "properties": {
574                    "text": {
575                        "type": "string",
576                        "description": "Text to echo"
577                    }
578                },
579                "required": ["text"]
580            })
581        }
582
583        async fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
584            let text = args
585                .get("text")
586                .and_then(|v| v.as_str())
587                .ok_or_else(|| ToolError::InvalidArgs("missing 'text' field".into()))?;
588            Ok(serde_json::json!({ "output": text }))
589        }
590    }
591
592    /// A tool that always fails for testing error paths.
593    struct FailTool;
594
595    #[async_trait]
596    impl Tool for FailTool {
597        fn name(&self) -> &str {
598            "fail"
599        }
600
601        fn description(&self) -> &str {
602            "A tool that always fails"
603        }
604
605        fn parameters(&self) -> serde_json::Value {
606            serde_json::json!({
607                "type": "object",
608                "properties": {}
609            })
610        }
611
612        async fn execute(&self, _args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
613            Err(ToolError::ExecutionFailed("intentional failure".into()))
614        }
615    }
616
617    /// A math tool for testing multiple registrations.
618    struct AddTool;
619
620    #[async_trait]
621    impl Tool for AddTool {
622        fn name(&self) -> &str {
623            "add"
624        }
625
626        fn description(&self) -> &str {
627            "Add two numbers"
628        }
629
630        fn parameters(&self) -> serde_json::Value {
631            serde_json::json!({
632                "type": "object",
633                "properties": {
634                    "a": { "type": "number" },
635                    "b": { "type": "number" }
636                },
637                "required": ["a", "b"]
638            })
639        }
640
641        async fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
642            let a = args
643                .get("a")
644                .and_then(|v| v.as_f64())
645                .ok_or_else(|| ToolError::InvalidArgs("missing 'a'".into()))?;
646            let b = args
647                .get("b")
648                .and_then(|v| v.as_f64())
649                .ok_or_else(|| ToolError::InvalidArgs("missing 'b'".into()))?;
650            Ok(serde_json::json!({ "result": a + b }))
651        }
652    }
653
654    /// Helper: build admin permissions (level 2, wildcard tool_access).
655    fn admin_permissions() -> UserPermissions {
656        UserPermissions {
657            level: 2,
658            tool_access: vec!["*".into()],
659            ..UserPermissions::default()
660        }
661    }
662
663    /// Helper: build user-level permissions with specific tools.
664    fn user_permissions(tools: Vec<&str>) -> UserPermissions {
665        UserPermissions {
666            level: 1,
667            tool_access: tools.into_iter().map(String::from).collect(),
668            ..UserPermissions::default()
669        }
670    }
671
672    /// Helper: build zero-trust permissions (empty tool_access).
673    fn zero_trust_permissions() -> UserPermissions {
674        UserPermissions::default()
675    }
676
677    #[test]
678    fn new_registry_is_empty() {
679        let registry = ToolRegistry::new();
680        assert!(registry.is_empty());
681        assert_eq!(registry.len(), 0);
682        assert!(registry.list().is_empty());
683    }
684
685    #[test]
686    fn default_registry_is_empty() {
687        let registry = ToolRegistry::default();
688        assert!(registry.is_empty());
689    }
690
691    #[test]
692    fn register_and_get() {
693        let mut registry = ToolRegistry::new();
694        registry.register(Arc::new(EchoTool));
695
696        let tool = registry.get("echo").unwrap();
697        assert_eq!(tool.name(), "echo");
698        assert_eq!(tool.description(), "Echo back the input text");
699    }
700
701    #[test]
702    fn get_nonexistent_returns_none() {
703        let registry = ToolRegistry::new();
704        assert!(registry.get("nonexistent").is_none());
705    }
706
707    #[test]
708    fn list_returns_sorted_names() {
709        let mut registry = ToolRegistry::new();
710        registry.register(Arc::new(EchoTool));
711        registry.register(Arc::new(AddTool));
712        registry.register(Arc::new(FailTool));
713
714        let names = registry.list();
715        assert_eq!(names, vec!["add", "echo", "fail"]);
716    }
717
718    #[test]
719    fn len_reflects_registered_count() {
720        let mut registry = ToolRegistry::new();
721        assert_eq!(registry.len(), 0);
722
723        registry.register(Arc::new(EchoTool));
724        assert_eq!(registry.len(), 1);
725
726        registry.register(Arc::new(AddTool));
727        assert_eq!(registry.len(), 2);
728    }
729
730    #[test]
731    fn register_replaces_existing() {
732        let mut registry = ToolRegistry::new();
733        registry.register(Arc::new(EchoTool));
734        registry.register(Arc::new(EchoTool)); // Same name, should replace.
735        assert_eq!(registry.len(), 1);
736    }
737
738    #[tokio::test]
739    async fn execute_echo_tool() {
740        let mut registry = ToolRegistry::new();
741        registry.register(Arc::new(EchoTool));
742
743        let result = registry
744            .execute("echo", serde_json::json!({ "text": "hello" }), None)
745            .await
746            .unwrap();
747
748        assert_eq!(result["output"], "hello");
749    }
750
751    #[tokio::test]
752    async fn execute_add_tool() {
753        let mut registry = ToolRegistry::new();
754        registry.register(Arc::new(AddTool));
755
756        let result = registry
757            .execute("add", serde_json::json!({ "a": 3, "b": 4 }), None)
758            .await
759            .unwrap();
760
761        assert_eq!(result["result"], 7.0);
762    }
763
764    #[tokio::test]
765    async fn execute_not_found() {
766        let registry = ToolRegistry::new();
767        let result = registry
768            .execute("missing", serde_json::json!({}), None)
769            .await;
770
771        assert!(result.is_err());
772        match result.unwrap_err() {
773            ToolError::NotFound(name) => assert_eq!(name, "missing"),
774            other => panic!("expected NotFound, got: {other}"),
775        }
776    }
777
778    #[tokio::test]
779    async fn execute_tool_that_fails() {
780        let mut registry = ToolRegistry::new();
781        registry.register(Arc::new(FailTool));
782
783        let result = registry
784            .execute("fail", serde_json::json!({}), None)
785            .await;
786        assert!(result.is_err());
787        match result.unwrap_err() {
788            ToolError::ExecutionFailed(msg) => {
789                assert_eq!(msg, "intentional failure");
790            }
791            other => panic!("expected ExecutionFailed, got: {other}"),
792        }
793    }
794
795    #[tokio::test]
796    async fn execute_with_invalid_args() {
797        let mut registry = ToolRegistry::new();
798        registry.register(Arc::new(EchoTool));
799
800        let result = registry
801            .execute("echo", serde_json::json!({}), None) // missing "text"
802            .await;
803
804        assert!(result.is_err());
805        match result.unwrap_err() {
806            ToolError::InvalidArgs(msg) => {
807                assert!(msg.contains("text"));
808            }
809            other => panic!("expected InvalidArgs, got: {other}"),
810        }
811    }
812
813    #[test]
814    fn schemas_openai_format() {
815        let mut registry = ToolRegistry::new();
816        registry.register(Arc::new(EchoTool));
817        registry.register(Arc::new(AddTool));
818
819        let schemas = registry.schemas();
820        assert_eq!(schemas.len(), 2);
821
822        // Schemas should be sorted by name: "add" before "echo".
823        let first = &schemas[0];
824        assert_eq!(first["type"], "function");
825        assert_eq!(first["function"]["name"], "add");
826        assert_eq!(first["function"]["description"], "Add two numbers");
827        assert!(
828            first["function"]["parameters"]["properties"]
829                .get("a")
830                .is_some()
831        );
832
833        let second = &schemas[1];
834        assert_eq!(second["type"], "function");
835        assert_eq!(second["function"]["name"], "echo");
836        assert_eq!(
837            second["function"]["description"],
838            "Echo back the input text"
839        );
840        assert!(
841            second["function"]["parameters"]["properties"]
842                .get("text")
843                .is_some()
844        );
845    }
846
847    #[test]
848    fn schemas_empty_registry() {
849        let registry = ToolRegistry::new();
850        assert!(registry.schemas().is_empty());
851    }
852
853    #[test]
854    fn schemas_for_tools_filters_by_pattern() {
855        let mut registry = ToolRegistry::new();
856        registry.register(Arc::new(EchoTool));
857        registry.register(Arc::new(AddTool));
858        registry.register(Arc::new(FailTool));
859
860        // Exact name filter
861        let schemas = registry.schemas_for_tools(&["echo".into()]);
862        assert_eq!(schemas.len(), 1);
863        assert_eq!(schemas[0]["function"]["name"], "echo");
864
865        // Glob filter
866        let schemas = registry.schemas_for_tools(&["*".into()]);
867        assert_eq!(schemas.len(), 3);
868
869        // No match
870        let schemas = registry.schemas_for_tools(&["nonexistent".into()]);
871        assert!(schemas.is_empty());
872
873        // Multiple patterns
874        let schemas = registry.schemas_for_tools(&["echo".into(), "add".into()]);
875        assert_eq!(schemas.len(), 2);
876    }
877
878    #[test]
879    fn tool_error_display() {
880        let err = ToolError::NotFound("web_search".into());
881        assert_eq!(err.to_string(), "tool not found: web_search");
882
883        let err = ToolError::InvalidArgs("missing required field".into());
884        assert_eq!(err.to_string(), "invalid arguments: missing required field");
885
886        let err = ToolError::ExecutionFailed("command failed".into());
887        assert_eq!(err.to_string(), "execution failed: command failed");
888
889        let err = ToolError::PermissionDenied {
890            tool: "test".into(),
891            reason: "no exec access".into(),
892        };
893        assert_eq!(
894            err.to_string(),
895            "permission denied for tool 'test': no exec access"
896        );
897
898        let err = ToolError::FileNotFound("/tmp/missing.txt".into());
899        assert_eq!(err.to_string(), "not found: /tmp/missing.txt");
900
901        let err = ToolError::InvalidPath("../../../etc/passwd".into());
902        assert_eq!(err.to_string(), "invalid path: ../../../etc/passwd");
903
904        let err = ToolError::Timeout(30);
905        assert_eq!(err.to_string(), "timeout after 30s");
906    }
907
908    #[test]
909    fn tool_trait_is_object_safe() {
910        // Verify Tool can be used as a trait object.
911        fn accepts_tool(_t: &dyn Tool) {}
912        let tool = EchoTool;
913        accepts_tool(&tool);
914    }
915
916    #[test]
917    fn tool_parameters_returns_valid_json_schema() {
918        let tool = EchoTool;
919        let params = tool.parameters();
920        assert_eq!(params["type"], "object");
921        assert!(params["properties"].is_object());
922        assert!(params["required"].is_array());
923    }
924
925    // ── Glob matching unit tests ──────────────────────────────────────
926
927    #[test]
928    fn test_glob_star_matches() {
929        assert!(glob_matches("file_*", "file_read"));
930        assert!(glob_matches("file_*", "file_write"));
931        assert!(!glob_matches("file_*", "web_search"));
932        assert!(glob_matches("*", "anything"));
933        assert!(glob_matches("*", ""));
934        assert!(glob_matches("myserver__*", "myserver__search"));
935        assert!(!glob_matches("myserver__*", "otherserver__search"));
936    }
937
938    #[test]
939    fn test_glob_question_mark() {
940        assert!(glob_matches("read_?", "read_a"));
941        assert!(!glob_matches("read_?", "read_file"));
942        assert!(glob_matches("?", "a"));
943        assert!(!glob_matches("?", ""));
944    }
945
946    #[test]
947    fn test_glob_exact_match_fast_path() {
948        // matches_any_pattern uses exact match when no wildcards.
949        assert!(matches_any_pattern("read_file", &["read_file".into()]));
950        assert!(!matches_any_pattern("write_file", &["read_file".into()]));
951    }
952
953    // ── Permission check unit tests ───────────────────────────────────
954
955    #[test]
956    fn test_wildcard_allows_all_tools() {
957        let perms = admin_permissions();
958        assert!(check_tool_permission("exec_shell", &perms, None).is_ok());
959        assert!(check_tool_permission("read_file", &perms, None).is_ok());
960        assert!(check_tool_permission("myserver__tool", &perms, None).is_ok());
961    }
962
963    #[test]
964    fn test_empty_allowlist_denies_all_tools() {
965        let perms = zero_trust_permissions();
966        let result = check_tool_permission("read_file", &perms, None);
967        assert!(result.is_err());
968        match result.unwrap_err() {
969            ToolError::PermissionDenied { tool, reason } => {
970                assert_eq!(tool, "read_file");
971                assert!(reason.contains("not in the allowed tools"));
972            }
973            other => panic!("expected PermissionDenied, got: {other}"),
974        }
975    }
976
977    #[test]
978    fn test_explicit_allowlist_permits_listed_tool() {
979        let perms = user_permissions(vec!["read_file", "write_file"]);
980        assert!(check_tool_permission("read_file", &perms, None).is_ok());
981        assert!(check_tool_permission("write_file", &perms, None).is_ok());
982    }
983
984    #[test]
985    fn test_explicit_allowlist_denies_unlisted_tool() {
986        let perms = user_permissions(vec!["read_file", "write_file"]);
987        assert!(check_tool_permission("exec_shell", &perms, None).is_err());
988    }
989
990    #[test]
991    fn test_denylist_overrides_wildcard() {
992        let perms = UserPermissions {
993            level: 2,
994            tool_access: vec!["*".into()],
995            tool_denylist: vec!["exec_shell".into()],
996            ..UserPermissions::default()
997        };
998        assert!(check_tool_permission("exec_shell", &perms, None).is_err());
999        assert!(check_tool_permission("read_file", &perms, None).is_ok());
1000    }
1001
1002    #[test]
1003    fn test_denylist_overrides_explicit_allow() {
1004        let perms = UserPermissions {
1005            level: 1,
1006            tool_access: vec!["exec_shell".into()],
1007            tool_denylist: vec!["exec_shell".into()],
1008            ..UserPermissions::default()
1009        };
1010        assert!(check_tool_permission("exec_shell", &perms, None).is_err());
1011    }
1012
1013    #[test]
1014    fn test_glob_allowlist_pattern() {
1015        let perms = UserPermissions {
1016            level: 1,
1017            tool_access: vec!["file_*".into()],
1018            ..UserPermissions::default()
1019        };
1020        assert!(check_tool_permission("file_read", &perms, None).is_ok());
1021        assert!(check_tool_permission("file_write", &perms, None).is_ok());
1022        assert!(check_tool_permission("web_search", &perms, None).is_err());
1023    }
1024
1025    #[test]
1026    fn test_glob_denylist_pattern() {
1027        let perms = UserPermissions {
1028            level: 2,
1029            tool_access: vec!["*".into()],
1030            tool_denylist: vec!["exec_*".into()],
1031            ..UserPermissions::default()
1032        };
1033        assert!(check_tool_permission("exec_shell", &perms, None).is_err());
1034        assert!(check_tool_permission("exec_spawn", &perms, None).is_err());
1035        assert!(check_tool_permission("read_file", &perms, None).is_ok());
1036    }
1037
1038    #[test]
1039    fn test_metadata_required_level_blocks_low_user() {
1040        let perms = UserPermissions {
1041            level: 1,
1042            tool_access: vec!["*".into()],
1043            ..UserPermissions::default()
1044        };
1045        let meta = ToolMetadata {
1046            required_permission_level: Some(2),
1047            ..ToolMetadata::default()
1048        };
1049        let result = check_tool_permission("admin_tool", &perms, Some(&meta));
1050        assert!(result.is_err());
1051        match result.unwrap_err() {
1052            ToolError::PermissionDenied { reason, .. } => {
1053                assert!(reason.contains("requires permission level 2"));
1054                assert!(reason.contains("user has level 1"));
1055            }
1056            other => panic!("expected PermissionDenied, got: {other}"),
1057        }
1058    }
1059
1060    #[test]
1061    fn test_metadata_required_level_allows_sufficient_user() {
1062        let perms = UserPermissions {
1063            level: 2,
1064            tool_access: vec!["*".into()],
1065            ..UserPermissions::default()
1066        };
1067        let meta = ToolMetadata {
1068            required_permission_level: Some(2),
1069            ..ToolMetadata::default()
1070        };
1071        assert!(check_tool_permission("admin_tool", &perms, Some(&meta)).is_ok());
1072    }
1073
1074    #[test]
1075    fn test_metadata_custom_permission_missing() {
1076        let perms = admin_permissions();
1077        let meta = ToolMetadata {
1078            required_custom_permissions: HashMap::from([(
1079                "exec_enabled".into(),
1080                serde_json::json!(true),
1081            )]),
1082            ..ToolMetadata::default()
1083        };
1084        let result = check_tool_permission("custom_tool", &perms, Some(&meta));
1085        assert!(result.is_err());
1086        match result.unwrap_err() {
1087            ToolError::PermissionDenied { reason, .. } => {
1088                assert!(reason.contains("exec_enabled"));
1089                assert!(reason.contains("not set"));
1090            }
1091            other => panic!("expected PermissionDenied, got: {other}"),
1092        }
1093    }
1094
1095    #[test]
1096    fn test_metadata_custom_permission_wrong_value() {
1097        let perms = UserPermissions {
1098            level: 2,
1099            tool_access: vec!["*".into()],
1100            custom_permissions: HashMap::from([(
1101                "exec_enabled".into(),
1102                serde_json::json!(false),
1103            )]),
1104            ..UserPermissions::default()
1105        };
1106        let meta = ToolMetadata {
1107            required_custom_permissions: HashMap::from([(
1108                "exec_enabled".into(),
1109                serde_json::json!(true),
1110            )]),
1111            ..ToolMetadata::default()
1112        };
1113        let result = check_tool_permission("custom_tool", &perms, Some(&meta));
1114        assert!(result.is_err());
1115    }
1116
1117    #[test]
1118    fn test_mcp_namespaced_tool_exact_match() {
1119        let perms = UserPermissions {
1120            level: 1,
1121            tool_access: vec!["myserver__search".into()],
1122            ..UserPermissions::default()
1123        };
1124        assert!(check_tool_permission("myserver__search", &perms, None).is_ok());
1125        assert!(check_tool_permission("myserver__exec", &perms, None).is_err());
1126    }
1127
1128    #[test]
1129    fn test_mcp_namespaced_tool_glob_match() {
1130        let perms = UserPermissions {
1131            level: 1,
1132            tool_access: vec!["myserver__*".into()],
1133            ..UserPermissions::default()
1134        };
1135        assert!(check_tool_permission("myserver__search", &perms, None).is_ok());
1136        assert!(check_tool_permission("otherserver__search", &perms, None).is_err());
1137    }
1138
1139    // ── Registry integration tests ────────────────────────────────────
1140
1141    #[tokio::test]
1142    async fn test_registry_execute_with_none_permissions() {
1143        let mut registry = ToolRegistry::new();
1144        registry.register(Arc::new(EchoTool));
1145
1146        // None permissions = backward compat, all tools allowed.
1147        let result = registry
1148            .execute("echo", serde_json::json!({ "text": "hello" }), None)
1149            .await;
1150        assert!(result.is_ok());
1151    }
1152
1153    #[tokio::test]
1154    async fn test_registry_execute_with_denied_permissions() {
1155        let mut registry = ToolRegistry::new();
1156        registry.register(Arc::new(EchoTool));
1157
1158        let perms = zero_trust_permissions();
1159        let result = registry
1160            .execute(
1161                "echo",
1162                serde_json::json!({ "text": "hello" }),
1163                Some(&perms),
1164            )
1165            .await;
1166        assert!(result.is_err());
1167        assert!(matches!(
1168            result.unwrap_err(),
1169            ToolError::PermissionDenied { .. }
1170        ));
1171    }
1172
1173    #[tokio::test]
1174    async fn test_registry_execute_not_found_before_permission_check() {
1175        let registry = ToolRegistry::new();
1176        let perms = zero_trust_permissions();
1177
1178        // NotFound should fire before PermissionDenied.
1179        let result = registry
1180            .execute("nonexistent", serde_json::json!({}), Some(&perms))
1181            .await;
1182        assert!(matches!(result.unwrap_err(), ToolError::NotFound(_)));
1183    }
1184
1185    // ── Security-critical tests ───────────────────────────────────────
1186
1187    #[test]
1188    fn test_zero_trust_blocked_from_exec_and_spawn() {
1189        let perms = zero_trust_permissions();
1190        assert!(check_tool_permission("exec_shell", &perms, None).is_err());
1191        assert!(check_tool_permission("spawn", &perms, None).is_err());
1192    }
1193
1194    #[test]
1195    fn test_user_level_blocked_from_exec_and_spawn() {
1196        let perms = user_permissions(vec![
1197            "read_file",
1198            "write_file",
1199            "edit_file",
1200            "list_dir",
1201            "web_search",
1202            "web_fetch",
1203            "message",
1204        ]);
1205        assert!(check_tool_permission("exec_shell", &perms, None).is_err());
1206        assert!(check_tool_permission("spawn", &perms, None).is_err());
1207    }
1208
1209    #[test]
1210    fn test_admin_can_call_all_tools() {
1211        let perms = admin_permissions();
1212        assert!(check_tool_permission("exec_shell", &perms, None).is_ok());
1213        assert!(check_tool_permission("spawn", &perms, None).is_ok());
1214        assert!(check_tool_permission("read_file", &perms, None).is_ok());
1215        assert!(check_tool_permission("myserver__tool", &perms, None).is_ok());
1216    }
1217
1218    #[test]
1219    fn test_permission_denied_error_includes_tool_name() {
1220        let perms = zero_trust_permissions();
1221        let result = check_tool_permission("exec_shell", &perms, None);
1222        match result.unwrap_err() {
1223            ToolError::PermissionDenied { tool, reason } => {
1224                assert_eq!(tool, "exec_shell");
1225                assert!(!reason.is_empty());
1226            }
1227            other => panic!("expected PermissionDenied, got: {other}"),
1228        }
1229    }
1230
1231    #[test]
1232    fn test_permission_denied_error_display_format() {
1233        let err = ToolError::PermissionDenied {
1234            tool: "exec_shell".into(),
1235            reason: "tool is explicitly denied for this user".into(),
1236        };
1237        assert_eq!(
1238            err.to_string(),
1239            "permission denied for tool 'exec_shell': tool is explicitly denied for this user"
1240        );
1241    }
1242
1243    // ── MCP metadata extraction ───────────────────────────────────────
1244
1245    #[test]
1246    fn test_extract_mcp_metadata() {
1247        let decl = serde_json::json!({
1248            "required_permission_level": 2,
1249            "required_custom_permissions": {
1250                "exec_enabled": true
1251            }
1252        });
1253        let meta = extract_mcp_metadata(&decl);
1254        assert_eq!(meta.required_permission_level, Some(2));
1255        assert_eq!(
1256            meta.required_custom_permissions.get("exec_enabled"),
1257            Some(&serde_json::json!(true))
1258        );
1259    }
1260
1261    #[test]
1262    fn test_extract_mcp_metadata_empty() {
1263        let decl = serde_json::json!({});
1264        let meta = extract_mcp_metadata(&decl);
1265        assert!(meta.required_permission_level.is_none());
1266        assert!(meta.required_custom_permissions.is_empty());
1267    }
1268
1269    // ── Register with metadata ────────────────────────────────────────
1270
1271    #[test]
1272    fn test_register_with_metadata_stores_metadata() {
1273        let mut registry = ToolRegistry::new();
1274        let meta = ToolMetadata {
1275            required_permission_level: Some(2),
1276            ..ToolMetadata::default()
1277        };
1278        registry.register_with_metadata(Arc::new(EchoTool), meta);
1279
1280        let stored = registry.get_metadata("echo").unwrap();
1281        assert_eq!(stored.required_permission_level, Some(2));
1282    }
1283
1284    #[tokio::test]
1285    async fn test_registry_execute_with_admin_and_metadata() {
1286        let mut registry = ToolRegistry::new();
1287        let meta = ToolMetadata {
1288            required_permission_level: Some(2),
1289            ..ToolMetadata::default()
1290        };
1291        registry.register_with_metadata(Arc::new(EchoTool), meta);
1292
1293        let perms = admin_permissions();
1294        let result = registry
1295            .execute(
1296                "echo",
1297                serde_json::json!({ "text": "hello" }),
1298                Some(&perms),
1299            )
1300            .await;
1301        assert!(result.is_ok());
1302    }
1303
1304    // ── filtered_tools tests ───────────────────────────────────────
1305
1306    #[test]
1307    fn filtered_tools_allow_empty_returns_all() {
1308        let mut registry = ToolRegistry::new();
1309        registry.register(Arc::new(EchoTool));
1310        registry.register(Arc::new(AddTool));
1311        registry.register(Arc::new(FailTool));
1312
1313        let filtered = registry.filtered_tools(&[], &[]);
1314        assert_eq!(filtered.len(), 3);
1315    }
1316
1317    #[test]
1318    fn filtered_tools_allow_restricts() {
1319        let mut registry = ToolRegistry::new();
1320        registry.register(Arc::new(EchoTool));
1321        registry.register(Arc::new(AddTool));
1322        registry.register(Arc::new(FailTool));
1323
1324        let filtered = registry.filtered_tools(&["echo".into(), "add".into()], &[]);
1325        assert_eq!(filtered.len(), 2);
1326        assert!(filtered.has("echo"));
1327        assert!(filtered.has("add"));
1328        assert!(!filtered.has("fail"));
1329    }
1330
1331    #[test]
1332    fn filtered_tools_deny_overrides_allow() {
1333        let mut registry = ToolRegistry::new();
1334        registry.register(Arc::new(EchoTool));
1335        registry.register(Arc::new(AddTool));
1336
1337        let filtered = registry.filtered_tools(
1338            &["echo".into(), "add".into()],
1339            &["echo".into()],
1340        );
1341        assert_eq!(filtered.len(), 1);
1342        assert!(filtered.has("add"));
1343        assert!(!filtered.has("echo"));
1344    }
1345
1346    #[test]
1347    fn filtered_tools_deny_only() {
1348        let mut registry = ToolRegistry::new();
1349        registry.register(Arc::new(EchoTool));
1350        registry.register(Arc::new(AddTool));
1351        registry.register(Arc::new(FailTool));
1352
1353        let filtered = registry.filtered_tools(&[], &["fail".into()]);
1354        assert_eq!(filtered.len(), 2);
1355        assert!(!filtered.has("fail"));
1356    }
1357
1358    #[test]
1359    fn filtered_tools_preserves_metadata() {
1360        let mut registry = ToolRegistry::new();
1361        let meta = ToolMetadata {
1362            required_permission_level: Some(2),
1363            ..ToolMetadata::default()
1364        };
1365        registry.register_with_metadata(Arc::new(EchoTool), meta);
1366        registry.register(Arc::new(AddTool));
1367
1368        let filtered = registry.filtered_tools(&["echo".into()], &[]);
1369        assert!(filtered.get_metadata("echo").is_some());
1370        assert_eq!(
1371            filtered.get_metadata("echo").unwrap().required_permission_level,
1372            Some(2)
1373        );
1374    }
1375
1376    #[tokio::test]
1377    async fn test_registry_execute_with_low_level_and_metadata() {
1378        let mut registry = ToolRegistry::new();
1379        let meta = ToolMetadata {
1380            required_permission_level: Some(2),
1381            ..ToolMetadata::default()
1382        };
1383        registry.register_with_metadata(Arc::new(EchoTool), meta);
1384
1385        let perms = UserPermissions {
1386            level: 1,
1387            tool_access: vec!["*".into()],
1388            ..UserPermissions::default()
1389        };
1390        let result = registry
1391            .execute(
1392                "echo",
1393                serde_json::json!({ "text": "hello" }),
1394                Some(&perms),
1395            )
1396            .await;
1397        assert!(matches!(
1398            result.unwrap_err(),
1399            ToolError::PermissionDenied { .. }
1400        ));
1401    }
1402}