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