UNPKG

bc-code-intelligence-mcp

Version:

BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows

608 lines 23.4 kB
/** * Configuration Validator * * Comprehensive validation for BCKB configuration with detailed error messages, * source accessibility checking, and security validation. */ import { access, stat, constants } from 'fs/promises'; import { resolve, join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { homedir } from 'os'; import { LayerSourceType, AuthType } from '../types/index.js'; export class ConfigurationValidator { /** * Validate complete BCKB configuration */ async validate(config) { const errors = []; const warnings = []; console.log('🔍 Validating BCKB configuration...'); // 1. Basic structure validation this.validateBasicStructure(config, errors); // 2. Layer validation await this.validateLayers(config.layers, errors, warnings); // 3. Performance settings validation this.validatePerformanceSettings(config, errors, warnings); // 4. Security settings validation this.validateSecuritySettings(config, errors, warnings); // 5. Cache settings validation this.validateCacheSettings(config, errors, warnings); // 6. Check for potential conflicts this.checkLayerConflicts(config.layers, errors, warnings); // 7. Generate quality score const score = this.calculateQualityScore(config, errors.length, warnings.length); const result = { valid: errors.length === 0, errors, warnings, score }; console.log(`${result.valid ? '✅' : '❌'} Configuration validation complete`); console.log(` Errors: ${errors.length}, Warnings: ${warnings.length}, Score: ${score}/100`); return result; } /** * Validate basic configuration structure */ validateBasicStructure(config, errors) { if (!config.layers || !Array.isArray(config.layers)) { errors.push({ field: 'layers', message: 'Configuration must have a layers array' }); return; } if (config.layers.length === 0) { errors.push({ field: 'layers', message: 'At least one layer must be configured' }); } // Check for required embedded layer const hasEmbeddedLayer = config.layers.some(layer => layer.source.type === LayerSourceType.EMBEDDED); if (!hasEmbeddedLayer) { errors.push({ field: 'layers', message: 'Configuration must include at least one embedded layer', suggestion: 'Add an embedded layer as the base knowledge source' }); } } /** * Validate all layers configuration */ async validateLayers(layers, errors, warnings) { const layerNames = new Set(); const layerPriorities = new Set(); for (const [index, layer] of layers.entries()) { const fieldPrefix = `layers[${index}]`; // Validate layer name uniqueness if (layerNames.has(layer.name)) { errors.push({ field: `${fieldPrefix}.name`, message: `Duplicate layer name: ${layer.name}`, value: layer.name }); } layerNames.add(layer.name); // Check for priority conflicts if (layerPriorities.has(layer.priority)) { warnings.push({ type: 'invalid_value', message: `Multiple layers have priority ${layer.priority}`, suggestion: 'Use unique priorities to ensure predictable layer ordering' }); } layerPriorities.add(layer.priority); // Validate layer name format if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(layer.name)) { errors.push({ field: `${fieldPrefix}.name`, message: 'Layer name must start with letter and contain only letters, numbers, underscores, and hyphens', value: layer.name }); } // Validate priority range if (layer.priority < 0 || layer.priority > 1000) { errors.push({ field: `${fieldPrefix}.priority`, message: 'Layer priority must be between 0 and 1000', value: layer.priority }); } // Validate source configuration await this.validateLayerSource(layer, `${fieldPrefix}.source`, errors, warnings); // Validate authentication if (layer.auth) { this.validateAuthentication(layer.auth, `${fieldPrefix}.auth`, errors, warnings); } // Validate cache duration format if (layer.cache_duration) { this.validateCacheDuration(layer.cache_duration, `${fieldPrefix}.cache_duration`, errors); } // Validate patterns if (layer.patterns) { this.validatePatterns(layer.patterns, `${fieldPrefix}.patterns`, warnings); } } } /** * Validate layer source configuration */ async validateLayerSource(layer, fieldPrefix, errors, warnings) { const source = layer.source; if (!source.type) { errors.push({ field: `${fieldPrefix}.type`, message: 'Layer source type is required' }); return; } switch (source.type) { case LayerSourceType.GIT: await this.validateGitSource(source, fieldPrefix, errors, warnings); break; case LayerSourceType.LOCAL: await this.validateLocalSource(source, fieldPrefix, errors, warnings); break; case LayerSourceType.EMBEDDED: await this.validateEmbeddedSource(source, fieldPrefix, errors, warnings); break; case LayerSourceType.HTTP: this.validateHttpSource(source, fieldPrefix, errors, warnings); break; case LayerSourceType.NPM: this.validateNpmSource(source, fieldPrefix, errors, warnings); break; default: errors.push({ field: `${fieldPrefix}.type`, message: `Unsupported layer source type: ${source.type}`, value: source.type }); } } /** * Validate Git source configuration */ async validateGitSource(source, fieldPrefix, errors, warnings) { if (!source.url) { errors.push({ field: `${fieldPrefix}.url`, message: 'Git source requires URL' }); return; } // Validate URL format try { const url = new URL(source.url); // Check for common Git hosting platforms const hostname = url.hostname.toLowerCase(); if (!['github.com', 'gitlab.com', 'bitbucket.org'].includes(hostname) && !hostname.includes('gitlab') && !hostname.includes('git')) { warnings.push({ type: 'invalid_value', message: `Unusual git hostname: ${hostname}`, suggestion: 'Ensure this is a valid git repository host' }); } // Warn about HTTP instead of HTTPS if (url.protocol === 'http:') { warnings.push({ type: 'security', message: 'Git URL uses HTTP instead of HTTPS', suggestion: 'Use HTTPS for better security' }); } } catch (error) { errors.push({ field: `${fieldPrefix}.url`, message: 'Invalid URL format', value: source.url }); } // Validate branch name if provided if (source.branch && !/^[a-zA-Z0-9/_.-]+$/.test(source.branch)) { warnings.push({ type: 'invalid_value', message: `Unusual branch name: ${source.branch}`, suggestion: 'Ensure branch name exists in the repository' }); } // Validate subpath if provided if (source.subpath) { if (source.subpath.startsWith('/') || source.subpath.includes('..')) { warnings.push({ type: 'security', message: `Potentially unsafe subpath: ${source.subpath}`, suggestion: 'Use relative paths without .. navigation' }); } } } /** * Validate local source configuration */ async validateLocalSource(source, fieldPrefix, errors, warnings) { if (!source.path) { errors.push({ field: `${fieldPrefix}.path`, message: 'Local source requires path' }); return; } // Resolve path let resolvedPath; try { resolvedPath = source.path.startsWith('~') ? source.path.replace('~', homedir()) : resolve(source.path); } catch (error) { errors.push({ field: `${fieldPrefix}.path`, message: 'Invalid path format', value: source.path }); return; } // Check if path exists and is accessible try { await access(resolvedPath, constants.R_OK); const stats = await stat(resolvedPath); if (!stats.isDirectory()) { errors.push({ field: `${fieldPrefix}.path`, message: 'Local source path must be a directory', value: source.path }); } } catch (error) { warnings.push({ type: 'invalid_value', message: `Local path not accessible: ${source.path}`, suggestion: 'Ensure directory exists and is readable' }); } } /** * Validate embedded source configuration */ async validateEmbeddedSource(source, fieldPrefix, errors, warnings) { // ES module: __dirname equivalent const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const embeddedPath = source.path || join(__dirname, '../embedded-knowledge'); try { await access(embeddedPath, constants.R_OK); const stats = await stat(embeddedPath); if (!stats.isDirectory()) { errors.push({ field: `${fieldPrefix}.path`, message: 'Embedded source path must be a directory', value: embeddedPath }); } } catch (error) { errors.push({ field: `${fieldPrefix}.path`, message: `Embedded knowledge directory not found: ${embeddedPath}`, suggestion: 'Ensure embedded knowledge is available' }); } } /** * Validate HTTP source configuration */ validateHttpSource(source, fieldPrefix, errors, warnings) { if (!source.url) { errors.push({ field: `${fieldPrefix}.url`, message: 'HTTP source requires URL' }); return Promise.resolve(); } try { const url = new URL(source.url); if (url.protocol !== 'https:') { warnings.push({ type: 'security', message: 'HTTP source should use HTTPS', suggestion: 'Use HTTPS for secure content delivery' }); } } catch (error) { errors.push({ field: `${fieldPrefix}.url`, message: 'Invalid HTTP URL format', value: source.url }); } // Note: HTTP layers are not implemented yet warnings.push({ type: 'deprecated', message: 'HTTP layers are not yet implemented', suggestion: 'Use git or local layers instead' }); return Promise.resolve(); } /** * Validate NPM source configuration */ validateNpmSource(source, fieldPrefix, errors, warnings) { if (!source.package) { errors.push({ field: `${fieldPrefix}.package`, message: 'NPM source requires package name' }); return; } // Validate NPM package name format if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(source.package)) { errors.push({ field: `${fieldPrefix}.package`, message: 'Invalid NPM package name format', value: source.package }); } // Note: NPM layers are not implemented yet warnings.push({ type: 'deprecated', message: 'NPM layers are not yet implemented', suggestion: 'Use git or local layers instead' }); } /** * Validate authentication configuration */ validateAuthentication(auth, fieldPrefix, errors, warnings) { if (!auth.type) { errors.push({ field: `${fieldPrefix}.type`, message: 'Authentication type is required' }); return; } switch (auth.type) { case AuthType.TOKEN: if (auth.token && !auth.token.startsWith('$')) { warnings.push({ type: 'security', message: 'Token appears to be hardcoded', suggestion: 'Use environment variable (token_env_var) instead' }); } if (!auth.token && !auth.token_env_var) { errors.push({ field: `${fieldPrefix}`, message: 'Token authentication requires either token or token_env_var' }); } break; case AuthType.SSH_KEY: if (auth.key_path && !auth.key_path.includes('~') && !auth.key_path.startsWith('/')) { warnings.push({ type: 'invalid_value', message: 'SSH key path should be absolute or use ~ notation', suggestion: 'Use ~/.ssh/id_rsa format' }); } break; case AuthType.BASIC: if (!auth.username) { errors.push({ field: `${fieldPrefix}.username`, message: 'Basic authentication requires username' }); } if (auth.password && !auth.password.startsWith('$')) { warnings.push({ type: 'security', message: 'Password appears to be hardcoded', suggestion: 'Use environment variable (password_env_var) instead' }); } if (!auth.password && !auth.password_env_var) { errors.push({ field: `${fieldPrefix}`, message: 'Basic authentication requires either password or password_env_var' }); } break; case AuthType.OAUTH: warnings.push({ type: 'deprecated', message: 'OAuth authentication is not yet fully implemented', suggestion: 'Use token or SSH key authentication instead' }); break; default: errors.push({ field: `${fieldPrefix}.type`, message: `Unsupported authentication type: ${auth.type}`, value: auth.type }); } } /** * Validate cache duration format */ validateCacheDuration(duration, fieldPrefix, errors) { const validPattern = /^((\d+)(ms|s|m|h|d|w)|permanent|immediate)$/; if (!validPattern.test(duration)) { errors.push({ field: fieldPrefix, message: 'Invalid cache duration format', value: duration, suggestion: 'Use format like "1h", "30m", "permanent", or "immediate"' }); } } /** * Validate file patterns */ validatePatterns(patterns, fieldPrefix, warnings) { for (const pattern of patterns) { // Check for potentially problematic patterns if (pattern.includes('**/**/**')) { warnings.push({ type: 'invalid_value', message: `Potentially inefficient pattern: ${pattern}`, suggestion: 'Simplify glob patterns for better performance' }); } if (pattern.startsWith('/') || pattern.includes('../')) { warnings.push({ type: 'security', message: `Potentially unsafe pattern: ${pattern}`, suggestion: 'Use relative patterns within the layer' }); } } } /** * Validate performance settings */ validatePerformanceSettings(config, errors, warnings) { const perf = config.performance; if (perf.max_concurrent_loads > 20) { warnings.push({ type: 'invalid_value', message: 'High concurrent load limit may cause resource exhaustion', suggestion: 'Consider reducing performance.max_concurrent_loads' }); } if (perf.load_timeout_ms < 5000) { warnings.push({ type: 'invalid_value', message: 'Very short load timeout may cause failures', suggestion: 'Consider increasing performance.load_timeout_ms' }); } if (perf.memory_limit_mb < 100) { warnings.push({ type: 'invalid_value', message: 'Low memory limit may impact functionality', suggestion: 'Consider increasing performance.memory_limit_mb' }); } if (perf.max_layers > 50) { warnings.push({ type: 'invalid_value', message: 'Very high layer limit may impact performance', suggestion: 'Consider reducing performance.max_layers' }); } } /** * Validate security settings */ validateSecuritySettings(config, errors, warnings) { const security = config.security; if (security.allow_http_sources && security.trusted_domains.length === 0) { warnings.push({ type: 'security', message: 'HTTP sources allowed without trusted domains', suggestion: 'Specify trusted_domains when allowing HTTP sources' }); } if (!security.validate_sources) { warnings.push({ type: 'security', message: 'Source validation is disabled', suggestion: 'Enable validate_sources for better security' }); } if (security.max_download_size_mb > 500) { warnings.push({ type: 'security', message: 'High download size limit may be risky', suggestion: 'Consider reducing max_download_size_mb' }); } } /** * Validate cache settings */ validateCacheSettings(config, errors, warnings) { const cache = config.cache; if (cache.max_size_mb < 10) { warnings.push({ type: 'invalid_value', message: 'Very low cache size may impact performance', suggestion: 'Consider increasing cache.max_size_mb' }); } // Validate TTL formats for (const [type, ttl] of Object.entries(cache.ttl)) { this.validateCacheDuration(ttl, `cache.ttl.${type}`, errors); } } /** * Check for layer configuration conflicts */ checkLayerConflicts(layers, errors, warnings) { // Check for conflicting git URLs const gitUrls = new Set(); const gitLayers = layers.filter(layer => layer.source.type === LayerSourceType.GIT); for (const layer of gitLayers) { const source = layer.source; if (source.url) { if (gitUrls.has(source.url)) { warnings.push({ type: 'invalid_value', message: `Multiple layers use same git URL: ${source.url}`, suggestion: 'Use different subpaths or branches to differentiate layers' }); } gitUrls.add(source.url); } } // Check for conflicting local paths const localPaths = new Set(); const localLayers = layers.filter(layer => layer.source.type === LayerSourceType.LOCAL); for (const layer of localLayers) { const source = layer.source; if (source.path) { const resolvedPath = resolve(source.path); if (localPaths.has(resolvedPath)) { warnings.push({ type: 'invalid_value', message: `Multiple layers use same local path: ${source.path}`, suggestion: 'Use different paths for each local layer' }); } localPaths.add(resolvedPath); } } } /** * Calculate configuration quality score */ calculateQualityScore(config, errorCount, warningCount) { let score = 100; // Deduct points for errors (major issues) score -= errorCount * 20; // Deduct points for warnings (minor issues) score -= warningCount * 5; // Bonus points for good practices const hasGitLayer = config.layers.some(l => l.source.type === LayerSourceType.GIT); const hasLocalLayer = config.layers.some(l => l.source.type === LayerSourceType.LOCAL); const hasCacheConfig = config.cache.strategy !== 'none'; const hasSecurityValidation = config.security.validate_sources; if (hasGitLayer) score += 5; // Using git layers if (hasLocalLayer) score += 5; // Using local overrides if (hasCacheConfig) score += 5; // Has caching enabled if (hasSecurityValidation) score += 5; // Has security validation return Math.max(0, Math.min(100, score)); } } //# sourceMappingURL=config-validator.js.map