1pub mod ask_user;
7pub(crate) mod bash;
8mod batch;
9mod bm25;
10mod code_intelligence;
11mod download;
12mod edit;
13mod generate_object;
14pub(crate) mod git;
15mod glob_tool;
16mod grep;
17mod hybrid_search;
18mod ls;
19mod patch;
20mod read;
21mod safe_http;
22mod search;
23mod semantic_search;
24mod update_plan;
25mod web_fetch;
26mod web_search;
27mod write;
28
29use super::registry::ToolRegistry;
30use std::sync::Arc;
31
32pub(crate) fn safe_http_source_url(value: &str) -> Option<String> {
35 let mut url = reqwest::Url::parse(value.trim()).ok()?;
36 if !matches!(url.scheme(), "http" | "https") || url.host_str()?.is_empty() {
37 return None;
38 }
39 url.set_username("").ok()?;
40 url.set_password(None).ok()?;
41 url.set_query(None);
42 url.set_fragment(None);
43 Some(url.to_string())
44}
45
46pub fn register_builtins(
56 registry: &ToolRegistry,
57 workspace_services: &crate::workspace::WorkspaceServices,
58) {
59 let capabilities = workspace_services.capabilities();
60 if capabilities.read {
61 registry.register_builtin(Arc::new(read::ReadTool));
62 registry.register_builtin(Arc::new(ls::LsTool));
63 }
64 if capabilities.write {
65 registry.register_builtin(Arc::new(write::WriteTool));
66 if workspace_services.local_root().is_some() {
67 registry.register_builtin(Arc::new(download::DownloadTool));
68 }
69 }
70 if capabilities.read && capabilities.write {
71 registry.register_builtin(Arc::new(edit::EditTool));
72 registry.register_builtin(Arc::new(patch::PatchTool));
73 }
74 if capabilities.exec {
75 registry.register_builtin(Arc::new(bash::BashTool));
76 }
77 let semantic_enabled = capabilities.read && workspace_services.workspace_retrieval().is_some();
78 let persistent_backend_enabled = capabilities.read && workspace_services.local_root().is_some();
81 if capabilities.search || semantic_enabled || persistent_backend_enabled {
82 let search = search::SearchTool::new(capabilities.read)
83 .with_backend_search(capabilities.search)
84 .with_semantic(semantic_enabled)
85 .with_persistent_backend(persistent_backend_enabled);
86 registry.register_builtin(Arc::new(search));
87 }
88 if workspace_services.code_intelligence().is_some() {
89 code_intelligence::register(registry);
90 }
91 if capabilities.git {
92 registry.register_builtin(Arc::new(git::GitTool));
93 }
94 registry.register_builtin(Arc::new(web_fetch::WebFetchTool));
95 registry.register_builtin(Arc::new(web_search::WebSearchTool::new()));
96 registry.register_builtin(Arc::new(update_plan::UpdatePlanTool));
98 registry.register_builtin(Arc::new(ask_user::AskUserTool));
99}
100
101#[cfg(test)]
102pub(crate) fn repository_tool_parameter_schemas() -> Vec<(String, serde_json::Value)> {
103 use crate::tools::Tool;
104
105 let read = read::ReadTool;
106 let search = search::SearchTool::new(true);
107 let edit = edit::EditTool;
108 vec![
109 (read.name().to_string(), read.parameters()),
110 (search.name().to_string(), search.parameters()),
111 (edit.name().to_string(), edit.parameters()),
112 ]
113}
114
115pub fn register_batch(registry: &Arc<ToolRegistry>) {
117 registry.register_builtin(Arc::new(batch::BatchTool::new_registry_bound(Arc::clone(
118 registry,
119 ))));
120}
121
122pub fn register_program(registry: &Arc<ToolRegistry>) {
124 register_program_with_catalog(
125 registry,
126 crate::program::ProgramCatalog::with_builtin_programs(),
127 );
128}
129
130pub fn register_program_with_catalog(
132 registry: &Arc<ToolRegistry>,
133 catalog: crate::program::ProgramCatalog,
134) {
135 registry.register_builtin(Arc::new(
136 crate::tools::ProgramTool::with_catalog_registry_bound(Arc::clone(registry), catalog),
137 ));
138}
139
140pub fn register_task(
147 registry: &Arc<ToolRegistry>,
148 llm_client: Arc<dyn crate::llm::LlmClient>,
149 agent_registry: Arc<crate::subagent::AgentRegistry>,
150 workspace: String,
151) {
152 register_task_with_mcp(
153 registry,
154 llm_client,
155 agent_registry,
156 workspace,
157 None,
158 None,
159 None,
160 );
161}
162
163pub fn register_task_with_mcp(
171 registry: &Arc<ToolRegistry>,
172 llm_client: Arc<dyn crate::llm::LlmClient>,
173 agent_registry: Arc<crate::subagent::AgentRegistry>,
174 workspace: String,
175 mcp_manager: Option<Arc<crate::mcp::manager::McpManager>>,
176 parent_context: Option<crate::child_run::ChildRunContext>,
177 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
178) {
179 register_task_with_mcp_managers(
180 registry,
181 llm_client,
182 agent_registry,
183 workspace,
184 mcp_manager.into_iter().collect(),
185 parent_context,
186 subagent_tracker,
187 );
188}
189
190pub fn register_task_with_mcp_managers(
195 registry: &Arc<ToolRegistry>,
196 llm_client: Arc<dyn crate::llm::LlmClient>,
197 agent_registry: Arc<crate::subagent::AgentRegistry>,
198 workspace: String,
199 mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
200 parent_context: Option<crate::child_run::ChildRunContext>,
201 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
202) {
203 register_task_internal(
204 registry,
205 llm_client,
206 agent_registry,
207 workspace,
208 mcp_managers,
209 Vec::new(),
210 parent_context,
211 subagent_tracker,
212 None,
213 );
214}
215
216#[allow(clippy::too_many_arguments)]
218pub(crate) fn register_task_with_mcp_managers_and_scheduler(
219 registry: &Arc<ToolRegistry>,
220 llm_client: Arc<dyn crate::llm::LlmClient>,
221 agent_registry: Arc<crate::subagent::AgentRegistry>,
222 workspace: String,
223 mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
224 parent_context: Option<crate::child_run::ChildRunContext>,
225 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
226 task_scheduler: Arc<crate::task_scheduler::TaskScheduler>,
227) {
228 register_task_with_mcp_sources_and_scheduler(
229 registry,
230 llm_client,
231 agent_registry,
232 workspace,
233 mcp_managers,
234 Vec::new(),
235 parent_context,
236 subagent_tracker,
237 task_scheduler,
238 );
239}
240
241#[allow(clippy::too_many_arguments)]
244pub(crate) fn register_task_with_mcp_sources_and_scheduler(
245 registry: &Arc<ToolRegistry>,
246 llm_client: Arc<dyn crate::llm::LlmClient>,
247 agent_registry: Arc<crate::subagent::AgentRegistry>,
248 workspace: String,
249 mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
250 mcp_bindings: Vec<Arc<crate::mcp::McpBinding>>,
251 parent_context: Option<crate::child_run::ChildRunContext>,
252 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
253 task_scheduler: Arc<crate::task_scheduler::TaskScheduler>,
254) {
255 register_task_internal(
256 registry,
257 llm_client,
258 agent_registry,
259 workspace,
260 mcp_managers,
261 mcp_bindings,
262 parent_context,
263 subagent_tracker,
264 Some(task_scheduler),
265 );
266}
267
268#[allow(clippy::too_many_arguments)]
269fn register_task_internal(
270 registry: &Arc<ToolRegistry>,
271 llm_client: Arc<dyn crate::llm::LlmClient>,
272 agent_registry: Arc<crate::subagent::AgentRegistry>,
273 workspace: String,
274 mcp_managers: Vec<Arc<crate::mcp::manager::McpManager>>,
275 mcp_bindings: Vec<Arc<crate::mcp::McpBinding>>,
276 parent_context: Option<crate::child_run::ChildRunContext>,
277 subagent_tracker: Option<Arc<crate::subagent_task_tracker::InMemorySubagentTaskTracker>>,
278 task_scheduler: Option<Arc<crate::task_scheduler::TaskScheduler>>,
279) {
280 use crate::tools::task::{TaskExecutor, TaskTool};
281 let mut executor =
282 TaskExecutor::with_mcp_managers(agent_registry, llm_client, workspace, mcp_managers)
283 .with_projected_mcp_bindings(mcp_bindings);
284 if let Some(ctx) = parent_context {
285 executor = executor.with_parent_context(ctx);
286 }
287 if let Some(tracker) = subagent_tracker {
288 executor = executor.with_subagent_tracker(tracker);
289 }
290 if let Some(task_scheduler) = task_scheduler {
291 executor = executor.with_task_scheduler(task_scheduler, false);
294 }
295 let executor = Arc::new(executor);
296 registry.register_builtin(Arc::new(TaskTool::new(Arc::clone(&executor))));
297 }
301
302pub(crate) fn register_skill(
304 registry: &Arc<ToolRegistry>,
305 llm_client: Arc<dyn crate::llm::LlmClient>,
306 skill_registry: Arc<crate::skills::SkillRegistry>,
307 tool_executor: Arc<crate::tools::ToolExecutor>,
308 base_config: crate::agent::AgentConfig,
309) {
310 use crate::tools::skill::{SearchSkillsTool, SkillTool};
311 registry.register_builtin(Arc::new(SearchSkillsTool::new(Arc::clone(&skill_registry))));
312 registry.register_builtin(Arc::new(SkillTool::new_registry_bound(
313 skill_registry,
314 llm_client,
315 tool_executor,
316 base_config,
317 )));
318}
319
320pub fn register_generate_object(
325 registry: &Arc<ToolRegistry>,
326 llm_client: Arc<dyn crate::llm::LlmClient>,
327) {
328 registry.register_builtin(Arc::new(generate_object::GenerateObjectTool::new(
329 llm_client,
330 )));
331}
332
333#[cfg(test)]
334mod tests {
335 use super::register_builtins;
336 use super::safe_http_source_url;
337 use crate::tools::registry::ToolRegistry;
338 use crate::workspace::{
339 ChunkCatalogLimits, ChunkingConfig, ManifestWorkspaceBackend, WorkspaceChunkingStrategy,
340 WorkspaceServices,
341 };
342 use std::sync::Arc;
343
344 #[test]
345 fn safe_source_url_removes_credentials_query_and_fragment() {
346 assert_eq!(
347 safe_http_source_url(
348 "HTTPS://user:password@Example.COM/report?access_token=secret#section"
349 )
350 .as_deref(),
351 Some("https://example.com/report")
352 );
353 assert!(safe_http_source_url("file:///tmp/source").is_none());
354 }
355
356 #[tokio::test]
357 async fn register_builtins_does_not_open_durable_a3s_vec() {
358 let temp = tempfile::tempdir().unwrap();
359 let backend = ManifestWorkspaceBackend::new(temp.path());
360 backend
361 .configure_chunk_catalog(
362 WorkspaceChunkingStrategy::Lines,
363 ChunkingConfig::default(),
364 ChunkCatalogLimits::default(),
365 )
366 .unwrap();
367 let services = WorkspaceServices::local_with_retrieval_backend(Arc::clone(&backend));
368 let registry = ToolRegistry::new(temp.path().to_path_buf());
369 register_builtins(®istry, &services);
370 assert!(
371 backend.persistent_index().is_none(),
372 "tool registration must not attach durable FTS"
373 );
374 assert!(
375 !temp.path().join(".a3s-code").join("index").exists(),
376 "tool registration must not create the durable index directory"
377 );
378 assert!(
379 registry.get("search").is_some(),
380 "local workspaces still register search for demand-driven durable FTS"
381 );
382 }
383
384 #[test]
385 fn register_builtins_hides_disabled_capabilities() {
386 use crate::workspace::{
387 LocalWorkspaceBackend, WorkspaceCapabilities, WorkspaceFileSystem, WorkspaceRef,
388 };
389
390 let temp = tempfile::tempdir().unwrap();
391 let backend = Arc::new(LocalWorkspaceBackend::new(temp.path().to_path_buf()));
392 let fs: Arc<dyn WorkspaceFileSystem> = backend;
393 let services = WorkspaceServices::builder(
394 WorkspaceRef::new("ws", temp.path().display().to_string()),
395 fs,
396 )
397 .capabilities(WorkspaceCapabilities {
398 read: true,
399 write: false,
400 exec: false,
401 search: false,
402 git: false,
403 code_intelligence: false,
404 })
405 .build();
406 let registry = ToolRegistry::new(temp.path().to_path_buf());
407 register_builtins(®istry, &services);
408
409 for present in [
410 "read",
411 "ls",
412 "web_fetch",
413 "web_search",
414 "update_plan",
415 "ask_user",
416 ] {
417 assert!(registry.contains(present), "{present} must stay registered");
418 }
419 for absent in [
420 "write",
421 "edit",
422 "patch",
423 "bash",
424 "git",
425 "download",
426 "code_symbols",
427 "code_navigation",
428 "code_diagnostics",
429 "parallel_task",
430 ] {
431 assert!(
432 !registry.contains(absent),
433 "{absent} must stay absent when its capability is off"
434 );
435 }
436 }
437}