a3s_code_core/tools/builtin/
mod.rs1pub(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
31pub(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
45pub 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 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 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
113pub fn register_batch(registry: &Arc<ToolRegistry>) {
115 registry.register_builtin(Arc::new(batch::BatchTool::new_registry_bound(Arc::clone(
116 registry,
117 ))));
118}
119
120pub fn register_program(registry: &Arc<ToolRegistry>) {
122 register_program_with_catalog(
123 registry,
124 crate::program::ProgramCatalog::with_builtin_programs(),
125 );
126}
127
128pub 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
138pub 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
161pub 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
188pub 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#[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#[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 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 }
299
300pub(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
318pub 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(®istry, &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}