use super::tool::{DynamicTool, DynamicToolDef, DynamicToolsConfig};
use crate::brain::tools::ToolRegistry;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct DynamicToolLoader;
impl DynamicToolLoader {
pub fn default_path() -> Option<PathBuf> {
let profile_path = crate::config::opencrabs_home().join("tools.toml");
if profile_path.exists() {
return Some(profile_path);
}
dirs::home_dir().map(|h| h.join(".opencrabs").join("tools.toml"))
}
pub fn load(path: &Path, registry: &Arc<ToolRegistry>) -> usize {
let config = Self::read_config(path);
let mut count = 0;
for def in config.tools {
if !def.enabled {
continue;
}
if registry.has_tool(&def.name) {
continue;
}
let name = def.name.clone();
registry.register(Arc::new(DynamicTool::new(def)));
count += 1;
tracing::info!("Registered dynamic tool: {name}");
}
if count > 0 {
tracing::info!("Loaded {count} dynamic tool(s) from {}", path.display());
}
count
}
pub fn list_tools_detailed(path: &Path) -> Vec<DynamicToolDef> {
Self::read_config(path).tools
}
pub fn add_tool(
path: &Path,
def: DynamicToolDef,
registry: &Arc<ToolRegistry>,
) -> anyhow::Result<()> {
let mut config = Self::read_config(path);
let name = def.name.clone();
config.tools.retain(|d| d.name != name);
let should_register = def.enabled;
config.tools.push(def.clone());
Self::write_config(path, &config)?;
registry.unregister(&name);
if should_register {
registry.register(Arc::new(DynamicTool::new(def)));
}
tracing::info!("Added dynamic tool: {name}");
Ok(())
}
pub fn remove_tool(
path: &Path,
name: &str,
registry: &Arc<ToolRegistry>,
) -> anyhow::Result<bool> {
let mut config = Self::read_config(path);
let before = config.tools.len();
config.tools.retain(|d| d.name != name);
let removed = config.tools.len() < before;
if removed {
Self::write_config(path, &config)?;
registry.unregister(name);
tracing::info!("Removed dynamic tool: {name}");
}
Ok(removed)
}
pub fn set_enabled(
path: &Path,
name: &str,
enabled: bool,
registry: &Arc<ToolRegistry>,
) -> anyhow::Result<bool> {
let mut config = Self::read_config(path);
let found = config.tools.iter_mut().find(|d| d.name == name);
match found {
Some(def) => {
def.enabled = enabled;
let def_clone = def.clone();
Self::write_config(path, &config)?;
if enabled {
registry.unregister(name);
registry.register(Arc::new(DynamicTool::new(def_clone)));
} else {
registry.unregister(name);
}
Ok(true)
}
None => Ok(false),
}
}
pub fn reload(path: &Path, registry: &Arc<ToolRegistry>) -> usize {
let config = Self::read_config(path);
for def in &config.tools {
registry.unregister(&def.name);
}
let mut count = 0;
for def in config.tools {
if def.enabled {
registry.register(Arc::new(DynamicTool::new(def)));
count += 1;
}
}
tracing::info!("Reloaded {count} dynamic tool(s) from {}", path.display());
count
}
fn read_config(path: &Path) -> DynamicToolsConfig {
match std::fs::read_to_string(path) {
Ok(content) => toml::from_str(&content).unwrap_or_default(),
Err(_) => DynamicToolsConfig::default(),
}
}
fn write_config(path: &Path, config: &DynamicToolsConfig) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, toml::to_string_pretty(config)?)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::tool::{ExecutorType, ParamDef};
use super::*;
use std::collections::HashMap;
use tempfile::TempDir;
fn tmp_path() -> (TempDir, PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("tools.toml");
(dir, path)
}
#[test]
fn test_load_nonexistent() {
let reg = Arc::new(ToolRegistry::new());
assert_eq!(DynamicToolLoader::load(Path::new("/nonexistent"), ®), 0);
}
#[test]
fn test_add_and_list() {
let (_dir, path) = tmp_path();
let reg = Arc::new(ToolRegistry::new());
let def = DynamicToolDef {
name: "ping".into(),
description: "Ping".into(),
executor: ExecutorType::Shell,
enabled: true,
requires_approval: false,
method: None,
url: None,
headers: HashMap::new(),
timeout_secs: 10,
command: Some("ping -c 1 {{host}}".into()),
params: vec![ParamDef {
name: "host".into(),
param_type: "string".into(),
description: "".into(),
required: true,
default: None,
coerce_empty_to: Default::default(),
coerce_null_to: Default::default(),
}],
};
DynamicToolLoader::add_tool(&path, def, ®).unwrap();
assert!(reg.has_tool("ping"));
assert_eq!(DynamicToolLoader::list_tools_detailed(&path).len(), 1);
}
#[test]
fn test_remove() {
let (_dir, path) = tmp_path();
let reg = Arc::new(ToolRegistry::new());
let def = DynamicToolDef {
name: "rm_me".into(),
description: "".into(),
executor: ExecutorType::Shell,
enabled: true,
requires_approval: false,
method: None,
url: None,
headers: HashMap::new(),
timeout_secs: 10,
command: Some("echo".into()),
params: vec![],
};
DynamicToolLoader::add_tool(&path, def, ®).unwrap();
assert!(DynamicToolLoader::remove_tool(&path, "rm_me", ®).unwrap());
assert!(!reg.has_tool("rm_me"));
}
#[test]
fn test_enable_disable() {
let (_dir, path) = tmp_path();
let reg = Arc::new(ToolRegistry::new());
let def = DynamicToolDef {
name: "tog".into(),
description: "".into(),
executor: ExecutorType::Shell,
enabled: true,
requires_approval: false,
method: None,
url: None,
headers: HashMap::new(),
timeout_secs: 10,
command: Some("echo".into()),
params: vec![],
};
DynamicToolLoader::add_tool(&path, def, ®).unwrap();
DynamicToolLoader::set_enabled(&path, "tog", false, ®).unwrap();
assert!(!reg.has_tool("tog"));
DynamicToolLoader::set_enabled(&path, "tog", true, ®).unwrap();
assert!(reg.has_tool("tog"));
}
#[test]
fn test_reload() {
let (_dir, path) = tmp_path();
let reg = Arc::new(ToolRegistry::new());
std::fs::write(&path, "[[tools]]\nname = \"disk\"\ndescription = \"From disk\"\nexecutor = \"shell\"\ncommand = \"echo\"").unwrap();
assert_eq!(DynamicToolLoader::reload(&path, ®), 1);
assert!(reg.has_tool("disk"));
}
#[test]
fn test_disabled_not_registered() {
let (_dir, path) = tmp_path();
let reg = Arc::new(ToolRegistry::new());
std::fs::write(&path, "[[tools]]\nname = \"on\"\ndescription = \"\"\nexecutor = \"shell\"\ncommand = \"echo\"\n\n[[tools]]\nname = \"off\"\ndescription = \"\"\nexecutor = \"shell\"\ncommand = \"echo\"\nenabled = false").unwrap();
assert_eq!(DynamicToolLoader::load(&path, ®), 1);
assert!(reg.has_tool("on"));
assert!(!reg.has_tool("off"));
}
}