claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.
687 lines (686 loc) • 26.6 kB
JavaScript
/**
* 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