codineer_tools/
registry.rs1use 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}