Skip to main content

construct/tools/
mod.rs

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