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