UNPKG

@sethdouglasford/claude-flow

Version:

Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology

1,392 lines 50.2 kB
/** * Enterprise Configuration Management for Claude-Flow * Features: Security masking, change tracking, multi-format support, credential management */ import { promises as fs } from "fs"; import { join } from "path"; import { homedir } from "os"; import { randomBytes, createCipheriv, createDecipheriv, scrypt } from "crypto"; import { deepMerge, safeParseJSON } from "../utils/helpers.js"; import { ConfigError, ValidationError } from "../utils/errors.js"; import { promisify } from "util"; import { existsSync } from "fs"; const scryptAsync = promisify(scrypt); /** * Security classifications for configuration paths */ const SECURITY_CLASSIFICATIONS = { "credentials": { level: "secret", encrypted: true }, "credentials.apiKey": { level: "secret", maskPattern: "****...****", encrypted: true }, "credentials.token": { level: "secret", maskPattern: "****...****", encrypted: true }, "credentials.password": { level: "secret", maskPattern: "********", encrypted: true }, "mcp.apiKey": { level: "confidential", maskPattern: "****...****" }, "logging.destination": { level: "internal" }, "orchestrator": { level: "internal" }, "terminal": { level: "public" }, }; /** * Sensitive configuration paths that should be masked in output */ const SENSITIVE_PATHS = [ "credentials", "apiKey", "token", "password", "secret", "key", "auth", ]; /** * Format parsers for different configuration file types */ const FORMAT_PARSERS = { json: { parse: JSON.parse, stringify: (obj) => JSON.stringify(obj, null, 2), extension: ".json", }, yaml: { parse: (content) => { // Simple YAML parser for basic key-value pairs const lines = content.split("\n"); const result = {}; const current = result; const stack = [result]; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const indent = line.length - line.trimStart().length; const colonIndex = trimmed.indexOf(":"); if (colonIndex === -1) continue; const key = trimmed.substring(0, colonIndex).trim(); const value = trimmed.substring(colonIndex + 1).trim(); // Simple value parsing let parsedValue = value; if (value === "true") parsedValue = true; else if (value === "false") parsedValue = false; else if (!isNaN(Number(value)) && value !== "") parsedValue = Number(value); else if (value.startsWith("\"") && value.endsWith("\"")) { parsedValue = value.slice(1, -1); } current[key] = parsedValue; } return result; }, stringify: (obj) => { const stringify = (obj, indent = 0) => { const spaces = " ".repeat(indent); let result = ""; for (const [key, value] of Object.entries(obj)) { if (typeof value === "object" && value !== null && !Array.isArray(value)) { result += `${spaces}${key}:\n${stringify(value, indent + 1)}`; } else { const formattedValue = typeof value === "string" ? `"${value}"` : String(value); result += `${spaces}${key}: ${formattedValue}\n`; } } return result; }; return stringify(obj); }, extension: ".yaml", }, toml: { parse: (content) => { // Simple TOML parser for basic sections and key-value pairs const lines = content.split("\n"); const result = {}; let currentSection = result; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; // Section header if (trimmed.startsWith("[") && trimmed.endsWith("]")) { const sectionName = trimmed.slice(1, -1); currentSection = result[sectionName] = {}; continue; } // Key-value pair const equalsIndex = trimmed.indexOf("="); if (equalsIndex === -1) continue; const key = trimmed.substring(0, equalsIndex).trim(); const value = trimmed.substring(equalsIndex + 1).trim(); // Simple value parsing let parsedValue = value; if (value === "true") parsedValue = true; else if (value === "false") parsedValue = false; else if (!isNaN(Number(value)) && value !== "") parsedValue = Number(value); else if (value.startsWith("\"") && value.endsWith("\"")) { parsedValue = value.slice(1, -1); } currentSection[key] = parsedValue; } return result; }, stringify: (obj) => { let result = ""; for (const [section, values] of Object.entries(obj)) { if (typeof values === "object" && values !== null && !Array.isArray(values)) { result += `[${section}]\n`; for (const [key, value] of Object.entries(values)) { const formattedValue = typeof value === "string" ? `"${value}"` : String(value); result += `${key} = ${formattedValue}\n`; } result += "\n"; } } return result; }, extension: ".toml", }, }; /** * Default configuration values */ const DEFAULT_CONFIG = { orchestrator: { maxConcurrentAgents: 10, taskQueueSize: 100, healthCheckInterval: 30000, // 30 seconds shutdownTimeout: 30000, // 30 seconds }, terminal: { type: "auto", poolSize: 5, recycleAfter: 10, // recycle after 10 uses healthCheckInterval: 60000, // 1 minute commandTimeout: 300000, // 5 minutes }, memory: { backend: "hybrid", cacheSizeMB: 100, syncInterval: 5000, // 5 seconds conflictResolution: "crdt", retentionDays: 30, }, coordination: { maxRetries: 3, retryDelay: 1000, // 1 second deadlockDetection: true, resourceTimeout: 60000, // 1 minute messageTimeout: 30000, // 30 seconds }, mcp: { transport: "stdio", port: 3000, tlsEnabled: false, }, logging: { level: "info", format: "json", destination: "console", }, credentials: { // Encrypted credentials storage }, security: { encryptionEnabled: true, auditLogging: true, maskSensitiveValues: true, allowEnvironmentOverrides: true, }, }; /** * Configuration manager */ export class ConfigManager { static instance; config; configPath; profiles = new Map(); currentProfile; userConfigDir; changeHistory = []; encryptionKey; validationRules = new Map(); formatParsers = FORMAT_PARSERS; constructor() { this.config = deepClone(DEFAULT_CONFIG); this.userConfigDir = this.getUserConfigDir(); this.initializeEncryption(); this.setupValidationRules(); } /** * Gets the singleton instance */ static getInstance() { if (!ConfigManager.instance) { ConfigManager.instance = new ConfigManager(); } return ConfigManager.instance; } /** * Initializes encryption key for sensitive configuration values */ initializeEncryption() { try { const keyFile = join(this.userConfigDir, ".encryption-key"); // Check if key file exists using ES module import if (existsSync(keyFile)) { // In a real implementation, this would be more secure this.encryptionKey = randomBytes(32); } else { this.encryptionKey = randomBytes(32); // Store key securely (in production, use proper key management) } } catch (error) { console.warn("Failed to initialize encryption:", error.message); } } /** * Sets up validation rules for configuration paths */ setupValidationRules() { // Orchestrator validation rules this.validationRules.set("orchestrator.maxConcurrentAgents", { type: "number", required: true, min: 1, max: 100, validator: (value, config) => { const numValue = value; if (numValue > (config.terminal?.poolSize ?? 1) * 2) { return "maxConcurrentAgents should not exceed 2x terminal pool size"; } return null; }, }); this.validationRules.set("orchestrator.taskQueueSize", { type: "number", required: true, min: 1, max: 10000, dependencies: ["orchestrator.maxConcurrentAgents"], validator: (value, config) => { const numValue = value; const maxAgents = config.orchestrator?.maxConcurrentAgents ?? 1; if (numValue < maxAgents * 10) { return "taskQueueSize should be at least 10x maxConcurrentAgents"; } return null; }, }); // Terminal validation rules this.validationRules.set("terminal.type", { type: "string", required: true, values: ["auto", "vscode", "native"], }); this.validationRules.set("terminal.poolSize", { type: "number", required: true, min: 1, max: 50, }); // Memory validation rules this.validationRules.set("memory.backend", { type: "string", required: true, values: ["sqlite", "markdown", "hybrid"], }); this.validationRules.set("memory.cacheSizeMB", { type: "number", required: true, min: 1, max: 10000, validator: (value) => { const numValue = value; if (numValue > 1000) { return "Large cache sizes may impact system performance"; } return null; }, }); // Security validation rules this.validationRules.set("security.encryptionEnabled", { type: "boolean", required: true, }); // Credentials validation this.validationRules.set("credentials.apiKey", { type: "string", pattern: /^[a-zA-Z0-9_-]+$/, validator: (value) => { const strValue = value; if (strValue && strValue.length < 16) { return "API key should be at least 16 characters long"; } return null; }, }); } /** * Loads configuration from file and environment */ async load(configPath) { try { let config = {}; // Load from file if provided if (configPath) { const fileConfig = await this.loadFromFile(configPath); config = this.mergeConfigs(config, fileConfig); } // Load from environment variables const envConfig = await this.loadFromEnv(); config = this.mergeConfigs(config, envConfig); // Merge with default configuration this.config = deepMergeConfig(DEFAULT_CONFIG, config); // Validate the final configuration this.validate(this.config); } catch (error) { throw new ConfigError(`Failed to load configuration: ${error.message}`); } } /** * Helper method to safely merge partial configs */ mergeConfigs(target, source) { return deepMerge(target, source); } /** * Gets the current configuration with optional security masking */ get(maskSensitive = false) { const config = deepClone(this.config); if (maskSensitive && this.config.security?.maskSensitiveValues) { return this.maskSensitiveValues(config); } return config; } /** * Gets configuration with security masking applied */ getSecure() { return this.get(true); } /** * Updates configuration values with change tracking */ update(updates, options = {}) { const oldConfig = deepClone(this.config); // Track changes before applying this.trackConfigChanges(oldConfig, updates, options); // Apply updates this.config = deepMergeConfig(this.config, updates); // Validate the updated configuration this.validateWithDependencies(this.config); return this.get(); } /** * Loads default configuration */ loadDefault() { this.config = deepClone(DEFAULT_CONFIG); } /** * Saves configuration to file with format support */ async save(path, format) { const savePath = path ?? this.configPath; if (!savePath) { throw new ConfigError("No configuration file path specified"); } const detectedFormat = format ?? this.detectFormat(savePath); const parser = this.formatParsers[detectedFormat]; if (!parser) { throw new ConfigError(`Unsupported format for saving: ${detectedFormat}`); } // Get configuration without sensitive values for saving const configToSave = this.getConfigForSaving(); const content = parser.stringify(configToSave); await fs.writeFile(savePath, content, "utf8"); // Record the save operation this.recordChange("CONFIG_SAVED", null, savePath, { source: "file" }); } /** * Gets configuration suitable for saving (excludes runtime-only values) */ getConfigForSaving() { const config = deepClone(this.config); // Remove encrypted credentials from the saved config // They should be stored separately in a secure location if (config.credentials) { delete config.credentials; } return config; } /** * Gets user configuration directory */ getUserConfigDir() { const home = homedir(); return join(home, ".claude-flow"); } /** * Creates user config directory if it doesn't exist */ async ensureUserConfigDir() { try { await fs.mkdir(this.userConfigDir, { recursive: true }); } catch (error) { if (error.code !== "EEXIST") { throw new ConfigError(`Failed to create config directory: ${error.message}`); } } } /** * Loads all profiles from the profiles directory */ async loadProfiles() { const profilesDir = join(this.userConfigDir, "profiles"); try { const entries = await fs.readdir(profilesDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith(".json")) { const profileName = entry.name.replace(".json", ""); const profilePath = join(profilesDir, entry.name); try { const content = await fs.readFile(profilePath, "utf8"); const profileConfig = safeParseJSON(content); if (profileConfig) { this.profiles.set(profileName, profileConfig); } } catch (error) { console.warn(`Failed to load profile ${profileName}: ${error.message}`); } } } } catch (error) { // Profiles directory doesn't exist - this is okay } } /** * Applies a named profile */ async applyProfile(profileName) { await this.loadProfiles(); const profile = this.profiles.get(profileName); if (!profile) { throw new ConfigError(`Profile "${profileName}" not found`); } this.config = deepMergeConfig(this.config, profile); this.currentProfile = profileName; this.validate(this.config); } /** * Saves current configuration as a profile */ async saveProfile(profileName, config) { await this.ensureUserConfigDir(); const profilesDir = join(this.userConfigDir, "profiles"); await fs.mkdir(profilesDir, { recursive: true }); const profileConfig = config ?? this.config; const profilePath = join(profilesDir, `${profileName}.json`); const content = JSON.stringify(profileConfig, null, 2); await fs.writeFile(profilePath, content, "utf8"); this.profiles.set(profileName, profileConfig); } /** * Deletes a profile */ async deleteProfile(profileName) { const profilePath = join(this.userConfigDir, "profiles", `${profileName}.json`); try { await fs.unlink(profilePath); this.profiles.delete(profileName); } catch (error) { if (error.code === "ENOENT") { throw new ConfigError(`Profile "${profileName}" not found`); } throw new ConfigError(`Failed to delete profile: ${error.message}`); } } /** * Lists all available profiles */ async listProfiles() { await this.loadProfiles(); return Array.from(this.profiles.keys()); } /** * Gets a specific profile configuration */ async getProfile(profileName) { await this.loadProfiles(); return this.profiles.get(profileName); } /** * Gets the current active profile name */ getCurrentProfile() { return this.currentProfile; } /** * Sets a configuration value by path with change tracking and validation */ set(path, value, options = {}) { const oldValue = this.getValue(path); // Record the change this.recordChange(path, oldValue, value, { user: options.user, reason: options.reason, source: options.source ?? "cli", }); // Encrypt sensitive values if (this.isSensitivePath(path) && this.config.security?.encryptionEnabled) { value = this.encryptValue(value); } const keys = path.split("."); let current = this.config; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!(key in current)) { current[key] = {}; } current = current[key]; } current[keys[keys.length - 1]] = value; // Validate the path-specific rule and dependencies this.validatePath(path, value, this.config); this.validateWithDependencies(this.config); } /** * Gets a configuration value by path with decryption for sensitive values */ getValue(path, decrypt = true) { const keys = path.split("."); let current = this.config; for (const key of keys) { if (current && typeof current === "object" && key in current) { current = current[key]; } else { return undefined; } } // Decrypt sensitive values if requested if (decrypt && this.isSensitivePath(path) && this.isEncryptedValue(current)) { try { return this.decryptValue(current); } catch (error) { console.warn(`Failed to decrypt value at path ${path}:`, error.message); return current; } } return current; } /** * Resets configuration to defaults */ reset() { this.config = deepClone(DEFAULT_CONFIG); delete this.currentProfile; } /** * Gets configuration schema for validation */ getSchema() { return { orchestrator: { maxConcurrentAgents: { type: "number", min: 1, max: 100 }, taskQueueSize: { type: "number", min: 1, max: 10000 }, healthCheckInterval: { type: "number", min: 1000, max: 300000 }, shutdownTimeout: { type: "number", min: 1000, max: 300000 }, }, terminal: { type: { type: "string", values: ["auto", "vscode", "native"] }, poolSize: { type: "number", min: 1, max: 50 }, recycleAfter: { type: "number", min: 1, max: 1000 }, healthCheckInterval: { type: "number", min: 1000, max: 3600000 }, commandTimeout: { type: "number", min: 1000, max: 3600000 }, }, memory: { backend: { type: "string", values: ["sqlite", "markdown", "hybrid"] }, cacheSizeMB: { type: "number", min: 1, max: 10000 }, syncInterval: { type: "number", min: 1000, max: 300000 }, conflictResolution: { type: "string", values: ["crdt", "timestamp", "manual"] }, retentionDays: { type: "number", min: 1, max: 3650 }, }, coordination: { maxRetries: { type: "number", min: 0, max: 100 }, retryDelay: { type: "number", min: 100, max: 60000 }, deadlockDetection: { type: "boolean" }, resourceTimeout: { type: "number", min: 1000, max: 3600000 }, messageTimeout: { type: "number", min: 1000, max: 300000 }, }, mcp: { transport: { type: "string", values: ["stdio", "http", "websocket"] }, port: { type: "number", min: 1, max: 65535 }, tlsEnabled: { type: "boolean" }, }, logging: { level: { type: "string", values: ["debug", "info", "warn", "error"] }, format: { type: "string", values: ["json", "text"] }, destination: { type: "string", values: ["console", "file"] }, }, }; } /** * Validates a value against schema */ validateValue(value, schema, path) { if (schema.type === "number") { if (typeof value !== "number" || isNaN(value)) { throw new ValidationError(`${path}: must be a number`); } if (schema.min !== undefined && value < schema.min) { throw new ValidationError(`${path}: must be at least ${schema.min}`); } if (schema.max !== undefined && value > schema.max) { throw new ValidationError(`${path}: must be at most ${schema.max}`); } } else if (schema.type === "string") { if (typeof value !== "string") { throw new ValidationError(`${path}: must be a string`); } if (schema.values && !schema.values.includes(value)) { throw new ValidationError(`${path}: must be one of [${schema.values.join(", ")}]`); } } else if (schema.type === "boolean") { if (typeof value !== "boolean") { throw new ValidationError(`${path}: must be a boolean`); } } } /** * Gets configuration diff between current and default */ getDiff() { const defaultConfig = DEFAULT_CONFIG; const diff = {}; const findDifferences = (current, defaults, path = "") => { for (const key in current) { const currentValue = current[key]; const defaultValue = defaults[key]; const fullPath = path ? `${path}.${key}` : key; if (typeof currentValue === "object" && currentValue !== null && !Array.isArray(currentValue)) { if (typeof defaultValue === "object" && defaultValue !== null) { const nestedDiff = {}; findDifferences(currentValue, defaultValue, fullPath); if (Object.keys(nestedDiff).length > 0) { if (!path) { diff[key] = nestedDiff; } } } } else if (currentValue !== defaultValue) { const pathParts = fullPath.split("."); let target = diff; for (let i = 0; i < pathParts.length - 1; i++) { if (!target[pathParts[i]]) { target[pathParts[i]] = {}; } target = target[pathParts[i]]; } target[pathParts[pathParts.length - 1]] = currentValue; } } }; findDifferences(this.config, defaultConfig); return diff; } /** * Exports configuration with metadata */ export() { return { version: "1.0.0", exported: new Date().toISOString(), profile: this.currentProfile, config: this.config, diff: this.getDiff(), }; } /** * Imports configuration from export */ import(data) { if (!data.config) { throw new ConfigError("Invalid configuration export format"); } this.validateWithDependencies(data.config); this.config = data.config; this.currentProfile = data.profile; // Record the import operation this.recordChange("CONFIG_IMPORTED", null, data.version || "unknown", { source: "file" }); } /** * Loads configuration from file with format detection */ async loadFromFile(path) { try { const content = await fs.readFile(path, "utf8"); const format = this.detectFormat(path, content); const parser = this.formatParsers[format]; if (!parser) { throw new ConfigError(`Unsupported configuration format: ${format}`); } const config = parser.parse(content); if (!config) { throw new ConfigError(`Invalid ${format.toUpperCase()} in configuration file: ${path}`); } return config; } catch (error) { if (error.code === "ENOENT") { // File doesn't exist, use defaults return {}; } throw new ConfigError(`Failed to load configuration from ${path}: ${error.message}`); } } /** * Detects configuration file format */ detectFormat(path, content) { const ext = path.split(".").pop()?.toLowerCase(); if (ext === "yaml" || ext === "yml") return "yaml"; if (ext === "toml") return "toml"; if (ext === "json") return "json"; // Try to detect from content if (content) { const trimmed = content.trim(); if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json"; if (trimmed.includes("=") && trimmed.includes("[")) return "toml"; if (trimmed.includes(":") && !trimmed.includes("=")) return "yaml"; } // Default to JSON return "json"; } /** * Loads configuration from environment variables */ async loadFromEnv() { const config = {}; // Orchestrator settings const maxAgents = process.env.CLAUDE_FLOW_MAX_AGENTS; if (maxAgents) { if (!config.orchestrator) { config.orchestrator = { maxConcurrentAgents: parseInt(maxAgents, 10), taskQueueSize: DEFAULT_CONFIG.orchestrator.taskQueueSize, healthCheckInterval: DEFAULT_CONFIG.orchestrator.healthCheckInterval, shutdownTimeout: DEFAULT_CONFIG.orchestrator.shutdownTimeout, }; } } // Terminal settings const terminalType = process.env.CLAUDE_FLOW_TERMINAL_TYPE; if (terminalType === "vscode" || terminalType === "native" || terminalType === "auto") { config.terminal = { ...DEFAULT_CONFIG.terminal, ...config.terminal, type: terminalType, }; } // Memory settings const memoryBackend = process.env.CLAUDE_FLOW_MEMORY_BACKEND; if (memoryBackend === "sqlite" || memoryBackend === "markdown" || memoryBackend === "hybrid") { config.memory = { ...DEFAULT_CONFIG.memory, ...config.memory, backend: memoryBackend, }; } // MCP settings const mcpTransport = process.env.CLAUDE_FLOW_MCP_TRANSPORT; if (mcpTransport === "stdio" || mcpTransport === "http" || mcpTransport === "websocket") { config.mcp = { ...DEFAULT_CONFIG.mcp, ...config.mcp, transport: mcpTransport, }; } const mcpPort = process.env.CLAUDE_FLOW_MCP_PORT; if (mcpPort) { config.mcp = { ...DEFAULT_CONFIG.mcp, ...config.mcp, port: parseInt(mcpPort, 10), }; } // Logging settings const logLevel = process.env.CLAUDE_FLOW_LOG_LEVEL; if (logLevel === "debug" || logLevel === "info" || logLevel === "warn" || logLevel === "error") { config.logging = { ...DEFAULT_CONFIG.logging, ...config.logging, level: logLevel, }; } // AWS Bedrock settings for Claude Code integration // Auto-detect AWS credentials and enable Bedrock if available await this.detectAndConfigureAWS(); // These environment variables are passed through to spawned Claude processes if (process.env.CLAUDE_CODE_USE_BEDROCK) { console.log("AWS Bedrock integration enabled for Claude Code", { region: process.env.AWS_REGION, model: process.env.ANTHROPIC_MODEL, smallFastModel: process.env.ANTHROPIC_SMALL_FAST_MODEL, }); } return config; } /** * Auto-detect AWS credentials and configure Bedrock integration */ async detectAndConfigureAWS() { // Skip if Bedrock is explicitly disabled if (process.env.CLAUDE_CODE_USE_BEDROCK === "false") { return; } // Skip if already explicitly configured if (process.env.CLAUDE_CODE_USE_BEDROCK === "true") { return; } try { // Check for AWS credentials in various sources const hasCredentials = await this.checkAWSCredentials(); if (hasCredentials) { console.log("🔍 AWS credentials detected - enabling Bedrock integration"); // Auto-configure Bedrock settings process.env.CLAUDE_CODE_USE_BEDROCK = "true"; // Set default region if not specified if (!process.env.AWS_REGION && !process.env.AWS_DEFAULT_REGION) { process.env.AWS_REGION = "us-east-1"; } // Set default Claude 4 models if not specified if (!process.env.ANTHROPIC_MODEL) { process.env.ANTHROPIC_MODEL = "anthropic.claude-opus-4-20250514-v1:0"; } if (!process.env.ANTHROPIC_SMALL_FAST_MODEL) { process.env.ANTHROPIC_SMALL_FAST_MODEL = "anthropic.claude-sonnet-4-20250514-v1:0"; } console.log("✅ Auto-configured AWS Bedrock with Claude 4 models"); } } catch (error) { // Silently fail - don't break the application if AWS detection fails console.debug("AWS credential detection failed:", error); } } /** * Check if AWS credentials are available from various sources */ async checkAWSCredentials() { // Check environment variables if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) { return true; } // Check AWS profile if (process.env.AWS_PROFILE) { return true; } // Check for AWS config/credentials files try { const os = await import("node:os"); const path = await import("node:path"); const fs = await import("node:fs"); const homeDir = os.homedir(); const awsConfigPath = path.join(homeDir, ".aws", "config"); const awsCredentialsPath = path.join(homeDir, ".aws", "credentials"); if (fs.existsSync(awsConfigPath) || fs.existsSync(awsCredentialsPath)) { return true; } } catch { // Ignore file system errors } // Check for IAM role (EC2 instance profile or ECS task role) try { const response = await fetch("http://169.254.169.254/latest/meta-data/iam/security-credentials/", { method: "GET", signal: AbortSignal.timeout(2000), // 2 second timeout }); if (response.ok) { return true; } } catch { // Ignore metadata service errors (not running on AWS or no role) } // Check if AWS CLI is configured and working try { const { spawn } = await import("node:child_process"); return new Promise((resolve) => { const child = spawn("aws", ["sts", "get-caller-identity", "--no-cli-pager"], { stdio: "ignore", }); child.on("exit", (code) => { resolve(code === 0); }); child.on("error", () => { resolve(false); }); // Timeout after 3 seconds setTimeout(() => { child.kill(); resolve(false); }, 3000); }); } catch { return false; } } /** * Validates configuration with dependency checking */ validateWithDependencies(config) { this.validate(config); // Check dependencies between configuration sections for (const [path, rule] of Array.from(this.validationRules.entries())) { if (rule.dependencies) { const value = this.getValueByPath(config, path); for (const depPath of rule.dependencies) { const depValue = this.getValueByPath(config, depPath); if (value && !depValue) { throw new ConfigError(`Configuration dependency not met: ${path} requires ${depPath}`); } } } } } /** * Validates a specific configuration path */ validatePath(path, value, config) { const rule = this.validationRules.get(path); if (!rule) return; // Type validation if (rule.type && typeof value !== rule.type) { throw new ConfigError(`Invalid type for ${path}: expected ${rule.type}, got ${typeof value}`); } // Required validation if (rule.required && (value === undefined || value === null)) { throw new ConfigError(`Required configuration missing: ${path}`); } // Range validation if (typeof value === "number") { if (rule.min !== undefined && value < rule.min) { throw new ConfigError(`Value for ${path} below minimum: ${value} < ${rule.min}`); } if (rule.max !== undefined && value > rule.max) { throw new ConfigError(`Value for ${path} above maximum: ${value} > ${rule.max}`); } } // Enum validation if (rule.values && !rule.values.includes(String(value))) { throw new ConfigError(`Invalid value for ${path}: ${value}. Valid values: ${rule.values.join(", ")}`); } // Pattern validation if (rule.pattern && typeof value === "string" && !rule.pattern.test(value)) { throw new ConfigError(`Invalid format for ${path}: ${value}`); } // Custom validation if (rule.validator && config) { const error = rule.validator(value, config); if (error) { throw new ConfigError(`Validation failed for ${path}: ${error}`); } } } /** * Gets a value from a configuration object by path */ getValueByPath(obj, path) { const keys = path.split("."); let current = obj; for (const key of keys) { if (current && typeof current === "object" && key in current) { current = current[key]; } else { return undefined; } } return current; } /** * Legacy validate method for backward compatibility */ validate(config) { this.validateWithDependencies(config); } /** * Gets available configuration templates */ getAvailableTemplates() { return ["minimal", "default", "development", "production"]; } /** * Creates a configuration from a template */ createTemplate(templateName) { const templates = { minimal: { orchestrator: { maxConcurrentAgents: 5, taskQueueSize: 50, healthCheckInterval: 60000, shutdownTimeout: 10000, }, terminal: { type: "auto", poolSize: 2, recycleAfter: 5, healthCheckInterval: 120000, commandTimeout: 300000, }, }, development: { logging: { level: "debug", format: "text", destination: "console", }, orchestrator: { maxConcurrentAgents: 20, taskQueueSize: 200, healthCheckInterval: 15000, shutdownTimeout: 5000, }, }, production: { logging: { level: "warn", format: "json", destination: "file", }, memory: { backend: "markdown", cacheSizeMB: 500, syncInterval: 10000, conflictResolution: "last-write", retentionDays: 90, }, }, }; const template = templates[templateName]; if (!template) { throw new ConfigError(`Unknown template: ${templateName}`); } return deepMerge(DEFAULT_CONFIG, template); } /** * Gets format parsers for different config file formats */ getFormatParsers() { return FORMAT_PARSERS; } /** * Validates a configuration file */ async validateFile(configPath) { const errors = []; try { const content = await fs.readFile(configPath, "utf8"); const config = JSON.parse(content); // Run validation try { this.validate(config); } catch (error) { errors.push(error.message); } // Additional validations if (!config.orchestrator) { errors.push("Missing required section: orchestrator"); } if (!config.terminal) { errors.push("Missing required section: terminal"); } if (!config.memory) { errors.push("Missing required section: memory"); } return { valid: errors.length === 0, errors, }; } catch (error) { errors.push(`Failed to read or parse file: ${error.message}`); return { valid: false, errors, }; } } /** * Backs up the current configuration */ async backup(backupPath) { const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const backupFile = backupPath || join(homedir(), ".claude-flow", "backups", `backup-${timestamp}.json`); await fs.mkdir(join(homedir(), ".claude-flow", "backups"), { recursive: true }); await fs.writeFile(backupFile, JSON.stringify(this.config, null, 2), "utf8"); return backupFile; } /** * Restores configuration from a backup */ async restore(backupPath) { const content = await fs.readFile(backupPath, "utf8"); const backupConfig = JSON.parse(content); // Validate the backup configuration this.validate(backupConfig); // Apply the backup this.config = backupConfig; // Save to current config file if (this.configPath) { await this.save(); } } /** * Gets configuration path history */ getPathHistory() { // In a real implementation, this would track all config paths used return this.configPath ? [this.configPath] : []; } /** * Gets configuration change history */ getChangeHistory() { // Convert string timestamps to Date objects for compatibility return this.changeHistory.map(change => ({ timestamp: new Date(change.timestamp), path: change.path, oldValue: change.oldValue, newValue: change.newValue, })); } /** * Masks sensitive values in configuration */ maskSensitiveValues(config) { const masked = JSON.parse(JSON.stringify(config)); const maskValue = (obj, path = "") => { for (const [key, value] of Object.entries(obj)) { const fullPath = path ? `${path}.${key}` : key; if (SENSITIVE_PATHS.some(sensitive => fullPath.toLowerCase().includes(sensitive.toLowerCase()) || key.toLowerCase().includes(sensitive.toLowerCase()))) { obj[key] = typeof value === "string" ? "****...****" : "[REDACTED]"; } else if (typeof value === "object" && value !== null) { maskValue(value, fullPath); } } }; maskValue(masked); return masked; } /** * Tracks configuration changes */ trackChanges(path, oldValue, newValue, options = {}) { const change = { timestamp: new Date().toISOString(), path, oldValue, newValue, user: options.user, reason: options.reason, source: options.source ?? "api", }; this.changeHistory.push(change); // Keep only last 100 changes if (this.changeHistory.length > 100) { this.changeHistory = this.changeHistory.slice(-100); } } /** * Track changes from config updates */ trackConfigChanges(oldConfig, updates, options = {}) { // For now, just track that an update occurred this.trackChanges("config", oldConfig, updates, options); } /** * Records a configuration change */ recordChange(path, oldValue, newValue, options = {}) { this.trackChanges(path, oldValue, newValue, options); } /** * Checks if a configuration path is sensitive */ isSensitivePath(path) { return SENSITIVE_PATHS.some(sensitive => path.toLowerCase().includes(sensitive.toLowerCase())); } /** * Encrypts a value if encryption is enabled */ encryptValue(value) { if (!this.encryptionKey) return value; try { const algorithm = "aes-256-gcm"; const iv = randomBytes(16); const cipher = createCipheriv(algorithm, this.encryptionKey, iv); let encrypted = cipher.update(value, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); // Return iv:authTag:encrypted format return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`; } catch (error) { console.warn("Failed to encrypt value:", error); return value; } } /** * Checks if a value is encrypted */ isEncryptedValue(value) { if (typeof value !== "string") return false; // New format: iv:authTag:encrypted (3 parts separated by colons) const parts = value.split(":"); return parts.length === 3 && parts[0].length === 32 && // IV is 16 bytes = 32 hex chars parts[1].length === 32 && // Auth tag is 16 bytes = 32 hex chars parts[2].length > 0; // Encrypted data } /** * Decrypts a value if it's encrypted */ decryptValue(value) { if (!this.encryptionKey) return value; try { const algorithm = "aes-256-gcm"; const parts = value.split(":"); if (parts.length !== 3) { // Not encrypted format, return as-is return value; } const iv = Buffer.from(parts[0], "hex"); const authTag = Buffer.from(parts[1], "hex"); const encrypted = parts[2]; const decipher = createDecipheriv(algorithm, this.encryptionKey, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } catch (error) { console.warn("Failed to decrypt value:", error); return value; } } } // Export singleton instance export const configManager = ConfigManager.getInstance(); // Helper function to load configuration export async function loadConfig(path) { await configManager.load(path); return configManager.get(); } function deepClone(obj, visited = new WeakMap()) { if (obj === null || typeof obj !== "object") { return obj; } // Check for circular reference if (visited.has(obj)) { return visited.get(obj); } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { const clonedArray = []; visited.set(obj, clonedArray); obj.forEach((item, index) => { clonedArray[index] = deepClone(item, visited); }); return clonedArray; } if (obj instanceof Map) { const map = new Map(); visited.set(obj, map); obj.forEach((value, key) => { map.set(key, deepClone(value, visited)); }); return map; } if (obj instanceof Set) { const set = new Set(); visited.set(obj, set); obj.forEach((value) => { set.add(deepClone(value, visited)); }); return set; } const cloned = {}; visited.set(obj, cloned); for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { cloned[key] = deepClone(obj[key], visited); } } return cloned; } export { SENSITIVE_PATHS, SECURITY_CLASSIFICATIONS, }; // Custom deepMerge for Config type function deepMergeConfig(target, ...sources) { const result = deepClone(target); for (const source of sources) { if (!source) continue; // Merge each section if (source.orchestrator) { result.orchestrator = { ...result.orchestrator, ...source.orchestrator }; } if (source.terminal) { result.terminal = { ...result.terminal, ...source.terminal }; } if (source.memory) { result.memory = { ...result.memory, ...source.memory }; } if (source.coordination) { result.coordination = { ...result.coordination, ...source.coordination }; } if (source.mcp) { result.mcp = { ...result.mcp, ...source.mcp }; } if (source.logging) { result.logging = { ...result.logging, ...source.logging }; } if (source.credentials) { result.credentials = { ...result.credentials, ...source.credentials }; } if (source.securi