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

1""" 

2Enhanced Input Validation Middleware for MoAI-ADK 

3 

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. 

7 

8Author: MoAI-ADK Core Team 

9Version: 1.0.0 

10""" 

11 

12import json 

13import logging 

14from dataclasses import dataclass, field 

15from enum import Enum 

16from typing import Any, Callable, Dict, List, Optional, Set, Tuple 

17 

18# Configure logging 

19logger = logging.getLogger(__name__) 

20 

21 

22class ValidationSeverity(Enum): 

23 """Input validation severity levels""" 

24 LOW = "low" 

25 MEDIUM = "medium" 

26 HIGH = "high" 

27 CRITICAL = "critical" 

28 

29 

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" 

38 

39 

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 

51 

52 

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 

62 

63 

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) 

75 

76 

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. 

81 

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 """ 

89 

90 def __init__(self, enable_logging: bool = True, enable_caching: bool = True): 

91 self.enable_logging = enable_logging 

92 self.enable_caching = enable_caching 

93 

94 # Tool parameter definitions 

95 self.tool_parameters = self._load_tool_parameter_definitions() 

96 

97 # Parameter mapping for compatibility 

98 self.parameter_mappings = self._load_parameter_mappings() 

99 

100 # Validation cache 

101 self.validation_cache = {} if enable_caching else None 

102 

103 # Statistics 

104 self.stats = { 

105 'validations_performed': 0, 

106 'auto_corrections': 0, 

107 'errors_resolved': 0, 

108 'transformations_applied': 0 

109 } 

110 

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 } 

327 

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", 

340 

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", 

347 

348 # Path mappings 

349 "search_path": "path", 

350 "base_path": "path", 

351 "root_dir": "path", 

352 "target_dir": "path", 

353 

354 # Glob tool mappings 

355 "glob_pattern": "pattern", 

356 "file_glob": "pattern", 

357 "search_pattern": "pattern", 

358 "match_pattern": "pattern", 

359 

360 # Read tool mappings 

361 "start_line": "offset", 

362 "begin_line": "offset", 

363 "from_line": "offset", 

364 "max_lines": "limit", 

365 "line_count": "limit", 

366 

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", 

376 

377 # Task tool mappings 

378 "agent_type": "subagent_type", 

379 "message": "prompt", 

380 "instruction": "prompt", 

381 "query": "prompt", 

382 

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", 

390 

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 } 

400 

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 

405 

406 def validate_and_normalize_input(self, tool_name: str, input_data: Dict[str, Any]) -> ValidationResult: 

407 """ 

408 Validate and normalize tool input data. 

409 

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() 

415 

416 self.stats['validations_performed'] += 1 

417 

418 result = ValidationResult( 

419 valid=True, 

420 normalized_input=input_data.copy() 

421 ) 

422 

423 try: 

424 # Get tool parameters 

425 tool_params = self.tool_parameters.get(tool_name, []) 

426 

427 if not tool_params: 

428 # Unknown tool - perform basic validation only 

429 result.warnings.append(f"Unknown tool: {tool_name}") 

430 return result 

431 

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) 

436 

437 # Step 2: Validate required parameters 

438 required_errors = self._validate_required_parameters(tool_params, result.normalized_input) 

439 result.errors.extend(required_errors) 

440 

441 # Step 3: Apply default values 

442 self._apply_default_values(tool_params, result.normalized_input) 

443 

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) 

447 

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 

452 

453 # Step 5: Normalize parameter formats 

454 transformations = self._normalize_parameter_formats(tool_params, result.normalized_input) 

455 result.transformations.extend(transformations) 

456 

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) 

460 

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 

465 

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 

471 

472 self.stats['transformations_applied'] += len(result.transformations) 

473 

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)}") 

478 

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 )) 

487 

488 if self.enable_logging: 

489 logger.error(f"Exception during input validation for {tool_name}: {e}") 

490 

491 result.processing_time_ms = (time.time() - start_time) * 1000 

492 

493 return result 

494 

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 = [] 

503 

504 # Get tool-specific parameters 

505 tool_params = self.tool_parameters.get(tool_name, []) 

506 

507 # Create mapping of all valid parameter names and aliases 

508 valid_names = set() 

509 alias_mapping = {} 

510 

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 

515 

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 

522 

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 

528 

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] 

533 

534 mapped_input[canonical_name] = original_value 

535 del mapped_input[param_name] 

536 

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 )) 

547 

548 else: 

549 # Unknown parameter - create error 

550 original_value = mapped_input[param_name] 

551 

552 # Suggest closest match 

553 suggestion = self._find_closest_parameter_match(param_name, valid_names) 

554 

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 )) 

563 

564 return mapped_input, errors 

565 

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} 

571 

572 # Exact match (case-insensitive) 

573 if param_lower in valid_lower: 

574 return valid_lower[param_lower] 

575 

576 # Find best match using Levenshtein distance 

577 best_match = None 

578 best_score = float('inf') 

579 

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) 

583 

584 if score < best_score and score < 0.7: # Similarity threshold 

585 best_score = score 

586 best_match = canonical_name 

587 

588 return best_match 

589 

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) 

597 

598 if len(s1) < len(s2): 

599 s1, s2 = s2, s1 

600 

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 

611 

612 distance = previous_row[-1] 

613 max_len = max(len(s1), len(s2)) 

614 return distance / max_len if max_len > 0 else 0 

615 

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 = [] 

623 

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 )) 

633 

634 return errors 

635 

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 

641 

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 = [] 

649 

650 for param in tool_params: 

651 if param.name not in input_data: 

652 continue 

653 

654 value = input_data[param.name] 

655 

656 # Type validation 

657 type_errors = self._validate_parameter_type(param, value, input_data) 

658 errors.extend(type_errors) 

659 

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 )) 

679 

680 return errors 

681 

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 = [] 

690 

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 } 

700 

701 validator = type_validators.get(param.param_type) 

702 if not validator: 

703 return errors 

704 

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 )) 

729 

730 return errors 

731 

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 

765 

766 return None 

767 

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 

775 

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 = [] 

779 

780 for param in tool_params: 

781 if param.name not in input_data: 

782 continue 

783 

784 value = input_data[param.name] 

785 

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}'") 

793 

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}'") 

801 

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()) 

809 

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 

815 

816 return transformations 

817 

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 = [] 

821 

822 for param in tool_params: 

823 if param.name not in input_data: 

824 continue 

825 

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] 

832 

833 warnings.append(f"Deprecated parameter '{deprecated_alias}' replaced with '{param.name}'. " 

834 f"This alias will be removed in future versions.") 

835 

836 return warnings 

837 

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 } 

846 

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 

850 

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 

854 

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 } 

863 

864 with open(output_path, 'w', encoding='utf-8') as f: 

865 json.dump(report, f, indent=2, ensure_ascii=False) 

866 

867 

868# Global instance for easy import 

869validation_middleware = EnhancedInputValidationMiddleware() 

870 

871 

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) 

875 

876 

877def get_validation_stats() -> Dict[str, Any]: 

878 """Convenience function to get validation statistics""" 

879 return validation_middleware.get_validation_stats() 

880 

881 

882if __name__ == "__main__": 

883 # Demo script for testing the input validation middleware 

884 print("🔧 MoAI-ADK Enhanced Input Validation Middleware Demo") 

885 print("=" * 60) 

886 

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 ] 

944 

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']}") 

949 

950 result = validate_tool_input(test_case['tool'], test_case['input']) 

951 

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") 

957 

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}") 

965 

966 if result.warnings: 

967 print(" Warnings:") 

968 for warning in result.warnings: 

969 print(f"{warning}") 

970 

971 if result.transformations: 

972 print(" Transformations:") 

973 for transform in result.transformations: 

974 print(f"{transform}") 

975 

976 print(f" Normalized input: {result.normalized_input}") 

977 

978 print("\n📊 Validation Statistics:") 

979 stats = get_validation_stats() 

980 for key, value in stats.items(): 

981 print(f" {key}: {value}") 

982 

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.")