Coverage for src / moai_adk / core / input_validation_middleware.py: 22.12%
321 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2Enhanced Input Validation Middleware for MoAI-ADK
4Production-ready input validation and normalization system that addresses tool input
5validation failures identified in Claude Code debug logs. Provides intelligent parameter
6mapping, version compatibility, and real-time input correction.
8Author: MoAI-ADK Core Team
9Version: 1.0.0
10"""
12import json
13import logging
14from dataclasses import dataclass, field
15from enum import Enum
16from typing import Any, Callable, Dict, List, Optional, Set, Tuple
18# Configure logging
19logger = logging.getLogger(__name__)
22class ValidationSeverity(Enum):
23 """Input validation severity levels"""
24 LOW = "low"
25 MEDIUM = "medium"
26 HIGH = "high"
27 CRITICAL = "critical"
30class ToolCategory(Enum):
31 """Categories of tools with specific validation requirements"""
32 SEARCH = "search"
33 FILE_OPERATIONS = "file_operations"
34 TEXT_PROCESSING = "text_processing"
35 DATA_ANALYSIS = "data_analysis"
36 SYSTEM = "system"
37 GENERAL = "general"
40@dataclass
41class ValidationError:
42 """Individual validation error details"""
43 code: str
44 message: str
45 path: List[str]
46 severity: ValidationSeverity
47 auto_corrected: bool = False
48 original_value: Any = None
49 corrected_value: Any = None
50 suggestion: Optional[str] = None
53@dataclass
54class ValidationResult:
55 """Result of input validation and normalization"""
56 valid: bool
57 normalized_input: Dict[str, Any]
58 errors: List[ValidationError] = field(default_factory=list)
59 warnings: List[str] = field(default_factory=list)
60 transformations: List[str] = field(default_factory=list)
61 processing_time_ms: float = 0.0
64@dataclass
65class ToolParameter:
66 """Tool parameter definition for validation"""
67 name: str
68 param_type: str
69 required: bool = False
70 default_value: Any = None
71 aliases: List[str] = field(default_factory=list)
72 validation_function: Optional[Callable] = None
73 description: str = ""
74 deprecated_aliases: List[str] = field(default_factory=list)
77class EnhancedInputValidationMiddleware:
78 """
79 Production-ready input validation middleware that addresses tool input validation
80 failures from Claude Code debug logs with intelligent parameter mapping and normalization.
82 Key Features:
83 - Smart parameter mapping for unrecognized keys
84 - Version compatibility support
85 - Real-time input normalization
86 - Comprehensive error tracking and correction
87 - Tool-specific validation rules
88 """
90 def __init__(self, enable_logging: bool = True, enable_caching: bool = True):
91 self.enable_logging = enable_logging
92 self.enable_caching = enable_caching
94 # Tool parameter definitions
95 self.tool_parameters = self._load_tool_parameter_definitions()
97 # Parameter mapping for compatibility
98 self.parameter_mappings = self._load_parameter_mappings()
100 # Validation cache
101 self.validation_cache = {} if enable_caching else None
103 # Statistics
104 self.stats = {
105 'validations_performed': 0,
106 'auto_corrections': 0,
107 'errors_resolved': 0,
108 'transformations_applied': 0
109 }
111 def _load_tool_parameter_definitions(self) -> Dict[str, List[ToolParameter]]:
112 """Load tool parameter definitions with compatibility information"""
113 return {
114 "Grep": [
115 ToolParameter(
116 name="pattern",
117 param_type="string",
118 required=True,
119 description="Regex pattern to search for",
120 aliases=["regex", "search_pattern"]
121 ),
122 ToolParameter(
123 name="output_mode",
124 param_type="string",
125 default_value="content",
126 description="Output format mode",
127 aliases=["mode", "format"],
128 validation_function=self._validate_grep_mode
129 ),
130 ToolParameter(
131 name="head_limit",
132 param_type="integer",
133 description="Limit number of results",
134 default_value=None,
135 aliases=["limit", "max_results", "count", "head"],
136 deprecated_aliases=["max"]
137 ),
138 ToolParameter(
139 name="path",
140 param_type="string",
141 description="Path to search in",
142 aliases=["directory", "folder", "search_path", "root"]
143 ),
144 ToolParameter(
145 name="file_pattern",
146 param_type="string",
147 description="File pattern to match",
148 aliases=["glob", "pattern", "files", "file_glob"]
149 ),
150 ToolParameter(
151 name="case_sensitive",
152 param_type="boolean",
153 default_value=False,
154 description="Case sensitive search",
155 aliases=["case", "ignore_case", "sensitive"]
156 ),
157 ToolParameter(
158 name="context_lines",
159 param_type="integer",
160 default_value=0,
161 description="Number of context lines",
162 aliases=["context", "before_context", "after_context", "C"]
163 )
164 ],
165 "Glob": [
166 ToolParameter(
167 name="pattern",
168 param_type="string",
169 required=True,
170 description="Glob pattern to match",
171 aliases=["glob", "file_pattern", "search_pattern"]
172 ),
173 ToolParameter(
174 name="path",
175 param_type="string",
176 description="Base path for glob",
177 aliases=["directory", "folder", "root", "base_path"]
178 ),
179 ToolParameter(
180 name="recursive",
181 param_type="boolean",
182 default_value=True,
183 description="Recursive directory search",
184 aliases=["recurse", "recursive_search"]
185 )
186 ],
187 "Read": [
188 ToolParameter(
189 name="file_path",
190 param_type="string",
191 required=True,
192 description="Path to file to read",
193 aliases=["path", "filename", "file", "source"]
194 ),
195 ToolParameter(
196 name="offset",
197 param_type="integer",
198 default_value=0,
199 description="Starting line number",
200 aliases=["start", "start_line", "begin", "from"]
201 ),
202 ToolParameter(
203 name="limit",
204 param_type="integer",
205 description="Number of lines to read",
206 aliases=["count", "max_lines", "lines", "size"]
207 )
208 ],
209 "Bash": [
210 ToolParameter(
211 name="command",
212 param_type="string",
213 required=True,
214 description="Command to execute",
215 aliases=["cmd", "execute", "run", "script"]
216 ),
217 ToolParameter(
218 name="timeout",
219 param_type="integer",
220 default_value=10000,
221 description="Timeout in milliseconds",
222 aliases=["timeout_ms", "max_time", "time_limit"]
223 ),
224 ToolParameter(
225 name="working_directory",
226 param_type="string",
227 description="Working directory for command",
228 aliases=["cwd", "work_dir", "directory", "folder"]
229 ),
230 ToolParameter(
231 name="environment",
232 param_type="dict",
233 description="Environment variables",
234 aliases=["env", "env_vars", "variables"]
235 )
236 ],
237 "Task": [
238 ToolParameter(
239 name="subagent_type",
240 param_type="string",
241 required=True,
242 description="Type of subagent to use",
243 aliases=["agent_type", "type", "agent", "model"]
244 ),
245 ToolParameter(
246 name="prompt",
247 param_type="string",
248 required=True,
249 description="Prompt for the subagent",
250 aliases=["message", "input", "query", "instruction"]
251 ),
252 ToolParameter(
253 name="context",
254 param_type="dict",
255 description="Additional context",
256 aliases=["data", "variables", "params"]
257 ),
258 ToolParameter(
259 name="debug",
260 param_type="boolean",
261 default_value=False,
262 description="Enable debug mode",
263 aliases=["verbose", "debug_mode"]
264 )
265 ],
266 "Write": [
267 ToolParameter(
268 name="file_path",
269 param_type="string",
270 required=True,
271 description="Path to file to write",
272 aliases=["path", "filename", "file", "destination"]
273 ),
274 ToolParameter(
275 name="content",
276 param_type="string",
277 required=True,
278 description="Content to write",
279 aliases=["data", "text", "body", "contents"]
280 ),
281 ToolParameter(
282 name="create_directories",
283 param_type="boolean",
284 default_value=False,
285 description="Create parent directories if needed",
286 aliases=["mkdir", "create_dirs", "make_dirs"]
287 ),
288 ToolParameter(
289 name="backup",
290 param_type="boolean",
291 default_value=False,
292 description="Create backup of existing file",
293 aliases=["backup_existing", "make_backup"]
294 )
295 ],
296 "Edit": [
297 ToolParameter(
298 name="file_path",
299 param_type="string",
300 required=True,
301 description="Path to file to edit",
302 aliases=["path", "filename", "file"]
303 ),
304 ToolParameter(
305 name="old_string",
306 param_type="string",
307 required=True,
308 description="String to replace",
309 aliases=["search", "find", "from", "original"]
310 ),
311 ToolParameter(
312 name="new_string",
313 param_type="string",
314 required=True,
315 description="Replacement string",
316 aliases=["replace", "to", "replacement"]
317 ),
318 ToolParameter(
319 name="replace_all",
320 param_type="boolean",
321 default_value=False,
322 description="Replace all occurrences",
323 aliases=["global", "all", "replace_all_occurrences"]
324 )
325 ]
326 }
328 def _load_parameter_mappings(self) -> Dict[str, Dict[str, str]]:
329 """Load parameter mapping for compatibility with different versions"""
330 return {
331 # Grep tool mappings
332 "grep_head_limit": "head_limit",
333 "grep_limit": "head_limit",
334 "grep_max": "head_limit",
335 "grep_count": "head_limit",
336 "grep_head": "head_limit",
337 "max_results": "head_limit",
338 "result_limit": "head_limit",
339 "num_results": "head_limit",
341 # Output mode mappings
342 "grep_mode": "output_mode",
343 "grep_format": "output_mode",
344 "output_format": "output_mode",
345 "display_mode": "output_mode",
346 "show_mode": "output_mode",
348 # Path mappings
349 "search_path": "path",
350 "base_path": "path",
351 "root_dir": "path",
352 "target_dir": "path",
354 # Glob tool mappings
355 "glob_pattern": "pattern",
356 "file_glob": "pattern",
357 "search_pattern": "pattern",
358 "match_pattern": "pattern",
360 # Read tool mappings
361 "start_line": "offset",
362 "begin_line": "offset",
363 "from_line": "offset",
364 "max_lines": "limit",
365 "line_count": "limit",
367 # Bash tool mappings
368 "cmd": "command",
369 "execute": "command",
370 "run_command": "command",
371 "timeout_ms": "timeout",
372 "max_time": "timeout",
373 "time_limit": "timeout",
374 "work_dir": "working_directory",
375 "cwd": "working_directory",
377 # Task tool mappings
378 "agent_type": "subagent_type",
379 "message": "prompt",
380 "instruction": "prompt",
381 "query": "prompt",
383 # Write tool mappings
384 "filename": "file_path",
385 "destination": "file_path",
386 "data": "content",
387 "text": "content",
388 "body": "content",
389 "make_backup": "backup",
391 # Edit tool mappings
392 "search": "old_string",
393 "find": "old_string",
394 "from": "old_string",
395 "replace": "new_string",
396 "to": "new_string",
397 "global": "replace_all",
398 "all": "replace_all"
399 }
401 def _validate_grep_mode(self, mode: str) -> bool:
402 """Validate grep output mode"""
403 valid_modes = ["content", "files_with_matches", "count"]
404 return mode in valid_modes
406 def validate_and_normalize_input(self, tool_name: str, input_data: Dict[str, Any]) -> ValidationResult:
407 """
408 Validate and normalize tool input data.
410 This is the main method that addresses the tool input validation failures
411 from the debug logs (Lines 476-495).
412 """
413 import time
414 start_time = time.time()
416 self.stats['validations_performed'] += 1
418 result = ValidationResult(
419 valid=True,
420 normalized_input=input_data.copy()
421 )
423 try:
424 # Get tool parameters
425 tool_params = self.tool_parameters.get(tool_name, [])
427 if not tool_params:
428 # Unknown tool - perform basic validation only
429 result.warnings.append(f"Unknown tool: {tool_name}")
430 return result
432 # Step 1: Map unrecognized parameters
433 mapped_input, mapping_errors = self._map_parameters(tool_name, result.normalized_input)
434 result.normalized_input = mapped_input
435 result.errors.extend(mapping_errors)
437 # Step 2: Validate required parameters
438 required_errors = self._validate_required_parameters(tool_params, result.normalized_input)
439 result.errors.extend(required_errors)
441 # Step 3: Apply default values
442 self._apply_default_values(tool_params, result.normalized_input)
444 # Step 4: Validate parameter values and apply type conversions
445 value_errors = self._validate_parameter_values(tool_params, result.normalized_input)
446 result.errors.extend(value_errors)
448 # Apply type conversions from errors
449 for error in value_errors:
450 if error.code == "type_conversion" and error.auto_corrected and error.corrected_value is not None:
451 result.normalized_input[error.path[0]] = error.corrected_value
453 # Step 5: Normalize parameter formats
454 transformations = self._normalize_parameter_formats(tool_params, result.normalized_input)
455 result.transformations.extend(transformations)
457 # Step 6: Check for deprecated parameters
458 deprecated_warnings = self._check_deprecated_parameters(tool_params, result.normalized_input)
459 result.warnings.extend(deprecated_warnings)
461 # Update valid status
462 critical_errors = [e for e in result.errors if e.severity == ValidationSeverity.CRITICAL]
463 if critical_errors:
464 result.valid = False
466 # Update statistics
467 auto_corrected = len([e for e in result.errors if e.auto_corrected])
468 self.stats['auto_corrections'] += auto_corrected
469 if auto_corrected > 0:
470 self.stats['errors_resolved'] += auto_corrected
472 self.stats['transformations_applied'] += len(result.transformations)
474 if self.enable_logging and (result.errors or result.warnings or result.transformations):
475 logger.info(f"Input validation for {tool_name}: "
476 f"valid={result.valid}, errors={len(result.errors)}, "
477 f"warnings={len(result.warnings)}, transformations={len(result.transformations)}")
479 except Exception as e:
480 result.valid = False
481 result.errors.append(ValidationError(
482 code="validation_exception",
483 message=f"Validation error: {str(e)}",
484 path=[],
485 severity=ValidationSeverity.CRITICAL
486 ))
488 if self.enable_logging:
489 logger.error(f"Exception during input validation for {tool_name}: {e}")
491 result.processing_time_ms = (time.time() - start_time) * 1000
493 return result
495 def _map_parameters(
496 self,
497 tool_name: str,
498 input_data: Dict[str, Any],
499 ) -> Tuple[Dict[str, Any], List[ValidationError]]:
500 """Map and rename unrecognized parameters to their canonical forms"""
501 mapped_input = input_data.copy()
502 errors = []
504 # Get tool-specific parameters
505 tool_params = self.tool_parameters.get(tool_name, [])
507 # Create mapping of all valid parameter names and aliases
508 valid_names = set()
509 alias_mapping = {}
511 for param in tool_params:
512 valid_names.add(param.name)
513 for alias in param.aliases + param.deprecated_aliases:
514 alias_mapping[alias] = param.name
516 # Add global parameter mappings
517 global_mapping_prefix = f"{tool_name.lower()}_"
518 for global_key, canonical_key in self.parameter_mappings.items():
519 if global_key.startswith(global_mapping_prefix):
520 short_key = global_key[len(global_mapping_prefix):]
521 alias_mapping[short_key] = canonical_key
523 # Check each input parameter
524 for param_name in list(mapped_input.keys()):
525 if param_name in valid_names:
526 # Parameter is already in canonical form
527 continue
529 if param_name in alias_mapping:
530 # Map to canonical name
531 canonical_name = alias_mapping[param_name]
532 original_value = mapped_input[param_name]
534 mapped_input[canonical_name] = original_value
535 del mapped_input[param_name]
537 errors.append(ValidationError(
538 code="parameter_mapped",
539 message=f"Mapped parameter '{param_name}' to '{canonical_name}'",
540 path=[param_name],
541 severity=ValidationSeverity.LOW,
542 auto_corrected=True,
543 original_value=original_value,
544 corrected_value=original_value,
545 suggestion=f"Use '{canonical_name}' instead of '{param_name}'"
546 ))
548 else:
549 # Unknown parameter - create error
550 original_value = mapped_input[param_name]
552 # Suggest closest match
553 suggestion = self._find_closest_parameter_match(param_name, valid_names)
555 errors.append(ValidationError(
556 code="unrecognized_parameter",
557 message=f"Unrecognized parameter: '{param_name}'",
558 path=[param_name],
559 severity=ValidationSeverity.HIGH,
560 original_value=original_value,
561 suggestion=suggestion
562 ))
564 return mapped_input, errors
566 def _find_closest_parameter_match(self, param_name: str, valid_names: Set[str]) -> Optional[str]:
567 """Find the closest matching valid parameter name"""
568 # Convert to lowercase for comparison
569 param_lower = param_name.lower()
570 valid_lower = {name.lower(): name for name in valid_names}
572 # Exact match (case-insensitive)
573 if param_lower in valid_lower:
574 return valid_lower[param_lower]
576 # Find best match using Levenshtein distance
577 best_match = None
578 best_score = float('inf')
580 for valid_lower_name, canonical_name in valid_lower.items():
581 # Simple similarity score
582 score = self._calculate_string_similarity(param_lower, valid_lower_name)
584 if score < best_score and score < 0.7: # Similarity threshold
585 best_score = score
586 best_match = canonical_name
588 return best_match
590 def _calculate_string_similarity(self, s1: str, s2: str) -> float:
591 """Calculate similarity between two strings (0-1, lower is more similar)"""
592 # Simple Levenshtein distance approximation
593 if not s1:
594 return len(s2)
595 if not s2:
596 return len(s1)
598 if len(s1) < len(s2):
599 s1, s2 = s2, s1
601 # Calculate distance
602 previous_row = list(range(len(s2) + 1))
603 for i, c1 in enumerate(s1):
604 current_row = [i + 1]
605 for j, c2 in enumerate(s2):
606 insertions = previous_row[j + 1] + 1
607 deletions = current_row[j] + 1
608 substitutions = previous_row[j] + (c1 != c2)
609 current_row.append(min(insertions, deletions, substitutions))
610 previous_row = current_row
612 distance = previous_row[-1]
613 max_len = max(len(s1), len(s2))
614 return distance / max_len if max_len > 0 else 0
616 def _validate_required_parameters(
617 self,
618 tool_params: List[ToolParameter],
619 input_data: Dict[str, Any],
620 ) -> List[ValidationError]:
621 """Validate that all required parameters are present"""
622 errors = []
624 for param in tool_params:
625 if param.required and param.name not in input_data:
626 errors.append(ValidationError(
627 code="missing_required_parameter",
628 message=f"Missing required parameter: '{param.name}'",
629 path=[],
630 severity=ValidationSeverity.CRITICAL,
631 suggestion=f"Add '{param.name}' parameter"
632 ))
634 return errors
636 def _apply_default_values(self, tool_params: List[ToolParameter], input_data: Dict[str, Any]) -> None:
637 """Apply default values for missing optional parameters"""
638 for param in tool_params:
639 if not param.required and param.name not in input_data and param.default_value is not None:
640 input_data[param.name] = param.default_value
642 def _validate_parameter_values(
643 self,
644 tool_params: List[ToolParameter],
645 input_data: Dict[str, Any],
646 ) -> List[ValidationError]:
647 """Validate parameter values against their types and constraints"""
648 errors = []
650 for param in tool_params:
651 if param.name not in input_data:
652 continue
654 value = input_data[param.name]
656 # Type validation
657 type_errors = self._validate_parameter_type(param, value, input_data)
658 errors.extend(type_errors)
660 # Custom validation function
661 if param.validation_function and not type_errors:
662 try:
663 if not param.validation_function(value):
664 errors.append(ValidationError(
665 code="validation_function_failed",
666 message=f"Parameter '{param.name}' failed custom validation",
667 path=[param.name],
668 severity=ValidationSeverity.HIGH,
669 original_value=value
670 ))
671 except Exception as e:
672 errors.append(ValidationError(
673 code="validation_function_error",
674 message=f"Error validating parameter '{param.name}': {str(e)}",
675 path=[param.name],
676 severity=ValidationSeverity.HIGH,
677 original_value=value
678 ))
680 return errors
682 def _validate_parameter_type(
683 self,
684 param: ToolParameter,
685 value: Any,
686 input_data: Dict[str, Any],
687 ) -> List[ValidationError]:
688 """Validate parameter value against expected type"""
689 errors = []
691 # Type mapping
692 type_validators = {
693 "string": lambda v: isinstance(v, str),
694 "integer": lambda v: isinstance(v, int) or (isinstance(v, str) and v.isdigit()),
695 "boolean": lambda v: isinstance(v, bool) or str(v).lower() in ['true', 'false', '1', '0'],
696 "dict": lambda v: isinstance(v, dict),
697 "list": lambda v: isinstance(v, list),
698 "float": lambda v: isinstance(v, float) or (isinstance(v, (int, str)) and self._is_float(v))
699 }
701 validator = type_validators.get(param.param_type)
702 if not validator:
703 return errors
705 if not validator(value):
706 # Try to auto-convert
707 converted_value = self._convert_parameter_type(value, param.param_type)
708 if converted_value is not None:
709 # Apply the converted value to input_data
710 input_data[param.name] = converted_value
711 errors.append(ValidationError(
712 code="type_conversion",
713 message=f"Converted parameter '{param.name}' from {type(value).__name__} to {param.param_type}",
714 path=[param.name],
715 severity=ValidationSeverity.LOW,
716 auto_corrected=True,
717 original_value=value,
718 corrected_value=converted_value
719 ))
720 else:
721 errors.append(ValidationError(
722 code="type_mismatch",
723 message=f"Parameter '{param.name}' expects {param.param_type}, got {type(value).__name__}",
724 path=[param.name],
725 severity=ValidationSeverity.HIGH,
726 original_value=value,
727 suggestion=f"Provide a {param.param_type} value for '{param.name}'"
728 ))
730 return errors
732 def _convert_parameter_type(self, value: Any, target_type: str) -> Any:
733 """Attempt to convert value to target type"""
734 try:
735 if target_type == "string":
736 return str(value)
737 elif target_type == "integer":
738 if isinstance(value, str):
739 # Handle negative numbers and leading/trailing whitespace
740 value = value.strip()
741 try:
742 return int(value)
743 except ValueError:
744 # Try to convert from float string
745 try:
746 return int(float(value))
747 except ValueError:
748 return None
749 elif isinstance(value, float):
750 return int(value)
751 elif isinstance(value, bool):
752 return int(value)
753 elif target_type == "boolean":
754 if isinstance(value, str):
755 return value.lower() in ['true', '1', 'yes', 'on']
756 elif isinstance(value, (int, float)):
757 return bool(value)
758 elif target_type == "float":
759 if isinstance(value, str):
760 return float(value)
761 elif isinstance(value, int):
762 return float(value)
763 except (ValueError, TypeError):
764 pass
766 return None
768 def _is_float(self, value) -> bool:
769 """Check if value can be converted to float"""
770 try:
771 float(value)
772 return True
773 except (ValueError, TypeError):
774 return False
776 def _normalize_parameter_formats(self, tool_params: List[ToolParameter], input_data: Dict[str, Any]) -> List[str]:
777 """Normalize parameter formats for consistency"""
778 transformations = []
780 for param in tool_params:
781 if param.name not in input_data:
782 continue
784 value = input_data[param.name]
786 # Normalize boolean values
787 if param.param_type == "boolean":
788 if isinstance(value, str):
789 normalized = value.lower() in ['true', '1', 'yes', 'on']
790 if value != str(normalized):
791 input_data[param.name] = normalized
792 transformations.append(f"Normalized '{param.name}' boolean from '{value}' to '{normalized}'")
794 # Normalize file paths
795 elif param.name in ["file_path", "path", "directory"] and isinstance(value, str):
796 # Convert to forward slashes and remove trailing slash
797 normalized = value.replace("\\", "/").rstrip("/")
798 if value != normalized:
799 input_data[param.name] = normalized
800 transformations.append(f"Normalized '{param.name}' path from '{value}' to '{normalized}'")
802 # Normalize numeric formats - Always attempt conversion for numeric types
803 elif param.param_type in ["integer", "float"] and isinstance(value, str):
804 try:
805 if param.param_type == "integer":
806 normalized = int(float(value.strip())) # Handle "123.0" -> 123
807 else: # float
808 normalized = float(value.strip())
810 input_data[param.name] = normalized
811 transformations.append(f"Normalized '{param.name}' {param.param_type} from '{value}' to '{normalized}'")
812 except ValueError:
813 # Keep original value if conversion fails
814 pass
816 return transformations
818 def _check_deprecated_parameters(self, tool_params: List[ToolParameter], input_data: Dict[str, Any]) -> List[str]:
819 """Check for deprecated parameter usage"""
820 warnings = []
822 for param in tool_params:
823 if param.name not in input_data:
824 continue
826 # Check if parameter name is deprecated
827 for deprecated_alias in param.deprecated_aliases:
828 if deprecated_alias in input_data and param.name != deprecated_alias:
829 value = input_data[deprecated_alias]
830 input_data[param.name] = value
831 del input_data[deprecated_alias]
833 warnings.append(f"Deprecated parameter '{deprecated_alias}' replaced with '{param.name}'. "
834 f"This alias will be removed in future versions.")
836 return warnings
838 def get_validation_stats(self) -> Dict[str, Any]:
839 """Get input validation statistics"""
840 return {
841 **self.stats,
842 'tools_configured': len(self.tool_parameters),
843 'parameter_mappings': len(self.parameter_mappings),
844 'cache_size': len(self.validation_cache) if self.validation_cache else 0
845 }
847 def register_tool_parameters(self, tool_name: str, parameters: List[ToolParameter]) -> None:
848 """Register custom tool parameters"""
849 self.tool_parameters[tool_name] = parameters
851 def add_parameter_mapping(self, from_key: str, to_key: str) -> None:
852 """Add custom parameter mapping"""
853 self.parameter_mappings[from_key] = to_key
855 def export_validation_report(self, output_path: str) -> None:
856 """Export validation report to file"""
857 report = {
858 'generated_at': __import__('time').time(),
859 'stats': self.get_validation_stats(),
860 'configured_tools': list(self.tool_parameters.keys()),
861 'parameter_mappings': self.parameter_mappings
862 }
864 with open(output_path, 'w', encoding='utf-8') as f:
865 json.dump(report, f, indent=2, ensure_ascii=False)
868# Global instance for easy import
869validation_middleware = EnhancedInputValidationMiddleware()
872def validate_tool_input(tool_name: str, input_data: Dict[str, Any]) -> ValidationResult:
873 """Convenience function for tool input validation"""
874 return validation_middleware.validate_and_normalize_input(tool_name, input_data)
877def get_validation_stats() -> Dict[str, Any]:
878 """Convenience function to get validation statistics"""
879 return validation_middleware.get_validation_stats()
882if __name__ == "__main__":
883 # Demo script for testing the input validation middleware
884 print("🔧 MoAI-ADK Enhanced Input Validation Middleware Demo")
885 print("=" * 60)
887 # Test cases that reproduce the debug log errors
888 test_cases = [
889 {
890 "name": "Grep with head_limit (debug log error)",
891 "tool": "Grep",
892 "input": {
893 "pattern": "test",
894 "head_limit": 10, # This was causing the error
895 "output_mode": "content"
896 }
897 },
898 {
899 "name": "Grep with alternative parameter names",
900 "tool": "Grep",
901 "input": {
902 "pattern": "test",
903 "max_results": 20, # Should be mapped to head_limit
904 "search_path": "/src" # Should be mapped to path
905 }
906 },
907 {
908 "name": "Grep with deprecated parameters",
909 "tool": "Grep",
910 "input": {
911 "pattern": "test",
912 "count": 15, # Deprecated alias
913 "folder": "/src" # Should be mapped to path
914 }
915 },
916 {
917 "name": "Read with parameter aliases",
918 "tool": "Read",
919 "input": {
920 "filename": "/path/to/file.txt", # Should be mapped to file_path
921 "start_line": 10, # Should be mapped to offset
922 "lines": 50 # Should be mapped to limit
923 }
924 },
925 {
926 "name": "Task with mixed parameter names",
927 "tool": "Task",
928 "input": {
929 "agent_type": "debug-helper", # Should be mapped to subagent_type
930 "message": "test message", # Should be mapped to prompt
931 "verbose": True # Should be mapped to debug
932 }
933 },
934 {
935 "name": "Bash with alternative parameters",
936 "tool": "Bash",
937 "input": {
938 "cmd": "ls -la", # Should be mapped to command
939 "cwd": "/home/user", # Should be mapped to working_directory
940 "timeout_ms": 5000 # Should be mapped to timeout
941 }
942 }
943 ]
945 for i, test_case in enumerate(test_cases, 1):
946 print(f"\n{i}. {test_case['name']}")
947 print(f" Tool: {test_case['tool']}")
948 print(f" Original input: {test_case['input']}")
950 result = validate_tool_input(test_case['tool'], test_case['input'])
952 print(f" Valid: {result.valid}")
953 print(f" Errors: {len(result.errors)}")
954 print(f" Warnings: {len(result.warnings)}")
955 print(f" Transformations: {len(result.transformations)}")
956 print(f" Processing time: {result.processing_time_ms:.2f}ms")
958 if result.errors:
959 print(" Error details:")
960 for error in result.errors:
961 status = "✅ AUTO-CORRECTED" if error.auto_corrected else "❌ NOT FIXED"
962 print(f" • {error.message} [{status}]")
963 if error.suggestion:
964 print(f" Suggestion: {error.suggestion}")
966 if result.warnings:
967 print(" Warnings:")
968 for warning in result.warnings:
969 print(f" • {warning}")
971 if result.transformations:
972 print(" Transformations:")
973 for transform in result.transformations:
974 print(f" • {transform}")
976 print(f" Normalized input: {result.normalized_input}")
978 print("\n📊 Validation Statistics:")
979 stats = get_validation_stats()
980 for key, value in stats.items():
981 print(f" {key}: {value}")
983 print("\n✨ Demo completed! The Enhanced Input Validation Middleware addresses")
984 print(" the tool input validation failures from the debug logs with automatic")
985 print(" parameter mapping and intelligent correction.")