Skip to main content

codineer_tools/
registry.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use api::ToolDefinition;
4use plugins::PluginTool;
5use runtime::PermissionMode;
6use serde_json::Value;
7
8use crate::execute_tool;
9use crate::specs::mvp_tool_specs;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ToolManifestEntry {
13    pub name: String,
14    pub source: ToolSource,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ToolSource {
19    Base,
20    Conditional,
21}
22
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub struct ToolRegistry {
25    entries: Vec<ToolManifestEntry>,
26}
27
28impl ToolRegistry {
29    #[must_use]
30    pub fn new(entries: Vec<ToolManifestEntry>) -> Self {
31        Self { entries }
32    }
33
34    #[must_use]
35    pub fn entries(&self) -> &[ToolManifestEntry] {
36        &self.entries
37    }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ToolSpec {
42    pub name: &'static str,
43    pub description: &'static str,
44    pub input_schema: Value,
45    pub required_permission: PermissionMode,
46}
47
48#[derive(Debug, Clone, PartialEq)]
49pub struct GlobalToolRegistry {
50    plugin_tools: Vec<PluginTool>,
51}
52
53impl GlobalToolRegistry {
54    #[must_use]
55    pub fn builtin() -> Self {
56        Self {
57            plugin_tools: Vec::new(),
58        }
59    }
60
61    pub fn with_plugin_tools(plugin_tools: Vec<PluginTool>) -> Result<Self, String> {
62        let builtin_names = mvp_tool_specs()
63            .into_iter()
64            .map(|spec| spec.name.to_string())
65            .collect::<BTreeSet<_>>();
66        let mut seen_plugin_names = BTreeSet::new();
67
68        for tool in &plugin_tools {
69            let name = tool.definition().name.clone();
70            if builtin_names.contains(&name) {
71                return Err(format!(
72                    "plugin tool `{name}` conflicts with a built-in tool name"
73                ));
74            }
75            if !seen_plugin_names.insert(name.clone()) {
76                return Err(format!("duplicate plugin tool name `{name}`"));
77            }
78        }
79
80        Ok(Self { plugin_tools })
81    }
82
83    pub fn normalize_allowed_tools(
84        &self,
85        values: &[String],
86    ) -> Result<Option<BTreeSet<String>>, String> {
87        if values.is_empty() {
88            return Ok(None);
89        }
90
91        let builtin_specs = mvp_tool_specs();
92        let canonical_names = builtin_specs
93            .iter()
94            .map(|spec| spec.name.to_string())
95            .chain(
96                self.plugin_tools
97                    .iter()
98                    .map(|tool| tool.definition().name.clone()),
99            )
100            .collect::<Vec<_>>();
101        let mut name_map = canonical_names
102            .iter()
103            .map(|name| (normalize_tool_name(name), name.clone()))
104            .collect::<BTreeMap<_, _>>();
105
106        for (alias, canonical) in [
107            ("read", "read_file"),
108            ("write", "write_file"),
109            ("edit", "edit_file"),
110            ("glob", "glob_search"),
111            ("grep", "grep_search"),
112        ] {
113            name_map.insert(alias.to_string(), canonical.to_string());
114        }
115
116        let mut allowed = BTreeSet::new();
117        for value in values {
118            for token in value
119                .split(|ch: char| ch == ',' || ch.is_whitespace())
120                .filter(|token| !token.is_empty())
121            {
122                let normalized = normalize_tool_name(token);
123                let canonical = name_map.get(&normalized).ok_or_else(|| {
124                    format!(
125                        "unsupported tool in --allowedTools: {token} (expected one of: {})",
126                        canonical_names.join(", ")
127                    )
128                })?;
129                allowed.insert(canonical.clone());
130            }
131        }
132
133        Ok(Some(allowed))
134    }
135
136    #[must_use]
137    pub fn definitions(&self, allowed_tools: Option<&BTreeSet<String>>) -> Vec<ToolDefinition> {
138        let builtin = mvp_tool_specs()
139            .into_iter()
140            .filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
141            .map(|spec| ToolDefinition {
142                name: spec.name.to_string(),
143                description: Some(spec.description.to_string()),
144                input_schema: spec.input_schema,
145            });
146        let plugin = self
147            .plugin_tools
148            .iter()
149            .filter(|tool| {
150                allowed_tools
151                    .is_none_or(|allowed| allowed.contains(tool.definition().name.as_str()))
152            })
153            .map(|tool| ToolDefinition {
154                name: tool.definition().name.clone(),
155                description: tool.definition().description.clone(),
156                input_schema: tool.definition().input_schema.clone(),
157            });
158        builtin.chain(plugin).collect()
159    }
160
161    #[must_use]
162    pub fn permission_specs(
163        &self,
164        allowed_tools: Option<&BTreeSet<String>>,
165    ) -> Vec<(String, PermissionMode)> {
166        let builtin = mvp_tool_specs()
167            .into_iter()
168            .filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
169            .map(|spec| (spec.name.to_string(), spec.required_permission));
170        let plugin = self
171            .plugin_tools
172            .iter()
173            .filter(|tool| {
174                allowed_tools
175                    .is_none_or(|allowed| allowed.contains(tool.definition().name.as_str()))
176            })
177            .map(|tool| {
178                (
179                    tool.definition().name.clone(),
180                    permission_mode_from_plugin(tool.required_permission()),
181                )
182            });
183        builtin.chain(plugin).collect()
184    }
185
186    pub fn execute(&self, name: &str, input: &Value) -> Result<String, String> {
187        if mvp_tool_specs().iter().any(|spec| spec.name == name) {
188            return execute_tool(name, input);
189        }
190        self.plugin_tools
191            .iter()
192            .find(|tool| tool.definition().name == name)
193            .ok_or_else(|| format!("unsupported tool: {name}"))?
194            .execute(input)
195            .map_err(|error| error.to_string())
196    }
197}
198
199fn normalize_tool_name(value: &str) -> String {
200    value.trim().replace('-', "_").to_ascii_lowercase()
201}
202
203fn permission_mode_from_plugin(value: &str) -> PermissionMode {
204    match value {
205        "danger-full-access" => PermissionMode::DangerFullAccess,
206        "workspace-write" => PermissionMode::WorkspaceWrite,
207        _ => PermissionMode::ReadOnly,
208    }
209}