Skip to main content

a3s_code_core/tools/
registry.rs

1//! Tool Registry
2//!
3//! Central registry for all tools (built-in and dynamic).
4//! Provides thread-safe registration, lookup, and execution.
5
6use 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
41/// Tool registry for managing all available tools
42pub struct ToolRegistry {
43    tools: RwLock<HashMap<String, Arc<dyn Tool>>>,
44    /// Names of builtin tools that cannot be overridden
45    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    /// Create a new tool registry
54    pub fn new(workspace: PathBuf) -> Self {
55        Self::with_artifact_limits(workspace, ArtifactStoreLimits::default())
56    }
57
58    /// Create a new tool registry with custom artifact retention limits.
59    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    /// Create a new tool registry with custom artifact limits and workspace backend.
68    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    /// Register a builtin tool (cannot be overridden by dynamic tools)
85    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    /// Register a tool
95    ///
96    /// If a tool with the same name already exists as a builtin, the registration
97    /// is rejected to prevent shadowing of core tools.
98    pub fn register(&self, tool: Arc<dyn Tool>) {
99        let name = tool.name().to_string();
100        // All operations that need both registry locks take `tools` first.
101        // This keeps the builtin check and insertion atomic with
102        // `register_builtin` and avoids lock-order inversion.
103        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    /// Register a dynamic tool and return the tool it shadowed.
117    ///
118    /// The lookup and replacement happen under one write lock so lifecycle
119    /// owners can later restore the exact prior registration without racing a
120    /// concurrent dynamic registration. The boolean is `false` when a builtin
121    /// owns the name and the dynamic registration was rejected.
122    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    /// Restore a shadowed registration only while `expected` still owns the
141    /// name.
142    ///
143    /// This compare-and-replace prevents one lifecycle owner from deleting or
144    /// overwriting a tool installed later by another dynamic source.
145    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    /// Register a dynamic tool only when no source currently owns its name.
171    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    /// Unregister a tool by name
183    ///
184    /// Returns true if the tool was found and removed.
185    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    /// Unregister all tools whose names start with the given prefix.
200    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    /// Get a tool by name
208    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    /// Check if a tool exists
227    pub fn contains(&self, name: &str) -> bool {
228        let tools = self.tools.read().unwrap();
229        tools.contains_key(name)
230    }
231
232    /// Get all tool definitions for LLM
233    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    /// List all registered tool names
245    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    /// Validate model- or orchestrator-supplied arguments against the tool's
253    /// declared JSON Schema before permissions or execution side effects.
254    ///
255    /// Low-level standalone registry calls remain compatibility-oriented and
256    /// do not invoke this automatically. The governed agent/session gateway is
257    /// the enforcement boundary.
258    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    /// Get the number of registered tools
334    pub fn len(&self) -> usize {
335        let tools = self.tools.read().unwrap();
336        tools.len()
337    }
338
339    /// Check if registry is empty
340    pub fn is_empty(&self) -> bool {
341        self.len() == 0
342    }
343
344    /// Get the tool context
345    pub fn context(&self) -> ToolContext {
346        self.context.read().unwrap().clone()
347    }
348
349    /// Return a clone of the registry's artifact store handle.
350    pub fn artifact_store(&self) -> ArtifactStore {
351        self.artifact_store.clone()
352    }
353
354    /// Get a stored tool artifact by URI.
355    pub fn get_artifact(&self, artifact_uri: &str) -> Option<ToolArtifact> {
356        self.artifact_store.get(artifact_uri)
357    }
358
359    /// Replace the trace sink used for compact tool/program execution events.
360    pub fn set_trace_sink(&self, sink: Arc<dyn TraceSink>) {
361        *self.trace_sink.write().unwrap() = sink;
362    }
363
364    /// Return the current trace sink.
365    pub fn trace_sink(&self) -> Arc<dyn TraceSink> {
366        Arc::clone(&self.trace_sink.read().unwrap())
367    }
368
369    /// Set the search configuration for the tool context
370    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    /// Set a sandbox executor so that `bash` tool calls use the sandbox even
376    /// when executed without an explicit `ToolContext` (i.e., via `execute()`).
377    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    /// Set environment overrides used by subprocess-backed tools when executed
383    /// without an explicit context.
384    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    /// Execute a tool by name using the registry's default context.
390    ///
391    /// This is the lowest-level standalone registry boundary. It does not run
392    /// agent/session permission, HITL, hook, budget, queue, timeout,
393    /// cancellation, or sanitization policy.
394    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    /// Execute a tool by name with an external caller-owned context.
400    ///
401    /// This remains a low-level ungoverned call; agent/session paths must use
402    /// their scoped invocation gateway instead.
403    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    /// Execute a tool and return raw output using the registry's default context
448    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    /// Execute a tool and return raw output with an external context
458    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;