Skip to main content

machi_tools/
registry.rs

1//! Tool registry with capability filtering.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use crate::error::{ToolError, codes};
7use crate::metadata::CapabilityFlag;
8use crate::tool::{DynTool, SharedTool, ToolDefinition};
9
10/// How nested/session capability mode filters tools.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "snake_case")]
13#[non_exhaustive]
14pub enum CapabilityMode {
15    /// All registered tools.
16    #[default]
17    Full,
18    /// Only tools admissible under read-only metadata rules.
19    ReadOnly,
20    /// Only tools that do not include execute/spawn (plan-friendly).
21    Plan,
22}
23
24impl CapabilityMode {
25    /// Parse common string forms (`full`, `read_only`, `plan`).
26    #[must_use]
27    pub fn parse(s: &str) -> Option<Self> {
28        match s.trim().to_ascii_lowercase().as_str() {
29            "full" => Some(Self::Full),
30            "read_only" | "read-only" | "readonly" => Some(Self::ReadOnly),
31            "plan" => Some(Self::Plan),
32            _ => None,
33        }
34    }
35
36    /// More restrictive of two modes (for definition ∩ request).
37    #[must_use]
38    pub const fn intersect(self, other: Self) -> Self {
39        use CapabilityMode::{Full, Plan, ReadOnly};
40        match (self, other) {
41            (ReadOnly, _) | (_, ReadOnly) => ReadOnly,
42            (Plan, _) | (_, Plan) => Plan,
43            (Full, Full) => Full,
44        }
45    }
46}
47
48/// Thread-safe tool registry.
49#[derive(Clone, Default)]
50pub struct ToolRegistry {
51    tools: Arc<HashMap<String, SharedTool>>,
52}
53
54impl std::fmt::Debug for ToolRegistry {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        let mut names: Vec<_> = self.tools.keys().map(String::as_str).collect();
57        names.sort_unstable();
58        f.debug_struct("ToolRegistry")
59            .field("tools", &names)
60            .finish()
61    }
62}
63
64impl ToolRegistry {
65    /// Empty registry.
66    #[must_use]
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Build from a list of tools (last wins on name collision).
72    #[must_use]
73    pub fn from_tools(tools: Vec<SharedTool>) -> Self {
74        let mut map = HashMap::new();
75        for tool in tools {
76            map.insert(tool.name().to_owned(), tool);
77        }
78        Self {
79            tools: Arc::new(map),
80        }
81    }
82
83    /// Lookup by name.
84    #[must_use]
85    pub fn get(&self, name: &str) -> Option<SharedTool> {
86        self.tools.get(name).cloned()
87    }
88
89    /// Require tool or error.
90    ///
91    /// # Errors
92    ///
93    /// Returns [`ToolError`] when the tool is missing.
94    pub fn require(&self, name: &str) -> Result<SharedTool, ToolError> {
95        self.get(name).ok_or_else(|| codes::not_found(name))
96    }
97
98    /// Definitions visible under a capability mode.
99    #[must_use]
100    pub fn definitions(&self, mode: CapabilityMode) -> Vec<ToolDefinition> {
101        let mut defs: Vec<_> = self
102            .tools
103            .values()
104            .filter(|t| self.allows(t.as_ref(), mode))
105            .map(|t| t.definition())
106            .collect();
107        defs.sort_by(|a, b| a.name.cmp(&b.name));
108        defs
109    }
110
111    /// Whether a tool is allowed under mode.
112    #[must_use]
113    pub fn allows(&self, tool: &dyn DynTool, mode: CapabilityMode) -> bool {
114        let _ = self;
115        let meta = tool.metadata();
116        match mode {
117            CapabilityMode::Full => true,
118            CapabilityMode::ReadOnly => meta.allowed_in_read_only(),
119            CapabilityMode::Plan => !meta
120                .capabilities
121                .iter()
122                .any(|c| matches!(c, CapabilityFlag::Execute | CapabilityFlag::Spawn)),
123        }
124    }
125
126    /// Number of tools.
127    #[must_use]
128    pub fn len(&self) -> usize {
129        self.tools.len()
130    }
131
132    /// True when empty.
133    #[must_use]
134    pub fn is_empty(&self) -> bool {
135        self.tools.is_empty()
136    }
137
138    /// Merge another registry; `other` wins on name collision.
139    #[must_use]
140    pub fn merge(&self, other: &Self) -> Self {
141        let mut map = (*self.tools).clone();
142        for (k, v) in other.tools.iter() {
143            map.insert(k.clone(), Arc::clone(v));
144        }
145        Self {
146            tools: Arc::new(map),
147        }
148    }
149
150    /// Tool names (sorted).
151    #[must_use]
152    pub fn names(&self) -> Vec<String> {
153        let mut n: Vec<_> = self.tools.keys().cloned().collect();
154        n.sort_unstable();
155        n
156    }
157}