claude-flow-multilang
Version:
Revolutionary multilingual AI orchestration framework with cultural awareness and DDD architecture
1,312 lines (1,139 loc) • 37.8 kB
text/typescript
/**
* 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 type { Config } from '../utils/types.js';
import { deepMerge, safeParseJSON } from '../utils/helpers.js';
import { ConfigError, ValidationError } from '../utils/errors.js';
// Format parsers
interface FormatParser {
parse(content: string): any;
stringify(obj: any): string;
extension: string;
}
// Configuration change record
interface ConfigChange {
timestamp: string;
path: string;
oldValue: any;
newValue: any;
user?: string;
reason?: string;
source: 'cli' | 'api' | 'file' | 'env';
}
// Security classification
interface SecurityClassification {
level: 'public' | 'internal' | 'confidential' | 'secret';
maskPattern?: string;
encrypted?: boolean;
}
// Validation rule
interface ValidationRule {
type: string;
required?: boolean;
min?: number;
max?: number;
values?: string[];
pattern?: RegExp;
validator?: (value: any, config: Config) => string | null;
dependencies?: string[];
}
/**
* Security classifications for configuration paths
*/
const SECURITY_CLASSIFICATIONS: Record<string, SecurityClassification> = {
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: Record<string, FormatParser> = {
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: any = {};
let current = result;
const stack: any[] = [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: any = 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: any, indent = 0): string => {
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: any = {};
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: any = 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: 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 {
private static instance: ConfigManager;
private config: Config;
private configPath?: string;
private profiles: Map<string, Partial<Config>> = new Map();
private currentProfile?: string;
private userConfigDir: string;
private changeHistory: ConfigChange[] = [];
private encryptionKey?: Buffer;
private validationRules: Map<string, ValidationRule> = new Map();
private formatParsers = FORMAT_PARSERS;
private 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(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
/**
* Initialize async components
*/
async init(): Promise<void> {
await this.initializeEncryption();
}
/**
* Initializes encryption for sensitive configuration values
*/
private async initializeEncryption(): Promise<void> {
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 as Error).message);
}
}
/**
* Sets up validation rules for configuration paths
*/
private setupValidationRules(): void {
// 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?: string): Promise<Config> {
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): Config {
const config = deepClone(this.config);
if (maskSensitive && this.config.security?.maskSensitiveValues) {
return this.maskSensitiveValues(config);
}
return config;
}
/**
* Gets configuration with security masking applied
*/
getSecure(): Config {
return this.get(true);
}
/**
* Gets all configuration values (alias for get method for backward compatibility)
*/
async getAll(): Promise<Config> {
return this.get();
}
/**
* Updates configuration values with change tracking
*/
update(
updates: Partial<Config>,
options: { user?: string; reason?: string; source?: 'cli' | 'api' | 'file' | 'env' } = {},
): Config {
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(): void {
this.config = deepClone(DEFAULT_CONFIG);
}
/**
* Saves configuration to file with format support
*/
async save(path?: string, format?: string): Promise<void> {
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)
*/
private getConfigForSaving(): Partial<Config> {
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
*/
private getUserConfigDir(): string {
const home = homedir();
return join(home, '.claude-flow');
}
/**
* Creates user config directory if it doesn't exist
*/
private async ensureUserConfigDir(): Promise<void> {
try {
await fs.mkdir(this.userConfigDir, { recursive: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
throw new ConfigError(`Failed to create config directory: ${(error as Error).message}`);
}
}
}
/**
* Loads all profiles from the profiles directory
*/
async loadProfiles(): Promise<void> {
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<Partial<Config>>(content);
if (profileConfig) {
this.profiles.set(profileName, profileConfig);
}
} catch (error) {
console.warn(`Failed to load profile ${profileName}: ${(error as Error).message}`);
}
}
}
} catch (error) {
// Profiles directory doesn't exist - this is okay
}
}
/**
* Applies a named profile
*/
async applyProfile(profileName: string): Promise<void> {
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: string, config?: Partial<Config>): Promise<void> {
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: string): Promise<void> {
const profilePath = join(this.userConfigDir, 'profiles', `${profileName}.json`);
try {
await fs.unlink(profilePath);
this.profiles.delete(profileName);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new ConfigError(`Profile '${profileName}' not found`);
}
throw new ConfigError(`Failed to delete profile: ${(error as Error).message}`);
}
}
/**
* Lists all available profiles
*/
async listProfiles(): Promise<string[]> {
await this.loadProfiles();
return Array.from(this.profiles.keys());
}
/**
* Gets a specific profile configuration
*/
async getProfile(profileName: string): Promise<Partial<Config> | undefined> {
await this.loadProfiles();
return this.profiles.get(profileName);
}
/**
* Gets the current active profile name
*/
getCurrentProfile(): string | undefined {
return this.currentProfile;
}
/**
* Sets a configuration value by path with change tracking and validation
*/
set(
path: string,
value: any,
options: { user?: string; reason?: string; source?: 'cli' | 'api' | 'file' | 'env' } = {},
): void {
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: any = 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: string, decrypt = true): any {
const keys = path.split('.');
let current: any = 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 as Error).message);
return current;
}
}
return current;
}
/**
* Resets configuration to defaults
*/
reset(): void {
this.config = deepClone(DEFAULT_CONFIG);
delete this.currentProfile;
}
/**
* Gets configuration schema for validation
*/
getSchema(): any {
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
*/
private validateValue(value: any, schema: any, path: string): void {
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(): any {
const defaultConfig = DEFAULT_CONFIG;
const diff: any = {};
const findDifferences = (current: any, defaults: any, path: string = '') => {
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(): any {
return {
version: '1.0.0',
exported: new Date().toISOString(),
profile: this.currentProfile,
config: this.config,
diff: this.getDiff(),
};
}
/**
* Imports configuration from export
*/
import(data: any): void {
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
*/
private async loadFromFile(path: string): Promise<Partial<Config>> {
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) as Partial<Config>;
if (!config) {
throw new ConfigError(`Invalid ${format.toUpperCase()} in configuration file: ${path}`);
}
return config;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// File doesn't exist, use defaults
return {};
}
throw new ConfigError(
`Failed to load configuration from ${path}: ${(error as Error).message}`,
);
}
}
/**
* Detects configuration file format
*/
private detectFormat(path: string, content?: string): string {
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
*/
private loadFromEnv(): Partial<Config> {
const config: Partial<Config> = {};
// Orchestrator settings
const maxAgents = process.env.CLAUDE_FLOW_MAX_AGENTS;
if (maxAgents) {
if (!config.orchestrator) {
config.orchestrator = {} as any;
}
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
*/
private validateWithDependencies(config: Config): void {
const errors: string[] = [];
const warnings: string[] = [];
// 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 as 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
*/
private validatePath(path: string, value: any, config?: Config): void {
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
*/
private getValueByPath(obj: any, path: string): any {
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
*/
private validate(config: Config): void {
this.validateWithDependencies(config);
}
/**
* Masks sensitive values in configuration
*/
private maskSensitiveValues(config: Config): Config {
const maskedConfig = deepClone(config);
// Recursively mask sensitive paths
const maskObject = (obj: any, path: string = ''): any => {
if (!obj || typeof obj !== 'object') return obj;
const masked: any = {};
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
*/
private trackChanges(
oldConfig: Config,
updates: Partial<Config>,
options: { user?: string; reason?: string; source?: 'cli' | 'api' | 'file' | 'env' },
): void {
// Simple implementation for tracking changes
for (const [key, value] of Object.entries(updates)) {
this.recordChange({
timestamp: new Date().toISOString(),
path: key,
oldValue: (oldConfig as any)[key],
newValue: value,
user: options.user,
reason: options.reason,
source: options.source || 'cli',
});
}
}
/**
* Records a configuration change
*/
private recordChange(change: ConfigChange): void {
this.changeHistory.push(change);
// Keep only last 1000 changes
if (this.changeHistory.length > 1000) {
this.changeHistory.shift();
}
}
/**
* Checks if a path contains sensitive information
*/
private isSensitivePath(path: string): boolean {
return SENSITIVE_PATHS.some((sensitive) =>
path.toLowerCase().includes(sensitive.toLowerCase()),
);
}
/**
* Encrypts a sensitive value
*/
private encryptValue(value: any): string {
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 as Error).message);
return value;
}
}
/**
* Decrypts a sensitive value
*/
private decryptValue(encryptedValue: string): any {
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 as Error).message);
return encryptedValue;
}
}
/**
* Checks if a value is encrypted
*/
private isEncryptedValue(value: any): boolean {
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?: string): Promise<Config> {
return await configManager.load(path);
}
function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
// Export types for external use
export type { FormatParser, ConfigChange, SecurityClassification, ValidationRule };
export { SENSITIVE_PATHS, SECURITY_CLASSIFICATIONS };
// Custom deepMerge for Config type
function deepMergeConfig(target: Config, ...sources: Partial<Config>[]): Config {
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;
}