claude_agent/tools/
matcher.rs

1//! Tool name matching utilities.
2
3/// Checks if a tool name matches an allowed tool pattern.
4///
5/// Supports patterns like:
6/// - `"Read"` - exact match
7/// - `"Bash(git:*)"` - scoped pattern (matches base tool "Bash")
8pub fn matches_tool_pattern(pattern: &str, tool_name: &str) -> bool {
9    if let Some(base) = pattern.split('(').next() {
10        base == tool_name || pattern == tool_name
11    } else {
12        pattern == tool_name
13    }
14}
15
16/// Checks if a tool is allowed based on a list of allowed patterns.
17///
18/// Returns `true` if:
19/// - The allowed list is empty (no restrictions)
20/// - The tool name matches any pattern in the list
21pub fn is_tool_allowed(allowed: &[String], tool_name: &str) -> bool {
22    if allowed.is_empty() {
23        return true;
24    }
25    allowed.iter().any(|p| matches_tool_pattern(p, tool_name))
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn test_exact_match() {
34        assert!(matches_tool_pattern("Read", "Read"));
35        assert!(!matches_tool_pattern("Read", "Write"));
36    }
37
38    #[test]
39    fn test_scoped_pattern() {
40        assert!(matches_tool_pattern("Bash(git:*)", "Bash"));
41        assert!(!matches_tool_pattern("Bash(git:*)", "Read"));
42    }
43
44    #[test]
45    fn test_is_tool_allowed_empty() {
46        let allowed: Vec<String> = vec![];
47        assert!(is_tool_allowed(&allowed, "Anything"));
48    }
49
50    #[test]
51    fn test_is_tool_allowed_restricted() {
52        let allowed = vec![
53            "Read".to_string(),
54            "Grep".to_string(),
55            "Bash(git:*)".to_string(),
56        ];
57        assert!(is_tool_allowed(&allowed, "Read"));
58        assert!(is_tool_allowed(&allowed, "Grep"));
59        assert!(is_tool_allowed(&allowed, "Bash"));
60        assert!(!is_tool_allowed(&allowed, "Write"));
61    }
62}