UNPKG

n8n-nodes-a2a-protocol

Version:

Agent2Agent (A2A) Protocol nodes for n8n - Enable agent interoperability, communication, and MCP integration

892 lines (878 loc) 32.5 kB
"use strict"; /** * N8N A2A Protocol - URL Utilities * Provides dynamic URL detection for A2A nodes based on n8n instance configuration */ Object.defineProperty(exports, "__esModule", { value: true }); exports.PREDEFINED_AGENTS = exports.DEFAULT_A2A_PORTS = void 0; exports.storeActiveServer = storeActiveServer; exports.getActiveServer = getActiveServer; exports.stopPreviousServerInstance = stopPreviousServerInstance; exports.cleanupActiveServer = cleanupActiveServer; exports.listActiveServers = listActiveServers; exports.isPortAvailable = isPortAvailable; exports.findNextAvailablePort = findNextAvailablePort; exports.checkA2APortsAvailability = checkA2APortsAvailability; exports.generatePortErrorMessage = generatePortErrorMessage; exports.getAvailableRegistryPort = getAvailableRegistryPort; exports.getAvailableAgentPort = getAvailableAgentPort; exports.checkPredefinedAgentsAvailability = checkPredefinedAgentsAvailability; exports.getRegistryPlaceholder = getRegistryPlaceholder; exports.getAgentPlaceholder = getAgentPlaceholder; exports.detectInstanceUrl = detectInstanceUrl; exports.buildUrl = buildUrl; exports.getDefaultAgentUrl = getDefaultAgentUrl; exports.getDefaultCallbackUrl = getDefaultCallbackUrl; exports.getDefaultRegistryUrl = getDefaultRegistryUrl; exports.createAgentEndpoints = createAgentEndpoints; exports.logUrlConfiguration = logUrlConfiguration; exports.detectRuntimeUrl = detectRuntimeUrl; exports.detectInstanceUrlEnhanced = detectInstanceUrlEnhanced; exports.getPortProcessInfo = getPortProcessInfo; exports.killProcess = killProcess; exports.handlePortConflict = handlePortConflict; exports.validateNodePort = validateNodePort; exports.validateWorkflowPorts = validateWorkflowPorts; const net_1 = require("net"); /** * Default A2A port configurations */ exports.DEFAULT_A2A_PORTS = { REGISTRY: 8080, AGENTS: { TRANSLATOR: 7000, CALCULATOR: 7001, CODING: 8000, ORCHESTRATOR: 9000, } }; /** * Predefined A2A agent configurations */ exports.PREDEFINED_AGENTS = [ { port: exports.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR, name: 'Translator Agent', id: 'translator-agent' }, { port: exports.DEFAULT_A2A_PORTS.AGENTS.CALCULATOR, name: 'Calculator Agent', id: 'calculator-agent' }, { port: exports.DEFAULT_A2A_PORTS.AGENTS.CODING, name: 'Coding Agent', id: 'coding-agent' }, { port: exports.DEFAULT_A2A_PORTS.AGENTS.ORCHESTRATOR, name: 'Orchestrator Agent', id: 'orchestrator-agent' }, ]; /** * Global A2A Server Registry using UUID-based server management * ⚠️ IMPROVED: Using UUID for unique server identification and proper cleanup */ const A2A_SERVER_REGISTRY = {}; // Server instances by UUID /** * Global storage for A2A node-to-server mapping * Maps nodeId to serverId for proper lifecycle management */ const A2A_NODE_SERVERS = new Map(); /** * Generate unique server ID */ function generateServerId() { return `server_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } /** * Store an A2A server using UUID-based registry pattern * ⚠️ IMPROVED: Following your UUID server registry approach */ function storeActiveServer(nodeId, server, port, serviceType) { // Close any existing server for this node first const existing = A2A_NODE_SERVERS.get(nodeId); if (existing) { closeServerById(existing.serverId); } // Generate unique server identifier const serverId = generateServerId(); // Store server in registry by serverId A2A_SERVER_REGISTRY[serverId] = server; // Map nodeId to serverId A2A_NODE_SERVERS.set(nodeId, { serverId, port, serviceType, startedAt: new Date().toISOString(), }); return serverId; } /** * Close a server by ID (following your approach) */ function closeServerById(serverId) { const server = A2A_SERVER_REGISTRY[serverId]; if (!server) { return false; } try { server.close(() => { delete A2A_SERVER_REGISTRY[serverId]; }); return true; } catch (error) { delete A2A_SERVER_REGISTRY[serverId]; return false; } } /** * Get stored server info for a node (using new registry pattern) */ function getActiveServer(nodeId) { return A2A_NODE_SERVERS.get(nodeId) || null; } /** * Stop previous server instance for a node (using UUID registry pattern) * ⚠️ IMPROVED: Uses your server registry approach for safe closure */ async function stopPreviousServerInstance(nodeId) { const nodeInfo = A2A_NODE_SERVERS.get(nodeId); if (!nodeInfo) { return { stopped: false, message: `No previous server instance found for node ${nodeId}` }; } const success = closeServerById(nodeInfo.serverId); A2A_NODE_SERVERS.delete(nodeId); if (success) { // Wait a moment for the port to be released await new Promise(resolve => setTimeout(resolve, 100)); return { stopped: true, message: `Successfully stopped previous ${nodeInfo.serviceType} server on port ${nodeInfo.port}` }; } else { return { stopped: false, message: `Failed to stop previous server` }; } } /** * Clean up stored server reference (using UUID registry pattern) * ⚠️ IMPROVED: Uses your server registry approach for safe cleanup */ function cleanupActiveServer(nodeId) { const nodeInfo = A2A_NODE_SERVERS.get(nodeId); if (nodeInfo) { closeServerById(nodeInfo.serverId); A2A_NODE_SERVERS.delete(nodeId); } } /** * List all stored active servers (using new registry pattern) */ function listActiveServers() { return Array.from(A2A_NODE_SERVERS.entries()).map(([nodeId, serverInfo]) => ({ nodeId, port: serverInfo.port, serviceType: serverInfo.serviceType, startedAt: serverInfo.startedAt, serverId: serverInfo.serverId })); } /** * Check if a specific port is available (both IPv4 and IPv6) */ async function isPortAvailable(port, host = 'localhost') { // Check IPv4 const ipv4Available = await new Promise((resolve) => { const server = (0, net_1.createServer)(); server.listen(port, host, () => { server.close(() => { resolve(true); }); }); server.on('error', () => { resolve(false); }); }); if (!ipv4Available) { return false; } // Check IPv6 (which is what's causing the EADDRINUSE error) const ipv6Available = await new Promise((resolve) => { const server = (0, net_1.createServer)(); // Try to bind to IPv6 specifically server.listen(port, '::', () => { server.close(() => { resolve(true); }); }); server.on('error', () => { resolve(false); }); }); return ipv4Available && ipv6Available; } /** * Find the next available port starting from a given port */ async function findNextAvailablePort(startPort, maxTries = 10) { for (let i = 0; i < maxTries; i++) { const port = startPort + i; if (await isPortAvailable(port)) { return port; } } return null; } /** * Check if all required A2A ports are available */ async function checkA2APortsAvailability() { const conflicts = []; const suggestions = []; // Check registry port const registryAvailable = await isPortAvailable(exports.DEFAULT_A2A_PORTS.REGISTRY); if (!registryAvailable) { conflicts.push({ port: exports.DEFAULT_A2A_PORTS.REGISTRY, service: 'A2A Registry' }); const altRegistryPort = await findNextAvailablePort(exports.DEFAULT_A2A_PORTS.REGISTRY + 1); if (altRegistryPort !== null) { suggestions.push({ port: altRegistryPort, service: 'A2A Registry (Alternative)' }); } } // Check agent ports for (const agent of exports.PREDEFINED_AGENTS) { const agentAvailable = await isPortAvailable(agent.port); if (!agentAvailable) { conflicts.push({ port: agent.port, service: `A2A Agent (${agent.name})` }); const altAgentPort = await findNextAvailablePort(agent.port + 100); if (altAgentPort !== null) { suggestions.push({ port: altAgentPort, service: `${agent.name} (Alternative)` }); } } } return { available: conflicts.length === 0, conflicts, suggestions, }; } /** * Generate port availability error message for users */ function generatePortErrorMessage(conflicts) { if (conflicts.length === 0) return ''; const conflictList = conflicts.map(c => ` • Port ${c.port} (${c.service})`).join('\n'); return `❌ A2A Port Conflicts Detected: ${conflictList} 🔧 Solutions: 1. Stop processes using these ports 2. Use different ports in your A2A configuration 3. Check for duplicate A2A instances running 💡 To find processes using these ports: Windows: netstat -ano | findstr :<PORT> Linux/Mac: lsof -i :<PORT> Please resolve port conflicts before starting A2A servers.`; } /** * Gets an available port for A2A registry with fallback */ async function getAvailableRegistryPort(preferredPort = exports.DEFAULT_A2A_PORTS.REGISTRY) { if (await isPortAvailable(preferredPort)) { return preferredPort; } // Try common registry ports const commonRegistryPorts = [8080, 8081, 8082, 8083, 8090, 9080]; for (const port of commonRegistryPorts) { if (await isPortAvailable(port)) { return port; } } // Find any available port starting from 8080 const availablePort = await findNextAvailablePort(8080); if (availablePort === null) { throw new Error('No available ports found for A2A registry'); } return availablePort; } /** * Gets an available port for A2A agent with fallback */ async function getAvailableAgentPort(preferredPort = exports.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR) { if (await isPortAvailable(preferredPort)) { return preferredPort; } // Find next available port in the 7000-9000 range const availablePort = await findNextAvailablePort(7000, 100); if (availablePort === null) { throw new Error('No available ports found for A2A agent'); } return availablePort; } /** * Checks availability of predefined A2A agent ports */ async function checkPredefinedAgentsAvailability() { const agents = [...exports.PREDEFINED_AGENTS]; for (const agent of agents) { agent.available = await isPortAvailable(agent.port); } return agents; } /** * Gets placeholder URLs using dynamic configuration */ function getRegistryPlaceholder() { const config = detectInstanceUrl(); return buildUrl(config, exports.DEFAULT_A2A_PORTS.REGISTRY); } function getAgentPlaceholder() { const config = detectInstanceUrl(); return buildUrl(config, exports.DEFAULT_A2A_PORTS.AGENTS.TRANSLATOR); } /** * Detects the current n8n instance URL configuration * Priority order: * 1. N8N standard environment variables (WEBHOOK_URL, N8N_EDITOR_BASE_URL, etc.) * 2. N8N configuration variables (N8N_HOST, N8N_PROTOCOL, N8N_PORT) * 3. Runtime detection from request context (if available) * 4. Common deployment environment variables * 5. SSL/TLS detection * 6. Default to localhost */ function detectInstanceUrl() { // 1. Check for standard n8n webhook URL (most reliable) if (process.env.WEBHOOK_URL) { try { const url = new URL(process.env.WEBHOOK_URL); return { protocol: url.protocol.slice(0, -1), hostname: url.hostname, port: url.port ? parseInt(url.port, 10) : undefined, }; } catch (error) { console.warn('⚠️ Invalid WEBHOOK_URL format:', process.env.WEBHOOK_URL); } } // 2. Check for n8n editor base URL if (process.env.N8N_EDITOR_BASE_URL) { try { const url = new URL(process.env.N8N_EDITOR_BASE_URL); return { protocol: url.protocol.slice(0, -1), hostname: url.hostname, port: url.port ? parseInt(url.port, 10) : undefined, }; } catch (error) { console.warn('⚠️ Invalid N8N_EDITOR_BASE_URL format:', process.env.N8N_EDITOR_BASE_URL); } } // 3. Check for n8n webhook tunnel URL if (process.env.WEBHOOK_TUNNEL_URL) { try { const url = new URL(process.env.WEBHOOK_TUNNEL_URL); return { protocol: url.protocol.slice(0, -1), hostname: url.hostname, port: url.port ? parseInt(url.port, 10) : undefined, }; } catch (error) { console.warn('⚠️ Invalid WEBHOOK_TUNNEL_URL format:', process.env.WEBHOOK_TUNNEL_URL); } } // 4. Check for explicit n8n host configuration if (process.env.N8N_HOST) { return { protocol: process.env.N8N_PROTOCOL || detectProtocol(), hostname: process.env.N8N_HOST, port: process.env.N8N_PORT ? parseInt(process.env.N8N_PORT, 10) : undefined, }; } // 5. Try to detect from browser/request context (when available) // This would work when nodes are executed in response to HTTP requests if (typeof window !== 'undefined' && window.location) { return { protocol: window.location.protocol.slice(0, -1), hostname: window.location.hostname, port: window.location.port ? parseInt(window.location.port, 10) : undefined, }; } // 6. Check for common deployment environment variables const commonHostVars = [ 'PUBLIC_URL', 'BASE_URL', 'CANONICAL_HOST', 'VIRTUAL_HOST', 'HOST', 'HOSTNAME', 'SERVER_NAME', 'DOMAIN', ]; for (const envVar of commonHostVars) { const value = process.env[envVar]; if (value) { try { // Try to parse as URL first if (value.startsWith('http')) { const url = new URL(value); return { protocol: url.protocol.slice(0, -1), hostname: url.hostname, port: url.port ? parseInt(url.port, 10) : undefined, }; } else { // Treat as hostname return { protocol: detectProtocol(), hostname: value, }; } } catch (error) { console.warn(`⚠️ Invalid ${envVar} format:`, value); } } } // 7. Default to localhost with protocol detection return { protocol: detectProtocol(), hostname: 'localhost', }; } /** * Detects the protocol (http/https) based on various indicators */ function detectProtocol() { // Check for n8n SSL configuration if (process.env.N8N_SSL_KEY && process.env.N8N_SSL_CERT) { return 'https'; } // Check for explicit SSL/TLS indicators const httpsIndicators = [ process.env.N8N_PROTOCOL === 'https', process.env.SSL_ENABLED === 'true', process.env.HTTPS === 'true', process.env.TLS === 'true', process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1', // Check if running on standard HTTPS port process.env.PORT === '443' || process.env.N8N_PORT === '443', ]; return httpsIndicators.some(indicator => indicator) ? 'https' : 'http'; } /** * Builds a complete URL from configuration and path */ function buildUrl(config, port, path = '') { const { protocol, hostname } = config; // Smart port handling let portStr = ''; if (config.port) { // Use explicit port from config portStr = `:${config.port}`; } else if (port !== 80 && port !== 443) { // Use provided port unless it's standard HTTP/HTTPS port if (!(protocol === 'https' && port === 443) && !(protocol === 'http' && port === 80)) { portStr = `:${port}`; } } const cleanPath = path.startsWith('/') ? path : `/${path}`; return `${protocol}://${hostname}${portStr}${cleanPath}`; } /** * Gets the default URL for A2A agent services */ function getDefaultAgentUrl(port) { const config = detectInstanceUrl(); return buildUrl(config, port); } /** * Gets the default callback URL pattern for A2A callbacks */ function getDefaultCallbackUrl(port) { const config = detectInstanceUrl(); return buildUrl(config, port, '/tasks/TASK_ID/status'); } /** * Gets the default registry URL */ function getDefaultRegistryUrl(port = exports.DEFAULT_A2A_PORTS.REGISTRY) { const config = detectInstanceUrl(); return buildUrl(config, port); } /** * Creates endpoint URLs for an agent */ function createAgentEndpoints(port) { const baseUrl = getDefaultAgentUrl(port); // Remove trailing slash to avoid double slashes const cleanBaseUrl = baseUrl.replace(/\/+$/, ''); return { endpoint: baseUrl, tasks: `${cleanBaseUrl}/tasks`, health: `${cleanBaseUrl}/health`, capabilities: `${cleanBaseUrl}/capabilities`, }; } /** * Logs the detected URL configuration for debugging */ function logUrlConfiguration(port) { const config = detectInstanceUrl(); const baseUrl = buildUrl(config, port); console.log('🌐 A2A URL Configuration:'); console.log(` Protocol: ${config.protocol}`); console.log(` Hostname: ${config.hostname}`); console.log(` Port: ${config.port || port}`); console.log(` Base URL: ${baseUrl}`); console.log(` Detection: ${getDetectionSource()}`); } function getDetectionSource() { // Check in same order as detectInstanceUrl() if (process.env.WEBHOOK_URL) return 'WEBHOOK_URL (n8n standard)'; if (process.env.N8N_EDITOR_BASE_URL) return 'N8N_EDITOR_BASE_URL (n8n standard)'; if (process.env.WEBHOOK_TUNNEL_URL) return 'WEBHOOK_TUNNEL_URL (n8n standard)'; if (process.env.N8N_HOST) return 'N8N_HOST environment variable'; if (typeof window !== 'undefined' && window.location) { return 'Browser location (runtime detection)'; } const commonHostVars = ['PUBLIC_URL', 'BASE_URL', 'CANONICAL_HOST', 'VIRTUAL_HOST', 'HOST', 'HOSTNAME', 'SERVER_NAME', 'DOMAIN']; for (const envVar of commonHostVars) { if (process.env[envVar]) return `${envVar} environment variable`; } return 'Default localhost fallback'; } /** * Attempts to detect the current n8n instance URL from runtime context * This can be called from within node execution to get more accurate detection */ function detectRuntimeUrl() { // Try to detect from various runtime sources // 1. Check if we're in a browser context if (typeof window !== 'undefined' && window.location) { return { protocol: window.location.protocol.slice(0, -1), hostname: window.location.hostname, port: window.location.port ? parseInt(window.location.port, 10) : undefined, }; } // 2. Check if we can access request headers (in Express context) try { // This would work if we're in an HTTP request context // Note: This is a conceptual approach - actual implementation would depend on n8n's internal structure if (global.req && global.req.headers) { const req = global.req; const host = req.headers.host; const protocol = req.headers['x-forwarded-proto'] || (req.secure ? 'https' : 'http'); if (host) { const [hostname, port] = host.split(':'); return { protocol: protocol, hostname, port: port ? parseInt(port, 10) : undefined, }; } } } catch (error) { // Runtime detection failed, that's ok } return null; } /** * Enhanced URL detection that tries runtime detection first, then falls back to environment */ function detectInstanceUrlEnhanced() { // Try runtime detection first const runtimeConfig = detectRuntimeUrl(); if (runtimeConfig) { return runtimeConfig; } // Fall back to environment-based detection return detectInstanceUrl(); } /** * Check if a port is being used by an A2A service and get process info */ async function getPortProcessInfo(port) { return new Promise((resolve) => { const { exec } = require('child_process'); // Use netstat to find the process using the port exec(`netstat -ano | findstr :${port}`, (error, stdout) => { if (error || !stdout) { resolve({ inUse: false }); return; } // Parse netstat output to get PID const lines = stdout.split('\n').filter(line => line.includes('LISTENING')); if (lines.length === 0) { resolve({ inUse: false }); return; } // Extract PID from the last column const pidMatch = lines[0].trim().split(/\s+/).pop(); const pid = pidMatch ? parseInt(pidMatch, 10) : undefined; if (!pid) { resolve({ inUse: true, pid: undefined }); return; } // Check what process is running with this PID exec(`tasklist /FI "PID eq ${pid}" /FO CSV`, (error, stdout) => { if (error) { resolve({ inUse: true, pid }); return; } const isNodeProcess = stdout.includes('node.exe'); if (!isNodeProcess) { resolve({ inUse: true, pid, isA2AService: false, serviceType: 'other', canRestart: false }); return; } // For Node.js processes, we assume they might be A2A services // We'll check by trying to hit the health endpoint checkA2AServiceType(port).then(serviceType => { resolve({ inUse: true, pid, isA2AService: serviceType !== 'other', serviceType, canRestart: serviceType === 'registry' || serviceType === 'agent' }); }).catch(() => { resolve({ inUse: true, pid, isA2AService: false, serviceType: 'other', canRestart: false }); }); }); }); }); } /** * Check if a service on a port is an A2A service and what type */ async function checkA2AServiceType(port) { try { // Try to hit the health endpoint const response = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(2000) }); if (response.ok) { const data = await response.json(); // Check for A2A Registry indicators if (data.registry_name || data.total_agents !== undefined) { return 'registry'; } // Check for A2A Agent indicators if (data.agent_id || data.processing_mode) { return 'agent'; } } return 'other'; } catch (error) { return 'other'; } } /** * Kill a process by PID (Windows-specific) */ async function killProcess(pid) { return new Promise((resolve) => { const { exec } = require('child_process'); exec(`taskkill /PID ${pid} /F`, (error) => { if (error) { console.error(`Failed to kill process ${pid}:`, error.message); resolve(false); } else { console.log(`✅ Successfully killed process ${pid}`); resolve(true); } }); }); } /** * Smart port management - kills A2A services for restart, errors for conflicts */ async function handlePortConflict(port, serviceType, serviceName) { const processInfo = await getPortProcessInfo(port); if (!processInfo.inUse) { return { canProceed: true, message: `Port ${port} is available`, action: 'proceed' }; } // If it's an A2A service of the same type, kill it and restart if (processInfo.isA2AService && processInfo.serviceType === serviceType && processInfo.pid) { console.log(`🔄 ${serviceName}: Detected existing ${serviceType} on port ${port} (PID: ${processInfo.pid})`); console.log(`🔄 ${serviceName}: Killing previous instance to restart...`); const killed = await killProcess(processInfo.pid); if (killed) { // Wait a moment for the port to be released await new Promise(resolve => setTimeout(resolve, 1000)); return { canProceed: true, message: `Killed previous ${serviceType} instance and restarting on port ${port}`, action: 'killed_and_restart' }; } else { return { canProceed: false, message: `Failed to kill previous ${serviceType} instance on port ${port}`, action: 'error' }; } } // If it's a different type of service, show conflict error const alternativePort = await findNextAvailablePort(port + 1, 10); const conflicts = [{ port, service: serviceName }]; const errorMessage = generatePortErrorMessage(conflicts); let suggestionMessage = ''; if (alternativePort) { suggestionMessage = `\n💡 Suggested alternative: Port ${alternativePort} is available.\n\nTo fix this:\n1. Change the port to ${alternativePort}\n2. Or stop the process using port ${port}`; } else { suggestionMessage = `\n💡 No nearby ports available. Try ports in different ranges.`; } return { canProceed: false, message: `${errorMessage}${suggestionMessage}`, action: 'error' }; } /** * Validate port availability and show user-friendly errors for workflow validation * ⚠️ ENHANCED: Performs comprehensive validation that prevents race conditions */ async function validateNodePort(port, serviceType, serviceName, nodeId) { // STEP 1: Check if this exact node already has a server on this port const storedServer = getActiveServer(nodeId); if (storedServer && storedServer.port === port) { return { isValid: true }; // Same node will replace itself } // STEP 2: Check if ANY other A2A server is using this port const allActiveServers = listActiveServers(); const conflictingServer = allActiveServers.find(server => server.port === port && server.nodeId !== nodeId); if (conflictingServer) { const alternativePort = await findNextAvailablePort(port + 1, 10); const errorMessage = `🚫 PORT CONFLICT: Cannot activate workflow ❌ CONFLICT DETAILS: • Node "${serviceName}" wants to use port ${port} • Port ${port} is already occupied by: ${conflictingServer.nodeId} • Conflicting service type: ${conflictingServer.serviceType} • Server ID: ${conflictingServer.serverId} 🔧 SOLUTIONS TO FIX THIS: 1. ✅ Change port ${port} to ${alternativePort || 'different port'} in node "${serviceName}" 2. Remove or disable the conflicting node: ${conflictingServer.nodeId} 3. Use unique ports for each A2A node in your workflow 💡 ACTION REQUIRED: Update the port number in "${serviceName}" to resolve this conflict before activating the workflow.`; return { isValid: false, errorMessage, suggestedPort: alternativePort !== null ? alternativePort : undefined }; } // STEP 3: Check if port is available (not used by external services) const portAvailable = await isPortAvailable(port); if (portAvailable) { return { isValid: true }; } // STEP 4: Port is in use by external service - check what's using it const processInfo = await getPortProcessInfo(port); const alternativePort = await findNextAvailablePort(port + 1, 10); const serviceTypeText = processInfo.isA2AService ? `external A2A ${processInfo.serviceType}` : `external service (PID: ${processInfo.pid})`; const errorMessage = `🚫 PORT CONFLICT: Cannot activate workflow ❌ CONFLICT DETAILS: • Node "${serviceName}" wants to use port ${port} • Port ${port} is already occupied by: ${serviceTypeText} • Process ID: ${processInfo.pid || 'unknown'} • Service Type: ${processInfo.isA2AService ? processInfo.serviceType : 'non-A2A'} 🔧 SOLUTIONS TO FIX THIS: 1. ✅ Change port ${port} to ${alternativePort || 'different port'} in node "${serviceName}" 2. Stop the conflicting process: ${processInfo.pid ? `taskkill /PID ${processInfo.pid} /F` : 'identify and stop the service'} 3. Check for duplicate A2A instances running outside n8n 💡 ACTION REQUIRED: Update the port number in "${serviceName}" to resolve this conflict before activating the workflow.`; return { isValid: false, errorMessage, suggestedPort: alternativePort !== null ? alternativePort : undefined }; } /** * Check for A2A port conflicts across multiple nodes */ async function validateWorkflowPorts(nodes) { const errors = []; // Check for duplicates within the workflow itself const portMap = new Map(); for (const node of nodes) { if (!portMap.has(node.port)) { portMap.set(node.port, []); } portMap.get(node.port).push({ nodeId: node.nodeId, serviceName: node.serviceName }); } // Check for internal conflicts for (const [port, nodeList] of portMap.entries()) { if (nodeList.length > 1) { const conflictNames = nodeList.map(n => n.serviceName).join(', '); const alternativePort = await findNextAvailablePort(port + 1, 10); // Add error for all conflicting nodes except the first one for (let i = 1; i < nodeList.length; i++) { errors.push({ nodeId: nodeList[i].nodeId, errorMessage: `🚫 PORT CONFLICT: Multiple nodes using same port ❌ CONFLICT DETAILS: • Port ${port} is used by multiple nodes in this workflow • Conflicting nodes: ${conflictNames} • Each A2A node must use a unique port number 🔧 SOLUTIONS TO FIX THIS: 1. ✅ Change this node to use port ${alternativePort || 'different port'} 2. Assign unique ports to each A2A node in your workflow 3. Check your workflow configuration for duplicate port assignments 💡 ACTION REQUIRED: Update the port number in this node to resolve the conflict before activating the workflow.`, suggestedPort: alternativePort !== null ? alternativePort : undefined }); } } } // Check external conflicts for each unique port const checkedPorts = new Set(); for (const node of nodes) { if (checkedPorts.has(node.port)) continue; checkedPorts.add(node.port); // Skip if already has internal conflict if (portMap.get(node.port).length > 1) continue; const validation = await validateNodePort(node.port, node.serviceType, node.serviceName, node.nodeId); if (!validation.isValid && validation.errorMessage) { errors.push({ nodeId: node.nodeId, errorMessage: validation.errorMessage, suggestedPort: validation.suggestedPort }); } } return { isValid: errors.length === 0, errors }; }