1pub mod ask_user;
19pub mod backup_tool;
20pub mod browser;
21pub mod browser_delegate;
22pub mod browser_open;
23pub mod calculator;
24pub mod canvas;
25pub mod claude_code;
26pub mod claude_code_runner;
27pub mod cli_discovery;
28pub mod cloud_ops;
29pub mod cloud_patterns;
30pub mod codex_cli;
31pub mod composio;
32pub mod content_search;
33pub mod cron_add;
34pub mod cron_list;
35pub mod cron_remove;
36pub mod cron_run;
37pub mod cron_runs;
38pub mod cron_update;
39pub mod data_management;
40pub mod delegate;
41pub mod discord_search;
42pub mod escalate;
43pub mod file_edit;
44pub mod file_read;
45pub mod file_write;
46pub mod gemini_cli;
47pub mod git_operations;
48pub mod glob_search;
49pub mod google_workspace;
50#[cfg(feature = "hardware")]
51pub mod hardware_board_info;
52#[cfg(feature = "hardware")]
53pub mod hardware_memory_map;
54#[cfg(feature = "hardware")]
55pub mod hardware_memory_read;
56pub mod http_request;
57pub mod image_gen;
58pub mod image_info;
59pub mod jira_tool;
60pub mod linkedin;
61pub mod linkedin_client;
62pub mod llm_task;
63pub mod mcp_client;
64pub mod mcp_deferred;
65pub mod mcp_protocol;
66pub mod mcp_tool;
67pub mod mcp_transport;
68pub mod microsoft365;
69pub mod model_routing_config;
70pub mod model_switch;
71pub mod node_capabilities;
72pub mod node_tool;
73pub mod notion_tool;
74pub mod opencode_cli;
75pub mod pdf_read;
76pub mod pipeline;
77pub mod poll;
78pub mod progress;
79pub mod project_intel;
80pub mod proxy_config;
81pub mod pushover;
82pub mod reaction;
83pub mod read_skill;
84pub mod report_template_tool;
85pub mod report_templates;
86pub mod schedule;
87pub mod schema;
88pub mod screenshot;
89pub mod security_ops;
90pub mod sessions;
91pub mod shell;
92pub mod skill_http;
93pub mod skill_tool;
94pub mod sop_advance;
95pub mod sop_approve;
96pub mod sop_execute;
97pub mod sop_list;
98pub mod sop_status;
99pub mod swarm;
100pub mod text_browser;
101pub mod tool_search;
102pub mod traits;
103pub mod verifiable_intent;
104pub mod weather_tool;
105pub mod web_fetch;
106mod web_search_provider_routing;
107pub mod web_search_tool;
108pub mod workspace_tool;
109
110pub use ask_user::AskUserTool;
111pub use backup_tool::BackupTool;
112pub use browser::{BrowserTool, ComputerUseConfig};
113#[allow(unused_imports)]
114pub use browser_delegate::{BrowserDelegateConfig, BrowserDelegateTool};
115pub use browser_open::BrowserOpenTool;
116pub use calculator::CalculatorTool;
117pub use canvas::{CanvasStore, CanvasTool};
118pub use claude_code::ClaudeCodeTool;
119pub use claude_code_runner::ClaudeCodeRunnerTool;
120pub use cloud_ops::CloudOpsTool;
121pub use cloud_patterns::CloudPatternsTool;
122pub use codex_cli::CodexCliTool;
123pub use composio::ComposioTool;
124pub use content_search::ContentSearchTool;
125pub use cron_add::CronAddTool;
126pub use cron_list::CronListTool;
127pub use cron_remove::CronRemoveTool;
128pub use cron_run::CronRunTool;
129pub use cron_runs::CronRunsTool;
130pub use cron_update::CronUpdateTool;
131pub use data_management::DataManagementTool;
132pub use delegate::DelegateTool;
133#[allow(unused_imports)]
135pub use delegate::{BackgroundDelegateResult, BackgroundTaskStatus};
136pub use discord_search::DiscordSearchTool;
137pub use escalate::EscalateToHumanTool;
138pub use file_edit::FileEditTool;
139pub use file_read::FileReadTool;
140pub use file_write::FileWriteTool;
141pub use gemini_cli::GeminiCliTool;
142pub use git_operations::GitOperationsTool;
143pub use glob_search::GlobSearchTool;
144pub use google_workspace::GoogleWorkspaceTool;
145#[cfg(feature = "hardware")]
146pub use hardware_board_info::HardwareBoardInfoTool;
147#[cfg(feature = "hardware")]
148pub use hardware_memory_map::HardwareMemoryMapTool;
149#[cfg(feature = "hardware")]
150pub use hardware_memory_read::HardwareMemoryReadTool;
151pub use http_request::HttpRequestTool;
152pub use image_gen::ImageGenTool;
153pub use image_info::ImageInfoTool;
154pub use jira_tool::JiraTool;
155pub use linkedin::LinkedInTool;
156pub use llm_task::LlmTaskTool;
157pub use mcp_client::McpRegistry;
158pub use mcp_deferred::{ActivatedToolSet, DeferredMcpToolSet};
159pub use mcp_tool::McpToolWrapper;
160pub use microsoft365::Microsoft365Tool;
161pub use model_routing_config::ModelRoutingConfigTool;
162pub use model_switch::ModelSwitchTool;
163#[allow(unused_imports)]
164pub use node_tool::NodeTool;
165pub use notion_tool::NotionTool;
166pub use opencode_cli::OpenCodeCliTool;
167pub use pdf_read::PdfReadTool;
168pub use poll::{ChannelMapHandle, PollTool};
169#[allow(unused_imports)]
170pub use progress::{
171 NoopProgressSink, ProgressHandle, ProgressSink, ProgressToken, SharedProgressSink, noop_sink,
172};
173pub use project_intel::ProjectIntelTool;
174pub use proxy_config::ProxyConfigTool;
175pub use pushover::PushoverTool;
176pub use reaction::ReactionTool;
177pub use read_skill::ReadSkillTool;
178pub use report_template_tool::ReportTemplateTool;
179pub use schedule::ScheduleTool;
180#[allow(unused_imports)]
181pub use schema::{CleaningStrategy, SchemaCleanr};
182pub use screenshot::ScreenshotTool;
183pub use security_ops::SecurityOpsTool;
184pub use sessions::{SessionsHistoryTool, SessionsListTool, SessionsSendTool};
185pub use shell::ShellTool;
186#[allow(unused_imports)]
187pub use skill_http::SkillHttpTool;
188#[allow(unused_imports)]
189pub use skill_tool::SkillShellTool;
190pub use sop_advance::SopAdvanceTool;
191pub use sop_approve::SopApproveTool;
192pub use sop_execute::SopExecuteTool;
193pub use sop_list::SopListTool;
194pub use sop_status::SopStatusTool;
195pub use swarm::SwarmTool;
196pub use text_browser::TextBrowserTool;
197pub use tool_search::ToolSearchTool;
198pub use traits::Tool;
199#[allow(unused_imports)]
200pub use traits::{ToolResult, ToolSpec};
201pub use verifiable_intent::VerifiableIntentTool;
202pub use weather_tool::WeatherTool;
203pub use web_fetch::WebFetchTool;
204pub use web_search_tool::WebSearchTool;
205pub use workspace_tool::WorkspaceTool;
206
207use crate::config::{Config, DelegateAgentConfig};
208use crate::memory::Memory;
209use crate::runtime::{NativeRuntime, RuntimeAdapter};
210use crate::security::{SecurityPolicy, create_sandbox};
211use async_trait::async_trait;
212use parking_lot::RwLock;
213use std::collections::HashMap;
214use std::sync::Arc;
215
216pub type DelegateParentToolsHandle = Arc<RwLock<Vec<Arc<dyn Tool>>>>;
219
220pub struct ArcToolRef(pub Arc<dyn Tool>);
222
223#[async_trait]
224impl Tool for ArcToolRef {
225 fn name(&self) -> &str {
226 self.0.name()
227 }
228
229 fn description(&self) -> &str {
230 self.0.description()
231 }
232
233 fn parameters_schema(&self) -> serde_json::Value {
234 self.0.parameters_schema()
235 }
236
237 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
238 self.0.execute(args).await
239 }
240}
241
242#[derive(Clone)]
243struct ArcDelegatingTool {
244 inner: Arc<dyn Tool>,
245}
246
247impl ArcDelegatingTool {
248 fn boxed(inner: Arc<dyn Tool>) -> Box<dyn Tool> {
249 Box::new(Self { inner })
250 }
251}
252
253#[async_trait]
254impl Tool for ArcDelegatingTool {
255 fn name(&self) -> &str {
256 self.inner.name()
257 }
258
259 fn description(&self) -> &str {
260 self.inner.description()
261 }
262
263 fn parameters_schema(&self) -> serde_json::Value {
264 self.inner.parameters_schema()
265 }
266
267 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
268 self.inner.execute(args).await
269 }
270}
271
272fn boxed_registry_from_arcs(tools: Vec<Arc<dyn Tool>>) -> Vec<Box<dyn Tool>> {
273 tools.into_iter().map(ArcDelegatingTool::boxed).collect()
274}
275
276pub fn default_tools(security: Arc<SecurityPolicy>) -> Vec<Box<dyn Tool>> {
278 default_tools_with_runtime(security, Arc::new(NativeRuntime::new()))
279}
280
281pub fn default_tools_with_runtime(
283 security: Arc<SecurityPolicy>,
284 runtime: Arc<dyn RuntimeAdapter>,
285) -> Vec<Box<dyn Tool>> {
286 vec![
287 Box::new(ShellTool::new(security.clone(), runtime)),
288 Box::new(FileReadTool::new(security.clone())),
289 Box::new(FileWriteTool::new(security.clone())),
290 Box::new(FileEditTool::new(security.clone())),
291 Box::new(GlobSearchTool::new(security.clone())),
292 Box::new(ContentSearchTool::new(security)),
293 ]
294}
295
296pub fn register_skill_tools(
302 tools_registry: &mut Vec<Box<dyn Tool>>,
303 skills: &[crate::skills::Skill],
304 security: Arc<SecurityPolicy>,
305) {
306 let skill_tools = crate::skills::skills_to_tools(skills, security);
307 let existing_names: std::collections::HashSet<String> = tools_registry
308 .iter()
309 .map(|t| t.name().to_string())
310 .collect();
311 for tool in skill_tools {
312 if existing_names.contains(tool.name()) {
313 tracing::warn!(
314 "Skill tool '{}' shadows built-in tool, skipping",
315 tool.name()
316 );
317 } else {
318 tools_registry.push(tool);
319 }
320 }
321}
322
323#[allow(
325 clippy::implicit_hasher,
326 clippy::too_many_arguments,
327 clippy::type_complexity
328)]
329pub fn all_tools(
330 config: Arc<Config>,
331 security: &Arc<SecurityPolicy>,
332 memory: Arc<dyn Memory>,
333 composio_key: Option<&str>,
334 composio_entity_id: Option<&str>,
335 browser_config: &crate::config::BrowserConfig,
336 http_config: &crate::config::HttpRequestConfig,
337 web_fetch_config: &crate::config::WebFetchConfig,
338 workspace_dir: &std::path::Path,
339 agents: &HashMap<String, DelegateAgentConfig>,
340 fallback_api_key: Option<&str>,
341 root_config: &crate::config::Config,
342 canvas_store: Option<CanvasStore>,
343) -> (
344 Vec<Box<dyn Tool>>,
345 Option<DelegateParentToolsHandle>,
346 Option<ChannelMapHandle>,
347 ChannelMapHandle,
348 Option<ChannelMapHandle>,
349 Option<ChannelMapHandle>,
350) {
351 all_tools_with_runtime(
352 config,
353 security,
354 Arc::new(NativeRuntime::new()),
355 memory,
356 composio_key,
357 composio_entity_id,
358 browser_config,
359 http_config,
360 web_fetch_config,
361 workspace_dir,
362 agents,
363 fallback_api_key,
364 root_config,
365 canvas_store,
366 )
367}
368
369#[allow(
371 clippy::implicit_hasher,
372 clippy::too_many_arguments,
373 clippy::type_complexity
374)]
375pub fn all_tools_with_runtime(
376 config: Arc<Config>,
377 security: &Arc<SecurityPolicy>,
378 runtime: Arc<dyn RuntimeAdapter>,
379 _memory: Arc<dyn Memory>,
380 composio_key: Option<&str>,
381 composio_entity_id: Option<&str>,
382 browser_config: &crate::config::BrowserConfig,
383 http_config: &crate::config::HttpRequestConfig,
384 web_fetch_config: &crate::config::WebFetchConfig,
385 workspace_dir: &std::path::Path,
386 agents: &HashMap<String, DelegateAgentConfig>,
387 fallback_api_key: Option<&str>,
388 root_config: &crate::config::Config,
389 canvas_store: Option<CanvasStore>,
390) -> (
391 Vec<Box<dyn Tool>>,
392 Option<DelegateParentToolsHandle>,
393 Option<ChannelMapHandle>,
394 ChannelMapHandle,
395 Option<ChannelMapHandle>,
396 Option<ChannelMapHandle>,
397) {
398 let has_shell_access = runtime.has_shell_access();
399 let sandbox = create_sandbox(&root_config.security);
400 let mut tool_arcs: Vec<Arc<dyn Tool>> = vec![
401 Arc::new(
402 ShellTool::new_with_sandbox(security.clone(), runtime, sandbox)
403 .with_timeout_secs(root_config.shell_tool.timeout_secs),
404 ),
405 Arc::new(FileReadTool::new(security.clone())),
406 Arc::new(FileWriteTool::new(security.clone())),
407 Arc::new(FileEditTool::new(security.clone())),
408 Arc::new(GlobSearchTool::new(security.clone())),
409 Arc::new(ContentSearchTool::new(security.clone())),
410 Arc::new(CronAddTool::new(config.clone(), security.clone())),
411 Arc::new(CronListTool::new(config.clone())),
412 Arc::new(CronRemoveTool::new(config.clone(), security.clone())),
413 Arc::new(CronUpdateTool::new(config.clone(), security.clone())),
414 Arc::new(CronRunTool::new(config.clone(), security.clone())),
415 Arc::new(CronRunsTool::new(config.clone())),
416 Arc::new(ScheduleTool::new(security.clone(), root_config.clone())),
418 Arc::new(ModelRoutingConfigTool::new(
419 config.clone(),
420 security.clone(),
421 )),
422 Arc::new(ModelSwitchTool::new(security.clone())),
423 Arc::new(ProxyConfigTool::new(config.clone(), security.clone())),
424 Arc::new(GitOperationsTool::new(
425 security.clone(),
426 workspace_dir.to_path_buf(),
427 )),
428 Arc::new(PushoverTool::new(
429 security.clone(),
430 workspace_dir.to_path_buf(),
431 )),
432 Arc::new(CalculatorTool::new()),
433 Arc::new(WeatherTool::new()),
434 Arc::new(CanvasTool::new(
435 canvas_store.unwrap_or_else(crate::tools::canvas::global_store),
436 )),
437 ];
438
439 {
444 let llm_task_provider = root_config
445 .default_provider
446 .clone()
447 .unwrap_or_else(|| "openrouter".to_string());
448 let llm_task_model = root_config
449 .default_model
450 .clone()
451 .unwrap_or_else(|| "openai/gpt-4o-mini".to_string());
452 let llm_task_runtime_options = crate::providers::ProviderRuntimeOptions {
453 auth_profile_override: None,
454 provider_api_url: root_config.api_url.clone(),
455 construct_dir: root_config
456 .config_path
457 .parent()
458 .map(std::path::PathBuf::from),
459 secrets_encrypt: root_config.secrets.encrypt,
460 reasoning_enabled: root_config.runtime.reasoning_enabled,
461 reasoning_effort: root_config.runtime.reasoning_effort.clone(),
462 provider_timeout_secs: Some(root_config.provider_timeout_secs),
463 extra_headers: root_config.extra_headers.clone(),
464 api_path: root_config.api_path.clone(),
465 provider_max_tokens: root_config.provider_max_tokens,
466 };
467 tool_arcs.push(Arc::new(LlmTaskTool::new(
468 security.clone(),
469 llm_task_provider,
470 llm_task_model,
471 root_config.default_temperature,
472 root_config.api_key.clone(),
473 llm_task_runtime_options,
474 )));
475 }
476
477 if matches!(
478 root_config.skills.prompt_injection_mode,
479 crate::config::SkillsPromptInjectionMode::Compact
480 ) {
481 tool_arcs.push(Arc::new(ReadSkillTool::new(
482 workspace_dir.to_path_buf(),
483 root_config.skills.open_skills_enabled,
484 root_config.skills.open_skills_dir.clone(),
485 )));
486 }
487
488 if browser_config.enabled {
489 tool_arcs.push(Arc::new(BrowserOpenTool::new(
491 security.clone(),
492 browser_config.allowed_domains.clone(),
493 )));
494 tool_arcs.push(Arc::new(BrowserTool::new_with_backend(
496 security.clone(),
497 browser_config.allowed_domains.clone(),
498 browser_config.session_name.clone(),
499 browser_config.backend.clone(),
500 browser_config.native_headless,
501 browser_config.native_webdriver_url.clone(),
502 browser_config.native_chrome_path.clone(),
503 ComputerUseConfig {
504 endpoint: browser_config.computer_use.endpoint.clone(),
505 api_key: browser_config.computer_use.api_key.clone(),
506 timeout_ms: browser_config.computer_use.timeout_ms,
507 allow_remote_endpoint: browser_config.computer_use.allow_remote_endpoint,
508 window_allowlist: browser_config.computer_use.window_allowlist.clone(),
509 max_coordinate_x: browser_config.computer_use.max_coordinate_x,
510 max_coordinate_y: browser_config.computer_use.max_coordinate_y,
511 },
512 )));
513 }
514
515 if root_config.browser_delegate.enabled {
517 if has_shell_access {
518 tool_arcs.push(Arc::new(BrowserDelegateTool::new(
519 security.clone(),
520 root_config.browser_delegate.clone(),
521 )));
522 } else {
523 tracing::warn!(
524 "browser_delegate: skipped registration because the current runtime does not allow shell access"
525 );
526 }
527 }
528
529 if http_config.enabled {
530 tool_arcs.push(Arc::new(HttpRequestTool::new(
531 security.clone(),
532 http_config.allowed_domains.clone(),
533 http_config.max_response_size,
534 http_config.timeout_secs,
535 http_config.allow_private_hosts,
536 )));
537 }
538
539 if web_fetch_config.enabled {
540 tool_arcs.push(Arc::new(WebFetchTool::new(
541 security.clone(),
542 web_fetch_config.allowed_domains.clone(),
543 web_fetch_config.blocked_domains.clone(),
544 web_fetch_config.max_response_size,
545 web_fetch_config.timeout_secs,
546 web_fetch_config.firecrawl.clone(),
547 web_fetch_config.allowed_private_hosts.clone(),
548 )));
549 }
550
551 if root_config.text_browser.enabled {
553 tool_arcs.push(Arc::new(TextBrowserTool::new(
554 security.clone(),
555 root_config.text_browser.preferred_browser.clone(),
556 root_config.text_browser.timeout_secs,
557 )));
558 }
559
560 if root_config.web_search.enabled {
562 tool_arcs.push(Arc::new(WebSearchTool::new_with_config(
563 root_config.web_search.provider.clone(),
564 root_config.web_search.brave_api_key.clone(),
565 root_config.web_search.searxng_instance_url.clone(),
566 root_config.web_search.max_results,
567 root_config.web_search.timeout_secs,
568 root_config.config_path.clone(),
569 root_config.secrets.encrypt,
570 )));
571 }
572
573 if root_config.notion.enabled {
575 let notion_api_key = if root_config.notion.api_key.trim().is_empty() {
576 std::env::var("NOTION_API_KEY").unwrap_or_default()
577 } else {
578 root_config.notion.api_key.trim().to_string()
579 };
580 if notion_api_key.trim().is_empty() {
581 tracing::warn!(
582 "Notion tool enabled but no API key found (set notion.api_key or NOTION_API_KEY env var)"
583 );
584 } else {
585 tool_arcs.push(Arc::new(NotionTool::new(notion_api_key, security.clone())));
586 }
587 }
588
589 if root_config.jira.enabled {
591 let api_token = if root_config.jira.api_token.trim().is_empty() {
592 std::env::var("JIRA_API_TOKEN").unwrap_or_default()
593 } else {
594 root_config.jira.api_token.trim().to_string()
595 };
596 if api_token.trim().is_empty() {
597 tracing::warn!(
598 "Jira tool enabled but no API token found (set jira.api_token or JIRA_API_TOKEN env var)"
599 );
600 } else if root_config.jira.base_url.trim().is_empty() {
601 tracing::warn!("Jira tool enabled but jira.base_url is empty — skipping registration");
602 } else if root_config.jira.email.trim().is_empty() {
603 tracing::warn!("Jira tool enabled but jira.email is empty — skipping registration");
604 } else {
605 tool_arcs.push(Arc::new(JiraTool::new(
606 root_config.jira.base_url.trim().to_string(),
607 root_config.jira.email.trim().to_string(),
608 api_token,
609 root_config.jira.allowed_actions.clone(),
610 security.clone(),
611 root_config.jira.timeout_secs,
612 )));
613 }
614 }
615
616 if root_config.project_intel.enabled {
618 tool_arcs.push(Arc::new(ProjectIntelTool::new(
619 root_config.project_intel.default_language.clone(),
620 root_config.project_intel.risk_sensitivity.clone(),
621 )));
622 tool_arcs.push(Arc::new(ReportTemplateTool::new()));
624 }
625
626 if root_config.security_ops.enabled {
628 tool_arcs.push(Arc::new(SecurityOpsTool::new(
629 root_config.security_ops.clone(),
630 )));
631 }
632
633 if root_config.backup.enabled {
635 tool_arcs.push(Arc::new(BackupTool::new(
636 workspace_dir.to_path_buf(),
637 root_config.backup.include_dirs.clone(),
638 root_config.backup.max_keep,
639 )));
640 }
641
642 if root_config.data_retention.enabled {
644 tool_arcs.push(Arc::new(DataManagementTool::new(
645 workspace_dir.to_path_buf(),
646 root_config.data_retention.retention_days,
647 )));
648 }
649
650 if root_config.cloud_ops.enabled {
652 tool_arcs.push(Arc::new(CloudOpsTool::new(root_config.cloud_ops.clone())));
653 tool_arcs.push(Arc::new(CloudPatternsTool::new()));
654 }
655
656 if root_config.google_workspace.enabled && has_shell_access {
658 tool_arcs.push(Arc::new(GoogleWorkspaceTool::new(
659 security.clone(),
660 root_config.google_workspace.allowed_services.clone(),
661 root_config.google_workspace.allowed_operations.clone(),
662 root_config.google_workspace.credentials_path.clone(),
663 root_config.google_workspace.default_account.clone(),
664 root_config.google_workspace.rate_limit_per_minute,
665 root_config.google_workspace.timeout_secs,
666 root_config.google_workspace.audit_log,
667 )));
668 } else if root_config.google_workspace.enabled {
669 tracing::warn!(
670 "google_workspace: skipped registration because shell access is unavailable"
671 );
672 }
673
674 if root_config.claude_code.enabled {
676 tool_arcs.push(Arc::new(ClaudeCodeTool::new(
677 security.clone(),
678 root_config.claude_code.clone(),
679 )));
680 }
681
682 if root_config.claude_code_runner.enabled {
684 let gateway_url = format!(
685 "http://{}:{}",
686 root_config.gateway.host, root_config.gateway.port
687 );
688 tool_arcs.push(Arc::new(ClaudeCodeRunnerTool::new(
689 security.clone(),
690 root_config.claude_code_runner.clone(),
691 gateway_url,
692 )));
693 }
694
695 if root_config.codex_cli.enabled {
697 tool_arcs.push(Arc::new(CodexCliTool::new(
698 security.clone(),
699 root_config.codex_cli.clone(),
700 )));
701 }
702
703 if root_config.gemini_cli.enabled {
705 tool_arcs.push(Arc::new(GeminiCliTool::new(
706 security.clone(),
707 root_config.gemini_cli.clone(),
708 )));
709 }
710
711 if root_config.opencode_cli.enabled {
713 tool_arcs.push(Arc::new(OpenCodeCliTool::new(
714 security.clone(),
715 root_config.opencode_cli.clone(),
716 )));
717 }
718
719 tool_arcs.push(Arc::new(PdfReadTool::new(security.clone())));
721
722 tool_arcs.push(Arc::new(ScreenshotTool::new(security.clone())));
724 tool_arcs.push(Arc::new(ImageInfoTool::new(security.clone())));
725
726 if let Ok(session_store) = crate::channels::session_store::SessionStore::new(workspace_dir) {
728 let backend: Arc<dyn crate::channels::session_backend::SessionBackend> =
729 Arc::new(session_store);
730 tool_arcs.push(Arc::new(SessionsListTool::new(backend.clone())));
731 tool_arcs.push(Arc::new(SessionsHistoryTool::new(
732 backend.clone(),
733 security.clone(),
734 )));
735 tool_arcs.push(Arc::new(SessionsSendTool::new(backend, security.clone())));
736 }
737
738 if root_config.linkedin.enabled {
740 tool_arcs.push(Arc::new(LinkedInTool::new(
741 security.clone(),
742 workspace_dir.to_path_buf(),
743 root_config.linkedin.api_version.clone(),
744 root_config.linkedin.content.clone(),
745 root_config.linkedin.image.clone(),
746 )));
747 }
748
749 if root_config.image_gen.enabled {
751 tool_arcs.push(Arc::new(ImageGenTool::new(
752 security.clone(),
753 workspace_dir.to_path_buf(),
754 root_config.image_gen.default_model.clone(),
755 root_config.image_gen.api_key_env.clone(),
756 )));
757 }
758
759 let channel_map_handle: ChannelMapHandle = Arc::new(RwLock::new(HashMap::new()));
761 tool_arcs.push(Arc::new(PollTool::new(
762 security.clone(),
763 Arc::clone(&channel_map_handle),
764 )));
765
766 if root_config.sop.sops_dir.is_some() {
768 let sop_engine = Arc::new(std::sync::Mutex::new(crate::sop::SopEngine::new(
769 root_config.sop.clone(),
770 )));
771 tool_arcs.push(Arc::new(SopListTool::new(Arc::clone(&sop_engine))));
772 tool_arcs.push(Arc::new(SopExecuteTool::new(Arc::clone(&sop_engine))));
773 tool_arcs.push(Arc::new(SopAdvanceTool::new(Arc::clone(&sop_engine))));
774 tool_arcs.push(Arc::new(SopApproveTool::new(Arc::clone(&sop_engine))));
775 tool_arcs.push(Arc::new(SopStatusTool::new(Arc::clone(&sop_engine))));
776 }
777
778 if let Some(key) = composio_key {
779 if !key.is_empty() {
780 tool_arcs.push(Arc::new(ComposioTool::new(
781 key,
782 composio_entity_id,
783 security.clone(),
784 )));
785 }
786 }
787
788 let reaction_tool = ReactionTool::new(security.clone());
790 let reaction_handle = reaction_tool.channel_map_handle();
791 tool_arcs.push(Arc::new(reaction_tool));
792
793 let ask_user_tool = AskUserTool::new(security.clone());
795 let ask_user_handle = ask_user_tool.channel_map_handle();
796 tool_arcs.push(Arc::new(ask_user_tool));
797
798 let escalate_tool = EscalateToHumanTool::new(security.clone(), workspace_dir.to_path_buf());
800 let escalate_handle = escalate_tool.channel_map_handle();
801 tool_arcs.push(Arc::new(escalate_tool));
802
803 if root_config.microsoft365.enabled {
805 let ms_cfg = &root_config.microsoft365;
806 let tenant_id = ms_cfg
807 .tenant_id
808 .as_deref()
809 .unwrap_or_default()
810 .trim()
811 .to_string();
812 let client_id = ms_cfg
813 .client_id
814 .as_deref()
815 .unwrap_or_default()
816 .trim()
817 .to_string();
818 if !tenant_id.is_empty() && !client_id.is_empty() {
819 if ms_cfg.auth_flow.trim() == "client_credentials"
821 && ms_cfg
822 .client_secret
823 .as_deref()
824 .map_or(true, |s| s.trim().is_empty())
825 {
826 tracing::error!(
827 "microsoft365: client_credentials auth_flow requires a non-empty client_secret"
828 );
829 return (
830 boxed_registry_from_arcs(tool_arcs),
831 None,
832 Some(reaction_handle),
833 channel_map_handle,
834 Some(ask_user_handle),
835 Some(escalate_handle),
836 );
837 }
838
839 let resolved = microsoft365::types::Microsoft365ResolvedConfig {
840 tenant_id,
841 client_id,
842 client_secret: ms_cfg.client_secret.clone(),
843 auth_flow: ms_cfg.auth_flow.clone(),
844 scopes: ms_cfg.scopes.clone(),
845 token_cache_encrypted: ms_cfg.token_cache_encrypted,
846 user_id: ms_cfg.user_id.as_deref().unwrap_or("me").to_string(),
847 };
848 let cache_dir = root_config.config_path.parent().unwrap_or(workspace_dir);
852 match Microsoft365Tool::new(resolved, security.clone(), cache_dir) {
853 Ok(tool) => tool_arcs.push(Arc::new(tool)),
854 Err(e) => {
855 tracing::error!("microsoft365: failed to initialize tool: {e}");
856 }
857 }
858 } else {
859 tracing::warn!(
860 "microsoft365: skipped registration because tenant_id or client_id is empty"
861 );
862 }
863 }
864
865 let delegate_fallback_credential = fallback_api_key.and_then(|value| {
867 let trimmed_value = value.trim();
868 (!trimmed_value.is_empty()).then(|| trimmed_value.to_owned())
869 });
870 let provider_runtime_options = crate::providers::ProviderRuntimeOptions {
871 auth_profile_override: None,
872 provider_api_url: root_config.api_url.clone(),
873 construct_dir: root_config
874 .config_path
875 .parent()
876 .map(std::path::PathBuf::from),
877 secrets_encrypt: root_config.secrets.encrypt,
878 reasoning_enabled: root_config.runtime.reasoning_enabled,
879 reasoning_effort: root_config.runtime.reasoning_effort.clone(),
880 provider_timeout_secs: Some(root_config.provider_timeout_secs),
881 provider_max_tokens: root_config.provider_max_tokens,
882 extra_headers: root_config.extra_headers.clone(),
883 api_path: root_config.api_path.clone(),
884 };
885
886 let delegate_handle: Option<DelegateParentToolsHandle> = if agents.is_empty() {
887 None
888 } else {
889 let delegate_agents: HashMap<String, DelegateAgentConfig> = agents
890 .iter()
891 .map(|(name, cfg)| (name.clone(), cfg.clone()))
892 .collect();
893 let parent_tools = Arc::new(RwLock::new(tool_arcs.clone()));
894 let delegate_tool = DelegateTool::new_with_options(
895 delegate_agents,
896 delegate_fallback_credential.clone(),
897 security.clone(),
898 provider_runtime_options.clone(),
899 )
900 .with_parent_tools(Arc::clone(&parent_tools))
901 .with_multimodal_config(root_config.multimodal.clone())
902 .with_delegate_config(root_config.delegate.clone())
903 .with_workspace_dir(workspace_dir.to_path_buf());
904 tool_arcs.push(Arc::new(delegate_tool));
905 Some(parent_tools)
906 };
907
908 if !root_config.swarms.is_empty() {
910 let swarm_agents: HashMap<String, DelegateAgentConfig> = agents
911 .iter()
912 .map(|(name, cfg)| (name.clone(), cfg.clone()))
913 .collect();
914 tool_arcs.push(Arc::new(SwarmTool::new(
915 root_config.swarms.clone(),
916 swarm_agents,
917 delegate_fallback_credential,
918 security.clone(),
919 provider_runtime_options,
920 )));
921 }
922
923 if root_config.workspace.enabled {
925 let workspaces_dir = if root_config.workspace.workspaces_dir.starts_with("~/") {
926 let home = directories::UserDirs::new()
927 .map(|u| u.home_dir().to_path_buf())
928 .unwrap_or_else(|| std::path::PathBuf::from("."));
929 home.join(&root_config.workspace.workspaces_dir[2..])
930 } else {
931 std::path::PathBuf::from(&root_config.workspace.workspaces_dir)
932 };
933 let ws_manager = crate::config::workspace::WorkspaceManager::new(workspaces_dir);
934 tool_arcs.push(Arc::new(WorkspaceTool::new(
935 Arc::new(tokio::sync::RwLock::new(ws_manager)),
936 security.clone(),
937 )));
938 }
939
940 if root_config.verifiable_intent.enabled {
942 let strictness = match root_config.verifiable_intent.strictness.as_str() {
943 "permissive" => crate::verifiable_intent::StrictnessMode::Permissive,
944 _ => crate::verifiable_intent::StrictnessMode::Strict,
945 };
946 tool_arcs.push(Arc::new(VerifiableIntentTool::new(
947 security.clone(),
948 strictness,
949 )));
950 }
951
952 #[cfg(feature = "plugins-wasm")]
954 {
955 let plugin_dir = config.plugins.plugins_dir.clone();
956 let plugin_path = if plugin_dir.starts_with("~/") {
957 let home = directories::UserDirs::new()
958 .map(|u| u.home_dir().to_path_buf())
959 .unwrap_or_else(|| std::path::PathBuf::from("."));
960 home.join(&plugin_dir[2..])
961 } else {
962 std::path::PathBuf::from(&plugin_dir)
963 };
964
965 if plugin_path.exists() && config.plugins.enabled {
966 match crate::plugins::host::PluginHost::new(
967 plugin_path.parent().unwrap_or(&plugin_path),
968 ) {
969 Ok(host) => {
970 let tool_manifests = host.tool_plugins();
971 let count = tool_manifests.len();
972 for manifest in tool_manifests {
973 tool_arcs.push(Arc::new(crate::plugins::wasm_tool::WasmTool::new(
974 manifest.name.clone(),
975 manifest.description.clone().unwrap_or_default(),
976 manifest.name.clone(),
977 "call".to_string(),
978 serde_json::json!({
979 "type": "object",
980 "properties": {
981 "input": {
982 "type": "string",
983 "description": "Input for the plugin"
984 }
985 },
986 "required": ["input"]
987 }),
988 )));
989 }
990 tracing::info!("Loaded {count} WASM plugin tools");
991 }
992 Err(e) => {
993 tracing::warn!("Failed to load WASM plugins: {e}");
994 }
995 }
996 }
997 }
998
999 if root_config.pipeline.enabled {
1001 let pipeline_tools: Vec<Arc<dyn Tool>> = tool_arcs.clone();
1002 tool_arcs.push(Arc::new(pipeline::PipelineTool::new(
1003 root_config.pipeline.clone(),
1004 pipeline_tools,
1005 )));
1006 }
1007
1008 (
1009 boxed_registry_from_arcs(tool_arcs),
1010 delegate_handle,
1011 Some(reaction_handle),
1012 channel_map_handle,
1013 Some(ask_user_handle),
1014 Some(escalate_handle),
1015 )
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021 use crate::config::{BrowserConfig, Config, MemoryConfig};
1022 use tempfile::TempDir;
1023
1024 fn test_config(tmp: &TempDir) -> Config {
1025 Config {
1026 workspace_dir: tmp.path().join("workspace"),
1027 config_path: tmp.path().join("config.toml"),
1028 ..Config::default()
1029 }
1030 }
1031
1032 #[test]
1033 fn default_tools_has_expected_count() {
1034 let security = Arc::new(SecurityPolicy::default());
1035 let tools = default_tools(security);
1036 assert_eq!(tools.len(), 6);
1037 }
1038
1039 #[test]
1040 fn all_tools_excludes_browser_when_disabled() {
1041 let tmp = TempDir::new().unwrap();
1042 let security = Arc::new(SecurityPolicy::default());
1043 let mem_cfg = MemoryConfig {
1044 backend: "none".into(),
1045 ..MemoryConfig::default()
1046 };
1047 let mem: Arc<dyn Memory> =
1048 Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1049
1050 let browser = BrowserConfig {
1051 enabled: false,
1052 allowed_domains: vec!["example.com".into()],
1053 session_name: None,
1054 ..BrowserConfig::default()
1055 };
1056 let http = crate::config::HttpRequestConfig::default();
1057 let cfg = test_config(&tmp);
1058
1059 let (tools, _, _, _, _, _) = all_tools(
1060 Arc::new(Config::default()),
1061 &security,
1062 mem,
1063 None,
1064 None,
1065 &browser,
1066 &http,
1067 &crate::config::WebFetchConfig::default(),
1068 tmp.path(),
1069 &HashMap::new(),
1070 None,
1071 &cfg,
1072 None,
1073 );
1074 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1075 assert!(!names.contains(&"browser_open"));
1076 assert!(names.contains(&"schedule"));
1077 assert!(names.contains(&"model_routing_config"));
1078 assert!(names.contains(&"pushover"));
1079 assert!(names.contains(&"proxy_config"));
1080 }
1081
1082 #[test]
1083 fn all_tools_includes_browser_when_enabled() {
1084 let tmp = TempDir::new().unwrap();
1085 let security = Arc::new(SecurityPolicy::default());
1086 let mem_cfg = MemoryConfig {
1087 backend: "none".into(),
1088 ..MemoryConfig::default()
1089 };
1090 let mem: Arc<dyn Memory> =
1091 Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1092
1093 let browser = BrowserConfig {
1094 enabled: true,
1095 allowed_domains: vec!["example.com".into()],
1096 session_name: None,
1097 ..BrowserConfig::default()
1098 };
1099 let http = crate::config::HttpRequestConfig::default();
1100 let cfg = test_config(&tmp);
1101
1102 let (tools, _, _, _, _, _) = all_tools(
1103 Arc::new(Config::default()),
1104 &security,
1105 mem,
1106 None,
1107 None,
1108 &browser,
1109 &http,
1110 &crate::config::WebFetchConfig::default(),
1111 tmp.path(),
1112 &HashMap::new(),
1113 None,
1114 &cfg,
1115 None,
1116 );
1117 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1118 assert!(names.contains(&"browser_open"));
1119 assert!(names.contains(&"content_search"));
1120 assert!(names.contains(&"model_routing_config"));
1121 assert!(names.contains(&"pushover"));
1122 assert!(names.contains(&"proxy_config"));
1123 }
1124
1125 #[test]
1126 fn default_tools_names() {
1127 let security = Arc::new(SecurityPolicy::default());
1128 let tools = default_tools(security);
1129 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1130 assert!(names.contains(&"shell"));
1131 assert!(names.contains(&"file_read"));
1132 assert!(names.contains(&"file_write"));
1133 assert!(names.contains(&"file_edit"));
1134 assert!(names.contains(&"glob_search"));
1135 assert!(names.contains(&"content_search"));
1136 }
1137
1138 #[test]
1139 fn default_tools_all_have_descriptions() {
1140 let security = Arc::new(SecurityPolicy::default());
1141 let tools = default_tools(security);
1142 for tool in &tools {
1143 assert!(
1144 !tool.description().is_empty(),
1145 "Tool {} has empty description",
1146 tool.name()
1147 );
1148 }
1149 }
1150
1151 #[test]
1152 fn default_tools_all_have_schemas() {
1153 let security = Arc::new(SecurityPolicy::default());
1154 let tools = default_tools(security);
1155 for tool in &tools {
1156 let schema = tool.parameters_schema();
1157 assert!(
1158 schema.is_object(),
1159 "Tool {} schema is not an object",
1160 tool.name()
1161 );
1162 assert!(
1163 schema["properties"].is_object(),
1164 "Tool {} schema has no properties",
1165 tool.name()
1166 );
1167 }
1168 }
1169
1170 #[test]
1171 fn tool_spec_generation() {
1172 let security = Arc::new(SecurityPolicy::default());
1173 let tools = default_tools(security);
1174 for tool in &tools {
1175 let spec = tool.spec();
1176 assert_eq!(spec.name, tool.name());
1177 assert_eq!(spec.description, tool.description());
1178 assert!(spec.parameters.is_object());
1179 }
1180 }
1181
1182 #[test]
1183 fn tool_result_serde() {
1184 let result = ToolResult {
1185 success: true,
1186 output: "hello".into(),
1187 error: None,
1188 };
1189 let json = serde_json::to_string(&result).unwrap();
1190 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
1191 assert!(parsed.success);
1192 assert_eq!(parsed.output, "hello");
1193 assert!(parsed.error.is_none());
1194 }
1195
1196 #[test]
1197 fn tool_result_with_error_serde() {
1198 let result = ToolResult {
1199 success: false,
1200 output: String::new(),
1201 error: Some("boom".into()),
1202 };
1203 let json = serde_json::to_string(&result).unwrap();
1204 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
1205 assert!(!parsed.success);
1206 assert_eq!(parsed.error.as_deref(), Some("boom"));
1207 }
1208
1209 #[test]
1210 fn tool_spec_serde() {
1211 let spec = ToolSpec {
1212 name: "test".into(),
1213 description: "A test tool".into(),
1214 parameters: serde_json::json!({"type": "object"}),
1215 };
1216 let json = serde_json::to_string(&spec).unwrap();
1217 let parsed: ToolSpec = serde_json::from_str(&json).unwrap();
1218 assert_eq!(parsed.name, "test");
1219 assert_eq!(parsed.description, "A test tool");
1220 }
1221
1222 #[test]
1223 fn all_tools_includes_delegate_when_agents_configured() {
1224 let tmp = TempDir::new().unwrap();
1225 let security = Arc::new(SecurityPolicy::default());
1226 let mem_cfg = MemoryConfig {
1227 backend: "none".into(),
1228 ..MemoryConfig::default()
1229 };
1230 let mem: Arc<dyn Memory> =
1231 Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1232
1233 let browser = BrowserConfig::default();
1234 let http = crate::config::HttpRequestConfig::default();
1235 let cfg = test_config(&tmp);
1236
1237 let mut agents = HashMap::new();
1238 agents.insert(
1239 "researcher".to_string(),
1240 DelegateAgentConfig {
1241 provider: "ollama".to_string(),
1242 model: "llama3".to_string(),
1243 system_prompt: None,
1244 api_key: None,
1245 temperature: None,
1246 max_depth: 3,
1247 agentic: false,
1248 allowed_tools: Vec::new(),
1249 max_iterations: 10,
1250 timeout_secs: None,
1251 agentic_timeout_secs: None,
1252 skills_directory: None,
1253 },
1254 );
1255
1256 let (tools, _, _, _, _, _) = all_tools(
1257 Arc::new(Config::default()),
1258 &security,
1259 mem,
1260 None,
1261 None,
1262 &browser,
1263 &http,
1264 &crate::config::WebFetchConfig::default(),
1265 tmp.path(),
1266 &agents,
1267 Some("delegate-test-credential"),
1268 &cfg,
1269 None,
1270 );
1271 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1272 assert!(names.contains(&"delegate"));
1273 }
1274
1275 #[test]
1276 fn all_tools_excludes_delegate_when_no_agents() {
1277 let tmp = TempDir::new().unwrap();
1278 let security = Arc::new(SecurityPolicy::default());
1279 let mem_cfg = MemoryConfig {
1280 backend: "none".into(),
1281 ..MemoryConfig::default()
1282 };
1283 let mem: Arc<dyn Memory> =
1284 Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1285
1286 let browser = BrowserConfig::default();
1287 let http = crate::config::HttpRequestConfig::default();
1288 let cfg = test_config(&tmp);
1289
1290 let (tools, _, _, _, _, _) = all_tools(
1291 Arc::new(Config::default()),
1292 &security,
1293 mem,
1294 None,
1295 None,
1296 &browser,
1297 &http,
1298 &crate::config::WebFetchConfig::default(),
1299 tmp.path(),
1300 &HashMap::new(),
1301 None,
1302 &cfg,
1303 None,
1304 );
1305 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1306 assert!(!names.contains(&"delegate"));
1307 }
1308
1309 #[test]
1310 fn all_tools_includes_read_skill_in_compact_mode() {
1311 let tmp = TempDir::new().unwrap();
1312 let security = Arc::new(SecurityPolicy::default());
1313 let mem_cfg = MemoryConfig {
1314 backend: "none".into(),
1315 ..MemoryConfig::default()
1316 };
1317 let mem: Arc<dyn Memory> =
1318 Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1319
1320 let browser = BrowserConfig::default();
1321 let http = crate::config::HttpRequestConfig::default();
1322 let mut cfg = test_config(&tmp);
1323 cfg.skills.prompt_injection_mode = crate::config::SkillsPromptInjectionMode::Compact;
1324
1325 let (tools, _, _, _, _, _) = all_tools(
1326 Arc::new(cfg.clone()),
1327 &security,
1328 mem,
1329 None,
1330 None,
1331 &browser,
1332 &http,
1333 &crate::config::WebFetchConfig::default(),
1334 tmp.path(),
1335 &HashMap::new(),
1336 None,
1337 &cfg,
1338 None,
1339 );
1340 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1341 assert!(names.contains(&"read_skill"));
1342 }
1343
1344 #[test]
1345 fn all_tools_excludes_read_skill_in_full_mode() {
1346 let tmp = TempDir::new().unwrap();
1347 let security = Arc::new(SecurityPolicy::default());
1348 let mem_cfg = MemoryConfig {
1349 backend: "none".into(),
1350 ..MemoryConfig::default()
1351 };
1352 let mem: Arc<dyn Memory> =
1353 Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1354
1355 let browser = BrowserConfig::default();
1356 let http = crate::config::HttpRequestConfig::default();
1357 let mut cfg = test_config(&tmp);
1358 cfg.skills.prompt_injection_mode = crate::config::SkillsPromptInjectionMode::Full;
1359
1360 let (tools, _, _, _, _, _) = all_tools(
1361 Arc::new(cfg.clone()),
1362 &security,
1363 mem,
1364 None,
1365 None,
1366 &browser,
1367 &http,
1368 &crate::config::WebFetchConfig::default(),
1369 tmp.path(),
1370 &HashMap::new(),
1371 None,
1372 &cfg,
1373 None,
1374 );
1375 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1376 assert!(!names.contains(&"read_skill"));
1377 }
1378}