Skip to main content

a3s_code_core/tools/builtin/
mod.rs

1//! Native Rust implementations of all built-in tools
2//!
3//! These replace the previous `a3s-tools` binary backend with direct Rust
4//! implementations that execute in-process. Each tool implements the `Tool` trait.
5
6pub mod ask_user;
7pub(crate) mod bash;
8mod batch;
9mod bm25;
10mod code_intelligence;
11mod download;
12mod edit;
13mod generate_object;
14pub(crate) mod git;
15mod glob_tool;
16mod grep;
17mod hybrid_search;
18mod ls;
19mod patch;
20mod read;
21mod safe_http;
22mod search;
23mod semantic_search;
24mod update_plan;
25mod web_fetch;
26mod web_search;
27mod write;
28
29use super::registry::ToolRegistry;
30use std::sync::Arc;
31
32/// Normalize a source URL before it can enter durable tool/task metadata.
33/// Credentials, query strings, and fragments are intentionally excluded.
34pub(crate) fn safe_http_source_url(value: &str) -> Option<String> {
35    let mut url = reqwest::Url::parse(value.trim()).ok()?;
36    if !matches!(url.scheme(), "http" | "https") || url.host_str()?.is_empty() {
37        return None;
38    }
39    url.set_username("").ok()?;
40    url.set_password(None).ok()?;
41    url.set_query(None);
42    url.set_fragment(None);
43    Some(url.to_string())
44}
45
46/// Register all baseline built-in tools with the registry, gated by
47/// workspace capabilities.
48///
49/// Tools whose required capability is missing are not registered, so the model
50/// never sees a tool the backend cannot service. `web_fetch` and `web_search`
51/// have no workspace capability and are always registered.
52///
53/// Note: `batch` is NOT registered here — it requires an `Arc<ToolRegistry>`
54/// and must be registered after the registry is wrapped in an Arc.
55pub fn register_builtins(
56    registry: &ToolRegistry,
57    workspace_services: &crate::workspace::WorkspaceServices,
58) {
59    let capabilities = workspace_services.capabilities();
60    if capabilities.read {
61        registry.register_builtin(Arc::new(read::ReadTool));
62        registry.register_builtin(Arc::new(ls::LsTool));
63    }
64    if capabilities.write {
65        registry.register_builtin(Arc::new(write::WriteTool));
66        if workspace_services.local_root().is_some() {
67            registry.register_builtin(Arc::new(download::DownloadTool));
68        }
69    }
70    if capabilities.read && capabilities.write {
71        registry.register_builtin(Arc::new(edit::EditTool));
72        registry.register_builtin(Arc::new(patch::PatchTool));
73    }
74    if capabilities.exec {
75        registry.register_builtin(Arc::new(bash::BashTool));
76    }
77    let semantic_enabled = capabilities.read && workspace_services.workspace_retrieval().is_some();
78    // Prefer durable FTS at call time when this workspace can host one. Do not
79    // open native indexes during tool registration (Loading / session build).
80    let persistent_backend_enabled = capabilities.read && workspace_services.local_root().is_some();
81    if capabilities.search || semantic_enabled || persistent_backend_enabled {
82        let search = search::SearchTool::new(capabilities.read)
83            .with_backend_search(capabilities.search)
84            .with_semantic(semantic_enabled)
85            .with_persistent_backend(persistent_backend_enabled);
86        registry.register_builtin(Arc::new(search));
87    }
88    if workspace_services.code_intelligence().is_some() {
89        code_intelligence::register(registry);
90    }
91    if capabilities.git {
92        registry.register_builtin(Arc::new(git::GitTool));
93    }
94    registry.register_builtin(Arc::new(web_fetch::WebFetchTool));
95    registry.register_builtin(Arc::new(web_search::WebSearchTool::new()));
96    // Session checklist updates (no workspace capability gate).
97    registry.register_builtin(Arc::new(update_plan::UpdatePlanTool));
98    registry.register_builtin(Arc::new(ask_user::AskUserTool));
99}
100
101#[cfg(test)]
102pub(crate) fn repository_tool_parameter_schemas() -> Vec<(String, serde_json::Value)> {
103    use crate::tools::Tool;
104
105    let read = read::ReadTool;
106    let search = search::SearchTool::new(true);
107    let edit = edit::EditTool;
108    vec![
109        (read.name().to_string(), read.parameters()),
110        (search.name().to_string(), search.parameters()),
111        (edit.name().to_string(), edit.parameters()),
112    ]
113}
114
115/// Register the batch tool. Must be called after the registry is wrapped in Arc.
116pub fn register_batch(registry: &Arc<ToolRegistry>) {
117    registry.register_builtin(Arc::new(batch::BatchTool::new_registry_bound(Arc::clone(
118        registry,
119    ))));
120}
121
122/// Register the programmatic tool calling wrapper.
123pub fn register_program(registry: &Arc<ToolRegistry>) {
124    register_program_with_catalog(
125        registry,
126        crate::program::ProgramCatalog::with_builtin_programs(),
127    );
128}
129
130/// Register the programmatic tool calling wrapper with a custom catalog.
131pub fn register_program_with_catalog(
132    registry: &Arc<ToolRegistry>,
133    catalog: crate::program::ProgramCatalog,
134) {
135    registry.register_builtin(Arc::new(
136        crate::tools::ProgramTool::with_catalog_registry_bound(Arc::clone(registry), catalog),
137    ));
138}
139
140/// Register the canonical `task` tool (multi-item fan-out included).
141///
142/// Model-visible `parallel_task` is not registered (`HARNESS-CONV4`).
143/// Must be called after the registry is wrapped in Arc. Requires an LLM client
144/// and the workspace path so child agent loops can be spawned inline.
145/// Optionally accepts an MCP manager so child sessions inherit MCP tools.
146pub fn register_task(
147    registry: &Arc<ToolRegistry>,
148    llm_client: Arc<dyn crate::llm::LlmClient>,
149    agent_registry: Arc<crate::subagent::AgentRegistry>,
150    workspace: String,
151) {
152    register_task_with_mcp(
153        registry,
154        llm_client,
155        agent_registry,
156        workspace,
157        None,
158        None,
159        None,
160    );
161}
162
163/// Register the task delegation tools with optional MCP manager and parent context.
164///
165/// When `mcp_manager` is provided, delegated child sessions will have access
166/// to all MCP tools from connected servers.
167/// When `parent_context` is provided, child runs inherit parent capabilities.
168/// When `subagent_tracker` is provided, each task registers a
169/// `CancellationToken` against it so callers can cancel by `task_id`.
170pub fn register_task_with_mcp(
171    registry: &Arc<ToolRegistry>,
172    llm_client: Arc<dyn crate::llm::LlmClient>,
173    agent_registry: Arc<crate::subagent::AgentRegistry>,
174    workspace: String,
175    mcp_manager: Option<Arc<crate::mcp::manager::McpManager>>,
176    parent_context: Option<crate::child_run::ChildRunContext>,
177    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
178) {
179    register_task_with_mcp_managers(
180        registry,
181        llm_client,
182        agent_registry,
183        workspace,
184        mcp_manager.into_iter().collect(),
185        parent_context,
186        subagent_tracker,
187    );
188}
189
190/// Register task delegation tools with ordered MCP capability sources.
191///
192/// Each manager keeps ownership of its own connections. Later sources shadow
193/// earlier sources on identical fully-qualified tool names inside child runs.
194pub fn register_task_with_mcp_managers(
195    registry: &Arc<ToolRegistry>,
196    llm_client: Arc<dyn crate::llm::LlmClient>,
197    agent_registry: Arc<crate::subagent::AgentRegistry>,
198    workspace: String,
199    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
200    parent_context: Option<crate::child_run::ChildRunContext>,
201    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
202) {
203    register_task_internal(
204        registry,
205        llm_client,
206        agent_registry,
207        workspace,
208        mcp_managers,
209        Vec::new(),
210        parent_context,
211        subagent_tracker,
212        None,
213    );
214}
215
216/// Register session task tools with the owning agent's shared scheduler.
217#[allow(clippy::too_many_arguments)]
218pub(crate) fn register_task_with_mcp_managers_and_scheduler(
219    registry: &Arc<ToolRegistry>,
220    llm_client: Arc<dyn crate::llm::LlmClient>,
221    agent_registry: Arc<crate::subagent::AgentRegistry>,
222    workspace: String,
223    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
224    parent_context: Option<crate::child_run::ChildRunContext>,
225    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
226    task_scheduler: Arc<crate::task_scheduler::TaskScheduler>,
227) {
228    register_task_with_mcp_sources_and_scheduler(
229        registry,
230        llm_client,
231        agent_registry,
232        workspace,
233        mcp_managers,
234        Vec::new(),
235        parent_context,
236        subagent_tracker,
237        task_scheduler,
238    );
239}
240
241/// Register Run-frozen task tools with compatibility managers and exact MCP
242/// capability bindings.
243#[allow(clippy::too_many_arguments)]
244pub(crate) fn register_task_with_mcp_sources_and_scheduler(
245    registry: &Arc<ToolRegistry>,
246    llm_client: Arc<dyn crate::llm::LlmClient>,
247    agent_registry: Arc<crate::subagent::AgentRegistry>,
248    workspace: String,
249    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
250    mcp_bindings: Vec<Arc<crate::mcp::McpBinding>>,
251    parent_context: Option<crate::child_run::ChildRunContext>,
252    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
253    task_scheduler: Arc<crate::task_scheduler::TaskScheduler>,
254) {
255    register_task_internal(
256        registry,
257        llm_client,
258        agent_registry,
259        workspace,
260        mcp_managers,
261        mcp_bindings,
262        parent_context,
263        subagent_tracker,
264        Some(task_scheduler),
265    );
266}
267
268#[allow(clippy::too_many_arguments)]
269fn register_task_internal(
270    registry: &Arc<ToolRegistry>,
271    llm_client: Arc<dyn crate::llm::LlmClient>,
272    agent_registry: Arc<crate::subagent::AgentRegistry>,
273    workspace: String,
274    mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
275    mcp_bindings: Vec<Arc<crate::mcp::McpBinding>>,
276    parent_context: Option<crate::child_run::ChildRunContext>,
277    subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
278    task_scheduler: Option<Arc<crate::task_scheduler::TaskScheduler>>,
279) {
280    use crate::tools::task::{TaskExecutor, TaskTool};
281    let mut executor =
282        TaskExecutor::with_mcp_managers(agent_registry, llm_client, workspace, mcp_managers)
283            .with_projected_mcp_bindings(mcp_bindings);
284    if let Some(ctx) = parent_context {
285        executor = executor.with_parent_context(ctx);
286    }
287    if let Some(tracker) = subagent_tracker {
288        executor = executor.with_subagent_tracker(tracker);
289    }
290    if let Some(task_scheduler) = task_scheduler {
291        // Model-visible task tools run inside an already-admitted parent turn.
292        // Only their detached background branch needs another global slot.
293        executor = executor.with_task_scheduler(task_scheduler, false);
294    }
295    let executor = Arc::new(executor);
296    registry.register_builtin(Arc::new(TaskTool::new(Arc::clone(&executor))));
297    // `parallel_task` is removed from the model-visible registry (`HARNESS-CONV4`).
298    // Fan-out uses the unified `task` tool; the historical ParallelTaskTool type
299    // stays crate-internal for unit tests only.
300}
301
302/// Register the Skill tool for skill-based tool access control.
303pub(crate) fn register_skill(
304    registry: &Arc<ToolRegistry>,
305    llm_client: Arc<dyn crate::llm::LlmClient>,
306    skill_registry: Arc<crate::skills::SkillRegistry>,
307    tool_executor: Arc<crate::tools::ToolExecutor>,
308    base_config: crate::agent::AgentConfig,
309) {
310    use crate::tools::skill::{SearchSkillsTool, SkillTool};
311    registry.register_builtin(Arc::new(SearchSkillsTool::new(Arc::clone(&skill_registry))));
312    registry.register_builtin(Arc::new(SkillTool::new_registry_bound(
313        skill_registry,
314        llm_client,
315        tool_executor,
316        base_config,
317    )));
318}
319
320/// Register the `generate_object` tool for structured JSON output.
321///
322/// Must be called after the registry is wrapped in Arc. Requires an LLM client
323/// so the tool can make its own LLM calls for object generation.
324pub fn register_generate_object(
325    registry: &Arc<ToolRegistry>,
326    llm_client: Arc<dyn crate::llm::LlmClient>,
327) {
328    registry.register_builtin(Arc::new(generate_object::GenerateObjectTool::new(
329        llm_client,
330    )));
331}
332
333#[cfg(test)]
334mod tests {
335    use super::register_builtins;
336    use super::safe_http_source_url;
337    use crate::tools::registry::ToolRegistry;
338    use crate::workspace::{
339        ChunkCatalogLimits, ChunkingConfig, ManifestWorkspaceBackend, WorkspaceChunkingStrategy,
340        WorkspaceServices,
341    };
342    use std::sync::Arc;
343
344    #[test]
345    fn safe_source_url_removes_credentials_query_and_fragment() {
346        assert_eq!(
347            safe_http_source_url(
348                "HTTPS://user:password@Example.COM/report?access_token=secret#section"
349            )
350            .as_deref(),
351            Some("https://example.com/report")
352        );
353        assert!(safe_http_source_url("file:///tmp/source").is_none());
354    }
355
356    #[tokio::test]
357    async fn register_builtins_does_not_open_durable_a3s_vec() {
358        let temp = tempfile::tempdir().unwrap();
359        let backend = ManifestWorkspaceBackend::new(temp.path());
360        backend
361            .configure_chunk_catalog(
362                WorkspaceChunkingStrategy::Lines,
363                ChunkingConfig::default(),
364                ChunkCatalogLimits::default(),
365            )
366            .unwrap();
367        let services = WorkspaceServices::local_with_retrieval_backend(Arc::clone(&backend));
368        let registry = ToolRegistry::new(temp.path().to_path_buf());
369        register_builtins(&registry, &services);
370        assert!(
371            backend.persistent_index().is_none(),
372            "tool registration must not attach durable FTS"
373        );
374        assert!(
375            !temp.path().join(".a3s-code").join("index").exists(),
376            "tool registration must not create the durable index directory"
377        );
378        assert!(
379            registry.get("search").is_some(),
380            "local workspaces still register search for demand-driven durable FTS"
381        );
382    }
383
384    #[test]
385    fn register_builtins_hides_disabled_capabilities() {
386        use crate::workspace::{
387            LocalWorkspaceBackend, WorkspaceCapabilities, WorkspaceFileSystem, WorkspaceRef,
388        };
389
390        let temp = tempfile::tempdir().unwrap();
391        let backend = Arc::new(LocalWorkspaceBackend::new(temp.path().to_path_buf()));
392        let fs: Arc<dyn WorkspaceFileSystem> = backend;
393        let services = WorkspaceServices::builder(
394            WorkspaceRef::new("ws", temp.path().display().to_string()),
395            fs,
396        )
397        .capabilities(WorkspaceCapabilities {
398            read: true,
399            write: false,
400            exec: false,
401            search: false,
402            git: false,
403            code_intelligence: false,
404        })
405        .build();
406        let registry = ToolRegistry::new(temp.path().to_path_buf());
407        register_builtins(&registry, &services);
408
409        for present in [
410            "read",
411            "ls",
412            "web_fetch",
413            "web_search",
414            "update_plan",
415            "ask_user",
416        ] {
417            assert!(registry.contains(present), "{present} must stay registered");
418        }
419        for absent in [
420            "write",
421            "edit",
422            "patch",
423            "bash",
424            "git",
425            "download",
426            "code_symbols",
427            "code_navigation",
428            "code_diagnostics",
429            "parallel_task",
430        ] {
431            assert!(
432                !registry.contains(absent),
433                "{absent} must stay absent when its capability is off"
434            );
435        }
436    }
437}