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