use crate::tools::native::{NativeTool, ToolSchema};
use dashmap::DashMap;
use std::sync::Arc;
pub struct ToolRegistry {
tools: DashMap<String, Arc<dyn NativeTool>>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: DashMap::new(),
}
}
pub fn register(&self, tool: Arc<dyn NativeTool>) {
let name = tool.name().to_owned();
tracing::debug!(tool = %name, "registering tool");
self.tools.insert(name, tool);
}
#[inline]
#[must_use]
pub fn get(&self, name: &str) -> Option<Arc<dyn NativeTool>> {
self.tools.get(name).map(|entry| Arc::clone(entry.value()))
}
#[must_use]
pub fn list(&self) -> Vec<ToolSchema> {
self.tools
.iter()
.map(|entry| entry.value().schema())
.collect()
}
#[inline]
#[must_use]
pub fn has(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub fn remove(&self, name: &str) -> bool {
self.tools.remove(name).is_some()
}
#[must_use]
pub fn count(&self) -> usize {
self.tools.len()
}
#[must_use]
pub fn is_tool_allowed(tool_name: &str, allowed_tools: &[String]) -> bool {
allowed_tools.is_empty() || allowed_tools.iter().any(|t| t == tool_name)
}
#[must_use]
pub fn get_allowed(&self, name: &str, allowed_tools: &[String]) -> Option<Arc<dyn NativeTool>> {
if !Self::is_tool_allowed(name, allowed_tools) {
tracing::warn!(tool = name, "tool call blocked: not in agent allow-list");
return None;
}
self.get(name)
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::builtin::echo::EchoTool;
use crate::tools::builtin::json_transform::JsonTransformTool;
#[test]
fn register_and_get() {
let reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
assert!(reg.has("echo"));
assert_eq!(reg.count(), 1);
let tool = reg.get("echo").expect("tool should exist");
assert_eq!(tool.name(), "echo");
}
#[test]
fn list_schemas() {
let reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
reg.register(Arc::new(JsonTransformTool));
let schemas = reg.list();
assert_eq!(schemas.len(), 2);
let names: Vec<&str> = schemas.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"echo"));
assert!(names.contains(&"json_transform"));
}
#[test]
fn remove_tool() {
let reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
assert!(reg.remove("echo"));
assert!(!reg.has("echo"));
assert_eq!(reg.count(), 0);
assert!(!reg.remove("echo"));
}
#[test]
fn get_missing_returns_none() {
let reg = ToolRegistry::new();
assert!(reg.get("nonexistent").is_none());
}
#[test]
fn register_multiple_tools_verify_count() {
let reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
reg.register(Arc::new(JsonTransformTool));
assert_eq!(reg.count(), 2);
assert!(reg.has("echo"));
assert!(reg.has("json_transform"));
}
#[test]
fn register_same_name_overwrites() {
let reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
assert_eq!(reg.count(), 1);
let schema_before = reg.get("echo").unwrap().schema();
assert_eq!(schema_before.parameters.len(), 1);
reg.register(Arc::new(EchoTool));
assert_eq!(reg.count(), 1);
assert!(reg.has("echo"));
}
#[tokio::test]
async fn concurrent_access() {
let reg = Arc::new(ToolRegistry::new());
let mut handles = Vec::new();
for i in 0..20 {
let reg_clone = reg.clone();
handles.push(tokio::spawn(async move {
if i % 2 == 0 {
reg_clone.register(Arc::new(EchoTool));
} else {
reg_clone.register(Arc::new(JsonTransformTool));
}
let _ = reg_clone.list();
let _ = reg_clone.get("echo");
let _ = reg_clone.has("json_transform");
reg_clone.count()
}));
}
for h in handles {
h.await.unwrap();
}
assert!(reg.has("echo"));
assert!(reg.has("json_transform"));
assert_eq!(reg.count(), 2);
}
#[test]
fn list_returns_schemas_for_all_registered() {
let reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
reg.register(Arc::new(JsonTransformTool));
let schemas = reg.list();
assert_eq!(schemas.len(), 2);
for schema in &schemas {
assert!(!schema.name.is_empty());
assert!(!schema.description.is_empty());
}
let names: Vec<&str> = schemas.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"echo"));
assert!(names.contains(&"json_transform"));
let echo_schema = schemas.iter().find(|s| s.name == "echo").unwrap();
assert_eq!(echo_schema.parameters.len(), 1);
assert_eq!(echo_schema.parameters[0].name, "message");
let jt_schema = schemas.iter().find(|s| s.name == "json_transform").unwrap();
assert_eq!(jt_schema.parameters.len(), 2);
}
#[test]
fn is_tool_allowed_empty_list_permits_all() {
assert!(ToolRegistry::is_tool_allowed("anything", &[]));
}
#[test]
fn is_tool_allowed_checks_list() {
let allowed = vec!["echo".into(), "json_transform".into()];
assert!(ToolRegistry::is_tool_allowed("echo", &allowed));
assert!(ToolRegistry::is_tool_allowed("json_transform", &allowed));
assert!(!ToolRegistry::is_tool_allowed("load_testing", &allowed));
}
#[test]
fn get_allowed_blocks_unlisted_tool() {
let reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
reg.register(Arc::new(JsonTransformTool));
let allowed = vec!["echo".into()];
assert!(reg.get_allowed("echo", &allowed).is_some());
assert!(reg.get_allowed("json_transform", &allowed).is_none());
}
#[test]
fn get_allowed_empty_list_allows_all() {
let reg = ToolRegistry::new();
reg.register(Arc::new(EchoTool));
assert!(reg.get_allowed("echo", &[]).is_some());
}
#[test]
fn get_allowed_missing_tool_returns_none() {
let reg = ToolRegistry::new();
assert!(reg.get_allowed("nonexistent", &[]).is_none());
}
#[test]
fn remove_nonexistent_returns_false() {
let reg = ToolRegistry::new();
assert!(!reg.remove("does_not_exist"));
assert!(!reg.remove(""));
assert!(!reg.remove("some_random_tool"));
}
}