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