UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

717 lines (642 loc) 20.8 kB
/** * Comprehensive Error Handling System * * Provides structured error handling, logging, recovery mechanisms, * and user-friendly error reporting across the entire application. */ import { logger } from '../logger.js'; import chalk from 'chalk'; // Error severity levels export enum ErrorSeverity { LOW = 'low', MEDIUM = 'medium', HIGH = 'high', CRITICAL = 'critical', } // Error categories export enum ErrorCategory { VALIDATION = 'validation', AUTHENTICATION = 'authentication', AUTHORIZATION = 'authorization', NETWORK = 'network', FILE_SYSTEM = 'file_system', MODEL = 'model', TOOL_EXECUTION = 'tool_execution', CONFIGURATION = 'configuration', SYSTEM = 'system', USER_INPUT = 'user_input', EXTERNAL_API = 'external_api', MCP_SERVICE = 'mcp_service', DATABASE = 'database', TIMEOUT = 'timeout', RATE_LIMIT = 'rate_limit', NOT_FOUND = 'not_found', CONFLICT = 'conflict', SECURITY = 'security', INFRASTRUCTURE = 'infrastructure', APPLICATION = 'application', } // Structured error interface export interface StructuredError { id: string; message: string; category: ErrorCategory; severity: ErrorSeverity; timestamp: number; context?: Record<string, any>; stackTrace?: string; userMessage?: string; suggestedActions?: string[]; recoverable: boolean; retryable: boolean; metadata?: Record<string, any>; } // Error response interface for APIs and tools export interface ErrorResponse { success: false; error: StructuredError; request_id?: string; service?: string; recovery_suggestions?: string[]; } // Success response interface export interface SuccessResponse<T = any> { success: true; data: T; request_id?: string; service?: string; metadata?: Record<string, any>; } // Union type for all responses export type ServiceResponse<T = any> = SuccessResponse<T> | ErrorResponse; /** * Error factory for creating structured errors */ export class ErrorFactory { private static errorCounter = 0; static createError( message: string, category: ErrorCategory, severity: ErrorSeverity = ErrorSeverity.MEDIUM, options: { context?: Record<string, any>; userMessage?: string; suggestedActions?: string[]; recoverable?: boolean; retryable?: boolean; metadata?: Record<string, any>; originalError?: Error; } = {} ): StructuredError { const id = `ERR_${Date.now()}_${++this.errorCounter}`; return { id, message, category, severity, timestamp: Date.now(), context: options.context, stackTrace: options.originalError?.stack, userMessage: options.userMessage || this.generateUserMessage(message, category), suggestedActions: options.suggestedActions || this.generateSuggestedActions(category), recoverable: options.recoverable ?? this.isRecoverable(category, severity), retryable: options.retryable ?? this.isRetryable(category), metadata: options.metadata, }; } private static generateUserMessage(message: string, category: ErrorCategory): string { const categoryMessages: Record<ErrorCategory, string> = { [ErrorCategory.VALIDATION]: 'Invalid input provided', [ErrorCategory.AUTHENTICATION]: 'Authentication failed', [ErrorCategory.AUTHORIZATION]: 'Access denied', [ErrorCategory.NETWORK]: 'Network connection issue', [ErrorCategory.FILE_SYSTEM]: 'File operation failed', [ErrorCategory.MODEL]: 'AI model processing error', [ErrorCategory.TOOL_EXECUTION]: 'Tool execution failed', [ErrorCategory.CONFIGURATION]: 'Configuration error', [ErrorCategory.SYSTEM]: 'System error occurred', [ErrorCategory.USER_INPUT]: 'Invalid user input', [ErrorCategory.EXTERNAL_API]: 'External service unavailable', [ErrorCategory.MCP_SERVICE]: 'MCP service error', [ErrorCategory.DATABASE]: 'Database operation failed', [ErrorCategory.TIMEOUT]: 'Operation timed out', [ErrorCategory.RATE_LIMIT]: 'Rate limit exceeded', [ErrorCategory.NOT_FOUND]: 'Resource not found', [ErrorCategory.CONFLICT]: 'Resource conflict detected', [ErrorCategory.SECURITY]: 'Security violation detected', [ErrorCategory.INFRASTRUCTURE]: 'Infrastructure error', [ErrorCategory.APPLICATION]: 'Application error', }; return categoryMessages[category] || 'An error occurred'; } private static generateSuggestedActions(category: ErrorCategory): string[] { const categoryActions: Record<ErrorCategory, string[]> = { [ErrorCategory.VALIDATION]: [ 'Check input format and required fields', 'Review parameter types and constraints', ], [ErrorCategory.AUTHENTICATION]: [ 'Verify API keys and credentials', 'Check authentication configuration', ], [ErrorCategory.AUTHORIZATION]: ['Verify user permissions', 'Check access rights and roles'], [ErrorCategory.NETWORK]: [ 'Check internet connection', 'Verify endpoint availability', 'Try again in a moment', ], [ErrorCategory.FILE_SYSTEM]: [ 'Check file permissions', 'Verify file path exists', 'Ensure sufficient disk space', ], [ErrorCategory.MODEL]: [ 'Check if AI model is running', 'Verify model configuration', 'Try with a different model', ], [ErrorCategory.TOOL_EXECUTION]: [ 'Review tool parameters', 'Check tool availability', 'Try alternative tools', ], [ErrorCategory.CONFIGURATION]: [ 'Review configuration settings', 'Check environment variables', 'Reset to default configuration', ], [ErrorCategory.SYSTEM]: [ 'Restart the application', 'Check system resources', 'Contact support if issue persists', ], [ErrorCategory.USER_INPUT]: [ 'Review input format', 'Check for special characters', 'Try simplified input', ], [ErrorCategory.EXTERNAL_API]: [ 'Check service status', 'Verify API credentials', 'Try again later', ], [ErrorCategory.MCP_SERVICE]: [ 'Check MCP server status', 'Verify service configuration', 'Try fallback options', ], [ErrorCategory.DATABASE]: [ 'Check database connection', 'Verify database credentials', 'Try reconnecting to database', ], [ErrorCategory.TIMEOUT]: [ 'Increase timeout limit', 'Check network connectivity', 'Try again later', ], [ErrorCategory.RATE_LIMIT]: [ 'Wait before retrying', 'Reduce request frequency', 'Check rate limit settings', ], [ErrorCategory.NOT_FOUND]: [ 'Check resource path', 'Verify resource exists', 'Update resource references', ], [ErrorCategory.CONFLICT]: [ 'Resolve resource conflicts', 'Check for duplicate entries', 'Update resource state', ], [ErrorCategory.SECURITY]: [ 'Review security permissions', 'Check authentication status', 'Contact security administrator', ], [ErrorCategory.INFRASTRUCTURE]: [ 'Check system health', 'Verify infrastructure status', 'Contact system administrator', ], [ErrorCategory.APPLICATION]: [ 'Restart application', 'Check application logs', 'Update application configuration', ], }; return categoryActions[category] || ['Try again', 'Contact support if issue persists']; } private static isRecoverable(category: ErrorCategory, severity: ErrorSeverity): boolean { if (severity === ErrorSeverity.CRITICAL) return false; const recoverableCategories = [ ErrorCategory.NETWORK, ErrorCategory.EXTERNAL_API, ErrorCategory.MCP_SERVICE, ErrorCategory.TOOL_EXECUTION, ErrorCategory.MODEL, ]; return recoverableCategories.includes(category); } private static isRetryable(category: ErrorCategory): boolean { const retryableCategories = [ ErrorCategory.NETWORK, ErrorCategory.EXTERNAL_API, ErrorCategory.MCP_SERVICE, ErrorCategory.MODEL, ]; return retryableCategories.includes(category); } } /** * Error handler with recovery mechanisms */ export class ErrorHandler { private static errorHistory: StructuredError[] = []; private static maxHistorySize = 100; /** * Handle error with logging and potential recovery */ static async handleError( error: Error | StructuredError, context?: Record<string, any> ): Promise<StructuredError> { const structuredError = this.ensureStructuredError(error, context); // Add to history this.errorHistory.push(structuredError); if (this.errorHistory.length > this.maxHistorySize) { this.errorHistory.shift(); } // Log error this.logError(structuredError); // Attempt recovery if possible if (structuredError.recoverable) { await this.attemptRecovery(structuredError); } return structuredError; } /** * Create error response for APIs and tools */ static createErrorResponse( error: Error | StructuredError, requestId?: string, service?: string ): ErrorResponse { const structuredError = this.ensureStructuredError(error); return { success: false, error: structuredError, request_id: requestId, service, recovery_suggestions: structuredError.suggestedActions, }; } /** * Create success response */ static createSuccessResponse<T>( data: T, requestId?: string, service?: string, metadata?: Record<string, any> ): SuccessResponse<T> { return { success: true, data, request_id: requestId, service, metadata, }; } /** * Wrap function with error handling */ static wrapWithErrorHandling<T extends any[], R>( fn: (...args: T) => Promise<R>, context?: Record<string, any> ): (...args: T) => Promise<ServiceResponse<R>> { return async (...args: T): Promise<ServiceResponse<R>> => { try { const result = await fn(...args); return this.createSuccessResponse(result); } catch (error) { const structuredError = await this.handleError(error as Error, context); return this.createErrorResponse(structuredError); } }; } /** * Retry function with exponential backoff */ static async retryWithBackoff<T>( fn: () => Promise<T>, maxRetries: number = 3, baseDelay: number = 1000, context?: Record<string, any> ): Promise<ServiceResponse<T>> { let lastError: StructuredError | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await fn(); return this.createSuccessResponse(result); } catch (error) { lastError = await this.handleError(error as Error, { ...context, attempt, maxRetries, }); if (attempt < maxRetries && lastError.retryable) { const delay = baseDelay * Math.pow(2, attempt - 1); logger.info(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); } else { break; } } } return this.createErrorResponse(lastError!); } /** * Get error statistics */ static getErrorStatistics(): { total: number; by_category: Record<ErrorCategory, number>; by_severity: Record<ErrorSeverity, number>; recent_errors: StructuredError[]; } { const byCategoryCount: Record<ErrorCategory, number> = {} as any; const bySeverityCount: Record<ErrorSeverity, number> = {} as any; // Initialize counts Object.values(ErrorCategory).forEach(cat => (byCategoryCount[cat] = 0)); Object.values(ErrorSeverity).forEach(sev => (bySeverityCount[sev] = 0)); // Count errors this.errorHistory.forEach(error => { byCategoryCount[error.category]++; bySeverityCount[error.severity]++; }); return { total: this.errorHistory.length, by_category: byCategoryCount, by_severity: bySeverityCount, recent_errors: this.errorHistory.slice(-10), }; } /** * Clear error history */ static clearErrorHistory(): void { this.errorHistory = []; } private static ensureStructuredError( error: Error | StructuredError, context?: Record<string, any> ): StructuredError { if ('id' in error && 'category' in error) { return error as StructuredError; } const originalError = error as Error; return ErrorFactory.createError( originalError.message || 'Unknown error', ErrorCategory.SYSTEM, ErrorSeverity.MEDIUM, { context, originalError, metadata: { error_name: originalError.name, error_type: typeof originalError, }, } ); } private static logError(error: StructuredError): void { const colorMap = { [ErrorSeverity.LOW]: chalk.yellow, [ErrorSeverity.MEDIUM]: chalk.yellowBright, [ErrorSeverity.HIGH]: chalk.red, [ErrorSeverity.CRITICAL]: chalk.redBright, }; const color = colorMap[error.severity] || chalk.red; logger.error( `${color(`[${error.severity.toUpperCase()}]`)} ${error.category}: ${error.message}`, { errorId: error.id, category: error.category, severity: error.severity, context: error.context, stackTrace: error.stackTrace, timestamp: error.timestamp, } ); // Also log to console for immediate visibility console.error(color(`❌ ${error.userMessage || error.message}`)); if (error.suggestedActions && error.suggestedActions.length > 0) { console.error(chalk.cyan('💡 Suggested actions:')); error.suggestedActions.forEach(action => { console.error(chalk.cyan(` • ${action}`)); }); } } private static async attemptRecovery(error: StructuredError): Promise<void> { logger.info(`Attempting recovery for error: ${error.id}`); try { switch (error.category) { case ErrorCategory.NETWORK: // Wait and retry network connections await new Promise(resolve => setTimeout(resolve, 2000)); break; case ErrorCategory.MODEL: // Try to reinitialize model connection logger.info('Attempting model recovery...'); break; case ErrorCategory.MCP_SERVICE: // Try to reconnect to MCP services logger.info('Attempting MCP service recovery...'); break; default: logger.debug(`No recovery mechanism for category: ${error.category}`); } } catch (recoveryError) { logger.warn('Recovery attempt failed:', recoveryError); } } } /** * Input validation with structured error responses */ export class InputValidator { /** * Validate required string field */ static validateString( value: any, fieldName: string, options: { minLength?: number; maxLength?: number; pattern?: RegExp; allowEmpty?: boolean; } = {} ): ServiceResponse<string> { if (value === undefined || value === null) { return ErrorHandler.createErrorResponse( ErrorFactory.createError( `${fieldName} is required`, ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM, { userMessage: `Please provide ${fieldName}`, suggestedActions: [`Provide a valid ${fieldName} value`], } ) ); } const stringValue = String(value); if (!options.allowEmpty && stringValue.trim().length === 0) { return ErrorHandler.createErrorResponse( ErrorFactory.createError( `${fieldName} cannot be empty`, ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM ) ); } if (options.minLength && stringValue.length < options.minLength) { return ErrorHandler.createErrorResponse( ErrorFactory.createError( `${fieldName} must be at least ${options.minLength} characters`, ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM ) ); } if (options.maxLength && stringValue.length > options.maxLength) { return ErrorHandler.createErrorResponse( ErrorFactory.createError( `${fieldName} must not exceed ${options.maxLength} characters`, ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM ) ); } if (options.pattern && !options.pattern.test(stringValue)) { return ErrorHandler.createErrorResponse( ErrorFactory.createError( `${fieldName} format is invalid`, ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM ) ); } return ErrorHandler.createSuccessResponse(stringValue); } /** * Validate and sanitize file path */ static validateFilePath(path: any): ServiceResponse<string> { const stringResult = this.validateString(path, 'file path'); if (!stringResult.success) return stringResult; const filePath = stringResult.data; // Check for directory traversal attempts if (filePath.includes('..') || filePath.includes('~')) { return ErrorHandler.createErrorResponse( ErrorFactory.createError( 'Invalid file path: directory traversal not allowed', ErrorCategory.VALIDATION, ErrorSeverity.HIGH, { context: { attempted_path: filePath }, userMessage: 'File path contains invalid characters', suggestedActions: ['Use relative paths within the project directory'], } ) ); } return ErrorHandler.createSuccessResponse(filePath); } /** * Sanitize user input to prevent injection attacks */ static sanitizeInput(input: string): string { if (!input || typeof input !== 'string') { return ''; } let sanitized = input; // SQL Injection protection - remove/escape SQL keywords and operators const sqlKeywords = [ 'drop', 'delete', 'insert', 'update', 'union', 'select', 'create', 'alter', 'truncate', 'exec', 'execute', 'sp_', 'xp_', 'cmd', 'shell', 'script', ]; sqlKeywords.forEach(keyword => { const regex = new RegExp(`\\b${keyword}\\b`, 'gi'); sanitized = sanitized.replace(regex, ''); }); // Remove SQL operators and comment markers sanitized = sanitized .replace(/--/g, '') // SQL comments .replace(/\/\*/g, '') // SQL block comment start .replace(/\*\//g, '') // SQL block comment end .replace(/;/g, '') // SQL statement terminator .replace(/'/g, '') // Single quotes .replace(/"/g, '') // Double quotes .replace(/`/g, '') // Backticks .replace(/\|/g, '') // Pipe operators .replace(/&/g, 'and'); // Ampersand // XSS protection - remove script tags and event handlers const xssPatterns = [ /<script[^>]*>.*?<\/script>/gi, /<iframe[^>]*>.*?<\/iframe>/gi, /<object[^>]*>.*?<\/object>/gi, /<embed[^>]*>.*?<\/embed>/gi, /<svg[^>]*>.*?<\/svg>/gi, /javascript:/gi, /vbscript:/gi, /data:/gi, /on\w+\s*=/gi, // Event handlers like onclick, onerror, etc. /<[^>]*on\w+[^>]*>/gi, // Any tag with event handlers ]; xssPatterns.forEach(pattern => { sanitized = sanitized.replace(pattern, ''); }); // Remove HTML tags completely sanitized = sanitized.replace(/<[^>]*>/g, ''); // Directory traversal protection sanitized = sanitized .replace(/\.\./g, '') // Remove .. sequences .replace(/~/g, '') // Remove home directory references .replace(/\\/g, '/') // Normalize path separators .replace(/\/\//g, '/'); // Remove double slashes // Encoding-based attack protection sanitized = sanitized .replace(/%2e/gi, '') // Encoded dots .replace(/%2f/gi, '') // Encoded slashes .replace(/%5c/gi, '') // Encoded backslashes .replace(/\\u[0-9a-f]{4}/gi, '') // Unicode escape sequences .replace(/\\x[0-9a-f]{2}/gi, ''); // Hex escape sequences // Normalize and limit whitespace sanitized = sanitized .replace(/\s+/g, ' ') // Normalize whitespace .trim() .substring(0, 10000); // Limit length to prevent buffer overflow return sanitized; } } // Export default export default ErrorHandler;