use crate::config::Config;
use crate::mcp_server::progress_wrap::ProgressEnvelope;
use crate::mcp_server::runtime::RuntimeHandles;
use crate::mcp_server::skills_tools::{
DefaultSkillExecutor, DiskSkillSource, SkillSource, SkillsDescribeTool, SkillsExecuteTool,
SkillsListTool,
};
use crate::security::SecurityPolicy;
use crate::tools::{
BrowserOpenTool, CalculatorTool, ComposioTool, ContentSearchTool, CronAddTool, CronListTool,
CronRemoveTool, CronRunTool, CronRunsTool, CronUpdateTool, DiscordSearchTool, FileEditTool,
FileReadTool, FileWriteTool, GitOperationsTool, GlobSearchTool, GoogleWorkspaceTool,
HttpRequestTool, ImageGenTool, ImageInfoTool, JiraTool, LinkedInTool, LlmTaskTool,
Microsoft365Tool, NotionTool, PdfReadTool, PollTool, ScreenshotTool, ShellTool, Tool,
WeatherTool, WebFetchTool, WebSearchTool, WorkspaceTool,
};
use std::path::PathBuf;
use std::sync::Arc;
pub type SkippedEntry = (String, String);
#[must_use]
pub fn build_default_tools(
workspace_dir: &std::path::Path,
) -> (Vec<Arc<dyn Tool>>, Vec<SkippedEntry>) {
let (tools, skipped) = build_baseline(workspace_dir);
(
tools,
skipped
.into_iter()
.map(|(n, r)| (n.to_string(), r.to_string()))
.collect(),
)
}
fn build_baseline(
workspace_dir: &std::path::Path,
) -> (Vec<Arc<dyn Tool>>, Vec<(&'static str, &'static str)>) {
let security = Arc::new(SecurityPolicy::default());
let runtime = Arc::new(crate::runtime::NativeRuntime::new());
let mut tools: Vec<Arc<dyn Tool>> = Vec::new();
tools.push(Arc::new(ShellTool::new(security.clone(), runtime.clone())));
tools.push(Arc::new(FileReadTool::new(security.clone())));
tools.push(Arc::new(FileWriteTool::new(security.clone())));
tools.push(Arc::new(FileEditTool::new(security.clone())));
tools.push(Arc::new(GlobSearchTool::new(security.clone())));
tools.push(Arc::new(ContentSearchTool::new(security.clone())));
tools.push(Arc::new(CalculatorTool::new()));
tools.push(Arc::new(WeatherTool::new()));
tools.push(Arc::new(GitOperationsTool::new(
security.clone(),
PathBuf::from(workspace_dir),
)));
tools.push(Arc::new(HttpRequestTool::new(
security.clone(),
vec!["*".to_string()],
10 * 1024 * 1024,
30,
false,
)));
tools.push(Arc::new(WebFetchTool::new(
security.clone(),
vec!["*".to_string()],
Vec::new(),
10 * 1024 * 1024,
30,
crate::config::schema::FirecrawlConfig::default(),
Vec::new(),
)));
tools.push(Arc::new(WebSearchTool::new(
"duckduckgo".to_string(),
None,
5,
15,
)));
tools.push(Arc::new(PdfReadTool::new(security.clone())));
tools.push(Arc::new(ScreenshotTool::new(security.clone())));
tools.push(Arc::new(ImageInfoTool::new(security.clone())));
tools.push(Arc::new(BrowserOpenTool::new(
security.clone(),
vec!["*".to_string()],
)));
let skipped: Vec<(&'static str, &'static str)> = vec![
("delegate", "needs agent config + credentials"),
("swarm", "needs agent config"),
("workspace", "needs WorkspaceManager"),
("cron_*", "needs boot Config + scheduler"),
("poll", "needs channel map handle"),
("reaction", "needs channel map handle"),
("ask_user", "needs channel map handle"),
("escalate", "needs channel map handle"),
("discord_search", "needs discord.db Memory backend"),
("llm_task", "needs provider creds"),
("image_gen", "needs API key"),
("sessions_*", "needs workspace SessionStore"),
("mcp_*", "wraps external MCP servers, out of scope"),
("browser (full)", "needs browser backend config"),
("browser_delegate", "needs delegate config"),
(
"claude_code / codex_cli / gemini_cli / opencode_cli",
"these are the clients connecting TO this daemon",
),
(
"security_ops / cloud_ops / cloud_patterns",
"needs opt-in config",
),
("sop_*", "needs sops_dir config"),
];
(tools, skipped)
}
#[must_use]
pub fn build_tools_with_config(
workspace_dir: &std::path::Path,
config: &Config,
) -> (Vec<Arc<dyn Tool>>, Vec<SkippedEntry>) {
let security = Arc::new(SecurityPolicy::default());
let (mut tools, baseline_skipped) = build_baseline(workspace_dir);
let mut skipped: Vec<SkippedEntry> = baseline_skipped
.into_iter()
.map(|(n, r)| (n.to_string(), r.to_string()))
.collect();
let skill_source: Arc<dyn SkillSource> = Arc::new(DiskSkillSource::new(
workspace_dir.to_path_buf(),
config.skills.open_skills_enabled,
config.skills.open_skills_dir.clone(),
));
tools.push(Arc::new(SkillsListTool::new(skill_source.clone())));
tools.push(Arc::new(SkillsDescribeTool::new(skill_source.clone())));
tools.push(Arc::new(SkillsExecuteTool::new(
skill_source,
Arc::new(DefaultSkillExecutor::new(security.clone())),
)));
if config.notion.enabled {
let key = if config.notion.api_key.trim().is_empty() {
std::env::var("NOTION_API_KEY").unwrap_or_default()
} else {
config.notion.api_key.trim().to_string()
};
if key.is_empty() {
skipped.push((
"notion".into(),
"enabled but notion.api_key / NOTION_API_KEY missing".into(),
));
} else {
let notion: Arc<dyn Tool> = Arc::new(NotionTool::new(key, security.clone()));
tools.push(
ProgressEnvelope::new(
notion,
"notion: sending request",
"notion: response received",
)
.into_arc(),
);
}
} else {
skipped.push(("notion".into(), "disabled (notion.enabled=false)".into()));
}
if config.jira.enabled {
let token = if config.jira.api_token.trim().is_empty() {
std::env::var("JIRA_API_TOKEN").unwrap_or_default()
} else {
config.jira.api_token.trim().to_string()
};
if token.is_empty() {
skipped.push((
"jira".into(),
"enabled but jira.api_token / JIRA_API_TOKEN missing".into(),
));
} else if config.jira.base_url.trim().is_empty() {
skipped.push(("jira".into(), "enabled but jira.base_url empty".into()));
} else if config.jira.email.trim().is_empty() {
skipped.push(("jira".into(), "enabled but jira.email empty".into()));
} else {
tools.push(Arc::new(JiraTool::new(
config.jira.base_url.trim().to_string(),
config.jira.email.trim().to_string(),
token,
config.jira.allowed_actions.clone(),
security.clone(),
config.jira.timeout_secs,
)));
}
} else {
skipped.push(("jira".into(), "disabled (jira.enabled=false)".into()));
}
if config.composio.enabled {
let key = match &config.composio.api_key {
Some(k) if !k.trim().is_empty() => k.trim().to_string(),
_ => std::env::var("COMPOSIO_API_KEY").unwrap_or_default(),
};
if key.is_empty() {
skipped.push((
"composio".into(),
"enabled but composio.api_key / COMPOSIO_API_KEY missing".into(),
));
} else {
tools.push(Arc::new(ComposioTool::new(
&key,
Some(&config.composio.entity_id),
security.clone(),
)));
}
} else {
skipped.push((
"composio".into(),
"disabled (composio.enabled=false)".into(),
));
}
if config.google_workspace.enabled {
let gws: Arc<dyn Tool> = Arc::new(GoogleWorkspaceTool::new(
security.clone(),
config.google_workspace.allowed_services.clone(),
config.google_workspace.allowed_operations.clone(),
config.google_workspace.credentials_path.clone(),
config.google_workspace.default_account.clone(),
config.google_workspace.rate_limit_per_minute,
config.google_workspace.timeout_secs,
config.google_workspace.audit_log,
));
tools.push(
ProgressEnvelope::new(
gws,
"google_workspace: invoking gws",
"google_workspace: done",
)
.into_arc(),
);
} else {
skipped.push((
"google_workspace".into(),
"disabled (google_workspace.enabled=false)".into(),
));
}
if config.linkedin.enabled {
tools.push(Arc::new(LinkedInTool::new(
security.clone(),
workspace_dir.to_path_buf(),
config.linkedin.api_version.clone(),
config.linkedin.content.clone(),
config.linkedin.image.clone(),
)));
} else {
skipped.push((
"linkedin".into(),
"disabled (linkedin.enabled=false)".into(),
));
}
if config.microsoft365.enabled {
let ms = &config.microsoft365;
let tenant_id = ms.tenant_id.as_deref().unwrap_or("").trim().to_string();
let client_id = ms.client_id.as_deref().unwrap_or("").trim().to_string();
if tenant_id.is_empty() || client_id.is_empty() {
skipped.push((
"microsoft365".into(),
"enabled but tenant_id or client_id empty".into(),
));
} else if ms.auth_flow.trim() == "client_credentials"
&& ms
.client_secret
.as_deref()
.is_none_or(|s| s.trim().is_empty())
{
skipped.push((
"microsoft365".into(),
"client_credentials flow needs a client_secret".into(),
));
} else {
let resolved = crate::tools::microsoft365::types::Microsoft365ResolvedConfig {
tenant_id,
client_id,
client_secret: ms.client_secret.clone(),
auth_flow: ms.auth_flow.clone(),
scopes: ms.scopes.clone(),
token_cache_encrypted: ms.token_cache_encrypted,
user_id: ms.user_id.as_deref().unwrap_or("me").to_string(),
};
let cache_dir = config.config_path.parent().unwrap_or(workspace_dir);
match Microsoft365Tool::new(resolved, security.clone(), cache_dir) {
Ok(tool) => tools.push(Arc::new(tool)),
Err(e) => skipped.push((
"microsoft365".into(),
format!("token cache init failed: {e}"),
)),
}
}
} else {
skipped.push((
"microsoft365".into(),
"disabled (microsoft365.enabled=false)".into(),
));
}
(tools, skipped)
}
#[must_use]
pub fn build_tools_with_runtime(
workspace_dir: &std::path::Path,
config: &Config,
runtime: &RuntimeHandles,
) -> (Vec<Arc<dyn Tool>>, Vec<SkippedEntry>) {
let security = Arc::new(SecurityPolicy::default());
let (mut tools, mut skipped) = build_tools_with_config(workspace_dir, config);
let config_arc = Arc::new(config.clone());
tools.push(Arc::new(CronAddTool::new(
config_arc.clone(),
security.clone(),
)));
tools.push(Arc::new(CronListTool::new(config_arc.clone())));
tools.push(Arc::new(CronRemoveTool::new(
config_arc.clone(),
security.clone(),
)));
tools.push(Arc::new(CronUpdateTool::new(
config_arc.clone(),
security.clone(),
)));
tools.push(Arc::new(CronRunTool::new(
config_arc.clone(),
security.clone(),
)));
tools.push(Arc::new(CronRunsTool::new(config_arc.clone())));
drop_skip(&mut skipped, "cron_*");
{
let provider = config
.default_provider
.clone()
.unwrap_or_else(|| "openrouter".into());
let model = config
.default_model
.clone()
.unwrap_or_else(|| "openai/gpt-4o-mini".into());
let runtime_opts = runtime
.provider_runtime_options
.as_deref()
.cloned()
.unwrap_or_else(|| crate::providers::ProviderRuntimeOptions {
auth_profile_override: None,
provider_api_url: config.api_url.clone(),
construct_dir: config.config_path.parent().map(std::path::PathBuf::from),
secrets_encrypt: config.secrets.encrypt,
reasoning_enabled: config.runtime.reasoning_enabled,
reasoning_effort: config.runtime.reasoning_effort.clone(),
provider_timeout_secs: Some(config.provider_timeout_secs),
extra_headers: config.extra_headers.clone(),
api_path: config.api_path.clone(),
provider_max_tokens: config.provider_max_tokens,
});
tools.push(Arc::new(LlmTaskTool::new(
security.clone(),
provider,
model,
config.default_temperature,
config.api_key.clone(),
runtime_opts,
)));
drop_skip(&mut skipped, "llm_task");
}
if config.image_gen.enabled {
tools.push(Arc::new(ImageGenTool::new(
security.clone(),
workspace_dir.to_path_buf(),
config.image_gen.default_model.clone(),
config.image_gen.api_key_env.clone(),
)));
}
drop_skip(&mut skipped, "image_gen");
if config.sop.sops_dir.is_some() {
let engine = Arc::new(std::sync::Mutex::new(crate::sop::SopEngine::new(
config.sop.clone(),
)));
tools.push(Arc::new(crate::tools::SopListTool::new(engine.clone())));
tools.push(Arc::new(crate::tools::SopExecuteTool::new(engine.clone())));
tools.push(Arc::new(crate::tools::SopAdvanceTool::new(engine.clone())));
tools.push(Arc::new(crate::tools::SopApproveTool::new(engine.clone())));
tools.push(Arc::new(crate::tools::SopStatusTool::new(engine)));
drop_skip(&mut skipped, "sop_*");
}
if let Some(ws) = runtime.workspace_manager.clone() {
tools.push(Arc::new(WorkspaceTool::new(ws, security.clone())));
drop_skip(&mut skipped, "workspace");
}
if let Some(map) = runtime.channel_map.clone() {
tools.push(Arc::new(PollTool::new(security.clone(), map)));
drop_skip(&mut skipped, "poll");
}
if runtime.reaction_channels.is_some() {
drop_skip(&mut skipped, "reaction");
}
if runtime.ask_user_channels.is_some() {
drop_skip(&mut skipped, "ask_user");
}
if runtime.escalate_channels.is_some() {
drop_skip(&mut skipped, "escalate");
}
if let Some(mem) = runtime.discord_memory.clone() {
tools.push(Arc::new(DiscordSearchTool::new(mem)));
drop_skip(&mut skipped, "discord_search");
}
if let Some(backend) = runtime.session_store.clone() {
tools.push(Arc::new(crate::tools::SessionsListTool::new(
backend.clone(),
)));
tools.push(Arc::new(crate::tools::SessionsHistoryTool::new(
backend.clone(),
security.clone(),
)));
tools.push(Arc::new(crate::tools::SessionsSendTool::new(
backend,
security.clone(),
)));
drop_skip(&mut skipped, "sessions_*");
}
if let (Some(agents), Some(opts)) = (
runtime.agent_config.clone(),
runtime.provider_runtime_options.clone(),
) {
if !agents.is_empty() {
let fallback = runtime
.fallback_api_key
.as_ref()
.map(|s| s.as_ref().to_string());
let delegate = crate::tools::DelegateTool::new_with_options(
(*agents).clone(),
fallback.clone(),
security.clone(),
(*opts).clone(),
);
tools.push(Arc::new(delegate));
drop_skip(&mut skipped, "delegate");
if !config.swarms.is_empty() {
tools.push(Arc::new(crate::tools::SwarmTool::new(
config.swarms.clone(),
(*agents).clone(),
fallback,
security.clone(),
(*opts).clone(),
)));
drop_skip(&mut skipped, "swarm");
}
}
}
if let Some(pre) = runtime.pre_built_tools.as_ref() {
let pre_names: std::collections::HashSet<String> =
pre.iter().map(|t| t.name().to_string()).collect();
tools.retain(|t| !pre_names.contains(t.name()));
tools.extend(pre.iter().cloned());
let present_names: std::collections::HashSet<String> =
tools.iter().map(|t| t.name().to_string()).collect();
skipped.retain(|(name, _)| {
if let Some(prefix) = name.strip_suffix('*') {
!present_names.iter().any(|n| n.starts_with(prefix))
} else {
!present_names.contains(name)
}
});
}
(tools, skipped)
}
fn drop_skip(skipped: &mut Vec<SkippedEntry>, name: &str) {
skipped.retain(|(n, _)| n != name);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
fn names(tools: &[Arc<dyn Tool>]) -> Vec<&str> {
tools.iter().map(|t| t.name()).collect()
}
#[test]
fn baseline_registry_is_stable() {
let (tools, _) = build_default_tools(std::path::Path::new("."));
let n = names(&tools);
for must in [
"shell",
"file_read",
"file_write",
"file_edit",
"glob_search",
"content_search",
"calculator",
"weather",
"git_operations",
"http_request",
"web_fetch",
"web_search_tool",
"pdf_read",
"screenshot",
"image_info",
"browser_open",
] {
assert!(n.contains(&must), "missing baseline tool `{must}`");
}
}
#[test]
fn empty_config_skips_all_integrations_but_keeps_baseline_and_skills() {
let config = Config::default();
let (tools, skipped) = build_tools_with_config(std::path::Path::new("."), &config);
let n = names(&tools);
assert!(n.contains(&"shell"));
assert!(n.contains(&"skills_list"));
assert!(n.contains(&"skills_describe"));
assert!(n.contains(&"skills_execute"));
assert!(!n.contains(&"notion"));
assert!(!n.contains(&"jira"));
assert!(!n.contains(&"composio"));
assert!(!n.contains(&"google_workspace"));
assert!(!n.contains(&"linkedin"));
assert!(!n.contains(&"microsoft365"));
let names_skipped: Vec<&str> = skipped.iter().map(|(n, _)| n.as_str()).collect();
for must in [
"notion",
"jira",
"composio",
"google_workspace",
"linkedin",
"microsoft365",
] {
assert!(
names_skipped.contains(&must),
"expected `{must}` in skipped list"
);
}
}
#[test]
fn notion_registered_when_api_key_present() {
let mut config = Config::default();
config.notion.enabled = true;
config.notion.api_key = "secret_dummy_for_test".into();
let (tools, _skipped) = build_tools_with_config(std::path::Path::new("."), &config);
let n = names(&tools);
assert!(n.contains(&"notion"), "expected notion tool; got {n:?}");
}
#[test]
fn jira_enabled_but_missing_creds_is_skipped() {
let mut config = Config::default();
config.jira.enabled = true; let (tools, skipped) = build_tools_with_config(std::path::Path::new("."), &config);
let n = names(&tools);
assert!(!n.contains(&"jira"));
assert!(skipped.iter().any(|(name, _)| name == "jira"));
}
}