1pub mod attribution;
19pub mod cron_add;
20pub(crate) mod cron_common;
21pub mod cron_list;
22pub mod cron_remove;
23pub mod cron_run;
24pub mod cron_runs;
25pub mod cron_update;
26pub mod delegate;
27pub mod file_read;
28pub mod model_switch;
29pub mod read_skill;
30pub mod schedule;
31pub mod security_ops;
32pub mod send_message_to_peer;
33pub mod shell;
34pub mod skill_http;
35pub mod skill_tool;
36pub mod sop_advance;
37pub mod sop_approve;
38pub mod sop_execute;
39pub mod sop_list;
40pub mod sop_status;
41pub mod spawn_subagent;
42pub mod verifiable_intent;
43
44pub use zeroclaw_tools::ask_user::AskUserTool;
46pub use zeroclaw_tools::ask_user::ChannelMapHandle;
47pub use zeroclaw_tools::backup_tool::BackupTool;
48pub use zeroclaw_tools::browser::{BrowserTool, ComputerUseConfig};
49pub use zeroclaw_tools::browser_delegate::BrowserDelegateTool;
50pub use zeroclaw_tools::browser_open::BrowserOpenTool;
51pub use zeroclaw_tools::calculator::CalculatorTool;
52pub use zeroclaw_tools::canvas::{ALLOWED_CONTENT_TYPES, MAX_CONTENT_SIZE};
53pub use zeroclaw_tools::canvas::{CanvasStore, CanvasTool};
54pub use zeroclaw_tools::channel_send::ChannelSendTool;
55pub use zeroclaw_tools::claude_code::ClaudeCodeTool;
56pub use zeroclaw_tools::claude_code_runner::ClaudeCodeRunnerTool;
57pub use zeroclaw_tools::cli_discovery::{DiscoveredCli, discover_cli_tools};
58pub use zeroclaw_tools::cloud_ops::CloudOpsTool;
59pub use zeroclaw_tools::cloud_patterns::CloudPatternsTool;
60pub use zeroclaw_tools::codex_cli::CodexCliTool;
61pub use zeroclaw_tools::composio::ComposioTool;
62pub use zeroclaw_tools::content_search::ContentSearchTool;
63pub use zeroclaw_tools::data_management::DataManagementTool;
64pub use zeroclaw_tools::discord_search::DiscordSearchTool;
65pub use zeroclaw_tools::escalate::EscalateToHumanTool;
66pub use zeroclaw_tools::file_download::FileDownloadTool;
67pub use zeroclaw_tools::file_edit::FileEditTool;
68pub use zeroclaw_tools::file_upload::FileUploadTool;
69pub use zeroclaw_tools::file_upload_bundle::FileUploadBundleTool;
70pub use zeroclaw_tools::file_write::FileWriteTool;
71pub use zeroclaw_tools::gemini_cli::GeminiCliTool;
72pub use zeroclaw_tools::git_operations::GitOperationsTool;
73pub use zeroclaw_tools::glob_search::GlobSearchTool;
74pub use zeroclaw_tools::google_workspace::GoogleWorkspaceTool;
75pub use zeroclaw_tools::hardware_board_info::HardwareBoardInfoTool;
76pub use zeroclaw_tools::hardware_memory_map::HardwareMemoryMapTool;
77pub use zeroclaw_tools::hardware_memory_read::HardwareMemoryReadTool;
78pub use zeroclaw_tools::http_request::HttpRequestTool;
79pub use zeroclaw_tools::image_gen::ImageGenTool;
80pub use zeroclaw_tools::image_info::ImageInfoTool;
81pub use zeroclaw_tools::jira_tool::JiraTool;
82pub use zeroclaw_tools::knowledge_tool::KnowledgeTool;
83pub use zeroclaw_tools::linkedin::LinkedInTool;
84pub use zeroclaw_tools::llm_task::LlmTaskTool;
85pub use zeroclaw_tools::mcp_client::McpRegistry;
86pub use zeroclaw_tools::mcp_deferred::{
87 ActivatedToolSet, DeferredMcpToolSet, build_deferred_tools_section,
88 build_deferred_tools_section_filtered,
89};
90pub use zeroclaw_tools::mcp_tool::McpToolWrapper;
91pub use zeroclaw_tools::memory_export::MemoryExportTool;
92pub use zeroclaw_tools::memory_forget::MemoryForgetTool;
93pub use zeroclaw_tools::memory_purge::MemoryPurgeTool;
94pub use zeroclaw_tools::memory_recall::MemoryRecallTool;
95pub use zeroclaw_tools::memory_store::MemoryStoreTool;
96pub use zeroclaw_tools::microsoft365::Microsoft365Tool;
97pub use zeroclaw_tools::model_routing_config::ModelRoutingConfigTool;
98pub use zeroclaw_tools::notion_tool::NotionTool;
99pub use zeroclaw_tools::opencode_cli::OpenCodeCliTool;
100#[cfg(feature = "rag-pdf")]
101pub use zeroclaw_tools::pdf_read::PdfReadTool;
102pub use zeroclaw_tools::pipeline::PipelineTool;
103pub use zeroclaw_tools::poll::PollTool;
104pub use zeroclaw_tools::project_intel::ProjectIntelTool;
105pub use zeroclaw_tools::proxy_config::ProxyConfigTool;
106pub use zeroclaw_tools::pushover::PushoverTool;
107pub use zeroclaw_tools::reaction::ReactionTool;
108pub use zeroclaw_tools::report_template_tool::ReportTemplateTool;
109pub use zeroclaw_tools::screenshot::ScreenshotTool;
110pub use zeroclaw_tools::sessions::{
111 SessionDeleteTool, SessionResetTool, SessionsCurrentTool, SessionsHistoryTool,
112 SessionsListTool, SessionsSendTool,
113};
114pub use zeroclaw_tools::text_browser::TextBrowserTool;
115pub use zeroclaw_tools::tool_search::ToolSearchTool;
116pub use zeroclaw_tools::weather_tool::WeatherTool;
117pub use zeroclaw_tools::web_fetch::WebFetchTool;
118pub use zeroclaw_tools::web_search_tool::WebSearchTool;
119pub use zeroclaw_tools::wrappers::{PathGuardedTool, RateLimitedTool};
120
121pub use zeroclaw_api::schema::{CleaningStrategy, SchemaCleanr};
123pub use zeroclaw_api::tool::{Tool, ToolResult, ToolSpec};
124
125pub use cron_add::CronAddTool;
127pub use cron_list::CronListTool;
128pub use cron_remove::CronRemoveTool;
129pub use cron_run::CronRunTool;
130pub use cron_runs::CronRunsTool;
131pub use cron_update::CronUpdateTool;
132pub use delegate::DelegateTool;
133pub use file_read::FileReadTool;
134pub use model_switch::ModelSwitchTool;
135pub use read_skill::ReadSkillTool;
136pub use schedule::ScheduleTool;
137pub use security_ops::SecurityOpsTool;
138pub use send_message_to_peer::SendMessageToPeerTool;
139pub use shell::ShellTool;
140pub use skill_http::SkillHttpTool;
141pub use skill_tool::{SkillBuiltinTool, SkillShellTool};
142pub use sop_advance::SopAdvanceTool;
143pub use sop_approve::SopApproveTool;
144pub use sop_execute::SopExecuteTool;
145pub use sop_list::SopListTool;
146pub use sop_status::SopStatusTool;
147pub use spawn_subagent::SpawnSubagentTool;
148pub use verifiable_intent::VerifiableIntentTool;
149
150use crate::platform::{NativeRuntime, RuntimeAdapter};
151use crate::security::{SecurityPolicy, create_sandbox};
152use async_trait::async_trait;
153use parking_lot::RwLock;
154use std::collections::HashMap;
155use std::sync::Arc;
156use zeroclaw_config::schema::{AliasedAgentConfig, Config};
157use zeroclaw_memory::Memory;
158
159pub type PerToolChannelHandle =
165 Arc<RwLock<HashMap<String, Arc<dyn zeroclaw_api::channel::Channel>>>>;
166
167pub type DelegateParentToolsHandle = Arc<RwLock<Vec<Arc<dyn Tool>>>>;
170
171pub struct ArcToolRef(pub Arc<dyn Tool>);
173#[async_trait]
176impl Tool for ArcToolRef {
177 fn name(&self) -> &str {
178 self.0.name()
179 }
180
181 fn description(&self) -> &str {
182 self.0.description()
183 }
184
185 fn parameters_schema(&self) -> serde_json::Value {
186 self.0.parameters_schema()
187 }
188
189 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
190 self.0.execute(args).await
191 }
192}
193
194#[derive(Clone)]
195struct ArcDelegatingTool {
196 inner: Arc<dyn Tool>,
197}
198
199impl ArcDelegatingTool {
200 fn boxed(inner: Arc<dyn Tool>) -> Box<dyn Tool> {
201 Box::new(Self { inner })
202 }
203}
204
205impl ::zeroclaw_api::attribution::Attributable for ArcDelegatingTool {
206 fn role(&self) -> ::zeroclaw_api::attribution::Role {
207 self.inner.role()
208 }
209 fn alias(&self) -> &str {
210 self.inner.alias()
211 }
212}
213
214#[async_trait]
215impl Tool for ArcDelegatingTool {
216 fn name(&self) -> &str {
217 self.inner.name()
218 }
219
220 fn description(&self) -> &str {
221 self.inner.description()
222 }
223
224 fn parameters_schema(&self) -> serde_json::Value {
225 self.inner.parameters_schema()
226 }
227
228 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
229 self.inner.execute(args).await
230 }
231}
232
233fn boxed_registry_from_arcs(tools: Vec<Arc<dyn Tool>>) -> Vec<Box<dyn Tool>> {
234 tools.into_iter().map(ArcDelegatingTool::boxed).collect()
235}
236
237pub fn default_tools(security: Arc<SecurityPolicy>) -> Vec<Box<dyn Tool>> {
239 default_tools_with_runtime(security, Arc::new(NativeRuntime::new()))
240}
241
242pub fn default_tools_with_runtime(
244 security: Arc<SecurityPolicy>,
245 runtime: Arc<dyn RuntimeAdapter>,
246) -> Vec<Box<dyn Tool>> {
247 vec![
248 Box::new(RateLimitedTool::new(
249 PathGuardedTool::new(ShellTool::new(security.clone(), runtime), security.clone()),
250 security.clone(),
251 )),
252 Box::new(RateLimitedTool::new(
253 PathGuardedTool::new(FileReadTool::new(security.clone()), security.clone()),
254 security.clone(),
255 )),
256 Box::new(RateLimitedTool::new(
257 PathGuardedTool::new(FileWriteTool::new(security.clone()), security.clone()),
258 security.clone(),
259 )),
260 Box::new(RateLimitedTool::new(
261 PathGuardedTool::new(FileEditTool::new(security.clone()), security.clone()),
262 security.clone(),
263 )),
264 Box::new(RateLimitedTool::new(
265 PathGuardedTool::new(GlobSearchTool::new(security.clone()), security.clone()),
266 security.clone(),
267 )),
268 Box::new(RateLimitedTool::new(
269 PathGuardedTool::new(ContentSearchTool::new(security.clone()), security.clone()),
270 security,
271 )),
272 ]
273}
274
275pub fn register_skill_tools(
281 tools_registry: &mut Vec<Box<dyn Tool>>,
282 skills: &[crate::skills::Skill],
283 security: Arc<SecurityPolicy>,
284) {
285 register_skill_tools_with_context(tools_registry, skills, security, &[]);
286}
287
288pub fn register_skill_tools_with_context(
293 tools_registry: &mut Vec<Box<dyn Tool>>,
294 skills: &[crate::skills::Skill],
295 security: Arc<SecurityPolicy>,
296 unfiltered_registry: &[Arc<dyn Tool>],
297) {
298 if skills.is_empty() {
299 return;
300 }
301
302 let before = tools_registry.len();
303 let skill_tools =
304 crate::skills::skills_to_tools_with_context(skills, security, unfiltered_registry);
305 let existing_names: std::collections::HashSet<String> = tools_registry
306 .iter()
307 .map(|t| t.name().to_string())
308 .collect();
309 for tool in skill_tools {
310 if existing_names.contains(tool.name()) {
311 ::zeroclaw_log::record!(
312 WARN,
313 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
314 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
315 &format!(
316 "Skill tool '{}' shadows built-in tool, skipping",
317 tool.name()
318 )
319 );
320 } else {
321 tools_registry.push(tool);
322 }
323 }
324 let registered = tools_registry.len() - before;
325
326 ::zeroclaw_log::record!(
332 INFO,
333 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
334 &format!(
335 "Registered {} skill tool(s) from {} skill(s): {}",
336 registered,
337 skills.len(),
338 skills
339 .iter()
340 .map(|s| s.name.as_str())
341 .collect::<Vec<_>>()
342 .join(", "),
343 )
344 );
345}
346
347pub async fn collect_mcp_elevation_arcs(registry: &Arc<McpRegistry>) -> Vec<Arc<dyn Tool>> {
357 let mut arcs: Vec<Arc<dyn Tool>> = Vec::new();
358 for name in registry.tool_names() {
359 if let Some(def) = registry.get_tool_def(&name).await {
360 arcs.push(Arc::new(McpToolWrapper::new(
361 name,
362 def,
363 Arc::clone(registry),
364 )));
365 }
366 }
367 arcs
368}
369
370pub const BUILTIN_TOOL_INTEGRATIONS: &[(&str, &str)] = &[
377 ("Shell", "Terminal command execution"),
378 ("File System", "Read/write files"),
379 ("Weather", "Forecasts & conditions (wttr.in)"),
380 (
381 "Spawn SubAgent",
382 "Spawn an ephemeral SubAgent that inherits this agent's identity",
383 ),
384];
385
386#[allow(clippy::type_complexity)]
391pub struct AllToolsResult {
392 pub tools: Vec<Box<dyn Tool>>,
393 pub delegate_handle: Option<DelegateParentToolsHandle>,
394 pub ask_user_handle: Option<PerToolChannelHandle>,
395 pub reaction_handle: PerToolChannelHandle,
396 pub poll_handle: Option<PerToolChannelHandle>,
397 pub escalate_handle: Option<PerToolChannelHandle>,
398 pub channel_send_handle: Option<PerToolChannelHandle>,
399 pub unfiltered_tool_arcs: Vec<Arc<dyn Tool>>,
402}
403
404#[allow(
406 clippy::implicit_hasher,
407 clippy::too_many_arguments,
408 clippy::type_complexity
409)]
410pub fn all_tools(
411 config: Arc<Config>,
412 security: &Arc<SecurityPolicy>,
413 risk_profile: &zeroclaw_config::schema::RiskProfileConfig,
414 agent_alias: &str,
415 memory: Arc<dyn Memory>,
416 composio_key: Option<&str>,
417 composio_entity_id: Option<&str>,
418 browser_config: &zeroclaw_config::schema::BrowserConfig,
419 http_config: &zeroclaw_config::schema::HttpRequestConfig,
420 web_fetch_config: &zeroclaw_config::schema::WebFetchConfig,
421 workspace_dir: &std::path::Path,
422 agents: &HashMap<String, AliasedAgentConfig>,
423 fallback_api_key: Option<&str>,
424 root_config: &zeroclaw_config::schema::Config,
425 canvas_store: Option<CanvasStore>,
426 is_subagent_caller: bool,
427) -> (
428 Vec<Box<dyn Tool>>,
429 Option<DelegateParentToolsHandle>,
430 Option<PerToolChannelHandle>,
431 PerToolChannelHandle,
432 Option<PerToolChannelHandle>,
433 Option<PerToolChannelHandle>,
434 Option<PerToolChannelHandle>,
435) {
436 let result = all_tools_with_runtime(
437 config,
438 security,
439 risk_profile,
440 agent_alias,
441 Arc::new(NativeRuntime::new()),
442 memory,
443 composio_key,
444 composio_entity_id,
445 browser_config,
446 http_config,
447 web_fetch_config,
448 workspace_dir,
449 agents,
450 fallback_api_key,
451 root_config,
452 canvas_store,
453 is_subagent_caller,
454 );
455 (
456 result.tools,
457 result.delegate_handle,
458 result.ask_user_handle,
459 result.reaction_handle,
460 result.poll_handle,
461 result.escalate_handle,
462 result.channel_send_handle,
463 )
464}
465
466#[allow(
468 clippy::implicit_hasher,
469 clippy::too_many_arguments,
470 clippy::type_complexity
471)]
472pub fn all_tools_with_runtime(
473 config: Arc<Config>,
474 security: &Arc<SecurityPolicy>,
475 risk_profile: &zeroclaw_config::schema::RiskProfileConfig,
476 agent_alias: &str,
477 runtime: Arc<dyn RuntimeAdapter>,
478 memory: Arc<dyn Memory>,
479 composio_key: Option<&str>,
480 composio_entity_id: Option<&str>,
481 browser_config: &zeroclaw_config::schema::BrowserConfig,
482 http_config: &zeroclaw_config::schema::HttpRequestConfig,
483 web_fetch_config: &zeroclaw_config::schema::WebFetchConfig,
484 workspace_dir: &std::path::Path,
485 agents: &HashMap<String, AliasedAgentConfig>,
486 fallback_api_key: Option<&str>,
487 root_config: &zeroclaw_config::schema::Config,
488 canvas_store: Option<CanvasStore>,
489 is_subagent_caller: bool,
490) -> AllToolsResult {
491 let has_shell_access = runtime.has_shell_access();
492 let runtime_kind = root_config.runtime.kind.as_str();
493 let sandbox_cfg = risk_profile.sandbox_config();
494 let sandbox = create_sandbox(&sandbox_cfg, runtime_kind, Some(&security.workspace_dir));
495 let mut tool_arcs: Vec<Arc<dyn Tool>> = vec![
496 Arc::new(RateLimitedTool::new(
497 PathGuardedTool::new(
498 ShellTool::new_with_sandbox(security.clone(), runtime, sandbox)
499 .with_timeout_secs(root_config.shell_tool.timeout_secs),
500 security.clone(),
501 ),
502 security.clone(),
503 )),
504 Arc::new(RateLimitedTool::new(
505 PathGuardedTool::new(FileReadTool::new(security.clone()), security.clone()),
506 security.clone(),
507 )),
508 Arc::new(RateLimitedTool::new(
509 PathGuardedTool::new(FileWriteTool::new(security.clone()), security.clone()),
510 security.clone(),
511 )),
512 Arc::new(RateLimitedTool::new(
513 PathGuardedTool::new(FileEditTool::new(security.clone()), security.clone()),
514 security.clone(),
515 )),
516 Arc::new(RateLimitedTool::new(
517 PathGuardedTool::new(GlobSearchTool::new(security.clone()), security.clone()),
518 security.clone(),
519 )),
520 Arc::new(RateLimitedTool::new(
521 PathGuardedTool::new(ContentSearchTool::new(security.clone()), security.clone()),
522 security.clone(),
523 )),
524 Arc::new(CronAddTool::new(
525 config.clone(),
526 security.clone(),
527 agent_alias,
528 )),
529 Arc::new(CronListTool::new(config.clone())),
530 Arc::new(CronRemoveTool::new(config.clone(), security.clone())),
531 Arc::new(CronUpdateTool::new(
532 config.clone(),
533 security.clone(),
534 agent_alias,
535 )),
536 Arc::new(CronRunTool::new(config.clone(), security.clone())),
537 Arc::new(CronRunsTool::new(config.clone())),
538 Arc::new(MemoryStoreTool::new(memory.clone(), security.clone())),
539 Arc::new(MemoryRecallTool::new(memory.clone())),
540 Arc::new(MemoryForgetTool::new(memory.clone(), security.clone())),
541 Arc::new(MemoryExportTool::new(memory.clone())),
542 Arc::new(MemoryPurgeTool::new(memory.clone(), security.clone())),
543 Arc::new(ScheduleTool::new(
544 security.clone(),
545 root_config.clone(),
546 agent_alias,
547 )),
548 Arc::new(
549 SpawnSubagentTool::new(Arc::new(root_config.clone()), agent_alias)
550 .with_subagent_caller(is_subagent_caller),
551 ),
552 Arc::new(SendMessageToPeerTool::new(
553 Arc::new(root_config.clone()),
554 agent_alias,
555 )),
556 Arc::new(ModelRoutingConfigTool::new(
557 config.clone(),
558 security.clone(),
559 )),
560 Arc::new(ModelSwitchTool::new(security.clone())),
561 Arc::new(ProxyConfigTool::new(config.clone(), security.clone())),
562 Arc::new(GitOperationsTool::new(
563 security.clone(),
564 workspace_dir.to_path_buf(),
565 )),
566 Arc::new(PushoverTool::new(
567 security.clone(),
568 workspace_dir.to_path_buf(),
569 )),
570 Arc::new(CalculatorTool::new()),
571 Arc::new(WeatherTool::new()),
572 Arc::new(CanvasTool::new(canvas_store.unwrap_or_default())),
573 ];
574
575 if root_config.channels.discord.values().any(|d| d.archive) {
580 match zeroclaw_memory::SqliteMemory::new_named("sqlite", workspace_dir, "discord") {
581 Ok(discord_mem) => {
582 tool_arcs.push(Arc::new(DiscordSearchTool::new(Arc::new(discord_mem))));
583 }
584 Err(e) => {
585 ::zeroclaw_log::record!(
586 WARN,
587 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
588 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
589 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
590 "discord_search: failed to open discord.db"
591 );
592 }
593 }
594 }
595
596 {
598 let llm_task_provider = root_config
599 .first_model_provider_type()
600 .unwrap_or("openrouter")
601 .to_string();
602 let llm_task_model = root_config
603 .first_model_provider()
604 .and_then(|e| e.model.clone())
605 .unwrap_or_else(|| "openai/gpt-4o-mini".to_string());
606 let llm_task_runtime_options =
607 zeroclaw_providers::provider_runtime_options_from_config(root_config);
608 tool_arcs.push(Arc::new(LlmTaskTool::new(
609 security.clone(),
610 llm_task_provider,
611 llm_task_model,
612 root_config
613 .first_model_provider()
614 .and_then(|e| e.temperature)
615 .unwrap_or(0.7),
616 root_config
617 .first_model_provider()
618 .and_then(|e| e.api_key.clone()),
619 llm_task_runtime_options,
620 )));
621 }
622
623 if matches!(
624 root_config.skills.prompt_injection_mode,
625 zeroclaw_config::schema::SkillsPromptInjectionMode::Compact
626 ) {
627 tool_arcs.push(Arc::new(ReadSkillTool::new(
628 root_config.data_dir.clone(),
629 root_config.skills.open_skills_enabled,
630 root_config.skills.open_skills_dir.clone(),
631 root_config.skills.allow_scripts,
632 )));
633 }
634
635 if browser_config.enabled {
636 match BrowserOpenTool::new(security.clone(), browser_config.allowed_domains.clone()) {
638 Ok(tool) => {
639 tool_arcs.push(Arc::new(tool));
640 }
641 Err(e) => {
642 ::zeroclaw_log::record!(
643 WARN,
644 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
645 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
646 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
647 "browser_open: failed to construct tool, skipping registration"
648 );
649 }
650 }
651 match BrowserTool::new_with_backend(
653 security.clone(),
654 browser_config.allowed_domains.clone(),
655 browser_config.session_name.clone(),
656 browser_config.backend.clone(),
657 browser_config.headed,
658 browser_config.native_headless,
659 browser_config.native_webdriver_url.clone(),
660 browser_config.native_chrome_path.clone(),
661 ComputerUseConfig {
662 endpoint: browser_config.computer_use.endpoint.clone(),
663 api_key: browser_config.computer_use.api_key.clone(),
664 timeout_ms: browser_config.computer_use.timeout_ms,
665 allow_remote_endpoint: browser_config.computer_use.allow_remote_endpoint,
666 window_allowlist: browser_config.computer_use.window_allowlist.clone(),
667 max_coordinate_x: browser_config.computer_use.max_coordinate_x,
668 max_coordinate_y: browser_config.computer_use.max_coordinate_y,
669 },
670 ) {
671 Ok(tool) => {
672 tool_arcs.push(Arc::new(RateLimitedTool::new(tool, security.clone())));
673 }
674 Err(e) => {
675 ::zeroclaw_log::record!(
676 WARN,
677 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
678 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
679 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
680 "browser: failed to construct tool, skipping registration"
681 );
682 }
683 }
684 }
685
686 if root_config.browser_delegate.enabled {
688 if has_shell_access {
689 tool_arcs.push(Arc::new(BrowserDelegateTool::new(
690 security.clone(),
691 root_config.browser_delegate.clone(),
692 )));
693 } else {
694 ::zeroclaw_log::record!(
695 WARN,
696 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
697 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
698 "browser_delegate: skipped registration because the current runtime does not allow shell access"
699 );
700 }
701 }
702
703 if http_config.enabled {
704 match HttpRequestTool::new(
705 security.clone(),
706 http_config.allowed_domains.clone(),
707 http_config.max_response_size,
708 http_config.timeout_secs,
709 http_config.allow_private_hosts,
710 ) {
711 Ok(tool) => {
712 tool_arcs.push(Arc::new(RateLimitedTool::new(tool, security.clone())));
713 }
714 Err(e) => {
715 ::zeroclaw_log::record!(
716 WARN,
717 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
718 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
719 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
720 "http_request: failed to construct tool, skipping registration"
721 );
722 }
723 }
724 }
725
726 if web_fetch_config.enabled {
727 match WebFetchTool::new(
728 security.clone(),
729 web_fetch_config.allowed_domains.clone(),
730 web_fetch_config.blocked_domains.clone(),
731 web_fetch_config.max_response_size,
732 web_fetch_config.timeout_secs,
733 web_fetch_config.firecrawl.clone(),
734 web_fetch_config.allowed_private_hosts.clone(),
735 ) {
736 Ok(tool) => {
737 tool_arcs.push(Arc::new(RateLimitedTool::new(tool, security.clone())));
738 }
739 Err(e) => {
740 ::zeroclaw_log::record!(
741 WARN,
742 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
743 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
744 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
745 "web_fetch: failed to construct tool, skipping registration"
746 );
747 }
748 }
749 }
750
751 if root_config.text_browser.enabled {
753 tool_arcs.push(Arc::new(TextBrowserTool::new(
754 security.clone(),
755 root_config.text_browser.preferred_browser.clone(),
756 root_config.text_browser.timeout_secs,
757 )));
758 }
759
760 if root_config.web_search.enabled {
762 tool_arcs.push(Arc::new(WebSearchTool::new_with_config(
763 root_config.web_search.search_provider.clone(),
764 root_config.web_search.brave_api_key.clone(),
765 root_config.web_search.tavily_api_key.clone(),
766 root_config.web_search.searxng_instance_url.clone(),
767 root_config.web_search.max_results,
768 root_config.web_search.timeout_secs,
769 root_config.config_path.clone(),
770 root_config.secrets.encrypt,
771 )));
772 }
773
774 if root_config.notion.enabled {
776 let notion_api_key = if root_config.notion.api_key.trim().is_empty() {
777 std::env::var("NOTION_API_KEY").unwrap_or_default()
778 } else {
779 root_config.notion.api_key.trim().to_string()
780 };
781 if notion_api_key.trim().is_empty() {
782 ::zeroclaw_log::record!(
783 WARN,
784 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
785 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
786 "Notion tool enabled but no API key found (set notion.api_key or NOTION_API_KEY env var)"
787 );
788 } else {
789 tool_arcs.push(Arc::new(NotionTool::new(notion_api_key, security.clone())));
790 }
791 }
792
793 if root_config.jira.enabled {
795 let api_token = if root_config.jira.api_token.trim().is_empty() {
796 std::env::var("JIRA_API_TOKEN").unwrap_or_default()
797 } else {
798 root_config.jira.api_token.trim().to_string()
799 };
800 if api_token.trim().is_empty() {
801 ::zeroclaw_log::record!(
802 WARN,
803 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
804 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
805 "Jira tool enabled but no API token found (set jira.api_token or JIRA_API_TOKEN env var)"
806 );
807 } else if root_config.jira.base_url.trim().is_empty() {
808 ::zeroclaw_log::record!(
809 WARN,
810 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
811 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
812 "Jira tool enabled but jira.base_url is empty — skipping registration"
813 );
814 } else {
815 let email = root_config
816 .jira
817 .email
818 .as_deref()
819 .map(str::trim)
820 .filter(|s| !s.is_empty())
821 .map(String::from);
822 if email.is_some() {
823 ::zeroclaw_log::record!(
824 INFO,
825 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
826 "Jira tool: Cloud mode (API v3, Basic auth)"
827 );
828 } else {
829 ::zeroclaw_log::record!(
830 INFO,
831 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
832 "Jira tool: Server/DC mode (API v2, Bearer auth)"
833 );
834 }
835 tool_arcs.push(Arc::new(JiraTool::new(
836 root_config.jira.base_url.trim().to_string(),
837 email,
838 api_token,
839 root_config.jira.allowed_actions.clone(),
840 security.clone(),
841 root_config.jira.timeout_secs,
842 )));
843 }
844 }
845
846 if root_config.project_intel.enabled {
848 tool_arcs.push(Arc::new(ProjectIntelTool::new(
849 root_config.project_intel.default_language.clone(),
850 root_config.project_intel.risk_sensitivity.clone(),
851 )));
852 tool_arcs.push(Arc::new(ReportTemplateTool::new()));
854 }
855
856 if root_config.security_ops.enabled {
858 tool_arcs.push(Arc::new(SecurityOpsTool::new(
859 root_config.security_ops.clone(),
860 )));
861 }
862
863 if root_config.backup.enabled {
865 tool_arcs.push(Arc::new(BackupTool::new(
866 workspace_dir.to_path_buf(),
867 root_config.backup.include_dirs.clone(),
868 root_config.backup.max_keep,
869 )));
870 }
871
872 if root_config.data_retention.enabled {
874 tool_arcs.push(Arc::new(DataManagementTool::new(
875 workspace_dir.to_path_buf(),
876 root_config.data_retention.retention_days,
877 )));
878 }
879
880 if root_config.cloud_ops.enabled {
882 tool_arcs.push(Arc::new(CloudOpsTool::new(root_config.cloud_ops.clone())));
883 tool_arcs.push(Arc::new(CloudPatternsTool::new()));
884 }
885
886 if root_config.google_workspace.enabled && has_shell_access {
888 tool_arcs.push(Arc::new(GoogleWorkspaceTool::new(
889 security.clone(),
890 root_config.google_workspace.allowed_services.clone(),
891 root_config.google_workspace.allowed_operations.clone(),
892 root_config.google_workspace.credentials_path.clone(),
893 root_config.google_workspace.default_account.clone(),
894 root_config.google_workspace.rate_limit_per_minute,
895 root_config.google_workspace.timeout_secs,
896 root_config.google_workspace.audit_log,
897 )));
898 } else if root_config.google_workspace.enabled {
899 ::zeroclaw_log::record!(
900 WARN,
901 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
902 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
903 "google_workspace: skipped registration because shell access is unavailable"
904 );
905 }
906
907 if root_config.claude_code.enabled {
909 tool_arcs.push(Arc::new(RateLimitedTool::new(
910 ClaudeCodeTool::new(security.clone(), root_config.claude_code.clone()),
911 security.clone(),
912 )));
913 }
914
915 if root_config.claude_code_runner.enabled {
917 let gateway_url = format!(
918 "http://{}:{}",
919 root_config.gateway.host, root_config.gateway.port
920 );
921 tool_arcs.push(Arc::new(RateLimitedTool::new(
922 ClaudeCodeRunnerTool::new(
923 security.clone(),
924 root_config.claude_code_runner.clone(),
925 gateway_url,
926 ),
927 security.clone(),
928 )));
929 }
930
931 if root_config.codex_cli.enabled {
933 tool_arcs.push(Arc::new(RateLimitedTool::new(
934 CodexCliTool::new(security.clone(), root_config.codex_cli.clone()),
935 security.clone(),
936 )));
937 }
938
939 if root_config.gemini_cli.enabled {
941 tool_arcs.push(Arc::new(RateLimitedTool::new(
942 GeminiCliTool::new(security.clone(), root_config.gemini_cli.clone()),
943 security.clone(),
944 )));
945 }
946
947 if root_config.opencode_cli.enabled {
949 tool_arcs.push(Arc::new(RateLimitedTool::new(
950 OpenCodeCliTool::new(security.clone(), root_config.opencode_cli.clone()),
951 security.clone(),
952 )));
953 }
954
955 #[cfg(feature = "rag-pdf")]
957 tool_arcs.push(Arc::new(RateLimitedTool::new(
958 PathGuardedTool::new(PdfReadTool::new(security.clone()), security.clone()),
959 security.clone(),
960 )));
961
962 tool_arcs.push(Arc::new(ScreenshotTool::new(security.clone())));
964 tool_arcs.push(Arc::new(RateLimitedTool::new(
965 PathGuardedTool::new(ImageInfoTool::new(security.clone()), security.clone()),
966 security.clone(),
967 )));
968
969 if let Ok(backend) =
977 zeroclaw_infra::make_session_backend(workspace_dir, &config.channels.session_backend)
978 {
979 tool_arcs.push(Arc::new(SessionsCurrentTool::new(backend.clone())));
980 tool_arcs.push(Arc::new(SessionsListTool::new(backend.clone())));
981 tool_arcs.push(Arc::new(SessionsHistoryTool::new(
982 backend.clone(),
983 security.clone(),
984 )));
985 tool_arcs.push(Arc::new(SessionsSendTool::new(backend, security.clone())));
986 }
994
995 if root_config.linkedin.enabled {
997 tool_arcs.push(Arc::new(LinkedInTool::new(
998 security.clone(),
999 workspace_dir.to_path_buf(),
1000 root_config.linkedin.api_version.clone(),
1001 root_config.linkedin.content.clone(),
1002 root_config.linkedin.image.clone(),
1003 )));
1004 }
1005
1006 if root_config.image_gen.enabled {
1008 tool_arcs.push(Arc::new(ImageGenTool::new(
1009 security.clone(),
1010 workspace_dir.to_path_buf(),
1011 root_config.image_gen.default_model.clone(),
1012 root_config.image_gen.api_key_env.clone(),
1013 )));
1014 }
1015
1016 if root_config
1018 .file_upload
1019 .url
1020 .as_deref()
1021 .is_some_and(|u| !u.trim().is_empty())
1022 {
1023 tool_arcs.push(Arc::new(FileUploadTool::new(
1024 security.clone(),
1025 root_config.file_upload.clone(),
1026 )));
1027 }
1028
1029 if root_config
1031 .file_upload_bundle
1032 .url
1033 .as_deref()
1034 .is_some_and(|u| !u.trim().is_empty())
1035 {
1036 tool_arcs.push(Arc::new(FileUploadBundleTool::new(
1037 security.clone(),
1038 root_config.file_upload_bundle.clone(),
1039 )));
1040 }
1041
1042 if root_config
1044 .file_download
1045 .url
1046 .as_deref()
1047 .is_some_and(|u| !u.trim().is_empty())
1048 {
1049 tool_arcs.push(Arc::new(FileDownloadTool::new(
1050 security.clone(),
1051 root_config.file_download.clone(),
1052 )));
1053 }
1054
1055 let poll_handle: PerToolChannelHandle = Arc::new(RwLock::new(HashMap::new()));
1057 tool_arcs.push(Arc::new(PollTool::new(
1058 security.clone(),
1059 Arc::clone(&poll_handle),
1060 )));
1061
1062 if root_config.sop.sops_dir.is_some() {
1064 let mut engine = crate::sop::SopEngine::new(root_config.sop.clone());
1065 engine.reload(workspace_dir);
1066 let sop_engine = Arc::new(std::sync::Mutex::new(engine));
1067 tool_arcs.push(Arc::new(SopListTool::new(Arc::clone(&sop_engine))));
1068 tool_arcs.push(Arc::new(SopExecuteTool::new(Arc::clone(&sop_engine))));
1069 tool_arcs.push(Arc::new(SopAdvanceTool::new(Arc::clone(&sop_engine))));
1070 tool_arcs.push(Arc::new(SopApproveTool::new(Arc::clone(&sop_engine))));
1071 tool_arcs.push(Arc::new(SopStatusTool::new(Arc::clone(&sop_engine))));
1072 }
1073
1074 if let Some(key) = composio_key
1075 && !key.is_empty()
1076 {
1077 tool_arcs.push(Arc::new(ComposioTool::new(
1078 key,
1079 composio_entity_id,
1080 security.clone(),
1081 )));
1082 }
1083
1084 let reaction_handle: PerToolChannelHandle = Arc::new(RwLock::new(HashMap::new()));
1086 let reaction_tool = ReactionTool::new(security.clone(), Arc::clone(&reaction_handle));
1087 tool_arcs.push(Arc::new(reaction_tool));
1088
1089 let ask_user_handle: Option<PerToolChannelHandle> = Some(Arc::new(RwLock::new(HashMap::new())));
1091 let ask_user_tool =
1092 AskUserTool::new(security.clone(), ask_user_handle.as_ref().cloned().unwrap());
1093 tool_arcs.push(Arc::new(ask_user_tool));
1094
1095 let escalate_handle: Option<PerToolChannelHandle> = Some(Arc::new(RwLock::new(HashMap::new())));
1097 let escalate_tool = EscalateToHumanTool::new(
1098 security.clone(),
1099 root_config.escalation.alert_channels.clone(),
1100 escalate_handle.as_ref().cloned().unwrap(),
1101 );
1102 tool_arcs.push(Arc::new(escalate_tool));
1103
1104 let channel_send_handle: Option<PerToolChannelHandle> =
1107 Some(Arc::new(RwLock::new(HashMap::new())));
1108 let default_targets = crate::channel_targets::build_default_targets_map(root_config);
1109 let channel_send_tool = ChannelSendTool::new(
1110 security.clone(),
1111 channel_send_handle.as_ref().cloned().unwrap(),
1112 Arc::new(parking_lot::RwLock::new(default_targets)),
1113 );
1114 tool_arcs.push(Arc::new(channel_send_tool));
1115
1116 if root_config.microsoft365.enabled {
1118 let ms_cfg = &root_config.microsoft365;
1119 let tenant_id = ms_cfg
1120 .tenant_id
1121 .as_deref()
1122 .unwrap_or_default()
1123 .trim()
1124 .to_string();
1125 let client_id = ms_cfg
1126 .client_id
1127 .as_deref()
1128 .unwrap_or_default()
1129 .trim()
1130 .to_string();
1131 if !tenant_id.is_empty() && !client_id.is_empty() {
1132 if ms_cfg.auth_flow.trim() == "client_credentials"
1134 && ms_cfg
1135 .client_secret
1136 .as_deref()
1137 .is_none_or(|s| s.trim().is_empty())
1138 {
1139 ::zeroclaw_log::record!(
1140 ERROR,
1141 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
1142 .with_outcome(::zeroclaw_log::EventOutcome::Failure),
1143 "microsoft365: client_credentials auth_flow requires a non-empty client_secret"
1144 );
1145 return AllToolsResult {
1146 unfiltered_tool_arcs: tool_arcs.clone(),
1147 tools: boxed_registry_from_arcs(tool_arcs),
1148 delegate_handle: None,
1149 ask_user_handle,
1150 reaction_handle,
1151 poll_handle: Some(poll_handle),
1152 escalate_handle,
1153 channel_send_handle,
1154 };
1155 }
1156
1157 let resolved = zeroclaw_tools::microsoft365::types::Microsoft365ResolvedConfig {
1158 tenant_id,
1159 client_id,
1160 client_secret: ms_cfg.client_secret.clone(),
1161 auth_flow: ms_cfg.auth_flow.clone(),
1162 scopes: ms_cfg.scopes.clone(),
1163 token_cache_encrypted: ms_cfg.token_cache_encrypted,
1164 user_id: ms_cfg.user_id.as_deref().unwrap_or("me").to_string(),
1165 };
1166 let cache_dir = root_config.config_path.parent().unwrap_or(workspace_dir);
1170 match Microsoft365Tool::new(resolved, security.clone(), cache_dir) {
1171 Ok(tool) => tool_arcs.push(Arc::new(tool)),
1172 Err(e) => {
1173 ::zeroclaw_log::record!(
1174 ERROR,
1175 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
1176 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
1177 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
1178 "microsoft365: failed to initialize tool"
1179 );
1180 }
1181 }
1182 } else {
1183 ::zeroclaw_log::record!(
1184 WARN,
1185 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
1186 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
1187 "microsoft365: skipped registration because tenant_id or client_id is empty"
1188 );
1189 }
1190 }
1191
1192 if root_config.knowledge.enabled {
1194 let db_path_str = root_config.knowledge.db_path.replace(
1195 '~',
1196 &directories::UserDirs::new()
1197 .map(|u| u.home_dir().to_string_lossy().to_string())
1198 .unwrap_or_else(|| ".".to_string()),
1199 );
1200 let db_path = std::path::PathBuf::from(&db_path_str);
1201 match zeroclaw_memory::knowledge_graph::KnowledgeGraph::new(
1202 &db_path,
1203 root_config.knowledge.max_nodes,
1204 ) {
1205 Ok(graph) => {
1206 tool_arcs.push(Arc::new(KnowledgeTool::new(Arc::new(graph))));
1207 }
1208 Err(e) => {
1209 ::zeroclaw_log::record!(
1210 WARN,
1211 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
1212 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
1213 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
1214 "knowledge graph disabled due to init error"
1215 );
1216 }
1217 }
1218 }
1219
1220 let delegate_global_credential = fallback_api_key.and_then(|value| {
1222 let trimmed_value = value.trim();
1223 (!trimmed_value.is_empty()).then(|| trimmed_value.to_owned())
1224 });
1225 let provider_runtime_options =
1226 zeroclaw_providers::provider_runtime_options_from_config(root_config);
1227
1228 let delegate_handle: Option<DelegateParentToolsHandle> = if agents.is_empty() {
1229 None
1230 } else {
1231 let delegate_agents: HashMap<String, AliasedAgentConfig> = agents
1232 .iter()
1233 .map(|(name, cfg)| (name.clone(), cfg.clone()))
1234 .collect();
1235 let parent_tools = Arc::new(RwLock::new(tool_arcs.clone()));
1236 let delegate_tool = DelegateTool::new_with_options(
1237 delegate_agents,
1238 delegate_global_credential.clone(),
1239 security.clone(),
1240 provider_runtime_options.clone(),
1241 )
1242 .with_parent_tools(Arc::clone(&parent_tools))
1243 .with_multimodal_config(root_config.multimodal.clone())
1244 .with_delegate_config(root_config.delegate.clone())
1245 .with_workspace_dir(workspace_dir.to_path_buf())
1246 .with_memory(memory.clone())
1247 .with_providers_models({
1248 let mut m: std::collections::HashMap<
1256 String,
1257 std::collections::HashMap<String, zeroclaw_config::schema::ModelProviderConfig>,
1258 > = std::collections::HashMap::new();
1259 for (t, a, base) in root_config.providers.models.iter_entries() {
1260 m.entry(t.to_string())
1261 .or_default()
1262 .insert(a.to_string(), base.clone());
1263 }
1264 m
1265 })
1266 .with_risk_profiles(root_config.risk_profiles.clone())
1267 .with_runtime_profiles(root_config.runtime_profiles.clone())
1268 .with_skill_bundles(root_config.skill_bundles.clone())
1269 .with_root_config(config.clone());
1270 tool_arcs.push(Arc::new(delegate_tool));
1271 Some(parent_tools)
1272 };
1273
1274 if root_config.verifiable_intent.enabled {
1276 let strictness = match root_config.verifiable_intent.strictness.as_str() {
1277 "permissive" => crate::verifiable_intent::StrictnessMode::Permissive,
1278 _ => crate::verifiable_intent::StrictnessMode::Strict,
1279 };
1280 tool_arcs.push(Arc::new(VerifiableIntentTool::new(
1281 security.clone(),
1282 strictness,
1283 )));
1284 }
1285
1286 #[cfg(feature = "plugins-wasm")]
1288 {
1289 let plugin_dir = config.plugins.plugins_dir.clone();
1290 let plugin_path = if plugin_dir.starts_with("~/") {
1291 let home = directories::UserDirs::new()
1292 .map(|u| u.home_dir().to_path_buf())
1293 .unwrap_or_else(|| std::path::PathBuf::from("."));
1294 home.join(plugin_dir.strip_prefix("~/").unwrap())
1295 } else {
1296 std::path::PathBuf::from(&plugin_dir)
1297 };
1298
1299 if plugin_path.exists() && config.plugins.enabled {
1300 match zeroclaw_plugins::host::PluginHost::new(
1301 plugin_path.parent().unwrap_or(&plugin_path),
1302 ) {
1303 Ok(host) => {
1304 let details = host.tool_plugin_details();
1305 let count = details.len();
1306 for (manifest, wasm_path) in details {
1307 tool_arcs.push(Arc::new(zeroclaw_plugins::wasm_tool::WasmTool::from_wasm(
1308 wasm_path.to_path_buf(),
1309 manifest.permissions.clone(),
1310 manifest.name.clone(),
1311 manifest.description.clone().unwrap_or_default(),
1312 )));
1313 }
1314 ::zeroclaw_log::record!(
1315 INFO,
1316 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
1317 .with_attrs(::serde_json::json!({"count": count})),
1318 "Loaded WASM plugin tools"
1319 );
1320 }
1321 Err(e) => {
1322 ::zeroclaw_log::record!(
1323 WARN,
1324 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
1325 .with_outcome(::zeroclaw_log::EventOutcome::Unknown)
1326 .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
1327 "Failed to load WASM plugins"
1328 );
1329 }
1330 }
1331 }
1332 }
1333
1334 if root_config.pipeline.enabled {
1336 let pipeline_tools: Vec<Arc<dyn Tool>> = tool_arcs.clone();
1337 tool_arcs.push(Arc::new(PipelineTool::new(
1338 root_config.pipeline.clone(),
1339 pipeline_tools,
1340 )));
1341 }
1342
1343 AllToolsResult {
1344 unfiltered_tool_arcs: tool_arcs.clone(),
1345 tools: boxed_registry_from_arcs(tool_arcs),
1346 delegate_handle,
1347 ask_user_handle,
1348 reaction_handle,
1349 poll_handle: Some(poll_handle),
1350 escalate_handle,
1351 channel_send_handle,
1352 }
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357 use super::*;
1358 use tempfile::TempDir;
1359 use zeroclaw_config::schema::{BrowserConfig, Config, MemoryConfig};
1360
1361 fn test_config(tmp: &TempDir) -> Config {
1362 Config {
1363 data_dir: tmp.path().join("data"),
1364 config_path: tmp.path().join("config.toml"),
1365 ..Config::default()
1366 }
1367 }
1368
1369 #[test]
1370 fn default_tools_has_expected_count() {
1371 let security = Arc::new(SecurityPolicy::default());
1372 let tools = default_tools(security);
1373 assert_eq!(tools.len(), 6);
1374 }
1375
1376 #[test]
1377 fn all_tools_excludes_browser_when_disabled() {
1378 let tmp = TempDir::new().unwrap();
1379 let security = Arc::new(SecurityPolicy::default());
1380 let mem_cfg = MemoryConfig {
1381 backend: "markdown".into(),
1382 ..MemoryConfig::default()
1383 };
1384 let mem: Arc<dyn Memory> =
1385 Arc::from(zeroclaw_memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1386
1387 let browser = BrowserConfig {
1388 enabled: false,
1389 allowed_domains: vec!["example.com".into()],
1390 session_name: None,
1391 ..BrowserConfig::default()
1392 };
1393 let http = zeroclaw_config::schema::HttpRequestConfig::default();
1394 let cfg = test_config(&tmp);
1395
1396 let (tools, _, _, _, _, _, _) = all_tools(
1397 Arc::new(Config::default()),
1398 &security,
1399 &zeroclaw_config::schema::RiskProfileConfig::default(),
1400 "test-agent",
1401 mem,
1402 None,
1403 None,
1404 &browser,
1405 &http,
1406 &zeroclaw_config::schema::WebFetchConfig::default(),
1407 tmp.path(),
1408 &HashMap::new(),
1409 None,
1410 &cfg,
1411 None,
1412 false,
1413 );
1414 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1415 assert!(!names.contains(&"browser_open"));
1416 assert!(names.contains(&"schedule"));
1417 assert!(names.contains(&"model_routing_config"));
1418 assert!(names.contains(&"pushover"));
1419 assert!(names.contains(&"proxy_config"));
1420 }
1421
1422 #[test]
1423 fn all_tools_includes_browser_when_enabled() {
1424 let tmp = TempDir::new().unwrap();
1425 let security = Arc::new(SecurityPolicy::default());
1426 let mem_cfg = MemoryConfig {
1427 backend: "markdown".into(),
1428 ..MemoryConfig::default()
1429 };
1430 let mem: Arc<dyn Memory> =
1431 Arc::from(zeroclaw_memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1432
1433 let browser = BrowserConfig {
1434 enabled: true,
1435 allowed_domains: vec!["example.com".into()],
1436 session_name: None,
1437 ..BrowserConfig::default()
1438 };
1439 let http = zeroclaw_config::schema::HttpRequestConfig::default();
1440 let cfg = test_config(&tmp);
1441
1442 let (tools, _, _, _, _, _, _) = all_tools(
1443 Arc::new(Config::default()),
1444 &security,
1445 &zeroclaw_config::schema::RiskProfileConfig::default(),
1446 "test-agent",
1447 mem,
1448 None,
1449 None,
1450 &browser,
1451 &http,
1452 &zeroclaw_config::schema::WebFetchConfig::default(),
1453 tmp.path(),
1454 &HashMap::new(),
1455 None,
1456 &cfg,
1457 None,
1458 false,
1459 );
1460 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1461 assert!(names.contains(&"browser_open"));
1462 assert!(names.contains(&"content_search"));
1463 assert!(names.contains(&"model_routing_config"));
1464 assert!(names.contains(&"pushover"));
1465 assert!(names.contains(&"proxy_config"));
1466 }
1467
1468 #[test]
1469 fn default_tools_names() {
1470 let security = Arc::new(SecurityPolicy::default());
1471 let tools = default_tools(security);
1472 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1473 assert!(names.contains(&"shell"));
1474 assert!(names.contains(&"file_read"));
1475 assert!(names.contains(&"file_write"));
1476 assert!(names.contains(&"file_edit"));
1477 assert!(names.contains(&"glob_search"));
1478 assert!(names.contains(&"content_search"));
1479 }
1480
1481 #[test]
1482 fn default_tools_all_have_descriptions() {
1483 let security = Arc::new(SecurityPolicy::default());
1484 let tools = default_tools(security);
1485 for tool in &tools {
1486 assert!(
1487 !tool.description().is_empty(),
1488 "Tool {} has empty description",
1489 tool.name()
1490 );
1491 }
1492 }
1493
1494 #[test]
1495 fn default_tools_all_have_schemas() {
1496 let security = Arc::new(SecurityPolicy::default());
1497 let tools = default_tools(security);
1498 for tool in &tools {
1499 let schema = tool.parameters_schema();
1500 assert!(
1501 schema.is_object(),
1502 "Tool {} schema is not an object",
1503 tool.name()
1504 );
1505 assert!(
1506 schema["properties"].is_object(),
1507 "Tool {} schema has no properties",
1508 tool.name()
1509 );
1510 }
1511 }
1512
1513 #[test]
1514 fn tool_spec_generation() {
1515 let security = Arc::new(SecurityPolicy::default());
1516 let tools = default_tools(security);
1517 for tool in &tools {
1518 let spec = tool.spec();
1519 assert_eq!(spec.name, tool.name());
1520 assert_eq!(spec.description, tool.description());
1521 assert!(spec.parameters.is_object());
1522 }
1523 }
1524
1525 #[test]
1526 fn tool_result_serde() {
1527 let result = ToolResult {
1528 success: true,
1529 output: "hello".into(),
1530 error: None,
1531 };
1532 let json = serde_json::to_string(&result).unwrap();
1533 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
1534 assert!(parsed.success);
1535 assert_eq!(parsed.output, "hello");
1536 assert!(parsed.error.is_none());
1537 }
1538
1539 #[test]
1540 fn tool_result_with_error_serde() {
1541 let result = ToolResult {
1542 success: false,
1543 output: String::new(),
1544 error: Some("boom".into()),
1545 };
1546 let json = serde_json::to_string(&result).unwrap();
1547 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
1548 assert!(!parsed.success);
1549 assert_eq!(parsed.error.as_deref(), Some("boom"));
1550 }
1551
1552 #[test]
1553 fn tool_spec_serde() {
1554 let spec = ToolSpec {
1555 name: "test".into(),
1556 description: "A test tool".into(),
1557 parameters: serde_json::json!({"type": "object"}),
1558 };
1559 let json = serde_json::to_string(&spec).unwrap();
1560 let parsed: ToolSpec = serde_json::from_str(&json).unwrap();
1561 assert_eq!(parsed.name, "test");
1562 assert_eq!(parsed.description, "A test tool");
1563 }
1564
1565 #[test]
1566 fn all_tools_includes_delegate_when_agents_configured() {
1567 let tmp = TempDir::new().unwrap();
1568 let security = Arc::new(SecurityPolicy::default());
1569 let mem_cfg = MemoryConfig {
1570 backend: "markdown".into(),
1571 ..MemoryConfig::default()
1572 };
1573 let mem: Arc<dyn Memory> =
1574 Arc::from(zeroclaw_memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1575
1576 let browser = BrowserConfig::default();
1577 let http = zeroclaw_config::schema::HttpRequestConfig::default();
1578 let cfg = test_config(&tmp);
1579
1580 let mut agents = HashMap::new();
1581 agents.insert(
1582 "researcher".to_string(),
1583 AliasedAgentConfig {
1584 model_provider: "ollama.researcher".into(),
1585 ..Default::default()
1586 },
1587 );
1588
1589 let (tools, _, _, _, _, _, _) = all_tools(
1590 Arc::new(Config::default()),
1591 &security,
1592 &zeroclaw_config::schema::RiskProfileConfig::default(),
1593 "test-agent",
1594 mem,
1595 None,
1596 None,
1597 &browser,
1598 &http,
1599 &zeroclaw_config::schema::WebFetchConfig::default(),
1600 tmp.path(),
1601 &agents,
1602 Some("delegate-test-credential"),
1603 &cfg,
1604 None,
1605 false,
1606 );
1607 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1608 assert!(names.contains(&"delegate"));
1609 }
1610
1611 #[test]
1612 fn all_tools_excludes_delegate_when_no_agents() {
1613 let tmp = TempDir::new().unwrap();
1614 let security = Arc::new(SecurityPolicy::default());
1615 let mem_cfg = MemoryConfig {
1616 backend: "markdown".into(),
1617 ..MemoryConfig::default()
1618 };
1619 let mem: Arc<dyn Memory> =
1620 Arc::from(zeroclaw_memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1621
1622 let browser = BrowserConfig::default();
1623 let http = zeroclaw_config::schema::HttpRequestConfig::default();
1624 let cfg = test_config(&tmp);
1625
1626 let (tools, _, _, _, _, _, _) = all_tools(
1627 Arc::new(Config::default()),
1628 &security,
1629 &zeroclaw_config::schema::RiskProfileConfig::default(),
1630 "test-agent",
1631 mem,
1632 None,
1633 None,
1634 &browser,
1635 &http,
1636 &zeroclaw_config::schema::WebFetchConfig::default(),
1637 tmp.path(),
1638 &HashMap::new(),
1639 None,
1640 &cfg,
1641 None,
1642 false,
1643 );
1644 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1645 assert!(!names.contains(&"delegate"));
1646 }
1647
1648 #[test]
1649 fn all_tools_includes_read_skill_in_compact_mode() {
1650 let tmp = TempDir::new().unwrap();
1651 let security = Arc::new(SecurityPolicy::default());
1652 let mem_cfg = MemoryConfig {
1653 backend: "markdown".into(),
1654 ..MemoryConfig::default()
1655 };
1656 let mem: Arc<dyn Memory> =
1657 Arc::from(zeroclaw_memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1658
1659 let browser = BrowserConfig::default();
1660 let http = zeroclaw_config::schema::HttpRequestConfig::default();
1661 let mut cfg = test_config(&tmp);
1662 cfg.skills.prompt_injection_mode =
1663 zeroclaw_config::schema::SkillsPromptInjectionMode::Compact;
1664
1665 let (tools, _, _, _, _, _, _) = all_tools(
1666 Arc::new(cfg.clone()),
1667 &security,
1668 &zeroclaw_config::schema::RiskProfileConfig::default(),
1669 "test-agent",
1670 mem,
1671 None,
1672 None,
1673 &browser,
1674 &http,
1675 &zeroclaw_config::schema::WebFetchConfig::default(),
1676 tmp.path(),
1677 &HashMap::new(),
1678 None,
1679 &cfg,
1680 None,
1681 false,
1682 );
1683 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1684 assert!(names.contains(&"read_skill"));
1685 }
1686
1687 #[test]
1688 fn all_tools_excludes_read_skill_in_full_mode() {
1689 let tmp = TempDir::new().unwrap();
1690 let security = Arc::new(SecurityPolicy::default());
1691 let mem_cfg = MemoryConfig {
1692 backend: "markdown".into(),
1693 ..MemoryConfig::default()
1694 };
1695 let mem: Arc<dyn Memory> =
1696 Arc::from(zeroclaw_memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
1697
1698 let browser = BrowserConfig::default();
1699 let http = zeroclaw_config::schema::HttpRequestConfig::default();
1700 let mut cfg = test_config(&tmp);
1701 cfg.skills.prompt_injection_mode = zeroclaw_config::schema::SkillsPromptInjectionMode::Full;
1702
1703 let (tools, _, _, _, _, _, _) = all_tools(
1704 Arc::new(cfg.clone()),
1705 &security,
1706 &zeroclaw_config::schema::RiskProfileConfig::default(),
1707 "test-agent",
1708 mem,
1709 None,
1710 None,
1711 &browser,
1712 &http,
1713 &zeroclaw_config::schema::WebFetchConfig::default(),
1714 tmp.path(),
1715 &HashMap::new(),
1716 None,
1717 &cfg,
1718 None,
1719 false,
1720 );
1721 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
1722 assert!(!names.contains(&"read_skill"));
1723 }
1724}