1use super::artifacts::{ArtifactStore, ArtifactStoreLimits, ToolArtifact};
7use super::types::{Tool, ToolCapabilities, ToolContext, ToolOutput};
8use super::ToolResult;
9use super::{
10 merge_tool_output_artifact_metadata, tool_output_artifact, truncate_tool_output_with_artifact,
11 ToolOutputArtifact,
12};
13use crate::llm::ToolDefinition;
14use crate::trace::{InMemoryTraceSink, TraceEvent, TraceSink};
15use anyhow::Result;
16use std::collections::HashMap;
17use std::hash::{Hash, Hasher};
18use std::path::PathBuf;
19use std::sync::{Arc, RwLock};
20
21const MAX_TOOL_SCHEMA_BYTES: usize = 256 * 1024;
22const MAX_ARGUMENT_VALIDATION_ERRORS: usize = 8;
23const MAX_ARGUMENT_VALIDATION_MESSAGE_BYTES: usize = 4 * 1024;
24const MAX_INLINE_CHANGE_BYTES: usize = 64 * 1024;
25const CHANGE_SIDE_PREVIEW_BYTES: usize = 8 * 1024;
26const CHANGE_DIFF_PREVIEW_BYTES: usize = 32 * 1024;
27const MAX_DIFF_COMPUTE_BYTES: usize = 1024 * 1024;
28
29#[derive(Clone)]
30enum CachedArgumentValidator {
31 Valid(Arc<jsonschema::Validator>),
32 Invalid(String),
33}
34
35#[derive(Clone)]
36struct ArgumentValidatorCacheEntry {
37 schema_fingerprint: u64,
38 validator: CachedArgumentValidator,
39}
40
41pub struct ToolRegistry {
43 tools: RwLock<HashMap<String, Arc<dyn Tool>>>,
44 builtins: RwLock<std::collections::HashSet<String>>,
46 context: RwLock<ToolContext>,
47 artifact_store: ArtifactStore,
48 trace_sink: RwLock<Arc<dyn TraceSink>>,
49 argument_validators: RwLock<HashMap<String, ArgumentValidatorCacheEntry>>,
50}
51
52impl ToolRegistry {
53 pub fn new(workspace: PathBuf) -> Self {
55 Self::with_artifact_limits(workspace, ArtifactStoreLimits::default())
56 }
57
58 pub fn with_artifact_limits(workspace: PathBuf, artifact_limits: ArtifactStoreLimits) -> Self {
60 Self::with_artifact_limits_and_workspace_services(
61 workspace.clone(),
62 artifact_limits,
63 crate::workspace::WorkspaceServices::local(workspace),
64 )
65 }
66
67 pub fn with_artifact_limits_and_workspace_services(
69 workspace: PathBuf,
70 artifact_limits: ArtifactStoreLimits,
71 workspace_services: Arc<crate::workspace::WorkspaceServices>,
72 ) -> Self {
73 let context = ToolContext::new(workspace).with_workspace_services(workspace_services);
74 Self {
75 tools: RwLock::new(HashMap::new()),
76 builtins: RwLock::new(std::collections::HashSet::new()),
77 context: RwLock::new(context),
78 artifact_store: ArtifactStore::with_limits(artifact_limits),
79 trace_sink: RwLock::new(Arc::new(InMemoryTraceSink::default())),
80 argument_validators: RwLock::new(HashMap::new()),
81 }
82 }
83
84 pub fn register_builtin(&self, tool: Arc<dyn Tool>) {
86 let name = tool.name().to_string();
87 let mut tools = self.tools.write().unwrap();
88 let mut builtins = self.builtins.write().unwrap();
89 tracing::debug!("Registering builtin tool: {}", name);
90 tools.insert(name.clone(), tool);
91 builtins.insert(name);
92 }
93
94 pub fn register(&self, tool: Arc<dyn Tool>) {
99 let name = tool.name().to_string();
100 let mut tools = self.tools.write().unwrap();
104 let builtins = self.builtins.read().unwrap();
105 if builtins.contains(&name) {
106 tracing::warn!(
107 "Rejected registration of tool '{}': cannot shadow builtin",
108 name
109 );
110 return;
111 }
112 tracing::debug!("Registering tool: {}", name);
113 tools.insert(name, tool);
114 }
115
116 pub(crate) fn register_with_shadow(
123 &self,
124 tool: Arc<dyn Tool>,
125 ) -> (bool, Option<Arc<dyn Tool>>) {
126 let name = tool.name().to_string();
127 let mut tools = self.tools.write().unwrap();
128 let builtins = self.builtins.read().unwrap();
129 if builtins.contains(&name) {
130 tracing::warn!(
131 "Rejected registration of tool '{}': cannot shadow builtin",
132 name
133 );
134 return (false, None);
135 }
136 tracing::debug!("Registering owned dynamic tool: {}", name);
137 (true, tools.insert(name, tool))
138 }
139
140 pub(crate) fn restore_if_same(
146 &self,
147 name: &str,
148 expected: &Arc<dyn Tool>,
149 replacement: Option<Arc<dyn Tool>>,
150 ) -> bool {
151 let mut tools = self.tools.write().unwrap();
152 let Some(current) = tools.get(name) else {
153 return false;
154 };
155 if !Arc::ptr_eq(current, expected) {
156 return false;
157 }
158
159 match replacement {
160 Some(tool) => {
161 tools.insert(name.to_string(), tool);
162 }
163 None => {
164 tools.remove(name);
165 }
166 }
167 true
168 }
169
170 pub(crate) fn register_if_absent(&self, tool: Arc<dyn Tool>) -> bool {
172 let name = tool.name().to_string();
173 let mut tools = self.tools.write().unwrap();
174 if tools.contains_key(&name) {
175 return false;
176 }
177 tracing::debug!("Registering previously absent dynamic tool: {}", name);
178 tools.insert(name, tool);
179 true
180 }
181
182 pub fn unregister(&self, name: &str) -> bool {
186 let mut tools = self.tools.write().unwrap();
187 let builtins = self.builtins.read().unwrap();
188 if builtins.contains(name) {
189 tracing::warn!(
190 "Rejected unregister of tool '{}': builtin tools cannot be removed through dynamic unregister",
191 name
192 );
193 return false;
194 }
195 tracing::debug!("Unregistering tool: {}", name);
196 tools.remove(name).is_some()
197 }
198
199 pub fn unregister_by_prefix(&self, prefix: &str) {
201 let mut tools = self.tools.write().unwrap();
202 let builtins = self.builtins.read().unwrap();
203 tools.retain(|name, _| builtins.contains(name) || !name.starts_with(prefix));
204 tracing::debug!("Unregistered tools with prefix: {}", prefix);
205 }
206
207 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
209 let tools = self.tools.read().unwrap();
210 tools.get(name).cloned()
211 }
212
213 pub(crate) fn capabilities(
214 &self,
215 name: &str,
216 args: &serde_json::Value,
217 ) -> Option<ToolCapabilities> {
218 self.get(name).map(|tool| tool.capabilities(args))
219 }
220
221 pub(crate) fn requires_confirmation(&self, name: &str, args: &serde_json::Value) -> bool {
222 self.get(name)
223 .is_some_and(|tool| tool.requires_confirmation(args))
224 }
225
226 pub fn contains(&self, name: &str) -> bool {
228 let tools = self.tools.read().unwrap();
229 tools.contains_key(name)
230 }
231
232 pub fn definitions(&self) -> Vec<ToolDefinition> {
234 let tools = self.tools.read().unwrap();
235 let mut definitions = tools
236 .values()
237 .map(|tool| tool.definition())
238 .collect::<Vec<_>>();
239 definitions.sort_by(|a, b| a.name.cmp(&b.name));
240 definitions
241 }
242
243 pub fn list(&self) -> Vec<String> {
245 let tools = self.tools.read().unwrap();
246 let mut names = tools.keys().cloned().collect::<Vec<_>>();
247 names.sort();
248 names
249 }
250
251 pub(crate) fn validate_arguments(
258 &self,
259 name: &str,
260 args: &serde_json::Value,
261 ) -> std::result::Result<(), String> {
262 let Some(tool) = self.get(name) else {
263 return Ok(());
264 };
265 let schema = tool.parameters();
266 let schema_bytes = serde_json::to_vec(&schema)
267 .map_err(|error| format!("tool parameter schema is not serializable: {error}"))?;
268 if schema_bytes.len() > MAX_TOOL_SCHEMA_BYTES {
269 return Err(format!(
270 "tool parameter schema exceeds the {} byte safety limit",
271 MAX_TOOL_SCHEMA_BYTES
272 ));
273 }
274 let mut hasher = std::collections::hash_map::DefaultHasher::new();
275 schema_bytes.hash(&mut hasher);
276 let schema_fingerprint = hasher.finish();
277 let cached = self
278 .argument_validators
279 .read()
280 .unwrap()
281 .get(name)
282 .filter(|entry| entry.schema_fingerprint == schema_fingerprint)
283 .cloned();
284 let validator = match cached.map(|entry| entry.validator) {
285 Some(CachedArgumentValidator::Valid(validator)) => validator,
286 Some(CachedArgumentValidator::Invalid(error)) => return Err(error),
287 None => {
288 let compiled = match jsonschema::draft202012::options().build(&schema) {
289 Ok(validator) => CachedArgumentValidator::Valid(Arc::new(validator)),
290 Err(error) => CachedArgumentValidator::Invalid(format!(
291 "tool has an invalid parameter schema: {error}"
292 )),
293 };
294 self.argument_validators.write().unwrap().insert(
295 name.to_string(),
296 ArgumentValidatorCacheEntry {
297 schema_fingerprint,
298 validator: compiled.clone(),
299 },
300 );
301 match compiled {
302 CachedArgumentValidator::Valid(validator) => validator,
303 CachedArgumentValidator::Invalid(error) => return Err(error),
304 }
305 }
306 };
307 let mut errors = validator
308 .iter_errors(args)
309 .take(MAX_ARGUMENT_VALIDATION_ERRORS + 1)
310 .map(|error| {
311 let path = error.instance_path().to_string();
312 if path.is_empty() {
313 format!("$: {error}")
314 } else {
315 format!("{path}: {error}")
316 }
317 })
318 .collect::<Vec<_>>();
319 if errors.is_empty() {
320 return Ok(());
321 }
322
323 let omitted = errors.len() > MAX_ARGUMENT_VALIDATION_ERRORS;
324 errors.truncate(MAX_ARGUMENT_VALIDATION_ERRORS);
325 let mut message = errors.join("; ");
326 if omitted {
327 message.push_str("; additional validation errors omitted");
328 }
329 Err(crate::text::truncate_utf8(&message, MAX_ARGUMENT_VALIDATION_MESSAGE_BYTES).to_string())
330 }
331
332 pub fn len(&self) -> usize {
334 let tools = self.tools.read().unwrap();
335 tools.len()
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.len() == 0
341 }
342
343 pub fn context(&self) -> ToolContext {
345 self.context.read().unwrap().clone()
346 }
347
348 pub fn artifact_store(&self) -> ArtifactStore {
350 self.artifact_store.clone()
351 }
352
353 pub fn get_artifact(&self, artifact_uri: &str) -> Option<ToolArtifact> {
355 self.artifact_store.get(artifact_uri)
356 }
357
358 pub fn set_trace_sink(&self, sink: Arc<dyn TraceSink>) {
360 *self.trace_sink.write().unwrap() = sink;
361 }
362
363 pub fn trace_sink(&self) -> Arc<dyn TraceSink> {
365 Arc::clone(&self.trace_sink.read().unwrap())
366 }
367
368 pub fn set_search_config(&self, config: crate::config::SearchConfig) {
370 let mut ctx = self.context.write().unwrap();
371 *ctx = ctx.clone().with_search_config(config);
372 }
373
374 pub fn set_sandbox(&self, sandbox: std::sync::Arc<dyn crate::sandbox::BashSandbox>) {
377 let mut ctx = self.context.write().unwrap();
378 *ctx = ctx.clone().with_sandbox(sandbox);
379 }
380
381 pub fn set_command_env(&self, env: Arc<HashMap<String, String>>) {
384 let mut ctx = self.context.write().unwrap();
385 *ctx = ctx.clone().with_command_env(env);
386 }
387
388 pub async fn execute(&self, name: &str, args: &serde_json::Value) -> Result<ToolResult> {
394 let ctx = self.context();
395 self.execute_with_context(name, args, &ctx).await
396 }
397
398 pub async fn execute_with_context(
403 &self,
404 name: &str,
405 args: &serde_json::Value,
406 ctx: &ToolContext,
407 ) -> Result<ToolResult> {
408 let start = std::time::Instant::now();
409
410 let tool = self.get(name);
411
412 let result = match tool {
413 Some(tool) => {
414 let mut output = tool.execute(args, ctx).await?;
415 self.compact_change_metadata(name, &mut output.metadata);
416 let original_content = output.content.clone();
417 let truncated = truncate_tool_output_with_artifact(name, &output.content);
418 output.content = truncated.content;
419 if let Some(artifact) = truncated.artifact {
420 self.store_tool_artifact(name, &original_content, &artifact);
421 output.metadata = Some(merge_tool_output_artifact_metadata(
422 output.metadata,
423 &artifact,
424 ));
425 }
426 Ok(ToolResult {
427 name: name.to_string(),
428 output: output.content,
429 exit_code: if output.success { 0 } else { 1 },
430 metadata: output.metadata,
431 images: output.images,
432 error_kind: output.error_kind,
433 })
434 }
435 None => Ok(ToolResult::error(name, format!("Unknown tool: {}", name))),
436 };
437
438 if let Ok(ref r) = result {
439 crate::telemetry::record_tool_result(r.exit_code, start.elapsed());
440 self.record_trace_event(name, r, start.elapsed());
441 }
442
443 result
444 }
445
446 pub async fn execute_raw(
448 &self,
449 name: &str,
450 args: &serde_json::Value,
451 ) -> Result<Option<ToolOutput>> {
452 let ctx = self.context();
453 self.execute_raw_with_context(name, args, &ctx).await
454 }
455
456 pub async fn execute_raw_with_context(
458 &self,
459 name: &str,
460 args: &serde_json::Value,
461 ctx: &ToolContext,
462 ) -> Result<Option<ToolOutput>> {
463 let tool = self.get(name);
464
465 match tool {
466 Some(tool) => {
467 let mut output = tool.execute(args, ctx).await?;
468 self.compact_change_metadata(name, &mut output.metadata);
469 let original_content = output.content.clone();
470 let truncated = truncate_tool_output_with_artifact(name, &output.content);
471 output.content = truncated.content;
472 if let Some(artifact) = truncated.artifact {
473 self.store_tool_artifact(name, &original_content, &artifact);
474 output.metadata = Some(merge_tool_output_artifact_metadata(
475 output.metadata,
476 &artifact,
477 ));
478 }
479 Ok(Some(output))
480 }
481 None => Ok(None),
482 }
483 }
484
485 fn store_tool_artifact(&self, tool_name: &str, content: &str, artifact: &ToolOutputArtifact) {
486 self.artifact_store.put(ToolArtifact {
487 artifact_id: artifact.artifact_id.clone(),
488 artifact_uri: artifact.artifact_uri.clone(),
489 tool_name: tool_name.to_string(),
490 content: content.to_string(),
491 original_bytes: artifact.original_bytes,
492 shown_bytes: artifact.shown_bytes,
493 });
494 }
495
496 fn compact_change_metadata(&self, tool_name: &str, metadata: &mut Option<serde_json::Value>) {
497 let Some(serde_json::Value::Object(object)) = metadata.as_mut() else {
498 return;
499 };
500 let before = object
501 .get("before")
502 .and_then(serde_json::Value::as_str)
503 .map(ToString::to_string);
504 let after = object
505 .get("after")
506 .and_then(serde_json::Value::as_str)
507 .map(ToString::to_string);
508 if before.is_none() && after.is_none() {
509 return;
510 }
511
512 let before_bytes = before.as_ref().map_or(0, String::len);
513 let after_bytes = after.as_ref().map_or(0, String::len);
514 let total_bytes = before_bytes.saturating_add(after_bytes);
515 let compacted = total_bytes > MAX_INLINE_CHANGE_BYTES;
516 let before_artifact = before.as_deref().and_then(|content| {
517 self.store_change_artifact(tool_name, "before", content, compacted)
518 });
519 let after_artifact = after
520 .as_deref()
521 .and_then(|content| self.store_change_artifact(tool_name, "after", content, compacted));
522
523 let unified_diff = if compacted && total_bytes <= MAX_DIFF_COMPUTE_BYTES {
524 let diff = similar::TextDiff::from_lines(
525 before.as_deref().unwrap_or_default(),
526 after.as_deref().unwrap_or_default(),
527 )
528 .unified_diff()
529 .context_radius(3)
530 .header("before", "after")
531 .to_string();
532 Some(bounded_head_tail(&diff, CHANGE_DIFF_PREVIEW_BYTES))
533 } else {
534 None
535 };
536
537 if compacted {
538 if let Some(content) = before.as_deref() {
539 object.insert(
540 "before".to_string(),
541 serde_json::Value::String(bounded_head_tail(
542 content,
543 CHANGE_SIDE_PREVIEW_BYTES,
544 )),
545 );
546 }
547 if let Some(content) = after.as_deref() {
548 object.insert(
549 "after".to_string(),
550 serde_json::Value::String(bounded_head_tail(
551 content,
552 CHANGE_SIDE_PREVIEW_BYTES,
553 )),
554 );
555 }
556 }
557
558 object.insert(
559 "change".to_string(),
560 serde_json::json!({
561 "compacted": compacted,
562 "before": before.as_deref().map(|content| serde_json::json!({
563 "bytes": content.len(),
564 "sha256": sha256::digest(content.as_bytes()),
565 "artifact": before_artifact,
566 })),
567 "after": after.as_deref().map(|content| serde_json::json!({
568 "bytes": content.len(),
569 "sha256": sha256::digest(content.as_bytes()),
570 "artifact": after_artifact,
571 })),
572 "unified_diff": unified_diff,
573 "diff_omitted": compacted && total_bytes > MAX_DIFF_COMPUTE_BYTES,
574 }),
575 );
576 }
577
578 fn store_change_artifact(
579 &self,
580 tool_name: &str,
581 side: &str,
582 content: &str,
583 store: bool,
584 ) -> Option<serde_json::Value> {
585 if !store || content.len() > self.artifact_store.limits().max_bytes {
586 return None;
587 }
588 let artifact = tool_output_artifact(&format!("{tool_name}-{side}"), content, 0);
589 self.store_tool_artifact(tool_name, content, &artifact);
590 Some(serde_json::json!({
591 "artifact_id": artifact.artifact_id,
592 "artifact_uri": artifact.artifact_uri,
593 }))
594 }
595
596 fn record_trace_event(&self, name: &str, result: &ToolResult, duration: std::time::Duration) {
597 let sink = self.trace_sink();
598 sink.record(TraceEvent::tool_execution(
599 name,
600 result.exit_code == 0,
601 result.exit_code,
602 duration,
603 result.output.len(),
604 result.metadata.as_ref(),
605 ));
606
607 if name == "program" {
608 sink.record(TraceEvent::program_execution(
609 name,
610 result.exit_code == 0,
611 result.exit_code,
612 duration,
613 result.output.len(),
614 result.metadata.as_ref(),
615 ));
616 }
617 }
618}
619
620fn bounded_head_tail(content: &str, max_bytes: usize) -> String {
621 if content.len() <= max_bytes {
622 return content.to_string();
623 }
624 let head_limit = max_bytes / 2;
625 let tail_limit = max_bytes.saturating_sub(head_limit);
626 let head = crate::text::truncate_utf8(content, head_limit);
627 let mut tail_start = content.len().saturating_sub(tail_limit);
628 while tail_start < content.len() && !content.is_char_boundary(tail_start) {
629 tail_start += 1;
630 }
631 format!(
632 "{}\n\n... [{} bytes omitted from middle] ...\n\n{}",
633 head,
634 content
635 .len()
636 .saturating_sub(head.len())
637 .saturating_sub(content.len().saturating_sub(tail_start)),
638 &content[tail_start..]
639 )
640}
641
642#[cfg(test)]
643#[path = "registry/tests.rs"]
644mod tests;