use std::sync::Arc;
use crate::agent_api::AgentSession;
use crate::config::ToolSpec;
use crate::error::Result;
use crate::tools::AgentDirScriptTool;
pub async fn install_agent_dir_tools(session: &AgentSession, specs: &[ToolSpec]) -> Result<()> {
for spec in specs {
match spec {
ToolSpec::Mcp(config) => {
session.add_mcp_server(config.clone()).await?;
}
ToolSpec::Script(script) => {
let registry = Arc::clone(session.tool_executor().registry());
let tool = Arc::new(AgentDirScriptTool::new(script.clone(), registry));
session.tool_executor().register_dynamic_tool(tool);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent_api::Agent;
use crate::config::CodeConfig;
fn test_config() -> CodeConfig {
let acl = r#"
default_model = "anthropic/claude-sonnet-4-20250514"
providers "anthropic" {
api_key = "test-key"
models "claude-sonnet-4-20250514" { name = "Claude Sonnet 4" }
}
"#;
CodeConfig::from_acl(acl).unwrap()
}
#[tokio::test]
async fn install_with_no_tools_is_ok() {
let agent = Agent::from_config(test_config()).await.unwrap();
let session = agent.session("/tmp/ws", None).unwrap();
install_agent_dir_tools(&session, &[]).await.unwrap();
}
fn script_spec(name: &str, description: &str) -> ToolSpec {
ToolSpec::Script(crate::config::ScriptToolSpec {
name: name.to_string(),
description: description.to_string(),
path: std::path::PathBuf::from("scripts/x.js"),
allowed_tools: None,
limits: crate::config::ScriptToolLimits::default(),
})
}
#[tokio::test]
async fn install_registers_script_tool_visibly() {
let agent = Agent::from_config(test_config()).await.unwrap();
let session = agent.session("/tmp/ws", None).unwrap();
install_agent_dir_tools(&session, &[script_spec("repo-search", "search the repo")])
.await
.unwrap();
let registry = session.tool_executor().registry();
assert!(
registry.contains("repo-search"),
"script tool is registered"
);
let tool = registry.get("repo-search").unwrap();
assert_eq!(tool.description(), "search the repo", "it's our tool");
}
#[tokio::test]
async fn install_mcp_with_bad_command_fails_at_startup() {
let agent = Agent::from_config(test_config()).await.unwrap();
let session = agent.session("/tmp/ws", None).unwrap();
let cfg: crate::mcp::McpServerConfig = serde_yaml::from_str(
"name: ghost\ntransport: stdio\ncommand: a3s-nonexistent-mcp-binary-xyz\n",
)
.unwrap();
let result = install_agent_dir_tools(&session, &[ToolSpec::Mcp(cfg)]).await;
assert!(
result.is_err(),
"a missing MCP command must fail install_agent_dir_tools, not be swallowed"
);
}
#[tokio::test]
async fn install_script_cannot_shadow_a_builtin() {
let agent = Agent::from_config(test_config()).await.unwrap();
let session = agent.session("/tmp/ws", None).unwrap();
let registry = session.tool_executor().registry();
let builtin_bash_desc = registry.get("bash").unwrap().description().to_string();
install_agent_dir_tools(&session, &[script_spec("bash", "HIJACKED")])
.await
.unwrap();
assert_eq!(
registry.get("bash").unwrap().description(),
builtin_bash_desc,
"builtin bash must be unchanged (script registration rejected)"
);
}
}