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