UNPKG

claude-flow-tbowman01

Version:

Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)

1,125 lines 40.4 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 { createHash, randomBytes, createCipheriv, createDecipheriv } from 'crypto'; import { safeParseJSON } from '../utils/helpers.js'; import { ConfigError, ValidationError } from '../utils/errors.js'; /** * 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 = {}; let 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.setupValidationRules(); // Encryption will be initialized via init() method } /** * Gets the singleton instance */ static getInstance() { if (!ConfigManager.instance) { ConfigManager.instance = new ConfigManager(); } return ConfigManager.instance; } /** * Initialize async components */ async init() { await this.initializeEncryption(); } /** * Initializes encryption for sensitive configuration values */ async initializeEncryption() { try { const keyFile = join(this.userConfigDir, '.encryption-key'); // Check if key file exists (simplified for demo) try { await fs.access(keyFile); // In a real implementation, this would be more secure this.encryptionKey = randomBytes(32); } catch { 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) => { if (value > config.terminal?.poolSize * 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 maxAgents = config.orchestrator?.maxConcurrentAgents || 1; if (value < 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) => { if (value > 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) => { if (value && value.length < 16) { return 'API key should be at least 16 characters long'; } return null; }, }); } /** * Loads configuration from various sources */ async load(configPath) { if (configPath !== undefined) { this.configPath = configPath; } // Start with defaults let config = deepClone(DEFAULT_CONFIG); // Load from file if specified if (configPath) { const fileConfig = await this.loadFromFile(configPath); config = deepMergeConfig(config, fileConfig); } // Load from environment variables const envConfig = this.loadFromEnv(); config = deepMergeConfig(config, envConfig); // Validate the final configuration this.validate(config); this.config = config; return config; } /** * 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); } /** * Gets all configuration values (alias for get method for backward compatibility) */ async getAll() { return this.get(); } /** * Updates configuration values with change tracking */ update(updates, options = {}) { const oldConfig = deepClone(this.config); // Track changes before applying this.trackChanges(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({ timestamp: new Date().toISOString(), path: 'CONFIG_SAVED', oldValue: null, newValue: 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({ timestamp: new Date().toISOString(), path, oldValue, newValue: 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.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({ timestamp: new Date().toISOString(), path: 'CONFIG_IMPORTED', oldValue: null, newValue: 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 */ loadFromEnv() { const config = {}; // Orchestrator settings const maxAgents = process.env.CLAUDE_FLOW_MAX_AGENTS; if (maxAgents) { if (!config.orchestrator) { config.orchestrator = {}; } config.orchestrator = { ...DEFAULT_CONFIG.orchestrator, ...config.orchestrator, maxConcurrentAgents: parseInt(maxAgents, 10), }; } // 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, }; } return config; } /** * Validates configuration with dependency checking */ validateWithDependencies(config) { const errors = []; const warnings = []; // Validate all paths with rules for (const [path, rule] of this.validationRules.entries()) { const value = this.getValueByPath(config, path); try { this.validatePath(path, value, config); } catch (error) { errors.push(error.message); } } // Additional cross-field validations if (config.orchestrator.maxConcurrentAgents > config.terminal.poolSize * 3) { warnings.push('High agent-to-terminal ratio may cause resource contention'); } if (config.memory.cacheSizeMB > 1000 && config.memory.backend === 'sqlite') { warnings.push('Large cache size with SQLite backend may impact performance'); } if (config.mcp.transport === 'http' && !config.mcp.tlsEnabled) { warnings.push('HTTP transport without TLS is not recommended for production'); } // Log warnings if (warnings.length > 0 && config.logging?.level === 'debug') { console.warn('Configuration warnings:', warnings); } // Throw errors if (errors.length > 0) { throw new ValidationError(`Configuration validation failed:\n${errors.join('\n')}`); } } /** * Validates a specific configuration path */ validatePath(path, value, config) { const rule = this.validationRules.get(path); if (!rule) return; const currentConfig = config || this.config; // Required validation if (rule.required && (value === undefined || value === null)) { throw new ValidationError(`${path} is required`); } if (value === undefined || value === null) return; // Type validation if (rule.type === 'number' && (typeof value !== 'number' || isNaN(value))) { throw new ValidationError(`${path} must be a number`); } if (rule.type === 'string' && typeof value !== 'string') { throw new ValidationError(`${path} must be a string`); } if (rule.type === 'boolean' && typeof value !== 'boolean') { throw new ValidationError(`${path} must be a boolean`); } // Range validation if (typeof value === 'number') { if (rule.min !== undefined && value < rule.min) { throw new ValidationError(`${path} must be at least ${rule.min}`); } if (rule.max !== undefined && value > rule.max) { throw new ValidationError(`${path} must be at most ${rule.max}`); } } // Values validation if (rule.values && !rule.values.includes(value)) { throw new ValidationError(`${path} must be one of: ${rule.values.join(', ')}`); } // Pattern validation if (rule.pattern && typeof value === 'string' && !rule.pattern.test(value)) { throw new ValidationError(`${path} does not match required pattern`); } // Custom validator if (rule.validator) { const result = rule.validator(value, currentConfig); if (result) { throw new ValidationError(`${path}: ${result}`); } } } /** * 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); } /** * Masks sensitive values in configuration */ maskSensitiveValues(config) { const maskedConfig = deepClone(config); // Recursively mask sensitive paths const maskObject = (obj, path = '') => { if (!obj || typeof obj !== 'object') return obj; const masked = {}; for (const [key, value] of Object.entries(obj)) { const currentPath = path ? `${path}.${key}` : key; if (this.isSensitivePath(currentPath)) { const classification = SECURITY_CLASSIFICATIONS[currentPath]; masked[key] = classification?.maskPattern || '****'; } else if (typeof value === 'object' && value !== null) { masked[key] = maskObject(value, currentPath); } else { masked[key] = value; } } return masked; }; return maskObject(maskedConfig); } /** * Tracks changes to configuration */ trackChanges(oldConfig, updates, options) { // Simple implementation for tracking changes for (const [key, value] of Object.entries(updates)) { this.recordChange({ timestamp: new Date().toISOString(), path: key, oldValue: oldConfig[key], newValue: value, user: options.user, reason: options.reason, source: options.source || 'cli', }); } } /** * Records a configuration change */ recordChange(change) { this.changeHistory.push(change); // Keep only last 1000 changes if (this.changeHistory.length > 1000) { this.changeHistory.shift(); } } /** * Checks if a path contains sensitive information */ isSensitivePath(path) { return SENSITIVE_PATHS.some((sensitive) => path.toLowerCase().includes(sensitive.toLowerCase())); } /** * Encrypts a sensitive value */ encryptValue(value) { if (!this.encryptionKey) { return value; // Return original if encryption not available } try { // Simplified encryption - in production use proper encryption const iv = randomBytes(16); const key = createHash('sha256').update(this.encryptionKey).digest(); const cipher = createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update(JSON.stringify(value), 'utf8', 'hex'); encrypted += cipher.final('hex'); return `encrypted:${iv.toString('hex')}:${encrypted}`; } catch (error) { console.warn('Failed to encrypt value:', error.message); return value; } } /** * Decrypts a sensitive value */ decryptValue(encryptedValue) { if (!this.encryptionKey || !this.isEncryptedValue(encryptedValue)) { return encryptedValue; } try { const parts = encryptedValue.replace('encrypted:', '').split(':'); if (parts.length !== 2) return encryptedValue; // Handle old format const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const key = createHash('sha256').update(this.encryptionKey).digest(); const decipher = createDecipheriv('aes-256-cbc', key, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } catch (error) { console.warn('Failed to decrypt value:', error.message); return encryptedValue; } } /** * Checks if a value is encrypted */ isEncryptedValue(value) { return typeof value === 'string' && value.startsWith('encrypted:'); } } // Export singleton instance export const configManager = ConfigManager.getInstance(); // Helper function to load configuration export async function loadConfig(path) { return await configManager.load(path); } function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } 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.security) { result.security = { ...result.security, ...source.security }; } } return result; } //# sourceMappingURL=config.js.map