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 .filter(|tool| tool.is_model_visible())
238 .map(|tool| tool.definition())
239 .collect::<Vec<_>>();
240 definitions.sort_by(|a, b| a.name.cmp(&b.name));
241 definitions
242 }
243
244 pub fn list(&self) -> Vec<String> {
246 let tools = self.tools.read().unwrap();
247 let mut names = tools.keys().cloned().collect::<Vec<_>>();
248 names.sort();
249 names
250 }
251
252 pub(crate) fn validate_arguments(
259 &self,
260 name: &str,
261 args: &serde_json::Value,
262 ) -> std::result::Result<(), String> {
263 let Some(tool) = self.get(name) else {
264 return Ok(());
265 };
266 let schema = tool.parameters();
267 let schema_bytes = serde_json::to_vec(&schema)
268 .map_err(|error| format!("tool parameter schema is not serializable: {error}"))?;
269 if schema_bytes.len() > MAX_TOOL_SCHEMA_BYTES {
270 return Err(format!(
271 "tool parameter schema exceeds the {} byte safety limit",
272 MAX_TOOL_SCHEMA_BYTES
273 ));
274 }
275 let mut hasher = std::collections::hash_map::DefaultHasher::new();
276 schema_bytes.hash(&mut hasher);
277 let schema_fingerprint = hasher.finish();
278 let cached = self
279 .argument_validators
280 .read()
281 .unwrap()
282 .get(name)
283 .filter(|entry| entry.schema_fingerprint == schema_fingerprint)
284 .cloned();
285 let validator = match cached.map(|entry| entry.validator) {
286 Some(CachedArgumentValidator::Valid(validator)) => validator,
287 Some(CachedArgumentValidator::Invalid(error)) => return Err(error),
288 None => {
289 let compiled = match jsonschema::draft202012::options().build(&schema) {
290 Ok(validator) => CachedArgumentValidator::Valid(Arc::new(validator)),
291 Err(error) => CachedArgumentValidator::Invalid(format!(
292 "tool has an invalid parameter schema: {error}"
293 )),
294 };
295 self.argument_validators.write().unwrap().insert(
296 name.to_string(),
297 ArgumentValidatorCacheEntry {
298 schema_fingerprint,
299 validator: compiled.clone(),
300 },
301 );
302 match compiled {
303 CachedArgumentValidator::Valid(validator) => validator,
304 CachedArgumentValidator::Invalid(error) => return Err(error),
305 }
306 }
307 };
308 let mut errors = validator
309 .iter_errors(args)
310 .take(MAX_ARGUMENT_VALIDATION_ERRORS + 1)
311 .map(|error| {
312 let path = error.instance_path().to_string();
313 if path.is_empty() {
314 format!("$: {error}")
315 } else {
316 format!("{path}: {error}")
317 }
318 })
319 .collect::<Vec<_>>();
320 if errors.is_empty() {
321 return Ok(());
322 }
323
324 let omitted = errors.len() > MAX_ARGUMENT_VALIDATION_ERRORS;
325 errors.truncate(MAX_ARGUMENT_VALIDATION_ERRORS);
326 let mut message = errors.join("; ");
327 if omitted {
328 message.push_str("; additional validation errors omitted");
329 }
330 Err(crate::text::truncate_utf8(&message, MAX_ARGUMENT_VALIDATION_MESSAGE_BYTES).to_string())
331 }
332
333 pub fn len(&self) -> usize {
335 let tools = self.tools.read().unwrap();
336 tools.len()
337 }
338
339 pub fn is_empty(&self) -> bool {
341 self.len() == 0
342 }
343
344 pub fn context(&self) -> ToolContext {
346 self.context.read().unwrap().clone()
347 }
348
349 pub fn artifact_store(&self) -> ArtifactStore {
351 self.artifact_store.clone()
352 }
353
354 pub fn get_artifact(&self, artifact_uri: &str) -> Option<ToolArtifact> {
356 self.artifact_store.get(artifact_uri)
357 }
358
359 pub fn set_trace_sink(&self, sink: Arc<dyn TraceSink>) {
361 *self.trace_sink.write().unwrap() = sink;
362 }
363
364 pub fn trace_sink(&self) -> Arc<dyn TraceSink> {
366 Arc::clone(&self.trace_sink.read().unwrap())
367 }
368
369 pub fn set_search_config(&self, config: crate::config::SearchConfig) {
371 let mut ctx = self.context.write().unwrap();
372 *ctx = ctx.clone().with_search_config(config);
373 }
374
375 pub fn set_sandbox(&self, sandbox: std::sync::Arc<dyn crate::sandbox::BashSandbox>) {
378 let mut ctx = self.context.write().unwrap();
379 *ctx = ctx.clone().with_sandbox(sandbox);
380 }
381
382 pub fn set_command_env(&self, env: Arc<HashMap<String, String>>) {
385 let mut ctx = self.context.write().unwrap();
386 *ctx = ctx.clone().with_command_env(env);
387 }
388
389 pub async fn execute(&self, name: &str, args: &serde_json::Value) -> Result<ToolResult> {
395 let ctx = self.context();
396 self.execute_with_context(name, args, &ctx).await
397 }
398
399 pub async fn execute_with_context(
404 &self,
405 name: &str,
406 args: &serde_json::Value,
407 ctx: &ToolContext,
408 ) -> Result<ToolResult> {
409 let start = std::time::Instant::now();
410
411 let tool = self.get(name);
412
413 let result = match tool {
414 Some(tool) => {
415 let mut output = tool.execute(args, ctx).await?;
416 self.compact_change_metadata(name, &mut output.metadata);
417 let original_content = output.content.clone();
418 let truncated = truncate_tool_output_with_artifact(name, &output.content);
419 output.content = truncated.content;
420 if let Some(artifact) = truncated.artifact {
421 self.store_tool_artifact(name, &original_content, &artifact);
422 output.metadata = Some(merge_tool_output_artifact_metadata(
423 output.metadata,
424 &artifact,
425 ));
426 }
427 Ok(ToolResult {
428 name: name.to_string(),
429 output: output.content,
430 exit_code: if output.success { 0 } else { 1 },
431 metadata: output.metadata,
432 images: output.images,
433 error_kind: output.error_kind,
434 })
435 }
436 None => Ok(ToolResult::error(name, format!("Unknown tool: {}", name))),
437 };
438
439 if let Ok(ref r) = result {
440 crate::telemetry::record_tool_result(r.exit_code, start.elapsed());
441 self.record_trace_event(name, r, start.elapsed());
442 }
443
444 result
445 }
446
447 pub async fn execute_raw(
449 &self,
450 name: &str,
451 args: &serde_json::Value,
452 ) -> Result<Option<ToolOutput>> {
453 let ctx = self.context();
454 self.execute_raw_with_context(name, args, &ctx).await
455 }
456
457 pub async fn execute_raw_with_context(
459 &self,
460 name: &str,
461 args: &serde_json::Value,
462 ctx: &ToolContext,
463 ) -> Result<Option<ToolOutput>> {
464 let tool = self.get(name);
465
466 match tool {
467 Some(tool) => {
468 let mut output = tool.execute(args, ctx).await?;
469 self.compact_change_metadata(name, &mut output.metadata);
470 let original_content = output.content.clone();
471 let truncated = truncate_tool_output_with_artifact(name, &output.content);
472 output.content = truncated.content;
473 if let Some(artifact) = truncated.artifact {
474 self.store_tool_artifact(name, &original_content, &artifact);
475 output.metadata = Some(merge_tool_output_artifact_metadata(
476 output.metadata,
477 &artifact,
478 ));
479 }
480 Ok(Some(output))
481 }
482 None => Ok(None),
483 }
484 }
485
486 fn store_tool_artifact(&self, tool_name: &str, content: &str, artifact: &ToolOutputArtifact) {
487 self.artifact_store.put(ToolArtifact {
488 artifact_id: artifact.artifact_id.clone(),
489 artifact_uri: artifact.artifact_uri.clone(),
490 tool_name: tool_name.to_string(),
491 content: content.to_string(),
492 original_bytes: artifact.original_bytes,
493 shown_bytes: artifact.shown_bytes,
494 });
495 }
496
497 fn compact_change_metadata(&self, tool_name: &str, metadata: &mut Option<serde_json::Value>) {
498 let Some(serde_json::Value::Object(object)) = metadata.as_mut() else {
499 return;
500 };
501 let before = object
502 .get("before")
503 .and_then(serde_json::Value::as_str)
504 .map(ToString::to_string);
505 let after = object
506 .get("after")
507 .and_then(serde_json::Value::as_str)
508 .map(ToString::to_string);
509 if before.is_none() && after.is_none() {
510 return;
511 }
512
513 let before_bytes = before.as_ref().map_or(0, String::len);
514 let after_bytes = after.as_ref().map_or(0, String::len);
515 let total_bytes = before_bytes.saturating_add(after_bytes);
516 let compacted = total_bytes > MAX_INLINE_CHANGE_BYTES;
517 let before_artifact = before.as_deref().and_then(|content| {
518 self.store_change_artifact(tool_name, "before", content, compacted)
519 });
520 let after_artifact = after
521 .as_deref()
522 .and_then(|content| self.store_change_artifact(tool_name, "after", content, compacted));
523
524 let unified_diff = if compacted && total_bytes <= MAX_DIFF_COMPUTE_BYTES {
525 let diff = similar::TextDiff::from_lines(
526 before.as_deref().unwrap_or_default(),
527 after.as_deref().unwrap_or_default(),
528 )
529 .unified_diff()
530 .context_radius(3)
531 .header("before", "after")
532 .to_string();
533 Some(bounded_head_tail(&diff, CHANGE_DIFF_PREVIEW_BYTES))
534 } else {
535 None
536 };
537
538 if compacted {
539 if let Some(content) = before.as_deref() {
540 object.insert(
541 "before".to_string(),
542 serde_json::Value::String(bounded_head_tail(
543 content,
544 CHANGE_SIDE_PREVIEW_BYTES,
545 )),
546 );
547 }
548 if let Some(content) = after.as_deref() {
549 object.insert(
550 "after".to_string(),
551 serde_json::Value::String(bounded_head_tail(
552 content,
553 CHANGE_SIDE_PREVIEW_BYTES,
554 )),
555 );
556 }
557 }
558
559 object.insert(
560 "change".to_string(),
561 serde_json::json!({
562 "compacted": compacted,
563 "before": before.as_deref().map(|content| serde_json::json!({
564 "bytes": content.len(),
565 "sha256": sha256::digest(content.as_bytes()),
566 "artifact": before_artifact,
567 })),
568 "after": after.as_deref().map(|content| serde_json::json!({
569 "bytes": content.len(),
570 "sha256": sha256::digest(content.as_bytes()),
571 "artifact": after_artifact,
572 })),
573 "unified_diff": unified_diff,
574 "diff_omitted": compacted && total_bytes > MAX_DIFF_COMPUTE_BYTES,
575 }),
576 );
577 }
578
579 fn store_change_artifact(
580 &self,
581 tool_name: &str,
582 side: &str,
583 content: &str,
584 store: bool,
585 ) -> Option<serde_json::Value> {
586 if !store || content.len() > self.artifact_store.limits().max_bytes {
587 return None;
588 }
589 let artifact = tool_output_artifact(&format!("{tool_name}-{side}"), content, 0);
590 self.store_tool_artifact(tool_name, content, &artifact);
591 Some(serde_json::json!({
592 "artifact_id": artifact.artifact_id,
593 "artifact_uri": artifact.artifact_uri,
594 }))
595 }
596
597 fn record_trace_event(&self, name: &str, result: &ToolResult, duration: std::time::Duration) {
598 let sink = self.trace_sink();
599 sink.record(TraceEvent::tool_execution(
600 name,
601 result.exit_code == 0,
602 result.exit_code,
603 duration,
604 result.output.len(),
605 result.metadata.as_ref(),
606 ));
607
608 if name == "program" {
609 sink.record(TraceEvent::program_execution(
610 name,
611 result.exit_code == 0,
612 result.exit_code,
613 duration,
614 result.output.len(),
615 result.metadata.as_ref(),
616 ));
617 }
618 }
619}
620
621fn bounded_head_tail(content: &str, max_bytes: usize) -> String {
622 if content.len() <= max_bytes {
623 return content.to_string();
624 }
625 let head_limit = max_bytes / 2;
626 let tail_limit = max_bytes.saturating_sub(head_limit);
627 let head = crate::text::truncate_utf8(content, head_limit);
628 let mut tail_start = content.len().saturating_sub(tail_limit);
629 while tail_start < content.len() && !content.is_char_boundary(tail_start) {
630 tail_start += 1;
631 }
632 format!(
633 "{}\n\n... [{} bytes omitted from middle] ...\n\n{}",
634 head,
635 content
636 .len()
637 .saturating_sub(head.len())
638 .saturating_sub(content.len().saturating_sub(tail_start)),
639 &content[tail_start..]
640 )
641}
642
643#[cfg(test)]
644#[path = "registry/tests.rs"]
645mod tests;