use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::mpsc;
use tracing::info;
use crate::config::AppConfig;
use crate::events::EventStore;
use crate::health::HealthProbeStore;
use crate::llm_runtime::SharedLlmRuntime;
use crate::mcp::McpRegistry;
use crate::state::SqliteStateStore;
use crate::tools::terminal::ApprovalRequest;
use crate::tools::ApprovalBroker;
#[cfg(feature = "browser")]
use crate::tools::BrowserTool;
#[cfg(feature = "computer_use")]
use crate::tools::ComputerUseTool;
#[cfg(feature = "slack")]
use crate::tools::ReadChannelHistoryTool;
use crate::tools::{
CheckEnvironmentTool, CliAgentTool, ConfigManagerTool, DiagnoseTool, EditFileTool,
GitCommitTool, GitInfoTool, GoalTraceTool, HealthProbeTool, HttpRequestTool, ManageApiTool,
ManageCliAgentsTool, ManageHttpAuthTool, ManageMcpTool, ManageMemoriesTool, ManageOAuthTool,
ManagePeopleTool, PolicyMetricsTool, ProjectInspectTool, ReadFileTool, RememberFactTool,
RunCommandTool, ScheduledGoalRunsTool, SearchFilesTool, SendFileTool, ServiceStatusTool,
ShareMemoryTool, SpawnAgentTool, SystemInfoTool, TerminalTool, ToolTraceTool, WebFetchTool,
WebSearchTool, WriteFileTool,
};
use crate::traits::store_prelude::*;
use crate::traits::Tool;
use crate::types::MediaMessage;
pub struct BaseToolsBundle {
pub tools: Vec<Arc<dyn Tool>>,
pub approval_tx: ApprovalBroker,
pub approval_rx: mpsc::Receiver<ApprovalRequest>,
pub media_tx: mpsc::Sender<MediaMessage>,
pub media_rx: mpsc::Receiver<MediaMessage>,
pub terminal_tool: Option<Arc<TerminalTool>>,
}
pub struct OptionalToolsOutcome {
pub has_cli_agents: bool,
pub inbox_dir: String,
pub cli_agent_tool: Option<Arc<CliAgentTool>>,
}
pub struct RuntimeToolsOutcome {
pub spawn_tool: Option<Arc<SpawnAgentTool>>,
pub oauth_gateway: Option<crate::oauth::OAuthGateway>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumCount, strum::EnumIter)]
enum BaseToolId {
SystemInfo,
Terminal,
RememberFact,
ShareMemory,
ManageMemories,
ScheduledGoalRuns,
GoalTrace,
ToolTrace,
PolicyMetrics,
ConfigManager,
WebFetch,
WebSearch,
ReadFile,
WriteFile,
EditFile,
SearchFiles,
ProjectInspect,
RunCommand,
GitInfo,
GitCommit,
CheckEnvironment,
ServiceStatus,
}
struct BaseToolSpec {
id: BaseToolId,
name: &'static str,
}
const BASE_TOOL_REGISTRY: &[BaseToolSpec] = &[
BaseToolSpec {
id: BaseToolId::SystemInfo,
name: "system_info",
},
BaseToolSpec {
id: BaseToolId::Terminal,
name: "terminal",
},
BaseToolSpec {
id: BaseToolId::RememberFact,
name: "remember_fact",
},
BaseToolSpec {
id: BaseToolId::ShareMemory,
name: "share_memory",
},
BaseToolSpec {
id: BaseToolId::ManageMemories,
name: "manage_memories",
},
BaseToolSpec {
id: BaseToolId::ScheduledGoalRuns,
name: "scheduled_goal_runs",
},
BaseToolSpec {
id: BaseToolId::GoalTrace,
name: "goal_trace",
},
BaseToolSpec {
id: BaseToolId::ToolTrace,
name: "tool_trace",
},
BaseToolSpec {
id: BaseToolId::PolicyMetrics,
name: "policy_metrics",
},
BaseToolSpec {
id: BaseToolId::ConfigManager,
name: "manage_config",
},
BaseToolSpec {
id: BaseToolId::WebFetch,
name: "web_fetch",
},
BaseToolSpec {
id: BaseToolId::WebSearch,
name: "web_search",
},
BaseToolSpec {
id: BaseToolId::ReadFile,
name: "read_file",
},
BaseToolSpec {
id: BaseToolId::WriteFile,
name: "write_file",
},
BaseToolSpec {
id: BaseToolId::EditFile,
name: "edit_file",
},
BaseToolSpec {
id: BaseToolId::SearchFiles,
name: "search_files",
},
BaseToolSpec {
id: BaseToolId::ProjectInspect,
name: "project_inspect",
},
BaseToolSpec {
id: BaseToolId::RunCommand,
name: "run_command",
},
BaseToolSpec {
id: BaseToolId::GitInfo,
name: "git_info",
},
BaseToolSpec {
id: BaseToolId::GitCommit,
name: "git_commit",
},
BaseToolSpec {
id: BaseToolId::CheckEnvironment,
name: "check_environment",
},
BaseToolSpec {
id: BaseToolId::ServiceStatus,
name: "service_status",
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumCount, strum::EnumIter)]
enum OptionalToolId {
Diagnose,
#[cfg(feature = "browser")]
Browser,
#[cfg(feature = "computer_use")]
ComputerUse,
SendFile,
CliAgents,
HealthProbe,
#[cfg(feature = "slack")]
SlackChannelHistory,
}
struct OptionalToolSpec {
id: OptionalToolId,
name: &'static str,
enabled_if: fn(&AppConfig) -> bool,
}
fn optional_enabled_diagnostics(config: &AppConfig) -> bool {
config.diagnostics.enabled
}
#[cfg(feature = "browser")]
fn optional_enabled_browser(config: &AppConfig) -> bool {
config.browser.enabled
}
#[cfg(feature = "computer_use")]
fn optional_enabled_computer_use(config: &AppConfig) -> bool {
config.computer_use.enabled
}
fn optional_enabled_files(config: &AppConfig) -> bool {
config.files.enabled
}
fn optional_enabled_cli_agents(config: &AppConfig) -> bool {
config.cli_agents.enabled
}
fn optional_enabled_always(_: &AppConfig) -> bool {
true
}
const OPTIONAL_TOOL_REGISTRY: &[OptionalToolSpec] = &[
OptionalToolSpec {
id: OptionalToolId::Diagnose,
name: "diagnose",
enabled_if: optional_enabled_diagnostics,
},
#[cfg(feature = "browser")]
OptionalToolSpec {
id: OptionalToolId::Browser,
name: "browser",
enabled_if: optional_enabled_browser,
},
#[cfg(feature = "computer_use")]
OptionalToolSpec {
id: OptionalToolId::ComputerUse,
name: "computer_use",
enabled_if: optional_enabled_computer_use,
},
OptionalToolSpec {
id: OptionalToolId::SendFile,
name: "send_file",
enabled_if: optional_enabled_files,
},
OptionalToolSpec {
id: OptionalToolId::CliAgents,
name: "cli_agents",
enabled_if: optional_enabled_cli_agents,
},
OptionalToolSpec {
id: OptionalToolId::HealthProbe,
name: "health_probe",
enabled_if: optional_enabled_always,
},
#[cfg(feature = "slack")]
OptionalToolSpec {
id: OptionalToolId::SlackChannelHistory,
name: "read_channel_history",
enabled_if: optional_enabled_always,
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumCount, strum::EnumIter)]
enum RuntimeToolId {
ManageMcp,
ManagePeople,
HttpRequest,
ManageHttpAuth,
ManageOauth,
ManageApi,
SpawnAgent,
}
struct RuntimeToolSpec {
id: RuntimeToolId,
name: &'static str,
enabled_if: fn(&AppConfig) -> bool,
depends_on: &'static [RuntimeToolId],
}
fn runtime_enabled_always(_: &AppConfig) -> bool {
true
}
fn runtime_enabled_http_request(_: &AppConfig) -> bool {
true
}
fn runtime_enabled_oauth(_: &AppConfig) -> bool {
true
}
fn runtime_enabled_spawn(config: &AppConfig) -> bool {
config.subagents.enabled
}
const RUNTIME_TOOL_MANIFEST: &[RuntimeToolSpec] = &[
RuntimeToolSpec {
id: RuntimeToolId::ManageMcp,
name: "manage_mcp",
enabled_if: runtime_enabled_always,
depends_on: &[],
},
RuntimeToolSpec {
id: RuntimeToolId::ManagePeople,
name: "manage_people",
enabled_if: runtime_enabled_always,
depends_on: &[],
},
RuntimeToolSpec {
id: RuntimeToolId::HttpRequest,
name: "http_request",
enabled_if: runtime_enabled_http_request,
depends_on: &[],
},
RuntimeToolSpec {
id: RuntimeToolId::ManageHttpAuth,
name: "manage_http_auth",
enabled_if: runtime_enabled_always,
depends_on: &[RuntimeToolId::HttpRequest],
},
RuntimeToolSpec {
id: RuntimeToolId::ManageOauth,
name: "manage_oauth",
enabled_if: runtime_enabled_oauth,
depends_on: &[RuntimeToolId::HttpRequest],
},
RuntimeToolSpec {
id: RuntimeToolId::ManageApi,
name: "manage_api",
enabled_if: runtime_enabled_always,
depends_on: &[RuntimeToolId::HttpRequest, RuntimeToolId::ManageOauth],
},
RuntimeToolSpec {
id: RuntimeToolId::SpawnAgent,
name: "spawn_agent",
enabled_if: runtime_enabled_spawn,
depends_on: &[],
},
];
fn runtime_tool_name(manifest: &[RuntimeToolSpec], id: RuntimeToolId) -> &'static str {
manifest
.iter()
.find(|spec| spec.id == id)
.map(|spec| spec.name)
.unwrap_or("<unknown>")
}
fn validate_runtime_manifest(manifest: &[RuntimeToolSpec]) -> anyhow::Result<()> {
let mut seen_ids: HashSet<RuntimeToolId> = HashSet::new();
let mut seen_names: HashSet<&'static str> = HashSet::new();
for spec in manifest {
anyhow::ensure!(
seen_ids.insert(spec.id),
"runtime manifest is invalid: duplicate tool id {}",
spec.name
);
anyhow::ensure!(
seen_names.insert(spec.name),
"runtime manifest is invalid: duplicate tool name {}",
spec.name
);
}
let defined_ids: HashSet<RuntimeToolId> = manifest.iter().map(|spec| spec.id).collect();
let mut declared: HashSet<RuntimeToolId> = HashSet::new();
for spec in manifest {
for dep in spec.depends_on {
anyhow::ensure!(
defined_ids.contains(dep),
"runtime manifest is invalid: tool {} declares unknown dependency {}",
spec.name,
runtime_tool_name(manifest, *dep)
);
anyhow::ensure!(
declared.contains(dep),
"runtime manifest is invalid: tool {} depends on {} but dependency appears after it",
spec.name,
runtime_tool_name(manifest, *dep)
);
}
declared.insert(spec.id);
}
Ok(())
}
pub async fn build_base_tools(
config: &AppConfig,
config_path: PathBuf,
state: Arc<SqliteStateStore>,
event_store: Arc<EventStore>,
approval_queue_capacity: usize,
media_queue_capacity: usize,
) -> anyhow::Result<BaseToolsBundle> {
let (approval_tx, approval_rx) = mpsc::channel(approval_queue_capacity);
let approval_tx = ApprovalBroker::new(approval_tx);
let (media_tx, media_rx) = mpsc::channel::<MediaMessage>(media_queue_capacity);
let mut tools: Vec<Arc<dyn Tool>> = Vec::with_capacity(BASE_TOOL_REGISTRY.len());
let mut terminal_tool: Option<Arc<TerminalTool>> = None;
for spec in BASE_TOOL_REGISTRY {
if !config.tools.is_enabled(spec.name) {
info!(
tool = spec.name,
"Skipped base tool from startup manifest (disabled in [tools].disabled)"
);
continue;
}
let built = build_base_tool(
spec.id,
config,
&config_path,
state.clone(),
event_store.clone(),
approval_tx.clone(),
)
.await?;
info!(
tool = spec.name,
"Registered base tool from startup manifest"
);
if terminal_tool.is_none() {
terminal_tool = built.terminal_tool.clone();
}
tools.push(built.tool);
}
Ok(BaseToolsBundle {
tools,
approval_tx,
approval_rx,
media_tx,
media_rx,
terminal_tool,
})
}
struct BuiltBaseTool {
tool: Arc<dyn Tool>,
terminal_tool: Option<Arc<TerminalTool>>,
}
#[allow(clippy::too_many_arguments)]
pub async fn register_optional_tools(
tools: &mut Vec<Arc<dyn Tool>>,
config: &AppConfig,
state: Arc<SqliteStateStore>,
event_store: Arc<EventStore>,
llm_runtime: SharedLlmRuntime,
health_store: Option<Arc<HealthProbeStore>>,
approval_tx: ApprovalBroker,
media_tx: mpsc::Sender<MediaMessage>,
) -> anyhow::Result<OptionalToolsOutcome> {
let inbox_dir = shellexpand::tilde(&config.files.inbox_dir).to_string();
let mut has_cli_agents = false;
let mut cli_agent_tool: Option<Arc<CliAgentTool>> = None;
for spec in OPTIONAL_TOOL_REGISTRY {
if !(spec.enabled_if)(config) {
continue;
}
let tools_before = tools.len();
match spec.id {
OptionalToolId::Diagnose => {
if !config.tools.is_enabled("self_diagnose") {
continue;
}
tools.push(Arc::new(DiagnoseTool::new(
event_store.clone(),
state.clone(),
llm_runtime.clone(),
config.diagnostics.max_events,
config.diagnostics.include_raw_tool_args,
)));
}
#[cfg(feature = "browser")]
OptionalToolId::Browser => {
if !config.tools.is_enabled("browser") {
continue;
}
let browser_tool = BrowserTool::new(
config.browser.clone(),
media_tx.clone(),
approval_tx.clone(),
inbox_dir.clone(),
)
.map_err(|e| anyhow::anyhow!("Invalid browser configuration: {e}"))?;
tools.push(Arc::new(browser_tool));
}
#[cfg(feature = "computer_use")]
OptionalToolId::ComputerUse => {
if !config.tools.is_enabled("computer_use") {
continue;
}
std::fs::create_dir_all(&inbox_dir)?;
let vision = crate::config::VisionConfig::from_files(&config.files);
tools.push(Arc::new(ComputerUseTool::new(
config.computer_use.clone(),
vision,
std::path::PathBuf::from(&inbox_dir),
approval_tx.clone(),
media_tx.clone(),
)));
}
OptionalToolId::SendFile => {
if !config.tools.is_enabled("send_file") {
continue;
}
std::fs::create_dir_all(&inbox_dir)?;
tools.push(Arc::new(SendFileTool::new(
media_tx.clone(),
&config.files.outbox_dirs,
&inbox_dir,
)));
}
OptionalToolId::CliAgents => {
let register_cli = config.tools.is_enabled("cli_agent");
let register_manage_cli = config.tools.is_enabled("manage_cli_agents");
if !register_cli && !register_manage_cli {
continue;
}
let cli_tool = CliAgentTool::discover(
config.cli_agents.clone(),
state.clone(),
llm_runtime.clone(),
approval_tx.clone(),
)
.await;
has_cli_agents = cli_tool.has_tools();
let arc = Arc::new(cli_tool);
cli_agent_tool = Some(arc.clone());
if register_cli {
tools.push(arc.clone());
if !has_cli_agents {
info!(
"CLI agents enabled but no tools found on system (cli_agent registered for runtime discovery)"
);
}
}
if register_manage_cli {
let manage_cli =
ManageCliAgentsTool::new(arc, state.clone(), approval_tx.clone());
tools.push(Arc::new(manage_cli));
}
}
OptionalToolId::HealthProbe => {
if !config.tools.is_enabled("health_probe") {
continue;
}
let Some(store) = &health_store else {
continue;
};
tools.push(Arc::new(HealthProbeTool::new(store.clone())));
}
#[cfg(feature = "slack")]
OptionalToolId::SlackChannelHistory => {
if !config.tools.is_enabled("read_channel_history") {
continue;
}
let mut slack_tokens: Vec<String> = config
.all_slack_bots()
.iter()
.map(|bot| bot.bot_token.clone())
.collect();
if let Ok(dynamic_bots) = state.get_dynamic_bots().await {
for bot in dynamic_bots {
if bot.channel_type == "slack" && !bot.bot_token.is_empty() {
slack_tokens.push(bot.bot_token.clone());
}
}
}
if slack_tokens.is_empty() {
continue;
}
info!(count = slack_tokens.len(), "Channel history tool enabled");
tools.push(Arc::new(ReadChannelHistoryTool::new(slack_tokens)));
}
}
for tool in &tools[tools_before..] {
info!(
group = spec.name,
tool = tool.name(),
"Registered optional tool from startup manifest"
);
}
}
Ok(OptionalToolsOutcome {
has_cli_agents,
inbox_dir,
cli_agent_tool,
})
}
pub async fn register_runtime_tools(
tools: &mut Vec<Arc<dyn Tool>>,
config: &AppConfig,
config_path: &Path,
http_profiles: crate::oauth::SharedHttpProfiles,
state: Arc<SqliteStateStore>,
mcp_registry: McpRegistry,
approval_tx: ApprovalBroker,
) -> anyhow::Result<RuntimeToolsOutcome> {
validate_runtime_manifest(RUNTIME_TOOL_MANIFEST)?;
let mut oauth_gateway: Option<crate::oauth::OAuthGateway> = None;
let mut spawn_tool: Option<Arc<SpawnAgentTool>> = None;
let mut http_request_tool: Option<Arc<HttpRequestTool>> = None;
let mut registered_runtime_tools: std::collections::HashSet<RuntimeToolId> =
std::collections::HashSet::new();
for spec in RUNTIME_TOOL_MANIFEST {
if !(spec.enabled_if)(config) {
info!(
tool = spec.name,
"Skipped runtime tool from startup manifest (condition disabled)"
);
continue;
}
let missing_dependencies: Vec<&'static str> = spec
.depends_on
.iter()
.filter(|dep| !registered_runtime_tools.contains(dep))
.map(|dep| runtime_tool_name(RUNTIME_TOOL_MANIFEST, *dep))
.collect();
if !missing_dependencies.is_empty() {
info!(
tool = spec.name,
missing_dependencies = ?missing_dependencies,
"Skipped runtime tool from startup manifest (dependencies not satisfied)"
);
continue;
}
if !config.tools.is_enabled(spec.name) {
info!(
tool = spec.name,
"Skipped runtime tool from startup manifest (disabled in [tools].disabled)"
);
continue;
}
register_runtime_tool_by_id(
spec.id,
tools,
config,
config_path,
state.clone(),
mcp_registry.clone(),
approval_tx.clone(),
http_profiles.clone(),
&mut http_request_tool,
&mut spawn_tool,
&mut oauth_gateway,
)
.await?;
registered_runtime_tools.insert(spec.id);
info!(
tool = spec.name,
dependencies = ?spec.depends_on.iter().map(|dep| runtime_tool_name(RUNTIME_TOOL_MANIFEST, *dep)).collect::<Vec<_>>(),
"Registered runtime tool from startup manifest"
);
}
Ok(RuntimeToolsOutcome {
spawn_tool,
oauth_gateway,
})
}
#[allow(clippy::too_many_arguments)]
async fn register_runtime_tool_by_id(
tool_id: RuntimeToolId,
tools: &mut Vec<Arc<dyn Tool>>,
config: &AppConfig,
config_path: &Path,
state: Arc<SqliteStateStore>,
mcp_registry: McpRegistry,
approval_tx: ApprovalBroker,
http_profiles: crate::oauth::SharedHttpProfiles,
http_request_tool: &mut Option<Arc<HttpRequestTool>>,
spawn_tool: &mut Option<Arc<SpawnAgentTool>>,
oauth_gateway: &mut Option<crate::oauth::OAuthGateway>,
) -> anyhow::Result<()> {
match tool_id {
RuntimeToolId::ManageMcp => {
let manage_mcp = ManageMcpTool::new(mcp_registry, approval_tx);
tools.push(Arc::new(manage_mcp));
}
RuntimeToolId::ManagePeople => {
tools.push(Arc::new(ManagePeopleTool::new(state.clone())));
if state.get_setting("people_enabled").await?.is_none() {
state
.set_setting(
"people_enabled",
if config.people.enabled {
"true"
} else {
"false"
},
)
.await?;
}
}
RuntimeToolId::HttpRequest => {
let tool = Arc::new(HttpRequestTool::new(http_profiles, approval_tx));
tools.push(tool.clone());
*http_request_tool = Some(tool);
}
RuntimeToolId::ManageHttpAuth => {
tools.push(Arc::new(ManageHttpAuthTool::new(
config_path.to_path_buf(),
http_profiles,
approval_tx,
state as Arc<dyn crate::traits::StateStore>,
)));
}
RuntimeToolId::ManageOauth => {
let callback_url = config
.oauth
.callback_url
.clone()
.unwrap_or_else(|| format!("http://localhost:{}", config.daemon.health_port));
let gateway =
crate::oauth::OAuthGateway::new(state.clone(), http_profiles, callback_url);
for name in crate::oauth::providers::builtin_provider_names() {
if let Some(provider) = crate::oauth::providers::get_builtin_provider(name) {
gateway.register_provider(provider).await;
}
}
for (name, provider_config) in &config.oauth.providers {
gateway
.register_config_provider(name, provider_config)
.await;
}
gateway.restore_connections().await;
if let Some(http_tool) = http_request_tool.as_ref() {
http_tool.set_oauth_gateway(gateway.clone()).await;
}
tools.push(Arc::new(ManageOAuthTool::new(
gateway.clone(),
state.clone() as Arc<dyn crate::traits::StateStore>,
config_path.to_path_buf(),
approval_tx,
)));
*oauth_gateway = Some(gateway);
}
RuntimeToolId::ManageApi => {
let gateway = oauth_gateway.clone().ok_or_else(|| {
anyhow::anyhow!("manage_api requires manage_oauth to be initialized first")
})?;
let skills_dir = if config.skills.enabled {
let dir = config_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join(&config.skills.dir);
std::fs::create_dir_all(&dir).ok();
Some(dir)
} else {
None
};
tools.push(Arc::new(ManageApiTool::new(
config_path.to_path_buf(),
skills_dir,
config.skills.registries.clone(),
config.search.clone(),
http_profiles,
approval_tx,
state.clone() as Arc<dyn crate::traits::StateStore>,
gateway,
)));
}
RuntimeToolId::SpawnAgent => {
let st = Arc::new(
SpawnAgentTool::new_deferred(
config.subagents.max_response_chars,
config.subagents.timeout_secs,
)
.with_state(state.clone() as Arc<dyn crate::traits::StateStore>),
);
tools.push(st.clone());
*spawn_tool = Some(st);
}
}
Ok(())
}
async fn build_base_tool(
tool_id: BaseToolId,
config: &AppConfig,
config_path: &Path,
state: Arc<SqliteStateStore>,
event_store: Arc<EventStore>,
approval_tx: ApprovalBroker,
) -> anyhow::Result<BuiltBaseTool> {
let built = match tool_id {
BaseToolId::SystemInfo => BuiltBaseTool {
tool: Arc::new(SystemInfoTool),
terminal_tool: None,
},
BaseToolId::Terminal => {
let terminal = Arc::new(
TerminalTool::new(
config.terminal.allowed_prefixes.clone(),
approval_tx,
config.terminal.initial_timeout_secs,
config.terminal.max_output_chars,
config.terminal.permission_mode,
state.pool(),
)
.await
.with_event_store(event_store)
.with_state(state as Arc<dyn crate::traits::StateStore>),
);
BuiltBaseTool {
tool: terminal.clone(),
terminal_tool: Some(terminal),
}
}
BaseToolId::RememberFact => BuiltBaseTool {
tool: Arc::new(RememberFactTool::new(state)),
terminal_tool: None,
},
BaseToolId::ShareMemory => BuiltBaseTool {
tool: Arc::new(ShareMemoryTool::new(state, approval_tx)),
terminal_tool: None,
},
BaseToolId::ManageMemories => BuiltBaseTool {
tool: Arc::new(ManageMemoriesTool::new(state).with_approval_tx(approval_tx.clone())),
terminal_tool: None,
},
BaseToolId::ScheduledGoalRuns => BuiltBaseTool {
tool: Arc::new(ScheduledGoalRunsTool::new(state)),
terminal_tool: None,
},
BaseToolId::GoalTrace => BuiltBaseTool {
tool: Arc::new(GoalTraceTool::new(state)),
terminal_tool: None,
},
BaseToolId::ToolTrace => BuiltBaseTool {
tool: Arc::new(ToolTraceTool::new(state)),
terminal_tool: None,
},
BaseToolId::PolicyMetrics => BuiltBaseTool {
tool: Arc::new(PolicyMetricsTool),
terminal_tool: None,
},
BaseToolId::ConfigManager => BuiltBaseTool {
tool: Arc::new(ConfigManagerTool::new(
config_path.to_path_buf(),
approval_tx,
)),
terminal_tool: None,
},
BaseToolId::WebFetch => BuiltBaseTool {
tool: Arc::new(WebFetchTool::new()),
terminal_tool: None,
},
BaseToolId::WebSearch => BuiltBaseTool {
tool: Arc::new(WebSearchTool::new(&config.search)),
terminal_tool: None,
},
BaseToolId::ReadFile => BuiltBaseTool {
tool: Arc::new(ReadFileTool),
terminal_tool: None,
},
BaseToolId::WriteFile => BuiltBaseTool {
tool: Arc::new(WriteFileTool),
terminal_tool: None,
},
BaseToolId::EditFile => BuiltBaseTool {
tool: Arc::new(EditFileTool),
terminal_tool: None,
},
BaseToolId::SearchFiles => BuiltBaseTool {
tool: Arc::new(SearchFilesTool),
terminal_tool: None,
},
BaseToolId::ProjectInspect => BuiltBaseTool {
tool: Arc::new(ProjectInspectTool),
terminal_tool: None,
},
BaseToolId::RunCommand => BuiltBaseTool {
tool: Arc::new(RunCommandTool),
terminal_tool: None,
},
BaseToolId::GitInfo => BuiltBaseTool {
tool: Arc::new(GitInfoTool),
terminal_tool: None,
},
BaseToolId::GitCommit => BuiltBaseTool {
tool: Arc::new(GitCommitTool),
terminal_tool: None,
},
BaseToolId::CheckEnvironment => BuiltBaseTool {
tool: Arc::new(CheckEnvironmentTool),
terminal_tool: None,
},
BaseToolId::ServiceStatus => BuiltBaseTool {
tool: Arc::new(ServiceStatusTool),
terminal_tool: None,
},
};
Ok(built)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::Agent;
use crate::llm_runtime::{router_from_models, SharedLlmRuntime};
use crate::memory::embeddings::EmbeddingService;
use crate::state::SqliteStateStore;
use crate::testing::MockProvider;
use crate::traits::{ModelProvider, StateStore};
use proptest::prelude::*;
use serde_json::json;
use std::collections::HashSet;
use tempfile::{NamedTempFile, TempDir};
async fn build_tool_schemas_for_contract_validation(
diagnostics_enabled: bool,
files_enabled: bool,
subagents_enabled: bool,
) -> anyhow::Result<Vec<serde_json::Value>> {
let mut config: AppConfig = toml::from_str(
r#"
[provider]
api_key = "test-key"
"#,
)?;
config.provider.models.apply_defaults(&config.provider.kind);
config.diagnostics.enabled = diagnostics_enabled;
config.files.enabled = files_enabled;
config.subagents.enabled = subagents_enabled;
config.cli_agents.enabled = false;
config.oauth.enabled = false;
config.tools.disabled.clear();
let io_temp = TempDir::new()?;
config.files.inbox_dir = io_temp.path().join("inbox").display().to_string();
config.files.outbox_dirs = vec![io_temp.path().join("outbox").display().to_string()];
let db_file = NamedTempFile::new()?;
let db_path = db_file.path().display().to_string();
let embedding_service = Arc::new(EmbeddingService::new()?);
let state = Arc::new(SqliteStateStore::new(&db_path, 100, None, embedding_service).await?);
let event_store = Arc::new(EventStore::new(state.pool()).await?);
let config_file = NamedTempFile::new()?;
let model_provider = Arc::new(MockProvider::new()) as Arc<dyn ModelProvider>;
let mut bundle = build_base_tools(
&config,
config_file.path().to_path_buf(),
state.clone(),
event_store.clone(),
32,
8,
)
.await?;
let llm_runtime = SharedLlmRuntime::new(
model_provider,
router_from_models(config.provider.models.clone()),
config.provider.kind,
config.provider.models.primary.clone(),
);
let _optional = register_optional_tools(
&mut bundle.tools,
&config,
state.clone(),
event_store.clone(),
llm_runtime,
None,
bundle.approval_tx.clone(),
bundle.media_tx.clone(),
)
.await?;
let mcp_registry = McpRegistry::new(state.clone() as Arc<dyn StateStore>);
let http_profiles: crate::oauth::SharedHttpProfiles =
Arc::new(tokio::sync::RwLock::new(config.http_auth.clone()));
let _runtime = register_runtime_tools(
&mut bundle.tools,
&config,
config_file.path(),
http_profiles,
state.clone(),
mcp_registry,
bundle.approval_tx.clone(),
)
.await?;
Ok(bundle
.tools
.iter()
.map(|tool| json!({"type":"function","function": tool.schema()}))
.collect())
}
#[test]
fn manifest_has_unique_tool_names() {
let mut seen = HashSet::new();
for tool in BASE_TOOL_REGISTRY {
assert!(
seen.insert(tool.name),
"duplicate tool name in manifest: {}",
tool.name
);
}
}
#[tokio::test]
async fn base_tool_registry_names_match_built_schema_names() {
let schemas = build_tool_schemas_for_contract_validation(false, false, false)
.await
.unwrap();
let built_base_names: Vec<&str> = schemas
.iter()
.take(BASE_TOOL_REGISTRY.len())
.map(|schema| {
schema["function"]["name"]
.as_str()
.expect("built tool schema should expose a function name")
})
.collect();
let registry_names: Vec<&str> = BASE_TOOL_REGISTRY.iter().map(|tool| tool.name).collect();
assert_eq!(built_base_names, registry_names);
}
#[test]
fn base_registry_covers_every_variant() {
use strum::{EnumCount, IntoEnumIterator};
assert_eq!(
BASE_TOOL_REGISTRY.len(),
BaseToolId::COUNT,
"BASE_TOOL_REGISTRY is missing an entry for a BaseToolId variant"
);
for id in BaseToolId::iter() {
assert!(
BASE_TOOL_REGISTRY.iter().any(|spec| spec.id == id),
"BaseToolId::{id:?} has no BASE_TOOL_REGISTRY entry (silently unregistered)"
);
}
}
#[test]
fn optional_registry_covers_every_variant() {
use strum::{EnumCount, IntoEnumIterator};
assert_eq!(
OPTIONAL_TOOL_REGISTRY.len(),
OptionalToolId::COUNT,
"OPTIONAL_TOOL_REGISTRY is missing an entry for an OptionalToolId variant"
);
for id in OptionalToolId::iter() {
assert!(
OPTIONAL_TOOL_REGISTRY.iter().any(|spec| spec.id == id),
"OptionalToolId::{id:?} has no OPTIONAL_TOOL_REGISTRY entry (silently unregistered)"
);
}
}
#[test]
fn runtime_registry_covers_every_variant() {
use strum::{EnumCount, IntoEnumIterator};
assert_eq!(
RUNTIME_TOOL_MANIFEST.len(),
RuntimeToolId::COUNT,
"RUNTIME_TOOL_MANIFEST is missing an entry for a RuntimeToolId variant"
);
for id in RuntimeToolId::iter() {
assert!(
RUNTIME_TOOL_MANIFEST.iter().any(|spec| spec.id == id),
"RuntimeToolId::{id:?} has no RUNTIME_TOOL_MANIFEST entry (silently unregistered)"
);
}
}
#[test]
fn optional_manifest_has_unique_tool_names() {
let mut seen = HashSet::new();
for tool in OPTIONAL_TOOL_REGISTRY {
assert!(
seen.insert(tool.name),
"duplicate optional tool name in manifest: {}",
tool.name
);
}
}
#[test]
fn runtime_manifest_has_unique_tool_names() {
let mut seen = HashSet::new();
for tool in RUNTIME_TOOL_MANIFEST {
assert!(
seen.insert(tool.name),
"duplicate runtime tool name in manifest: {}",
tool.name
);
}
}
#[test]
fn runtime_manifest_matches_expected_pipeline_order() {
let names: Vec<&'static str> = RUNTIME_TOOL_MANIFEST.iter().map(|tool| tool.name).collect();
assert_eq!(
names,
vec![
"manage_mcp",
"manage_people",
"http_request",
"manage_http_auth",
"manage_oauth",
"manage_api",
"spawn_agent",
]
);
}
#[test]
fn runtime_manifest_dependencies_are_declared_before_dependents() {
let mut seen = HashSet::new();
for spec in RUNTIME_TOOL_MANIFEST {
for dep in spec.depends_on {
assert!(
seen.contains(dep),
"runtime tool {} depends on {} but dependency appears after it",
spec.name,
runtime_tool_name(RUNTIME_TOOL_MANIFEST, *dep)
);
}
seen.insert(spec.id);
}
}
#[test]
fn runtime_manifest_validator_accepts_current_manifest() {
assert!(validate_runtime_manifest(RUNTIME_TOOL_MANIFEST).is_ok());
}
#[test]
fn runtime_manifest_validator_rejects_duplicate_ids() {
let manifest = [
RuntimeToolSpec {
id: RuntimeToolId::ManageMcp,
name: "manage_mcp",
enabled_if: runtime_enabled_always,
depends_on: &[],
},
RuntimeToolSpec {
id: RuntimeToolId::ManageMcp,
name: "manage_mcp_duplicate",
enabled_if: runtime_enabled_always,
depends_on: &[],
},
];
let err = validate_runtime_manifest(&manifest)
.unwrap_err()
.to_string();
assert!(err.contains("duplicate tool id"));
}
#[test]
fn runtime_manifest_validator_rejects_unknown_dependencies() {
let manifest = [RuntimeToolSpec {
id: RuntimeToolId::ManageOauth,
name: "manage_oauth",
enabled_if: runtime_enabled_always,
depends_on: &[RuntimeToolId::HttpRequest],
}];
let err = validate_runtime_manifest(&manifest)
.unwrap_err()
.to_string();
assert!(err.contains("unknown dependency"));
}
#[test]
fn runtime_manifest_validator_rejects_dependency_order() {
let manifest = [
RuntimeToolSpec {
id: RuntimeToolId::ManageOauth,
name: "manage_oauth",
enabled_if: runtime_enabled_always,
depends_on: &[RuntimeToolId::HttpRequest],
},
RuntimeToolSpec {
id: RuntimeToolId::HttpRequest,
name: "http_request",
enabled_if: runtime_enabled_always,
depends_on: &[],
},
];
let err = validate_runtime_manifest(&manifest)
.unwrap_err()
.to_string();
assert!(err.contains("dependency appears after it"));
}
#[tokio::test]
async fn built_tools_have_schemas_that_match_agent_contract() {
let schemas = build_tool_schemas_for_contract_validation(true, true, true)
.await
.unwrap();
assert!(!schemas.is_empty(), "expected startup to register tools");
for schema in schemas {
let tool_name = schema["function"]["name"]
.as_str()
.unwrap_or("<unknown>")
.to_string();
let result = Agent::validate_tool_definition_contract(&schema);
assert!(
result.is_ok(),
"tool schema contract failed for {}: {:?}",
tool_name,
result.err()
);
}
}
#[tokio::test]
async fn disabled_tools_are_omitted_from_base_registry() {
let mut config: AppConfig = toml::from_str(
r#"
[provider]
api_key = "test-key"
[tools]
disabled = ["policy_metrics", "goal_trace", "check_environment"]
"#,
)
.unwrap();
config.provider.models.apply_defaults(&config.provider.kind);
let db_file = NamedTempFile::new().unwrap();
let embedding_service = Arc::new(EmbeddingService::new().unwrap());
let state = Arc::new(
SqliteStateStore::new(
&db_file.path().display().to_string(),
100,
None,
embedding_service,
)
.await
.unwrap(),
);
let event_store = Arc::new(EventStore::new(state.pool()).await.unwrap());
let config_file = NamedTempFile::new().unwrap();
let bundle = build_base_tools(
&config,
config_file.path().to_path_buf(),
state,
event_store,
8,
8,
)
.await
.unwrap();
let names: Vec<&str> = bundle.tools.iter().map(|t| t.name()).collect();
assert!(!names.contains(&"policy_metrics"));
assert!(!names.contains(&"goal_trace"));
assert!(!names.contains(&"check_environment"));
assert!(names.contains(&"terminal"));
}
#[tokio::test]
async fn generic_api_tools_register_without_existing_profiles() {
let schemas = build_tool_schemas_for_contract_validation(true, true, true)
.await
.unwrap();
let names: Vec<String> = schemas
.iter()
.filter_map(|schema| schema["function"]["name"].as_str())
.map(ToString::to_string)
.collect();
assert!(names.contains(&"manage_api".to_string()));
assert!(names.contains(&"http_request".to_string()));
assert!(names.contains(&"manage_http_auth".to_string()));
assert!(names.contains(&"manage_oauth".to_string()));
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(8))]
#[test]
fn built_tool_schema_contract_holds_under_random_feature_flags(
diagnostics_enabled in any::<bool>(),
files_enabled in any::<bool>(),
subagents_enabled in any::<bool>(),
) {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async move {
let schemas = build_tool_schemas_for_contract_validation(
diagnostics_enabled,
files_enabled,
subagents_enabled,
)
.await
.unwrap();
assert!(!schemas.is_empty());
for schema in schemas {
let tool_name = schema["function"]["name"]
.as_str()
.unwrap_or("<unknown>")
.to_string();
let result = Agent::validate_tool_definition_contract(&schema);
assert!(
result.is_ok(),
"tool schema contract failed for {}: {:?}",
tool_name,
result.err()
);
}
});
}
}
}