strands_agents/tools/
validator.rs

1//! Tool validation and preparation utilities.
2//!
3//! This module provides functions for validating and preparing tools
4//! before they are used in an agent's event loop.
5
6use std::collections::HashSet;
7
8use tracing::{debug, warn};
9
10use crate::types::errors::StrandsError;
11use crate::types::tools::{ToolSpec, ToolUse};
12
13/// Maximum allowed length for a tool name.
14pub const MAX_TOOL_NAME_LENGTH: usize = 64;
15
16/// Minimum allowed length for a tool name.
17pub const MIN_TOOL_NAME_LENGTH: usize = 1;
18
19/// Validates a single tool specification.
20///
21/// This function checks that the tool spec meets requirements:
22/// - Has a valid name (non-empty, proper length, valid characters)
23/// - Has a description
24/// - Has a valid input schema if provided
25///
26/// # Arguments
27///
28/// * `spec` - The tool specification to validate
29///
30/// # Returns
31///
32/// `Ok(())` if valid, or an error describing the validation failure.
33pub fn validate_tool_spec(spec: &ToolSpec) -> Result<(), StrandsError> {
34
35    if spec.name.is_empty() {
36        return Err(StrandsError::InvalidToolName {
37            name: spec.name.clone(),
38            reason: "Tool name cannot be empty".to_string(),
39        });
40    }
41
42    if spec.name.len() > MAX_TOOL_NAME_LENGTH {
43        return Err(StrandsError::InvalidToolName {
44            name: spec.name.clone(),
45            reason: format!(
46                "Tool name exceeds maximum length of {} characters",
47                MAX_TOOL_NAME_LENGTH
48            ),
49        });
50    }
51
52
53    if !spec.name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
54        return Err(StrandsError::InvalidToolName {
55            name: spec.name.clone(),
56            reason: "Tool name can only contain alphanumeric characters, underscores, and hyphens"
57                .to_string(),
58        });
59    }
60
61
62    if spec.description.is_empty() {
63        warn!(
64            tool_name = %spec.name,
65            "Tool has empty description, which may reduce LLM effectiveness"
66        );
67    }
68
69    Ok(())
70}
71
72/// Validates a collection of tool specifications.
73///
74/// Checks each tool and ensures there are no duplicate names.
75///
76/// # Arguments
77///
78/// * `specs` - A slice of tool specifications to validate
79///
80/// # Returns
81///
82/// `Ok(())` if all tools are valid with no duplicates.
83pub fn validate_tool_specs(specs: &[ToolSpec]) -> Result<(), StrandsError> {
84    let mut seen_names = HashSet::new();
85
86    for spec in specs {
87
88        validate_tool_spec(spec)?;
89
90
91        if !seen_names.insert(&spec.name) {
92            return Err(StrandsError::DuplicateToolName {
93                name: spec.name.clone(),
94            });
95        }
96    }
97
98    debug!(tool_count = specs.len(), "Validated tool specifications");
99    Ok(())
100}
101
102/// Validates and prepares tools for use in an agent.
103///
104/// This function performs comprehensive validation on tool specifications
105/// and prepares them for use in the agent's event loop. It:
106/// - Validates each tool specification
107/// - Checks for duplicate names
108/// - Optionally filters out invalid tools instead of failing
109///
110/// # Arguments
111///
112/// * `specs` - A mutable reference to the tool specifications
113/// * `strict` - If true, returns error on first invalid tool; if false, filters them out
114///
115/// # Returns
116///
117/// The validated tool specifications.
118pub fn validate_and_prepare_tools(
119    specs: Vec<ToolSpec>,
120    strict: bool,
121) -> Result<Vec<ToolSpec>, StrandsError> {
122    let mut validated = Vec::with_capacity(specs.len());
123    let mut seen_names = HashSet::new();
124
125    for spec in specs {
126        match validate_tool_spec(&spec) {
127            Ok(()) => {
128                if seen_names.insert(spec.name.clone()) {
129                    validated.push(spec);
130                } else if strict {
131                    return Err(StrandsError::DuplicateToolName { name: spec.name });
132                } else {
133                    warn!(
134                        tool_name = %spec.name,
135                        "Duplicate tool name found, skipping"
136                    );
137                }
138            }
139            Err(e) => {
140                if strict {
141                    return Err(e);
142                }
143                warn!(error = %e, "Invalid tool specification, skipping");
144            }
145        }
146    }
147
148    debug!(
149        total = validated.len(),
150        "Tools validated and prepared"
151    );
152
153    Ok(validated)
154}
155
156/// Validates a tool use request against registered tools.
157///
158/// Checks that the tool use references a valid, registered tool
159/// and that the input parameters are appropriate.
160///
161/// # Arguments
162///
163/// * `tool_use` - The tool use request to validate
164/// * `registered_tools` - Names of registered tools
165///
166/// # Returns
167///
168/// `Ok(())` if the tool use is valid.
169pub fn validate_tool_use(
170    tool_use: &ToolUse,
171    registered_tools: &HashSet<String>,
172) -> Result<(), StrandsError> {
173
174    if !registered_tools.contains(&tool_use.name) {
175        return Err(StrandsError::InvalidToolUseName {
176            name: tool_use.name.clone(),
177            available_tools: registered_tools.iter().cloned().collect(),
178        });
179    }
180
181
182    if tool_use.tool_use_id.is_empty() {
183        return Err(StrandsError::ToolValidationError {
184            message: format!("Tool use '{}' has empty tool_use_id", tool_use.name),
185        });
186    }
187
188    Ok(())
189}
190
191/// Result of tool use validation.
192#[derive(Debug, Clone)]
193pub struct ToolUseValidationResult {
194    /// Valid tool uses that can be executed.
195    pub valid: Vec<ToolUse>,
196    /// Invalid tool uses with their error reasons.
197    pub invalid: Vec<(ToolUse, String)>,
198}
199
200impl ToolUseValidationResult {
201    /// Returns true if all tool uses were valid.
202    pub fn all_valid(&self) -> bool {
203        self.invalid.is_empty()
204    }
205
206    /// Returns the count of valid tool uses.
207    pub fn valid_count(&self) -> usize {
208        self.valid.len()
209    }
210
211    /// Returns the count of invalid tool uses.
212    pub fn invalid_count(&self) -> usize {
213        self.invalid.len()
214    }
215}
216
217/// Validates multiple tool uses at once.
218///
219/// # Arguments
220///
221/// * `tool_uses` - The tool uses to validate
222/// * `registered_tools` - Names of registered tools
223///
224/// # Returns
225///
226/// A result containing both valid and invalid tool uses.
227pub fn validate_tool_uses(
228    tool_uses: &[ToolUse],
229    registered_tools: &HashSet<String>,
230) -> ToolUseValidationResult {
231    let mut valid = Vec::new();
232    let mut invalid = Vec::new();
233
234    for tool_use in tool_uses {
235        match validate_tool_use(tool_use, registered_tools) {
236            Ok(()) => valid.push(tool_use.clone()),
237            Err(e) => invalid.push((tool_use.clone(), e.to_string())),
238        }
239    }
240
241    ToolUseValidationResult { valid, invalid }
242}
243
244/// Checks if a tool name is valid according to naming rules.
245///
246/// # Arguments
247///
248/// * `name` - The tool name to check
249///
250/// # Returns
251///
252/// `true` if the name is valid.
253pub fn is_valid_tool_name(name: &str) -> bool {
254    !name.is_empty()
255        && name.len() <= MAX_TOOL_NAME_LENGTH
256        && name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
257}
258
259/// Sanitizes a tool name to make it valid.
260///
261/// Replaces invalid characters with underscores and truncates if needed.
262///
263/// # Arguments
264///
265/// * `name` - The tool name to sanitize
266///
267/// # Returns
268///
269/// A valid tool name.
270pub fn sanitize_tool_name(name: &str) -> String {
271    let sanitized: String = name
272        .chars()
273        .map(|c| {
274            if c.is_alphanumeric() || c == '_' || c == '-' {
275                c
276            } else {
277                '_'
278            }
279        })
280        .collect();
281
282
283    if sanitized.len() > MAX_TOOL_NAME_LENGTH {
284        sanitized[..MAX_TOOL_NAME_LENGTH].to_string()
285    } else if sanitized.is_empty() {
286        "unnamed_tool".to_string()
287    } else {
288        sanitized
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_validate_tool_spec_valid() {
298        let spec = ToolSpec::new("valid_tool", "A valid tool");
299        assert!(validate_tool_spec(&spec).is_ok());
300    }
301
302    #[test]
303    fn test_validate_tool_spec_empty_name() {
304        let spec = ToolSpec::new("", "Description");
305        let result = validate_tool_spec(&spec);
306        assert!(result.is_err());
307        assert!(matches!(result, Err(StrandsError::InvalidToolName { .. })));
308    }
309
310    #[test]
311    fn test_validate_tool_spec_invalid_chars() {
312        let spec = ToolSpec::new("invalid tool!", "Description");
313        let result = validate_tool_spec(&spec);
314        assert!(result.is_err());
315    }
316
317    #[test]
318    fn test_validate_tool_specs_no_duplicates() {
319        let specs = vec![
320            ToolSpec::new("tool1", "Tool 1"),
321            ToolSpec::new("tool2", "Tool 2"),
322        ];
323        assert!(validate_tool_specs(&specs).is_ok());
324    }
325
326    #[test]
327    fn test_validate_tool_specs_with_duplicates() {
328        let specs = vec![
329            ToolSpec::new("tool1", "Tool 1"),
330            ToolSpec::new("tool1", "Tool 1 duplicate"),
331        ];
332        let result = validate_tool_specs(&specs);
333        assert!(result.is_err());
334        assert!(matches!(result, Err(StrandsError::DuplicateToolName { .. })));
335    }
336
337    #[test]
338    fn test_validate_and_prepare_tools_strict() {
339        let specs = vec![
340            ToolSpec::new("valid", "Valid tool"),
341            ToolSpec::new("", "Invalid tool"),
342        ];
343        let result = validate_and_prepare_tools(specs, true);
344        assert!(result.is_err());
345    }
346
347    #[test]
348    fn test_validate_and_prepare_tools_lenient() {
349        let specs = vec![
350            ToolSpec::new("valid", "Valid tool"),
351            ToolSpec::new("", "Invalid tool"),
352        ];
353        let result = validate_and_prepare_tools(specs, false).unwrap();
354        assert_eq!(result.len(), 1);
355        assert_eq!(result[0].name, "valid");
356    }
357
358    #[test]
359    fn test_validate_tool_use_valid() {
360        let tool_use = ToolUse::new("my_tool", "123", serde_json::json!({}));
361        let registered = HashSet::from(["my_tool".to_string()]);
362        assert!(validate_tool_use(&tool_use, &registered).is_ok());
363    }
364
365    #[test]
366    fn test_validate_tool_use_not_found() {
367        let tool_use = ToolUse::new("unknown", "123", serde_json::json!({}));
368        let registered = HashSet::from(["my_tool".to_string()]);
369        let result = validate_tool_use(&tool_use, &registered);
370        assert!(result.is_err());
371        assert!(matches!(result, Err(StrandsError::InvalidToolUseName { .. })));
372    }
373
374    #[test]
375    fn test_validate_tool_uses_mixed() {
376        let tool_uses = vec![
377            ToolUse::new("valid", "1", serde_json::json!({})),
378            ToolUse::new("invalid", "2", serde_json::json!({})),
379        ];
380        let registered = HashSet::from(["valid".to_string()]);
381        let result = validate_tool_uses(&tool_uses, &registered);
382        
383        assert_eq!(result.valid_count(), 1);
384        assert_eq!(result.invalid_count(), 1);
385        assert!(!result.all_valid());
386    }
387
388    #[test]
389    fn test_is_valid_tool_name() {
390        assert!(is_valid_tool_name("valid_tool"));
391        assert!(is_valid_tool_name("valid-tool"));
392        assert!(is_valid_tool_name("ValidTool123"));
393        assert!(!is_valid_tool_name(""));
394        assert!(!is_valid_tool_name("invalid tool"));
395        assert!(!is_valid_tool_name("invalid!tool"));
396    }
397
398    #[test]
399    fn test_sanitize_tool_name() {
400        assert_eq!(sanitize_tool_name("valid_tool"), "valid_tool");
401        assert_eq!(sanitize_tool_name("invalid tool"), "invalid_tool");
402        assert_eq!(sanitize_tool_name("test@#$"), "test___");
403        assert_eq!(sanitize_tool_name(""), "unnamed_tool");
404    }
405}
406