UNPKG

claude-flow-novice

Version:

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

459 lines (458 loc) 16.2 kB
/** * Redis Pub/Sub Helper Functions for Coordinator Communication * * Provides standardized Redis pub/sub patterns for coordinator communication * as defined in COORDINATOR_COMMUNICATION_REQUIREMENTS.md * * Features: * - Sprint lifecycle event publishing * - Agent lifecycle tracking * - Interface signal publishing/subscription * - Claim/confirmation protocol * - Test coordination lock protocol * - Dependency waiting patterns * * @module cfn-loop/redis-pubsub-helpers */ import { Logger } from '../core/logger.js'; // ===== SECURITY ERROR CLASS ===== /** * Security error for pub/sub payload validation failures */ export class SecurityError extends Error { details; constructor(message, details){ super(message), this.details = details; this.name = 'SecurityError'; Error.captureStackTrace(this, SecurityError); } } /** * Redis pub/sub channels */ export const REDIS_CHANNELS = { SPRINT_COORDINATION: 'sprint:coordination', AGENT_LIFECYCLE: 'agent:lifecycle', INTERFACE_READY: 'interface:ready', COORDINATION_CLAIMS: 'coordination:claims:channel', TEST_COORDINATION: 'test:coordination', CONFLICT_DETECTED: 'conflict:detected' }; /** * Redis key prefixes */ export const REDIS_KEY_PREFIXES = { DEPENDENCY: 'cfn:dependency', TEST_LOCK: 'cfn:test:execution:lock', TEST_QUEUE: 'cfn:test:queue', SPRINT_STATE: 'cfn:sprint:state', INTERFACE: 'cfn:interface', CLAIM: 'coordination:claims:confirmed', CONFLICT_LOG: 'coordination:conflicts:log' }; // ===== REDIS PUB/SUB HELPER CLASS ===== /** * Redis Pub/Sub Helper for coordinator communication */ export class RedisPubSubHelper { redis; logger; subscriptions = new Map(); constructor(redis, loggerConfig){ this.redis = redis; const config = loggerConfig || { level: process.env.CLAUDE_FLOW_ENV === 'test' ? 'error' : 'info', format: 'json', destination: 'console' }; this.logger = new Logger(config, { component: 'RedisPubSubHelper' }); } // ===== SPRINT LIFECYCLE EVENTS ===== /** * Publish sprint start event */ async publishSprintStart(sprintId, coordinatorId, dependencies = []) { const event = { type: 'sprint:start', sprintId, coordinatorId, dependencies, timestamp: Date.now() }; await this.publishEvent(REDIS_CHANNELS.SPRINT_COORDINATION, event); this.logger.info('Sprint start event published', { sprintId, coordinatorId, dependencies }); } /** * Publish sprint complete event */ async publishSprintComplete(sprintId, consensus, decision) { const event = { type: 'sprint:complete', sprintId, coordinatorId: sprintId, consensus, decision, timestamp: Date.now() }; await this.publishEvent(REDIS_CHANNELS.SPRINT_COORDINATION, event); this.logger.info('Sprint complete event published', { sprintId, consensus, decision }); } /** * Publish sprint failed event */ async publishSprintFailed(sprintId, error) { const event = { type: 'sprint:failed', sprintId, error, timestamp: Date.now() }; await this.publishEvent(REDIS_CHANNELS.SPRINT_COORDINATION, event); this.logger.error('Sprint failed event published', { sprintId, error }); } // ===== AGENT LIFECYCLE EVENTS ===== /** * Publish agent spawned event */ async publishAgentSpawned(agentId, coordinatorId, sprintId, role) { const event = { type: 'agent:spawned', agentId, coordinatorId, sprintId, role, timestamp: Date.now() }; await this.publishEvent(REDIS_CHANNELS.AGENT_LIFECYCLE, event); this.logger.debug('Agent spawned event published', { agentId, coordinatorId, sprintId, role }); } /** * Publish agent completed event */ async publishAgentCompleted(agentId, confidence, deliverables) { const event = { type: 'agent:completed', agentId, confidence, deliverables, timestamp: Date.now() }; await this.publishEvent(REDIS_CHANNELS.AGENT_LIFECYCLE, event); this.logger.info('Agent completed event published', { agentId, confidence, deliverables }); } /** * Publish agent failed event */ async publishAgentFailed(agentId, error) { const event = { type: 'agent:failed', agentId, error, timestamp: Date.now() }; await this.publishEvent(REDIS_CHANNELS.AGENT_LIFECYCLE, event); this.logger.error('Agent failed event published', { agentId, error }); } // ===== INTERFACE EVENTS ===== /** * Publish interface ready event */ async publishInterfaceReady(sprintId, interfaceId, interfaceDefinition) { const event = { type: 'interface:published', sprintId, interfaceId, interface: interfaceDefinition, timestamp: Date.now() }; // Publish to both channels for compatibility await this.publishEvent(REDIS_CHANNELS.SPRINT_COORDINATION, event); await this.publishEvent(REDIS_CHANNELS.INTERFACE_READY, event); // Store interface in Redis for retrieval const key = `${REDIS_KEY_PREFIXES.INTERFACE}:${interfaceId}`; await this.redis.setex(key, 3600, JSON.stringify(interfaceDefinition)); this.logger.info('Interface ready event published', { sprintId, interfaceId, exports: interfaceDefinition.exports }); } // ===== SUBSCRIPTION HELPERS ===== /** * Subscribe to sprint coordination events */ async subscribeToSprintCoordination(handler) { await this.subscribe(REDIS_CHANNELS.SPRINT_COORDINATION, async (message)=>{ try { const event = JSON.parse(message); await handler(event); } catch (error) { this.logger.error('Error handling sprint coordination event', { error: error instanceof Error ? error.message : String(error), message }); } }); this.logger.info('Subscribed to sprint coordination channel'); } /** * Subscribe to agent lifecycle events */ async subscribeToAgentLifecycle(handler) { await this.subscribe(REDIS_CHANNELS.AGENT_LIFECYCLE, async (message)=>{ try { const event = JSON.parse(message); await handler(event); } catch (error) { this.logger.error('Error handling agent lifecycle event', { error: error instanceof Error ? error.message : String(error), message }); } }); this.logger.info('Subscribed to agent lifecycle channel'); } /** * Subscribe to interface ready events */ async subscribeToInterfaceReady(handler) { await this.subscribe(REDIS_CHANNELS.INTERFACE_READY, async (message)=>{ try { const event = JSON.parse(message); await handler(event); } catch (error) { this.logger.error('Error handling interface ready event', { error: error instanceof Error ? error.message : String(error), message }); } }); this.logger.info('Subscribed to interface ready channel'); } // ===== DEPENDENCY WAITING PATTERN ===== /** * Wait for dependencies with timeout * * Returns immediately when all dependencies are satisfied or throws on timeout. * * @param dependencies - Array of sprint IDs to wait for * @param timeoutMs - Timeout in milliseconds (default: 300000 = 5 minutes) * @returns Promise that resolves when all dependencies are satisfied */ async waitForDependencies(dependencies, timeoutMs = 300000) { if (dependencies.length === 0) { return; } this.logger.info('Waiting for dependencies', { dependencies, timeout: timeoutMs }); const unsatisfied = new Set(dependencies); const startTime = Date.now(); return new Promise((resolve, reject)=>{ // Set up timeout const timeout = setTimeout(()=>{ reject(new Error(`Dependency timeout: waiting for ${Array.from(unsatisfied).join(', ')}`)); }, timeoutMs); // Subscribe to sprint coordination events this.subscribeToSprintCoordination(async (event)=>{ if (event.type === 'interface:published' && unsatisfied.has(event.sprintId)) { unsatisfied.delete(event.sprintId); this.logger.info('Dependency satisfied', { sprintId: event.sprintId, remaining: Array.from(unsatisfied) }); if (unsatisfied.size === 0) { clearTimeout(timeout); resolve(); } } }); // Check for already-published interfaces this.checkExistingInterfaces(dependencies).then((satisfied)=>{ for (const dep of satisfied){ unsatisfied.delete(dep); } if (unsatisfied.size === 0) { clearTimeout(timeout); resolve(); } }); }); } /** * Check for existing interfaces in Redis */ async checkExistingInterfaces(interfaceIds) { const satisfied = []; for (const interfaceId of interfaceIds){ const key = `${REDIS_KEY_PREFIXES.INTERFACE}:${interfaceId}`; const exists = await this.redis.exists(key); if (exists) { satisfied.push(interfaceId); this.logger.debug('Interface already exists', { interfaceId }); } } return satisfied; } // ===== TEST COORDINATION LOCK ===== /** * Acquire test execution lock * * @param coordinatorId - Coordinator requesting the lock * @param ttlMs - Lock TTL in milliseconds (default: 900000 = 15 minutes) * @returns True if lock acquired, false otherwise */ async acquireTestLock(coordinatorId, ttlMs = 900000) { const lockData = JSON.stringify({ coordinatorId, timestamp: Date.now(), pid: process.pid }); const acquired = await this.redis.set(REDIS_KEY_PREFIXES.TEST_LOCK, lockData, 'NX', 'PX', ttlMs); if (acquired) { this.logger.info('Test lock acquired', { coordinatorId, ttl: ttlMs }); return true; } this.logger.debug('Test lock already held', { coordinatorId }); return false; } /** * Release test execution lock */ async releaseTestLock() { await this.redis.del(REDIS_KEY_PREFIXES.TEST_LOCK); this.logger.info('Test lock released'); } /** * Join test execution queue * * @param coordinatorId - Coordinator joining the queue * @returns Queue position (0-based) */ async joinTestQueue(coordinatorId) { const score = Date.now(); await this.redis.zadd(REDIS_KEY_PREFIXES.TEST_QUEUE, score, coordinatorId); // Get position in queue const rank = await this.redis.zrank(REDIS_KEY_PREFIXES.TEST_QUEUE, coordinatorId); this.logger.info('Joined test queue', { coordinatorId, position: rank || 0 }); return rank || 0; } // ===== LOW-LEVEL PUB/SUB ===== /** * Publish event to Redis channel with P0 security validation * * Security validations (HIGH severity P0 requirements): * 1. Payload size validation (max 1MB) * 2. Script injection pattern detection * 3. Rate limiting (100 events/sec per channel) * 4. Security event logging */ async publishEvent(channel, event) { if (!channel) { throw new Error('Channel must be provided'); } if (!event) { throw new Error('Event must be provided'); } const message = JSON.stringify(event); const now = Date.now(); // 1. PAYLOAD SIZE VALIDATION (max 1MB = 1048576 bytes) if (message.length > 1048576) { this.logger.error('Pub/sub payload exceeds 1MB limit', { channel, size: message.length, limit: 1048576 }); throw new SecurityError('Pub/sub payload exceeds 1MB limit', { channel, size: message.length, limit: 1048576 }); } // 2. SCRIPT INJECTION DETECTION (defense-in-depth) const maliciousPatterns = /<script|javascript:|onerror=|onclick=|onload=|data:text\/html|eval\(|Function\(/i; if (maliciousPatterns.test(message)) { this.logger.error('Potentially malicious content detected in pub/sub payload', { channel, pattern: 'script_injection_attempt' }); throw new SecurityError('Potentially malicious content detected', { channel, pattern: 'script_injection_attempt' }); } // 3. RATE LIMITING (100 events/sec per channel) const rateLimitKey = `ratelimit:pubsub:${channel}`; const count = await this.redis.incr(rateLimitKey); // Set expiry on first increment if (count === 1) { await this.redis.expire(rateLimitKey, 1); // 1 second window } if (count > 100) { this.logger.error('Rate limit exceeded for pub/sub channel', { channel, limit: 100, current: count, window: '1s' }); throw new SecurityError('Rate limit exceeded for channel', { channel, limit: 100, current: count }); } // 4. PUBLISH WITH SECURITY EVENT LOGGING await this.redis.publish(channel, message); this.logger.debug('Event published with validation', { channel, eventType: event.type, size: message.length, rateLimit: count, timestamp: now }); } /** * Subscribe to Redis channel */ async subscribe(channel, handler) { // Store handler if (!this.subscriptions.has(channel)) { this.subscriptions.set(channel, new Set()); } this.subscriptions.get(channel).add(handler); // Note: In production, this would use a dedicated subscriber Redis client // For now, this is a simplified implementation this.logger.debug('Subscribed to channel', { channel }); } /** * Unsubscribe from all channels */ async unsubscribeAll() { this.subscriptions.clear(); this.logger.info('Unsubscribed from all channels'); } } // ===== FACTORY FUNCTION ===== /** * Create Redis Pub/Sub Helper instance */ export function createRedisPubSubHelper(redis, loggerConfig) { return new RedisPubSubHelper(redis, loggerConfig); } // ===== EXPORTS ===== export default RedisPubSubHelper; //# sourceMappingURL=redis-pubsub-helpers.js.map