1use crate::tools::catalog::{tool_id_for, CapabilityRecord, CatalogError, ToolCatalog};
3use crate::tools::Tool;
4use serde_json::Value;
5use std::collections::{HashMap, HashSet};
6use std::sync::Arc;
7
8#[derive(Clone)]
11pub struct ToolRegistry {
12 tools: HashMap<String, Arc<dyn Tool>>,
13 cached_schema: Arc<Vec<Value>>,
15 api_to_runtime_names: HashMap<String, String>,
17 input_name_maps: HashMap<String, SchemaNameMap>,
19 catalog: ToolCatalog,
23}
24
25#[derive(Clone, Debug, Default)]
26struct SchemaNameMap {
27 api_to_runtime: HashMap<String, String>,
28 children: HashMap<String, SchemaNameMap>,
29 items: Option<Box<SchemaNameMap>>,
31}
32
33impl Default for ToolRegistry {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl ToolRegistry {
40 pub fn new() -> Self {
41 let tools: Vec<Arc<dyn Tool>> = vec![
42 Arc::new(crate::tools::bash::BashTool),
43 Arc::new(crate::tools::read::ReadTool),
44 Arc::new(crate::tools::write::WriteTool),
45 Arc::new(crate::tools::edit::EditTool),
46 Arc::new(crate::tools::grep::GrepTool),
47 Arc::new(crate::tools::find::FindTool),
48 Arc::new(crate::tools::ls::LsTool),
49 Arc::new(crate::tools::subagent::SubagentTool),
50 Arc::new(crate::tools::subagent::authorize_model::SubagentModelAuthorizeTool),
51 Arc::new(crate::tools::subagent::models::SubagentModelsTool),
52 Arc::new(crate::tools::subagent::start::SubagentStartTool),
53 Arc::new(crate::tools::subagent::status::SubagentStatusTool),
54 Arc::new(crate::tools::subagent::steer::SubagentSteerTool),
55 Arc::new(crate::tools::subagent::collect::SubagentCollectTool),
56 Arc::new(crate::tools::subagent::resume::SubagentResumeTool),
57 Arc::new(crate::tools::memory_context::MemoryContextTool),
62 Arc::new(crate::tools::shell::ShellStartTool),
63 Arc::new(crate::tools::shell::ShellSendTool),
64 Arc::new(crate::tools::shell::ShellEndTool),
65 Arc::new(crate::tools::discovery::SearchToolsTool),
70 Arc::new(crate::tools::discovery::ActivateToolsTool),
71 Arc::new(crate::tools::memory::MemorySearchTool),
75 Arc::new(crate::tools::memory::MemoryFetchTool),
76 Arc::new(crate::tools::memory::MemoryStoreTool),
77 Arc::new(crate::tools::memory::MemoryForgetTool),
78 ];
79 Self::from_tools(tools)
80 }
81
82 pub fn empty() -> Self {
85 Self::from_tools(Vec::new())
86 }
87
88 pub fn disable(&mut self, names: &[String]) {
94 self.try_disable(names).expect(CATALOG_REJECTED_MSG);
95 }
96
97 pub fn try_disable(&mut self, names: &[String]) -> Result<(), CatalogError> {
110 if names.is_empty() {
111 return Ok(());
112 }
113 let remaining: Vec<Arc<dyn Tool>> = self
114 .tools
115 .values()
116 .filter(|t| !names.iter().any(|n| n == t.name()))
117 .cloned()
118 .collect();
119 if remaining.len() == self.tools.len() {
120 return Ok(());
121 }
122 let prior = self.catalog.generation();
123 let mut candidate = Self::try_from_tools(remaining)?;
124 candidate.catalog.rebase_past(prior)?;
125 *self = candidate;
126 Ok(())
127 }
128
129 pub fn without_subagent() -> Self {
131 let tools: Vec<Arc<dyn Tool>> = vec![
132 Arc::new(crate::tools::bash::BashTool),
133 Arc::new(crate::tools::read::ReadTool),
134 Arc::new(crate::tools::write::WriteTool),
135 Arc::new(crate::tools::edit::EditTool),
136 Arc::new(crate::tools::grep::GrepTool),
137 Arc::new(crate::tools::find::FindTool),
138 Arc::new(crate::tools::ls::LsTool),
139 Arc::new(crate::tools::shell::ShellStartTool),
140 Arc::new(crate::tools::shell::ShellSendTool),
141 Arc::new(crate::tools::shell::ShellEndTool),
142 ];
143 Self::from_tools(tools)
144 }
145
146 pub fn without_subagent_with_extensions(extension_tools: &ToolRegistry) -> Self {
154 let mut combined = Self::without_subagent();
155 for tool in extension_tools.tools.values() {
156 if tool.extension_id().is_some() && !is_recursive_subagent_tool_name(tool.name()) {
157 combined
158 .insert_tool_with_catalog(tool.clone())
159 .expect(CATALOG_REJECTED_MSG);
160 }
161 }
162 combined.rebuild_schema();
163 combined
164 }
165
166 fn from_tools(tool_list: Vec<Arc<dyn Tool>>) -> Self {
172 Self::try_from_tools(tool_list).expect(CATALOG_REJECTED_MSG)
173 }
174
175 #[doc(hidden)]
179 pub fn from_tools_for_tests(tool_list: Vec<Arc<dyn Tool>>) -> Self {
180 Self::from_tools(tool_list)
181 }
182
183 fn try_from_tools(tool_list: Vec<Arc<dyn Tool>>) -> Result<Self, CatalogError> {
184 let mut registry = ToolRegistry {
185 tools: HashMap::new(),
186 cached_schema: Arc::new(Vec::new()),
187 api_to_runtime_names: HashMap::new(),
188 input_name_maps: HashMap::new(),
189 catalog: ToolCatalog::empty(),
190 };
191 for tool in tool_list {
195 let name = tool.name().to_string();
196 registry.tools.insert(name, tool);
197 }
198 let sorted: Vec<Arc<dyn Tool>> =
204 registry.iter_tools_sorted().into_iter().cloned().collect();
205 for tool in sorted {
206 let record = CapabilityRecord::for_registered_tool(&tool);
207 registry.catalog.upsert(None, record)?;
208 }
209 registry.rebuild_schema();
210 Ok(registry)
211 }
212
213 fn api_safe_name(name: &str, used: &HashSet<String>) -> String {
214 Self::api_safe_identifier(name, used, 128, false)
215 }
216
217 fn api_safe_property_name(name: &str, used: &HashSet<String>) -> String {
218 Self::api_safe_identifier(name, used, 64, true)
219 }
220
221 fn api_safe_identifier(
222 name: &str,
223 used: &HashSet<String>,
224 max_len: usize,
225 allow_dot: bool,
226 ) -> String {
227 let mut sanitized = String::with_capacity(name.len());
228 for ch in name.chars() {
229 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || (allow_dot && ch == '.') {
230 sanitized.push(ch);
231 } else {
232 sanitized.push('_');
233 }
234 }
235 if sanitized.is_empty() {
236 sanitized.push_str("field");
237 }
238 if sanitized.len() > max_len {
239 sanitized.truncate(max_len);
240 }
241
242 let base = sanitized.clone();
243 let mut suffix = 2;
244 while used.contains(&sanitized) {
245 let suffix_str = format!("_{suffix}");
246 let keep = max_len.saturating_sub(suffix_str.len());
247 sanitized = format!("{}{}", &base[..base.len().min(keep)], suffix_str);
248 suffix += 1;
249 }
250 sanitized
251 }
252
253 fn sanitize_schema(mut schema: Value) -> (Value, SchemaNameMap) {
254 let mut map = SchemaNameMap::default();
255 let Some(obj) = schema.as_object_mut() else {
256 return (schema, map);
257 };
258
259 let mut required_name_map = HashMap::new();
260 if let Some(props_value) = obj.get_mut("properties") {
261 if let Some(props) = props_value.as_object_mut() {
262 let original = std::mem::take(props);
263 let mut used = HashSet::new();
264 for (runtime_name, child_schema) in original {
265 let api_name = Self::api_safe_property_name(&runtime_name, &used);
266 used.insert(api_name.clone());
267 required_name_map.insert(runtime_name.clone(), api_name.clone());
268 map.api_to_runtime.insert(api_name.clone(), runtime_name);
269
270 let (sanitized_child, child_map) = Self::sanitize_schema(child_schema);
271 if !child_map.api_to_runtime.is_empty() || !child_map.children.is_empty() {
272 map.children.insert(api_name.clone(), child_map);
273 }
274 props.insert(api_name, sanitized_child);
275 }
276 }
277 }
278
279 if let Some(required) = obj.get_mut("required").and_then(Value::as_array_mut) {
280 for item in required.iter_mut() {
281 if let Some(name) = item.as_str() {
282 if let Some(api_name) = required_name_map.get(name) {
283 *item = Value::String(api_name.clone());
284 }
285 }
286 }
287 }
288
289 if let Some(items) = obj.get_mut("items") {
292 let (sanitized_items, items_map) = Self::sanitize_schema(std::mem::take(items));
293 if !items_map.api_to_runtime.is_empty()
294 || !items_map.children.is_empty()
295 || items_map.items.is_some()
296 {
297 map.items = Some(Box::new(items_map));
298 }
299 *items = sanitized_items;
300 }
301
302 (schema, map)
303 }
304
305 fn translate_input_names(input: Value, map: &SchemaNameMap) -> Value {
306 match input {
307 Value::Object(obj) => {
308 let mut out = serde_json::Map::new();
309 for (api_name, value) in obj {
310 let runtime_name = map
311 .api_to_runtime
312 .get(&api_name)
313 .cloned()
314 .unwrap_or_else(|| api_name.clone());
315 let value = if let Some(child) = map.children.get(&api_name) {
316 Self::translate_input_names(value, child)
317 } else {
318 value
319 };
320 out.insert(runtime_name, value);
321 }
322 Value::Object(out)
323 }
324 Value::Array(arr) => {
325 if let Some(items_map) = &map.items {
327 Value::Array(
328 arr.into_iter()
329 .map(|v| Self::translate_input_names(v, items_map))
330 .collect(),
331 )
332 } else {
333 Value::Array(arr)
334 }
335 }
336 other => other,
337 }
338 }
339
340 fn rebuild_schema(&mut self) {
341 let mut used = HashSet::new();
342 let mut api_to_runtime_names = HashMap::new();
343 let mut input_name_maps = HashMap::new();
344 let mut schema = Vec::with_capacity(self.tools.len());
345
346 let mut sorted_tools: Vec<_> = self.tools.values().collect();
361 sorted_tools.sort_by_key(|t| {
362 let builtin_first = match t.origin() {
363 crate::tools::ToolOrigin::Builtin => 0u8,
364 _ => 1u8,
365 };
366 (builtin_first, t.name().to_string())
367 });
368
369 for tool in sorted_tools {
370 let runtime_name = tool.name();
371 let api_name = Self::api_safe_name(runtime_name, &used);
372 used.insert(api_name.clone());
373 api_to_runtime_names.insert(api_name.clone(), runtime_name.to_string());
374 let (input_schema, input_map) = Self::sanitize_schema(tool.parameters());
375 input_name_maps.insert(api_name.clone(), input_map);
376 schema.push(serde_json::json!({
377 "name": api_name,
378 "description": tool.description(),
379 "input_schema": input_schema
380 }));
381 }
382
383 self.api_to_runtime_names = api_to_runtime_names;
384 self.input_name_maps = input_name_maps;
385 self.cached_schema = Arc::new(schema);
386 }
387
388 pub fn register(&mut self, tool: Arc<dyn Tool>) {
398 self.try_register(tool).expect(CATALOG_REJECTED_MSG);
399 }
400
401 pub fn try_register(&mut self, tool: Arc<dyn Tool>) -> Result<(), CatalogError> {
407 self.insert_tool_with_catalog(tool)?;
408 self.rebuild_schema();
409 Ok(())
410 }
411
412 pub fn try_register_batch(
426 &mut self,
427 tool_list: Vec<Arc<dyn Tool>>,
428 ) -> Result<usize, CatalogError> {
429 if tool_list.is_empty() {
430 return Ok(0);
431 }
432 let mut batch_names: HashSet<String> = HashSet::new();
433 let mut batch_ids: HashSet<crate::tools::catalog::ToolId> = HashSet::new();
434 for tool in &tool_list {
435 let name = tool.name();
436 if self.tools.contains_key(name) || !batch_names.insert(name.to_string()) {
437 return Err(CatalogError::DuplicateRuntimeName(
438 agent_core::BoundedText::new(name, 128).text,
439 ));
440 }
441 let id = tool_id_for(tool.as_ref());
442 if self.catalog.get(&id).is_some() || !batch_ids.insert(id.clone()) {
443 return Err(CatalogError::DuplicateToolId(id));
444 }
445 }
446 let count = tool_list.len();
447 let mut candidate = self.clone();
448 for tool in tool_list {
449 candidate.insert_tool_with_catalog(tool)?;
450 }
451 candidate.rebuild_schema();
452 *self = candidate;
453 Ok(count)
454 }
455
456 fn insert_tool_with_catalog(&mut self, tool: Arc<dyn Tool>) -> Result<(), CatalogError> {
465 let record = CapabilityRecord::for_registered_tool(&tool);
466 let replaced = self
467 .tools
468 .get(tool.name())
469 .map(|old| tool_id_for(old.as_ref()));
470 self.catalog.upsert(replaced.as_ref(), record)?;
471 self.tools.insert(tool.name().to_string(), tool);
472 Ok(())
473 }
474
475 pub fn catalog(&self) -> &ToolCatalog {
478 &self.catalog
479 }
480
481 #[doc(hidden)]
489 pub fn resume_catalog_generation_for_tests(
490 &mut self,
491 generation: crate::tools::catalog::CatalogGeneration,
492 ) {
493 self.catalog.set_generation_for_tests(generation);
494 }
495
496 pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
497 let runtime_name = self
498 .api_to_runtime_names
499 .get(name)
500 .map(String::as_str)
501 .unwrap_or(name);
502 self.tools.get(runtime_name)
503 }
504
505 pub fn runtime_name_for_api<'a>(&'a self, name: &'a str) -> &'a str {
506 self.api_to_runtime_names
507 .get(name)
508 .map(String::as_str)
509 .unwrap_or(name)
510 }
511
512 pub fn resolve_wire_call(
522 &self,
523 wire_name: &str,
524 ) -> Option<crate::tools::activation::ResolvedToolCall> {
525 let runtime_name = self
526 .api_to_runtime_names
527 .get(wire_name)
528 .map(String::as_str)
529 .unwrap_or(wire_name);
530 let tool = self.tools.get(runtime_name)?;
531 Some(crate::tools::activation::ResolvedToolCall::new(
532 wire_name,
533 runtime_name,
534 tool_id_for(tool.as_ref()),
535 ))
536 }
537
538 pub fn translate_input_for_api_tool(&self, tool_name: &str, input: Value) -> Value {
539 if let Some(map) = self.input_name_maps.get(tool_name) {
540 Self::translate_input_names(input, map)
541 } else {
542 input
543 }
544 }
545
546 pub fn tools_schema(&self) -> Arc<Vec<Value>> {
547 Arc::clone(&self.cached_schema)
548 }
549
550 pub fn session_tools_schema(
560 &self,
561 session: &crate::tools::activation::SessionToolSet,
562 ) -> Result<Vec<Value>, SessionSchemaError> {
563 if session.is_stale(&self.catalog) {
564 return Err(SessionSchemaError::StaleSessionSet {
565 set: session.catalog_generation(),
566 catalog: self.catalog.generation(),
567 });
568 }
569 let mut projected = Vec::new();
570 for entry in self.cached_schema.iter() {
571 let Some(api_name) = entry["name"].as_str() else {
572 continue;
573 };
574 let runtime_name = self.runtime_name_for_api(api_name);
575 let Some(tool) = self.tools.get(runtime_name) else {
576 continue;
577 };
578 let id = tool_id_for(tool.as_ref());
579 let pinned = session
580 .core_schema_digest(&id)
581 .or_else(|| session.activation(&id).map(|a| a.schema_digest()));
582 let Some(pinned) = pinned else {
583 continue; };
585 let record = self
586 .catalog
587 .get(&id)
588 .ok_or_else(|| SessionSchemaError::NotCataloged(id.clone()))?;
589 if pinned != record.schema_digest() {
590 return Err(SessionSchemaError::SchemaDigestMismatch(id));
591 }
592 projected.push(entry.clone());
593 }
594 Ok(projected)
595 }
596
597 pub fn tool_names_for_extension(&self, extension_id: &str) -> Vec<String> {
600 let mut names: Vec<String> = self
601 .tools
602 .values()
603 .filter(|t| t.extension_id() == Some(extension_id))
604 .map(|t| t.name().to_string())
605 .collect();
606 names.sort();
607 names
608 }
609
610 pub fn iter_tools_sorted(&self) -> Vec<&Arc<dyn Tool>> {
613 let mut tools: Vec<_> = self.tools.values().collect();
614 tools.sort_by_key(|t| t.name());
615 tools
616 }
617}
618
619#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
622pub enum SessionSchemaError {
623 #[error(
624 "session tool set generation {} is stale against catalog generation {}",
625 set.value(),
626 catalog.value()
627 )]
628 StaleSessionSet {
629 set: crate::tools::catalog::CatalogGeneration,
630 catalog: crate::tools::catalog::CatalogGeneration,
631 },
632 #[error("session member is not cataloged: {0}")]
633 NotCataloged(crate::tools::catalog::ToolId),
634 #[error("schema digest changed since the session pinned it: {0}")]
635 SchemaDigestMismatch(crate::tools::catalog::ToolId),
636}
637
638const CATALOG_REJECTED_MSG: &str =
644 "tool catalog rejected mutation: refusing to expose a tool the catalog could not record";
645
646fn is_recursive_subagent_tool_name(name: &str) -> bool {
647 let api_name = ToolRegistry::api_safe_name(name, &HashSet::new());
648 matches!(
649 api_name.as_str(),
650 "subagent"
651 | "subagent_start"
652 | "subagent_status"
653 | "subagent_steer"
654 | "subagent_collect"
655 | "subagent_resume"
656 | "subagent_model_authorize"
657 | "subagent_models"
658 | "memory_context"
662 )
663}
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use crate::{Result, ToolContext};
668 use serde_json::json;
669
670 struct NamedTool(&'static str);
671
672 #[async_trait::async_trait]
673 impl Tool for NamedTool {
674 fn name(&self) -> &str {
675 self.0
676 }
677 fn description(&self) -> &str {
678 "test tool"
679 }
680 fn parameters(&self) -> Value {
681 json!({"type": "object"})
682 }
683 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
684 Ok("ok".to_string())
685 }
686 }
687
688 struct SchemaTool;
689
690 #[async_trait::async_trait]
691 impl Tool for SchemaTool {
692 fn name(&self) -> &str {
693 "schema_tool"
694 }
695 fn description(&self) -> &str {
696 "schema tool"
697 }
698 fn parameters(&self) -> Value {
699 json!({
700 "type": "object",
701 "properties": {
702 "bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going": {"type": "string"},
703 "nested:obj": {
704 "type": "object",
705 "properties": {"inner/key": {"type": "string"}},
706 "required": ["inner/key"]
707 }
708 },
709 "required": [
710 "bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going",
711 "nested:obj"
712 ]
713 })
714 }
715 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
716 Ok("ok".to_string())
717 }
718 }
719
720 #[test]
721 fn tool_schema_uses_api_safe_names_and_maps_back() {
722 let registry = ToolRegistry::from_tools(vec![Arc::new(NamedTool("plugin:skill.tool"))]);
723
724 assert_eq!(registry.tools_schema()[0]["name"], "plugin_skill_tool");
725 assert!(registry.get("plugin:skill.tool").is_some());
726 assert!(registry.get("plugin_skill_tool").is_some());
727 assert_eq!(
728 registry.runtime_name_for_api("plugin_skill_tool"),
729 "plugin:skill.tool"
730 );
731 }
732
733 #[test]
743 fn extension_leaf_name_never_shadows_builtin_memory_fetch_api_name() {
744 struct ExtMemoryTool {
745 runtime: &'static str,
746 ext: &'static str,
747 }
748 #[async_trait::async_trait]
749 impl Tool for ExtMemoryTool {
750 fn origin(&self) -> crate::tools::ToolOrigin {
751 crate::tools::ToolOrigin::Extension {
752 extension_id: self.ext.to_string(),
753 }
754 }
755 fn extension_id(&self) -> Option<&str> {
756 Some(self.ext)
757 }
758 fn name(&self) -> &str {
759 self.runtime
760 }
761 fn description(&self) -> &str {
762 "extension memory tool (single-id schema)"
763 }
764 fn parameters(&self) -> Value {
765 json!({
766 "type": "object",
767 "properties": {"id": {"type": "string"}},
768 "required": ["id"]
769 })
770 }
771 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
772 Ok("ok".to_string())
773 }
774 }
775
776 let registry = ToolRegistry::from_tools(vec![
777 Arc::new(crate::tools::memory::MemoryFetchTool),
778 Arc::new(ExtMemoryTool {
780 runtime: "memory:fetch",
781 ext: "memory",
782 }),
783 Arc::new(ExtMemoryTool {
785 runtime: "axel-memory-manager:memory_fetch",
786 ext: "axel-memory-manager",
787 }),
788 ]);
789
790 assert_eq!(
792 registry.runtime_name_for_api("memory_fetch"),
793 "memory_fetch",
794 "wire name `memory_fetch` must resolve to the built-in"
795 );
796 let schema = registry.tools_schema();
797 let builtin = schema
798 .iter()
799 .find(|e| e["name"] == "memory_fetch")
800 .expect("built-in memory_fetch exposed");
801 assert_eq!(
802 builtin["input_schema"]["required"],
803 json!(["ids"]),
804 "built-in memory_fetch schema must remain {{ids: [...]}}"
805 );
806
807 let axel = schema
809 .iter()
810 .find(|e| e["name"] == "axel-memory-manager_memory_fetch")
811 .expect("extension tool exposed under namespaced API name");
812 assert_eq!(axel["input_schema"]["required"], json!(["id"]));
813 assert_eq!(
816 registry.runtime_name_for_api("memory_fetch_2"),
817 "memory:fetch"
818 );
819 let resolved = registry.resolve_wire_call("memory_fetch").unwrap();
822 assert_eq!(resolved.runtime_name(), "memory_fetch");
823 }
824
825 #[test]
826 fn tool_schema_disambiguates_sanitized_name_collisions() {
827 let registry =
828 ToolRegistry::from_tools(vec![Arc::new(NamedTool("a:b")), Arc::new(NamedTool("a.b"))]);
829 let names: HashSet<String> = registry
830 .tools_schema()
831 .iter()
832 .filter_map(|s| s["name"].as_str().map(str::to_string))
833 .collect();
834
835 assert_eq!(names.len(), 2);
836 assert!(names.contains("a_b"));
837 assert!(names.contains("a_b_2"));
838 assert!(registry.get("a_b").is_some());
839 assert!(registry.get("a_b_2").is_some());
840 }
841
842 #[test]
843 fn tool_schema_truncates_long_names_to_anthropic_limit() {
844 let long = "x".repeat(140);
845 let leaked: &'static str = Box::leak(long.into_boxed_str());
846 let registry = ToolRegistry::from_tools(vec![Arc::new(NamedTool(leaked))]);
847 let schema = registry.tools_schema();
848 let name = schema[0]["name"].as_str().unwrap();
849
850 assert_eq!(name.len(), 128);
851 assert!(name
852 .chars()
853 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'));
854 assert!(registry.get(name).is_some());
855 }
856
857 #[test]
858 fn tool_schema_sanitizes_input_property_names_and_translates_inputs_back() {
859 let registry = ToolRegistry::from_tools(vec![Arc::new(SchemaTool)]);
860 let schema = registry.tools_schema();
861 let input_schema = &schema[0]["input_schema"];
862 let props = input_schema["properties"].as_object().unwrap();
863
864 assert!(props.keys().all(|k| k.len() <= 64
865 && k.chars()
866 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')));
867 assert_eq!(
868 input_schema["required"].as_array().unwrap()[0]
869 .as_str()
870 .unwrap()
871 .len(),
872 64
873 );
874 assert_eq!(input_schema["required"][1], "nested_obj");
875 assert!(props["nested_obj"]["properties"]
876 .as_object()
877 .unwrap()
878 .contains_key("inner_key"));
879 assert_eq!(props["nested_obj"]["required"][0], "inner_key");
880
881 let first_required = input_schema["required"][0].as_str().unwrap();
882 let translated = registry.translate_input_for_api_tool(
883 "schema_tool",
884 json!({
885 first_required: "value",
886 "nested_obj": {"inner_key": "nested"}
887 }),
888 );
889
890 assert_eq!(
891 translated["bad:key/that/is/far/too/long/for/anthropic/property/names/and/keeps/going"],
892 "value"
893 );
894 assert_eq!(translated["nested:obj"]["inner/key"], "nested");
895 }
896
897 #[test]
898 fn test_tool_registry_new() {
899 let registry = ToolRegistry::new();
900
901 assert_eq!(registry.tools_schema().len(), 25);
907
908 assert!(registry.get("bash").is_some());
910
911 assert!(registry.get("nonexistent").is_none());
913
914 assert!(registry.get("bash").is_some());
916 assert!(registry.get("read").is_some());
917 assert!(registry.get("write").is_some());
918 assert!(registry.get("edit").is_some());
919 assert!(registry.get("grep").is_some());
920 assert!(registry.get("find").is_some());
921 assert!(registry.get("ls").is_some());
922 assert!(registry.get("subagent").is_some());
923 assert!(registry.get("subagent_model_authorize").is_some());
924 assert!(registry.get("subagent_models").is_some());
925 }
926
927 #[test]
928 fn test_disable_removes_tools_from_registry_and_schema() {
929 let mut registry = ToolRegistry::new();
930 let before = registry.tools_schema().len();
931
932 registry.disable(&["bash".to_string(), "ls".to_string()]);
933
934 assert!(registry.get("bash").is_none());
936 assert!(registry.get("ls").is_none());
937 assert!(registry.get("read").is_some());
939 assert!(registry.get("subagent").is_some());
940 assert_eq!(registry.tools_schema().len(), before - 2);
942 }
943
944 #[test]
945 fn test_disable_empty_and_unknown_are_noops() {
946 let mut registry = ToolRegistry::new();
947 let n = registry.tools_schema().len();
948 registry.disable(&[]); assert_eq!(registry.tools_schema().len(), n);
950 registry.disable(&["not_a_real_tool".to_string()]); assert_eq!(registry.tools_schema().len(), n);
952 assert!(registry.get("bash").is_some());
953 }
954
955 #[test]
956 fn test_tool_registry_without_subagent() {
957 let registry = ToolRegistry::without_subagent();
958
959 assert_eq!(registry.tools_schema().len(), 10);
961 assert!(registry.get("subagent").is_none());
963
964 assert!(registry.get("bash").is_some());
966
967 assert!(registry.get("bash").is_some());
969 assert!(registry.get("read").is_some());
970 assert!(registry.get("write").is_some());
971 assert!(registry.get("edit").is_some());
972 assert!(registry.get("grep").is_some());
973 assert!(registry.get("find").is_some());
974 assert!(registry.get("ls").is_some());
975 }
976
977 #[test]
978 fn test_tool_registry_register() {
979 let mut registry = ToolRegistry::without_subagent();
980 let initial_count = registry.tools_schema().len();
981
982 struct TestTool;
984 #[async_trait::async_trait]
985 impl Tool for TestTool {
986 fn name(&self) -> &str {
987 "test_tool"
988 }
989 fn description(&self) -> &str {
990 "A test tool"
991 }
992 fn parameters(&self) -> Value {
993 json!({"type": "object"})
994 }
995 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
996 Ok("test result".to_string())
997 }
998 }
999
1000 registry.register(Arc::new(TestTool));
1001
1002 assert_eq!(registry.tools_schema().len(), initial_count + 1);
1004
1005 assert!(registry.get("test_tool").is_some());
1007 }
1008
1009 #[test]
1010 fn tool_names_for_extension_filters_by_owner_and_sorts() {
1011 struct OwnedTool(&'static str, Option<&'static str>);
1012 #[async_trait::async_trait]
1013 impl Tool for OwnedTool {
1014 fn name(&self) -> &str {
1015 self.0
1016 }
1017 fn description(&self) -> &str {
1018 "owned"
1019 }
1020 fn parameters(&self) -> Value {
1021 json!({"type": "object"})
1022 }
1023 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
1024 Ok("ok".to_string())
1025 }
1026 fn extension_id(&self) -> Option<&str> {
1027 self.1
1028 }
1029 }
1030
1031 let mut registry = ToolRegistry::without_subagent();
1032 registry.register(Arc::new(OwnedTool("alpha:zed", Some("alpha"))));
1033 registry.register(Arc::new(OwnedTool("alpha:bar", Some("alpha"))));
1034 registry.register(Arc::new(OwnedTool("beta:thing", Some("beta"))));
1035
1036 assert_eq!(
1037 registry.tool_names_for_extension("alpha"),
1038 vec!["alpha:bar".to_string(), "alpha:zed".to_string()]
1039 );
1040 assert_eq!(
1041 registry.tool_names_for_extension("beta"),
1042 vec!["beta:thing".to_string()]
1043 );
1044 assert!(registry.tool_names_for_extension("ghost").is_empty());
1045 assert!(registry.tool_names_for_extension("bash").is_empty());
1047 }
1048
1049 struct OwnedTool(&'static str, Option<&'static str>);
1050 #[async_trait::async_trait]
1051 impl Tool for OwnedTool {
1052 fn name(&self) -> &str {
1053 self.0
1054 }
1055 fn description(&self) -> &str {
1056 "owned"
1057 }
1058 fn parameters(&self) -> Value {
1059 json!({"type": "object"})
1060 }
1061 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
1062 Ok("ok".to_string())
1063 }
1064 fn extension_id(&self) -> Option<&str> {
1065 self.1
1066 }
1067 }
1068
1069 #[test]
1070 fn without_subagent_excludes_subagent_tools() {
1071 let registry = ToolRegistry::without_subagent();
1072 assert!(registry.get("subagent").is_none());
1073 assert!(registry.get("subagent_start").is_none());
1074 assert!(registry.get("subagent_status").is_none());
1075 assert!(registry.get("subagent_steer").is_none());
1076 assert!(registry.get("subagent_collect").is_none());
1077 assert!(registry.get("subagent_resume").is_none());
1078 assert!(registry.get("bash").is_some());
1080 assert!(registry.get("read").is_some());
1081 }
1082
1083 #[test]
1084 fn without_subagent_with_extensions_includes_extension_tools() {
1085 let mut other = ToolRegistry::empty();
1086 other.register(Arc::new(OwnedTool("alpha:do_thing", Some("alpha"))));
1087
1088 let merged = ToolRegistry::without_subagent_with_extensions(&other);
1089
1090 assert!(merged.get("alpha:do_thing").is_some());
1092 assert!(merged.get("bash").is_some());
1094 assert!(merged.get("read").is_some());
1095 assert!(merged.get("subagent_start").is_none());
1097 }
1098
1099 #[test]
1100 fn without_subagent_with_extensions_excludes_built_ins_from_other_registry() {
1101 let other = ToolRegistry::new();
1106
1107 let merged = ToolRegistry::without_subagent_with_extensions(&other);
1108
1109 let bash = merged.get("bash").expect("bash present");
1111 assert!(bash.extension_id().is_none());
1112 assert!(merged.get("subagent_start").is_none());
1114 assert!(merged.get("subagent").is_none());
1115 }
1116
1117 #[test]
1118 fn without_subagent_with_extensions_rejects_recursive_tool_name_collisions() {
1119 let mut other = ToolRegistry::empty();
1120 for name in [
1121 "subagent",
1122 "subagent_start",
1123 "subagent_status",
1124 "subagent_steer",
1125 "subagent_collect",
1126 "subagent_resume",
1127 "subagent_model_authorize",
1128 "subagent_models",
1129 "memory_context",
1130 ] {
1131 other.register(Arc::new(OwnedTool(name, Some("malicious-extension"))));
1132 }
1133
1134 let merged = ToolRegistry::without_subagent_with_extensions(&other);
1135 for name in [
1136 "subagent",
1137 "subagent_start",
1138 "subagent_status",
1139 "subagent_steer",
1140 "subagent_collect",
1141 "subagent_resume",
1142 "subagent_model_authorize",
1143 "subagent_models",
1144 "memory_context",
1145 ] {
1146 assert!(merged.get(name).is_none(), "{name} must stay unavailable");
1147 }
1148 }
1149
1150 #[test]
1151 fn without_subagent_with_extensions_rejects_api_sanitized_recursive_collisions() {
1152 let mut other = ToolRegistry::empty();
1153 for runtime_name in [
1154 "subagent:start",
1155 "subagent:status",
1156 "subagent:steer",
1157 "subagent:collect",
1158 "subagent:resume",
1159 "subagent:model:authorize",
1160 "subagent:models",
1161 "memory:context",
1162 ] {
1163 other.register(Arc::new(OwnedTool(runtime_name, Some("subagent"))));
1164 }
1165
1166 let merged = ToolRegistry::without_subagent_with_extensions(&other);
1167 for api_name in [
1168 "subagent_start",
1169 "subagent_status",
1170 "subagent_steer",
1171 "subagent_collect",
1172 "subagent_resume",
1173 "subagent_model_authorize",
1174 "subagent_models",
1175 "memory_context",
1176 ] {
1177 assert!(
1178 merged.get(api_name).is_none(),
1179 "API-safe recursive name {api_name} must stay unavailable"
1180 );
1181 }
1182 }
1183
1184 #[test]
1188 fn memory_context_present_in_primary_registry_absent_in_recursive_ones() {
1189 assert!(ToolRegistry::new().get("memory_context").is_some());
1190 assert!(ToolRegistry::without_subagent()
1191 .get("memory_context")
1192 .is_none());
1193
1194 let merged = ToolRegistry::without_subagent_with_extensions(&ToolRegistry::new());
1195 assert!(
1196 merged.get("memory_context").is_none(),
1197 "recursive registries must never expose memory_context"
1198 );
1199 }
1200
1201 #[test]
1202 fn without_subagent_with_extensions_does_not_overwrite_existing_builtin() {
1203 let mut other = ToolRegistry::empty();
1209 other.register(Arc::new(OwnedTool("ext:custom", Some("ext"))));
1210
1211 let merged = ToolRegistry::without_subagent_with_extensions(&other);
1212 assert!(merged.get("ext:custom").is_some());
1213 assert!(merged.get("bash").is_some());
1214 assert!(merged.get("bash").unwrap().extension_id().is_none());
1215 }
1216
1217 struct McpTwin(&'static str);
1219
1220 #[async_trait::async_trait]
1221 impl Tool for McpTwin {
1222 fn name(&self) -> &str {
1223 self.0
1224 }
1225 fn description(&self) -> &str {
1226 "mcp twin"
1227 }
1228 fn parameters(&self) -> Value {
1229 json!({"type": "object"})
1230 }
1231 async fn execute(&self, _params: Value, _ctx: ToolContext) -> Result<String> {
1232 Ok("ok".to_string())
1233 }
1234 fn origin(&self) -> crate::tools::ToolOrigin {
1235 crate::tools::ToolOrigin::Mcp {
1236 server_id: "srv".to_string(),
1237 server_tool_name: "shared".to_string(),
1238 }
1239 }
1240 }
1241
1242 #[test]
1243 #[should_panic(expected = "tool catalog rejected mutation")]
1244 fn from_tools_with_duplicate_capability_identity_fails_closed() {
1245 let _ = ToolRegistry::from_tools(vec![
1249 Arc::new(McpTwin("twin_one")),
1250 Arc::new(McpTwin("twin_two")),
1251 ]);
1252 }
1253}