UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

687 lines (686 loc) 26.6 kB
/** * CFN Configuration Validator * Validates JSON configurations against the CFN schema with detailed error reporting * * @version 1.0.0 * @description Type-safe validation library with 95%+ accuracy */ /** * ConfigValidator: Main validation class * Provides schema validation, error reporting, and env var export functionality */ export class ConfigValidator { schema; initialized = true; constructor(schema){ if (schema) { this.schema = schema; } else { // Use the default schema embedded below this.schema = this.getDefaultSchema(); } } /** * Get default schema definition */ getDefaultSchema() { return { $schema: 'https://json-schema.org/draft/2020-12/schema', $id: 'https://claude-flow-novice.local/schemas/cfn-config-v1.json', title: 'CFN Configuration Schema v1.0', description: 'Canonical JSON schema for all Claude Flow Novice configuration files', version: '1.0.0', type: 'object' }; } /** * Main validation method using built-in validators */ validate(config) { const errors = []; const warnings = []; if (typeof config !== 'object' || config === null) { return { valid: false, errors: [ { field: 'root', message: 'Configuration must be a valid JSON object', path: '/', code: 'INVALID_TYPE' } ], warnings, configType: 'unknown' }; } const configObj = config; const configType = this.detectConfigType(configObj); // Validate based on detected type switch(configType){ case 'agent-whitelist': return this.validateAgentWhitelist(configObj); case 'mcp-servers': return this.validateMCPServers(configObj); case 'skill-requirements': return this.validateSkillRequirements(configObj); case 'runtime-contract': return this.validateRuntimeContract(configObj); case 'team': return this.validateTeamConfig(configObj); default: return { valid: false, errors: [ { field: 'root', message: 'Unknown configuration type. Must include agents, servers, tools, variables, or team', path: '/', code: 'UNKNOWN_CONFIG_TYPE' } ], warnings, configType: 'unknown' }; } } /** * Validate JSON string */ validateJSON(jsonString) { try { const config = JSON.parse(jsonString); return this.validate(config); } catch (error) { return { valid: false, errors: [ { field: 'json', message: `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`, path: 'root', code: 'JSON_PARSE_ERROR' } ], warnings: [], configType: 'unknown' }; } } /** * Validate Agent Whitelist Configuration */ validateAgentWhitelist(config) { const errors = []; const warnings = []; // Check version if (!config.version || typeof config.version !== 'string') { errors.push({ field: 'version', message: 'Missing required field: version (string)', path: '/version', code: 'MISSING_REQUIRED' }); } else if (!/^\d+\.\d+\.\d+$/.test(config.version)) { errors.push({ field: 'version', message: 'Invalid version format. Expected X.Y.Z format', path: '/version', value: config.version, code: 'INVALID_FORMAT' }); } // Check agents array if (!Array.isArray(config.agents)) { errors.push({ field: 'agents', message: 'Missing required field: agents (array)', path: '/agents', code: 'MISSING_REQUIRED' }); } else { for(let i = 0; i < config.agents.length; i++){ const agent = config.agents[i]; if (!agent.type || typeof agent.type !== 'string' || agent.type.length === 0) { errors.push({ field: `agents[${i}].type`, message: 'Agent type is required and must be non-empty string', path: `/agents/${i}/type`, code: 'INVALID_AGENT' }); } if (!agent.displayName || typeof agent.displayName !== 'string') { errors.push({ field: `agents[${i}].displayName`, message: 'Agent displayName is required', path: `/agents/${i}/displayName`, code: 'INVALID_AGENT' }); } if (!Array.isArray(agent.skills)) { errors.push({ field: `agents[${i}].skills`, message: 'Agent skills must be an array', path: `/agents/${i}/skills`, code: 'INVALID_AGENT' }); } } } if (!config.lastUpdated) { warnings.push('Agent configuration missing lastUpdated field (recommended)'); } return { valid: errors.length === 0, errors, warnings, configType: 'agent-whitelist' }; } /** * Validate MCP Servers Configuration */ validateMCPServers(config) { const errors = []; const warnings = []; // Check version if (!config.version || typeof config.version !== 'string') { errors.push({ field: 'version', message: 'Missing required field: version', path: '/version', code: 'MISSING_REQUIRED' }); } // Check servers object if (!config.servers || typeof config.servers !== 'object') { errors.push({ field: 'servers', message: 'Missing required field: servers (object)', path: '/servers', code: 'MISSING_REQUIRED' }); } else { const servers = config.servers; for(const serverName in servers){ if (Object.prototype.hasOwnProperty.call(servers, serverName)) { const serverConfig = servers[serverName]; if (!serverConfig.endpoint || typeof serverConfig.endpoint !== 'string') { errors.push({ field: `servers.${serverName}.endpoint`, message: 'Server endpoint is required', path: `/servers/${serverName}/endpoint`, code: 'INVALID_SERVER' }); } else if (!this.isValidURL(serverConfig.endpoint)) { errors.push({ field: `servers.${serverName}.endpoint`, message: 'Server endpoint must be a valid URL', path: `/servers/${serverName}/endpoint`, value: serverConfig.endpoint, code: 'INVALID_URL' }); } if (!Array.isArray(serverConfig.requiredSkills)) { errors.push({ field: `servers.${serverName}.requiredSkills`, message: 'Server requiredSkills must be an array', path: `/servers/${serverName}/requiredSkills`, code: 'INVALID_SERVER' }); } if (!serverConfig.auth || typeof serverConfig.auth !== 'object') { errors.push({ field: `servers.${serverName}.auth`, message: 'Server auth is required', path: `/servers/${serverName}/auth`, code: 'INVALID_SERVER' }); } if (serverConfig.timeoutMs !== undefined && typeof serverConfig.timeoutMs !== 'number') { errors.push({ field: `servers.${serverName}.timeoutMs`, message: 'timeoutMs must be a number', path: `/servers/${serverName}/timeoutMs`, code: 'INVALID_TYPE' }); } else if (serverConfig.timeoutMs !== undefined && serverConfig.timeoutMs < 1000) { errors.push({ field: `servers.${serverName}.timeoutMs`, message: 'timeoutMs must be at least 1000ms', path: `/servers/${serverName}/timeoutMs`, value: serverConfig.timeoutMs, code: 'CONSTRAINT_VIOLATION' }); } } } } return { valid: errors.length === 0, errors, warnings, configType: 'mcp-servers' }; } /** * Validate Skill Requirements Configuration */ validateSkillRequirements(config) { const errors = []; const warnings = []; if (!config.version || typeof config.version !== 'string') { errors.push({ field: 'version', message: 'Missing required field: version', path: '/version', code: 'MISSING_REQUIRED' }); } if (!config.tools || typeof config.tools !== 'object') { errors.push({ field: 'tools', message: 'Missing required field: tools (object)', path: '/tools', code: 'MISSING_REQUIRED' }); } else { const tools = config.tools; for(const toolName in tools){ if (Object.prototype.hasOwnProperty.call(tools, toolName)) { const toolConfig = tools[toolName]; if (!toolConfig.displayName || typeof toolConfig.displayName !== 'string') { errors.push({ field: `tools.${toolName}.displayName`, message: 'Tool displayName is required', path: `/tools/${toolName}/displayName`, code: 'INVALID_TOOL' }); } if (!Array.isArray(toolConfig.requiredSkills) || toolConfig.requiredSkills.length === 0) { errors.push({ field: `tools.${toolName}.requiredSkills`, message: 'Tool requiredSkills must be a non-empty array', path: `/tools/${toolName}/requiredSkills`, code: 'INVALID_TOOL' }); } } } } return { valid: errors.length === 0, errors, warnings, configType: 'skill-requirements' }; } /** * Validate Runtime Contract Configuration */ validateRuntimeContract(config) { const errors = []; const warnings = []; if (!config.version || typeof config.version !== 'string') { errors.push({ field: 'version', message: 'Missing required field: version', path: '/version', code: 'MISSING_REQUIRED' }); } if (config.variables && typeof config.variables === 'object') { const variables = config.variables; for(const varName in variables){ if (Object.prototype.hasOwnProperty.call(variables, varName)) { const varConfig = variables[varName]; if (!varConfig.description || typeof varConfig.description !== 'string') { errors.push({ field: `variables.${varName}.description`, message: 'Variable description is required', path: `/variables/${varName}/description`, code: 'INVALID_VARIABLE' }); } const varType = varConfig.type; const validTypes = [ 'string', 'integer', 'number', 'boolean' ]; if (!varConfig.type || validTypes.indexOf(varType) === -1) { errors.push({ field: `variables.${varName}.type`, message: 'Variable type must be one of: string, integer, number, boolean', path: `/variables/${varName}/type`, code: 'INVALID_VARIABLE' }); } } } } return { valid: errors.length === 0, errors, warnings, configType: 'runtime-contract' }; } /** * Validate Team Configuration */ validateTeamConfig(config) { const errors = []; const warnings = []; if (!config.team || typeof config.team !== 'object') { errors.push({ field: 'team', message: 'Missing required field: team (object)', path: '/team', code: 'MISSING_REQUIRED' }); return { valid: false, errors, warnings, configType: 'team' }; } const team = config.team; if (!team.id || typeof team.id !== 'string' || team.id.length === 0) { errors.push({ field: 'team.id', message: 'Team id is required and must be non-empty', path: '/team/id', code: 'INVALID_TEAM' }); } else if (!/^[a-z0-9-]+$/.test(team.id)) { errors.push({ field: 'team.id', message: 'Team id must contain only lowercase letters, numbers, and hyphens', path: '/team/id', value: team.id, code: 'INVALID_FORMAT' }); } if (!team.name || typeof team.name !== 'string') { errors.push({ field: 'team.name', message: 'Team name is required', path: '/team/name', code: 'INVALID_TEAM' }); } // Validate workspace if present (supports both diskQuota and disk_quota) if (team.workspace && typeof team.workspace === 'object') { const workspace = team.workspace; const diskQuota = this.getPropertyValue(workspace, 'diskQuota'); if (diskQuota !== undefined && !this.isValidDiskQuota(diskQuota)) { errors.push({ field: 'team.workspace.diskQuota', message: 'Invalid disk quota format. Expected format: <number><UNIT> (e.g., 100GB)', path: '/team/workspace/diskQuota', value: diskQuota, code: 'INVALID_FORMAT' }); } } // Validate resources if present (supports both camelCase and snake_case naming) if (team.resources && typeof team.resources === 'object') { const resources = team.resources; const cpuCores = this.getPropertyValue(resources, 'cpuCores'); const maxAgents = this.getPropertyValue(resources, 'maxAgents'); // Validate cpuCores (supports cpuCores and cpu_cores) if (cpuCores !== undefined) { if (typeof cpuCores !== 'number' || cpuCores < 0) { errors.push({ field: 'team.resources.cpuCores', message: 'cpuCores must be a non-negative number', path: '/team/resources/cpuCores', value: cpuCores, code: 'INVALID_TYPE' }); } } // Validate maxAgents (supports maxAgents and max_agents) if (maxAgents !== undefined) { if (typeof maxAgents !== 'number' || maxAgents < 1 || !Number.isInteger(maxAgents)) { errors.push({ field: 'team.resources.maxAgents', message: 'maxAgents must be a positive integer', path: '/team/resources/maxAgents', value: maxAgents, code: 'INVALID_TYPE' }); } } } // Validate network if present if (team.network && typeof team.network === 'object') { const network = team.network; if (network.coordinatorIp && !this.isValidIPv4(network.coordinatorIp)) { errors.push({ field: 'team.network.coordinatorIp', message: 'Invalid IPv4 format', path: '/team/network/coordinatorIp', value: network.coordinatorIp, code: 'INVALID_FORMAT' }); } } return { valid: errors.length === 0, errors, warnings, configType: 'team' }; } /** * Detect configuration type */ detectConfigType(config) { // Check for agent whitelist (has 'agents' array) if ('agents' in config && Array.isArray(config.agents)) { return 'agent-whitelist'; } // Check for MCP servers (has 'servers' object) if ('servers' in config && typeof config.servers === 'object') { return 'mcp-servers'; } // Check for skill requirements (has 'tools' object) if ('tools' in config && typeof config.tools === 'object') { return 'skill-requirements'; } // Check for runtime contract (has 'variables' object) if ('variables' in config && typeof config.variables === 'object') { return 'runtime-contract'; } // Check for team config (has 'team' object) if ('team' in config && typeof config.team === 'object') { return 'team'; } return 'unknown'; } /** * Export configuration as environment variables * Handles type preservation and validation */ exportEnvVars(config) { if (typeof config !== 'object' || config === null) { throw new Error('Configuration must be an object'); } const configObj = config; if (!('variables' in configObj)) { throw new Error('Configuration is not a valid runtime contract'); } const envMap = {}; const variables = configObj.variables; if (typeof variables !== 'object' || variables === null) { throw new Error('Variables must be an object'); } const varsObj = variables; for(const key in varsObj){ if (Object.prototype.hasOwnProperty.call(varsObj, key)) { const variable = varsObj[key]; const varObj = variable; if (varObj.value !== null && varObj.value !== undefined) { const varType = varObj.type; envMap[key] = this.coerceToCorrectType(varObj.value, varType); } } } return envMap; } /** * Coerce value to correct type without loss */ coerceToCorrectType(value, type) { if (type === 'integer' && typeof value === 'string') { return parseInt(value, 10); } if (type === 'number' && typeof value === 'string') { return parseFloat(value); } if (type === 'boolean' && typeof value === 'string') { return value.toLowerCase() === 'true'; } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return value; } return String(value); } /** * Format validation errors for display */ formatErrors(result) { if (result.valid) { return 'Configuration is valid.'; } let output = `Validation failed with ${result.errors.length} error(s):\n\n`; for (const error of result.errors){ output += `[${error.code}] ${error.field || 'root'}\n`; output += ` ${error.message}\n`; if (error.value !== undefined) { output += ` Current value: ${JSON.stringify(error.value)}\n`; } output += '\n'; } if (result.warnings.length > 0) { output += `\nWarnings (${result.warnings.length}):\n`; for (const warning of result.warnings){ output += ` - ${warning}\n`; } } return output; } /** * Helper: Validate URL format */ isValidURL(url) { if (typeof url !== 'string') return false; try { new URL(url); return true; } catch { return false; } } /** * Helper: Validate disk quota format */ isValidDiskQuota(quota) { if (typeof quota !== 'string') return false; return /^\d+[KMGTPE]B$/.test(quota); } /** * Helper: Validate IPv4 format */ isValidIPv4(ip) { if (typeof ip !== 'string') return false; const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; if (!ipv4Pattern.test(ip)) return false; const parts = ip.split('.'); return parts.every((part)=>{ const num = parseInt(part, 10); return num >= 0 && num <= 255; }); } /** * Helper: Normalize field name (support both snake_case and camelCase) * Examples: disk_quota → diskQuota, cpu_cores → cpuCores, maxAgents → maxAgents */ normalizeFieldName(name) { if (!name.includes('_')) return name; return name.replace(/_([a-z])/g, (_, letter)=>letter.toUpperCase()); } /** * Helper: Get property value supporting both naming conventions * @param obj Object to search * @param field Field name in any convention (camelCase or snake_case) * @returns Value if found, undefined otherwise */ getPropertyValue(obj, field) { // Try camelCase version first if (field in obj) return obj[field]; // Try snake_case version const snakeCase = field.replace(/[A-Z]/g, (letter)=>`_${letter.toLowerCase()}`); if (snakeCase in obj) return obj[snakeCase]; // Try normalizing if it's snake_case input const camelCase = this.normalizeFieldName(field); if (camelCase in obj) return obj[camelCase]; return undefined; } } /** * Singleton instance for global usage */ let validatorInstance = null; /** * Get or create validator instance */ export function getValidator(schema) { if (!validatorInstance) { validatorInstance = new ConfigValidator(schema); } return validatorInstance; } /** * Validate configuration object */ export function validateConfig(config) { return getValidator().validate(config); } /** * Validate JSON string */ export function validateJSON(jsonString) { return getValidator().validateJSON(jsonString); } /** * Export environment variables from runtime contract */ export function exportEnvVars(config) { return getValidator().exportEnvVars(config); } /** * Check if configuration is valid (boolean shortcut) */ export function isValidConfig(config) { return getValidator().validate(config).valid; } /** * Reset validator instance (useful for testing) */ export function resetValidator() { validatorInstance = null; } /** * Validate multiple configuration files efficiently * @param filePaths Array of file paths to validate * @returns Map of file path to validation result * @throws Error if file cannot be read */ export function validateConfigFiles(filePaths) { const results = {}; const validator = getValidator(); for (const filePath of filePaths){ try { // Dynamically import fs module to read file const fs = require('fs'); const content = fs.readFileSync(filePath, 'utf-8'); const config = JSON.parse(content); results[filePath] = validator.validate(config); } catch (error) { results[filePath] = { valid: false, errors: [ { field: 'file', message: `Failed to read or parse file: ${error instanceof Error ? error.message : String(error)}`, path: filePath, code: 'FILE_READ_ERROR' } ], warnings: [], configType: 'unknown' }; } } return results; } export default ConfigValidator; //# sourceMappingURL=config-validator.js.map