UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

868 lines (866 loc) 37.5 kB
/** * Agent Executor * * Executes CLI-spawned agents by: * 1. Checking custom routing configuration (z.ai vs Anthropic) * 2. Invoking the appropriate API * 3. Managing agent lifecycle and output */ import { spawn } from 'child_process'; import { exec } from 'child_process'; import { promisify } from 'util'; import { createClient } from 'redis'; import { getAgentId } from './agent-prompt-builder.js'; import { buildCLIAgentSystemPrompt, loadContextFromEnv } from './cli-agent-context.js'; import { loadMessages, storeMessage, getCurrentFork, formatMessagesForAPI } from './conversation-fork.js'; import { convertToolNames } from './tool-definitions.js'; import { AgentCommandProcessor } from './coordination/agent-messaging.js'; import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import os from 'os'; import { execSync } from 'child_process'; const execAsync = promisify(exec); // DEBUG: File-based logging for background agents (stdio: 'ignore' masks errors) const AGENT_ID = process.env.AGENT_ID || 'unknown'; const LOG_FILE = `/tmp/cfn-agent-${AGENT_ID}.log`; function debugLog(message, data) { const timestamp = new Date().toISOString(); const logEntry = data ? `${timestamp} [${AGENT_ID}] ${message} ${JSON.stringify(data)}\n` : `${timestamp} [${AGENT_ID}] ${message}\n`; try { fsSync.appendFileSync(LOG_FILE, logEntry); } catch (err) { // Ignore logging errors } } debugLog('=== Agent Executor Started ==='); /** * Detect project root dynamically * Uses PROJECT_ROOT env var if set, otherwise tries git, falls back to cwd */ function getProjectRoot() { // 1. Check environment variable if (process.env.PROJECT_ROOT) { return process.env.PROJECT_ROOT; } // 2. Try git rev-parse (most reliable) try { const gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8', cwd: process.cwd(), stdio: [ 'pipe', 'pipe', 'ignore' ] }).trim(); if (gitRoot) { return gitRoot; } } catch { // Fall through to next method } // 3. Fall back to current working directory return process.cwd(); } const projectRoot = getProjectRoot(); // Bug #6 Fix: Read Redis connection parameters from process.env // ENV-001: Standardized environment variable naming (REDIS_PASSWORD for all deployments) // ROOT CAUSE #2 FIX: Don't fall back to REDIS_PASSWORD from parent env (CLI mode has no password) // FIX: Default to 'localhost' for CLI mode (host execution), not 'cfn-redis' (Docker) // CLI agents run on the host and connect to Redis at localhost:6379 // Docker agents should have CFN_REDIS_HOST explicitly set to their container network hostname const redisHost = process.env.CFN_REDIS_HOST || 'localhost'; const redisPort = process.env.CFN_REDIS_PORT || '6379'; const redisPassword = process.env.CFN_REDIS_PASSWORD || ''; /** * Validate task ID format to prevent command injection * Allows: optional namespace prefix (e.g., "cli:", "task:"), alphanumeric, hyphens, underscores, dots * Pattern: /^([a-z]+:)?[a-zA-Z0-9_.-]+$/ * - Optional prefix: lowercase letters followed by colon * - Main ID: alphanumeric, hyphens, underscores, dots * @param taskId - The task ID to validate * @throws Error if taskId is invalid */ function validateTaskId(taskId) { if (!taskId || !/^([a-z]+:)?[a-zA-Z0-9_.-]+$/.test(taskId)) { throw new Error(`Invalid task ID format: "${taskId}". Must contain optional namespace prefix (e.g., "cli:") and alphanumeric characters, hyphens, underscores, or dots.`); } } /** * Validate agent ID format to prevent command injection * Allows: alphanumeric, hyphens, underscores * @param agentId - The agent ID to validate * @throws Error if agentId is invalid */ function validateAgentId(agentId) { if (!agentId || !/^[a-zA-Z0-9_-]+$/.test(agentId)) { throw new Error(`Invalid agent ID format: "${agentId}". Must contain only alphanumeric characters, hyphens, and underscores.`); } } /** * Create a Redis client with proper connection handling * Uses environment variables for connection configuration * @returns Promise<RedisClientType> - Connected Redis client */ async function createRedisClient() { debugLog('createRedisClient: Starting Redis connection attempt'); debugLog('Redis config', { host: redisHost, port: redisPort, hasPassword: !!redisPassword }); console.error('[DEBUG] createRedisClient: Starting Redis connection...'); console.error(`[DEBUG] Redis config: host=${redisHost}, port=${redisPort}, hasPassword=${!!redisPassword}`); const portNum = parseInt(redisPort, 10); debugLog(`createRedisClient: Creating client for ${redisHost}:${portNum}`); const client = createClient({ socket: { host: redisHost, port: portNum, reconnectStrategy: (retries)=>Math.min(retries * 50, 500), connectTimeout: 5000 }, password: redisPassword || undefined }); client.on('error', (err)=>{ debugLog('Redis Client Error event triggered', { error: err.message, stack: err.stack }); console.error('[DEBUG] Redis Client Error:', err); }); debugLog('createRedisClient: Client created, calling connect()'); console.error('[DEBUG] createRedisClient: Client created, attempting connection...'); await client.connect(); debugLog('createRedisClient: ✓ Connected successfully'); console.error('[DEBUG] createRedisClient: ✓ Connected successfully'); return client; } /** * Parse context string into environment variables * * Parses context string like "TASK_ID='xyz' MODE='mvp' MAX_ITERATIONS=5" * and sets the values as environment variables. * * Supports: * - Single quoted: KEY='value with spaces' * - Double quoted: KEY="value with spaces" * - Unquoted: KEY=value (no spaces in value) * * @param contextString - Context string from --context parameter * @returns Object with parsed key-value pairs */ function parseContextToEnv(contextString) { if (!contextString) return {}; const envVars = {}; // Match KEY='value' or KEY="value" or KEY=value patterns // For quoted values: capture everything between quotes // For unquoted values: capture until next space or end of string const regex = /(\w+)=(?:(['"])([^\2]*?)\2|([^\s]+))/g; let match; while((match = regex.exec(contextString)) !== null){ const [, key, , quotedValue, unquotedValue] = match; const value = quotedValue !== undefined ? quotedValue : unquotedValue; envVars[key] = value; // Also set in process.env for current process process.env[key] = value; } return envVars; } /** * Extract confidence score from agent output * Looks for patterns like: * - "confidence: 0.85" * - "Confidence: 0.90" * - "confidence score: 0.95" * - "self-confidence: 0.88" */ function extractConfidence(output) { if (!output) return 0.85; // Try multiple patterns const patterns = [ /confidence:\s*([0-9.]+)/i, /confidence\s+score:\s*([0-9.]+)/i, /self-confidence:\s*([0-9.]+)/i, /my\s+confidence:\s*([0-9.]+)/i ]; for (const pattern of patterns){ const match = output.match(pattern); if (match && match[1]) { const score = parseFloat(match[1]); if (score >= 0 && score <= 1) { return score; } } } // Default to 0.85 if not found return 0.85; } /** * Execute CFN Loop protocol after agent completes work * * Steps: * 1. Signal completion to orchestrator and Main Chat via Redis (includes confidence) * 2. Exit cleanly (orchestrator spawns next agent if needed) * * Note: Confidence is included in the completion signal JSON payload. * The deprecated report-completion.sh script has been removed (Bug #4 fix). */ async function executeCFNProtocol(taskId, agentId, output, iteration, enableIterations = false, maxIterations = 10) { console.error('[DEBUG] executeCFNProtocol: ENTRY POINT REACHED'); console.log(`\n[CFN Protocol] Starting for agent ${agentId}`); console.log(`[CFN Protocol] Task ID: ${taskId}, Iteration: ${iteration}`); // SECURITY FIX: Validate inputs to prevent command injection console.error('[DEBUG] executeCFNProtocol: Validating taskId and agentId...'); validateTaskId(taskId); validateAgentId(agentId); console.error('[DEBUG] executeCFNProtocol: Validation passed'); let redisClient = null; try { // Step 1: Signal completion using Redis client (NOT shell commands) debugLog('executeCFNProtocol: Starting Step 1 (Redis signaling)', { taskId, agentId, iteration }); console.error('[DEBUG] executeCFNProtocol: Starting Step 1 (Redis signaling)...'); console.log('[CFN Protocol] Step 1: Signaling completion...'); debugLog('executeCFNProtocol: Calling createRedisClient()'); redisClient = await createRedisClient(); debugLog('executeCFNProtocol: Redis client obtained successfully'); console.error('[DEBUG] executeCFNProtocol: Redis client obtained'); // Signal to orchestrator (CFN Loop coordination) - using parameterized Redis call const orchestratorKey = `swarm:${taskId}:${agentId}:done`; debugLog('executeCFNProtocol: Sending orchestrator signal', { key: orchestratorKey }); console.error(`[DEBUG] executeCFNProtocol: Sending orchestrator signal to key: swarm:${taskId}:${agentId}:done`); await redisClient.lPush(orchestratorKey, 'complete'); debugLog('executeCFNProtocol: Orchestrator signal sent successfully'); console.error('[DEBUG] executeCFNProtocol: Orchestrator signal sent successfully'); console.log('[CFN Protocol] ✓ Orchestrator signal sent'); // Signal to Main Chat (CLI mode coordination - correct key format) - using parameterized Redis call const mainChatKey = `cfn:cli:${taskId}:completion`; debugLog('executeCFNProtocol: Preparing Main Chat signal', { key: mainChatKey }); console.error(`[DEBUG] executeCFNProtocol: Preparing Main Chat signal for key: cfn:cli:${taskId}:completion`); const confidence = extractConfidence(output); const agentMetadata = JSON.stringify({ agentId, taskId, status: 'completed', iteration, confidence }); debugLog('executeCFNProtocol: Agent metadata prepared', { metadata: agentMetadata, confidence }); await redisClient.lPush(mainChatKey, agentMetadata); debugLog('executeCFNProtocol: Main Chat signal sent successfully'); console.error('[DEBUG] executeCFNProtocol: Main Chat signal sent successfully'); console.log('[CFN Protocol] ✓ Main Chat signal sent'); console.log(`[CFN Protocol] ✓ Confidence reported: ${confidence}`); // Step 2: Exit cleanly (BUG #18 FIX - removed waiting mode) // Orchestrator will spawn appropriate specialist agent for next iteration // This enables adaptive agent specialization based on feedback type debugLog('executeCFNProtocol: Step 2 - Exiting cleanly'); console.error('[DEBUG] executeCFNProtocol: Step 2 - Exiting cleanly...'); console.log('[CFN Protocol] Step 2: Exiting cleanly (iteration complete)'); console.log('[CFN Protocol] Protocol complete\n'); debugLog('executeCFNProtocol: ✓ PROTOCOL COMPLETED SUCCESSFULLY'); console.error('[DEBUG] executeCFNProtocol: ✓ PROTOCOL COMPLETED SUCCESSFULLY'); } catch (error) { const errorDetails = error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : { error: String(error) }; debugLog('executeCFNProtocol: ERROR CAUGHT', errorDetails); console.error('[DEBUG] executeCFNProtocol: ERROR CAUGHT:', error); console.error('[CFN Protocol] Error:', error); throw error; } finally{ // Always close the Redis connection debugLog('executeCFNProtocol: FINALLY block - cleaning up Redis client'); console.error('[DEBUG] executeCFNProtocol: FINALLY block - cleaning up Redis client...'); if (redisClient) { await redisClient.quit(); debugLog('executeCFNProtocol: Redis client closed successfully'); console.error('[DEBUG] executeCFNProtocol: Redis client closed'); } else { debugLog('executeCFNProtocol: No Redis client to close'); } debugLog('executeCFNProtocol: FINALLY block complete'); console.error('[DEBUG] executeCFNProtocol: FINALLY block complete'); } } /** * Start command processor for bidirectional messaging with Main Chat * * Enables Main Chat to send commands to running agents: * - status: Request agent status update * - redirect: Redirect agent to new task context * - abort: Request clean agent abort * - pause: Pause agent for N seconds */ function startCommandProcessor(taskId, agentId, state) { // Only start if Redis is available and we have valid IDs if (!taskId || !agentId) { debugLog('startCommandProcessor: Skipping - missing taskId or agentId'); return null; } try { debugLog('startCommandProcessor: Creating processor', { taskId, agentId }); const processor = new AgentCommandProcessor({ taskId, agentId, redisHost: redisHost, redisPort: parseInt(redisPort, 10), redisPassword: redisPassword || undefined }); // Register redirect handler - updates agent context processor.onCommand('redirect', async (command)=>{ debugLog('Command received: redirect', command.payload); console.log(`[agent-msg] Received redirect command`); if (command.payload?.newTask) { state.redirectContext = command.payload.newTask; state.lastCommandTime = new Date().toISOString(); console.log(`[agent-msg] New context: ${state.redirectContext}`); } return { agentId, taskId, status: 'running', timestamp: new Date().toISOString(), currentStep: 'Acknowledged redirect', metadata: { redirectedTo: state.redirectContext } }; }); // Register custom status handler with more detail processor.onCommand('status', async (_command)=>{ debugLog('Command received: status request'); console.log(`[agent-msg] Received status request`); return { agentId, taskId, status: state.paused ? 'paused' : state.abortRequested ? 'aborting' : 'running', timestamp: new Date().toISOString(), metadata: { paused: state.paused, abortRequested: state.abortRequested, redirectContext: state.redirectContext, lastCommandTime: state.lastCommandTime } }; }); // Register abort handler processor.onCommand('abort', async (_command)=>{ debugLog('Command received: abort request'); console.log(`[agent-msg] Received abort request - will exit after current task`); state.abortRequested = true; state.lastCommandTime = new Date().toISOString(); return { agentId, taskId, status: 'aborting', timestamp: new Date().toISOString() }; }); // Start processor in background (non-blocking) processor.start(3).catch((err)=>{ debugLog('Command processor error', { error: err.message }); console.error('[agent-msg] Command processor error:', err); }); debugLog('startCommandProcessor: Started successfully'); console.log(`[agent-msg] Command processor started for ${agentId}`); return processor; } catch (err) { debugLog('startCommandProcessor: Failed to start', { error: err instanceof Error ? err.message : String(err) }); console.error('[agent-msg] Failed to start command processor:', err); return null; } } /** * Check if custom routing (non-Anthropic) is enabled * * BUG FIX: Now checks both CLAUDE_API_PROVIDER (legacy) and PROVIDER (from --provider flag) */ async function isCustomRoutingEnabled() { // Check environment variables - support both conventions const envProvider = process.env.CLAUDE_API_PROVIDER || process.env.PROVIDER; if (envProvider && envProvider !== 'anthropic') { debugLog('isCustomRoutingEnabled: Custom provider from env', { provider: envProvider }); return true; } // Check config file (.claude/config/api-provider.json) try { const configPath = path.join(projectRoot, '.claude', 'config', 'api-provider.json'); const config = JSON.parse(await fs.readFile(configPath, 'utf-8')); const isCustom = config.provider && config.provider !== 'anthropic'; debugLog('isCustomRoutingEnabled: Config file result', { provider: config.provider, isCustom }); return isCustom; } catch { return false; } } /** * Get API provider configuration * * Resolution order: * 1. CLAUDE_API_PROVIDER env var (legacy) * 2. PROVIDER env var (from CLI --provider flag) * 3. Config file * 4. Default to Z.ai (cost-effective fallback) * * BUG FIX: Previously only returned 'anthropic' | 'zai', now supports all providers * BUG FIX: Previously defaulted to Anthropic, now defaults to Z.ai */ async function getAPIProvider() { // Check environment variables const envProvider = process.env.CLAUDE_API_PROVIDER || process.env.PROVIDER; if (envProvider) { const normalized = envProvider.toLowerCase(); if ([ 'anthropic', 'zai', 'z.ai', 'kimi', 'openrouter' ].includes(normalized)) { const result = normalized === 'z.ai' ? 'zai' : normalized; debugLog('getAPIProvider: Resolved from env', { envProvider, result }); console.error(`[agent-executor] Provider from env: ${result}`); return result; } } // Check config file try { const configPath = path.join(projectRoot, '.claude', 'config', 'api-provider.json'); const config = JSON.parse(await fs.readFile(configPath, 'utf-8')); if (config.provider) { const normalized = config.provider.toLowerCase(); if ([ 'anthropic', 'zai', 'z.ai', 'kimi', 'openrouter' ].includes(normalized)) { const result = normalized === 'z.ai' ? 'zai' : normalized; debugLog('getAPIProvider: Resolved from config', { configProvider: config.provider, result }); console.error(`[agent-executor] Provider from config: ${result}`); return result; } } } catch { // Config file doesn't exist, use default } // Default to Z.ai (cost-effective fallback) debugLog('getAPIProvider: Using default', { result: 'zai' }); console.error('[agent-executor] Provider defaulting to: zai'); return 'zai'; } /** * Execute agent using direct API calls */ async function executeViaAPI(definition, prompt, context) { debugLog('executeViaAPI: FUNCTION ENTRY', { agentType: definition.name }); console.error('[DEBUG] executeViaAPI: FUNCTION ENTRY'); const agentId = getAgentId(definition, context); debugLog('executeViaAPI: Agent initialized', { agentId, taskId: context.taskId, iteration: context.iteration, hasContext: !!context.context }); console.error(`[DEBUG] executeViaAPI: Generated agentId=${agentId}`); console.error(`[DEBUG] executeViaAPI: context.taskId=${context.taskId}, context.iteration=${context.iteration}`); console.log(`[agent-executor] Executing agent via API: ${definition.name}`); console.log(`[agent-executor] Agent ID: ${agentId}`); console.log(`[agent-executor] Model: ${definition.model}`); console.log(''); // BUG #24 FIX: Parse and inject context environment variables // This enables CLI-spawned agents to access TASK_ID, MODE, etc. in Bash tool if (context.context) { console.log(`[agent-executor] Parsing context: ${context.context}`); const contextEnv = parseContextToEnv(context.context); console.log(`[agent-executor] Injected env vars: ${Object.keys(contextEnv).join(', ')}`); } // v2.0: Initialize runtime state for bidirectional messaging const runtimeState = { paused: false, abortRequested: false }; // v2.0: Start command processor for Main Chat communication let commandProcessor = null; if (context.taskId) { commandProcessor = startCommandProcessor(context.taskId, agentId, runtimeState); } try { // Check for conversation fork (Sprint 4 enhancement) const forkId = process.env.FORK_ID || await getCurrentFork(context.taskId || '', agentId); const iteration = context.iteration || 1; let systemPrompt; let messages = []; if (forkId && iteration > 1) { // Continue from fork (iterations 2+) console.log(`[agent-executor] Continuing from fork: ${forkId}`); // Load fork messages const forkMessages = await loadMessages(context.taskId || '', agentId, forkId); console.log(`[agent-executor] Loaded ${forkMessages.length} messages from fork`); // Extract system prompt from first message (it's always the system message) // The fork messages are assistant/user pairs, we need to add system separately systemPrompt = forkMessages[0]?.content || ''; // Format remaining messages for API messages = formatMessagesForAPI(forkMessages.slice(1)); // Add new user message with feedback messages.push({ role: 'user', content: prompt }); console.log(`[agent-executor] Fork continuation: ${messages.length} messages`); } else { // New conversation (iteration 1) console.log('[agent-executor] Starting new conversation'); console.log('[agent-executor] Building system prompt with context...'); const contextOptions = loadContextFromEnv(); contextOptions.agentType = definition.name; if (context.taskId) contextOptions.taskId = context.taskId; if (context.iteration) contextOptions.iteration = context.iteration; systemPrompt = await buildCLIAgentSystemPrompt(contextOptions); console.log('[agent-executor] System prompt built successfully'); // Initial user message messages = [ { role: 'user', content: prompt } ]; } console.log(''); // Dynamic import to avoid bundling issues const { executeAgentAPI } = await import('./anthropic-client.js'); // Convert agent tool names to Anthropic API format debugLog('[TOOL DEBUG] definition.tools check', { hasTools: !!definition.tools, toolsLength: definition.tools?.length || 0, toolNames: definition.tools || [] }); console.error(`[TOOL DEBUG] definition.tools: ${JSON.stringify(definition.tools)}`); const tools = definition.tools && definition.tools.length > 0 ? convertToolNames(definition.tools) : undefined; debugLog('[TOOL DEBUG] converted tools', { hasConvertedTools: !!tools, convertedToolsCount: tools?.length || 0, convertedToolNames: tools?.map((t)=>t.name) || [] }); console.error(`[TOOL DEBUG] Converted tools: ${tools?.map((t)=>t.name).join(', ') || 'NONE'}`); const result = await executeAgentAPI(definition.name, agentId, definition.model, prompt, systemPrompt, messages.length > 1 ? messages : undefined, undefined, tools // Pass converted tools ); // Store messages in conversation history (for future forking) if (context.taskId) { // Store user message const userMessage = { role: 'user', content: prompt, iteration, timestamp: new Date().toISOString() }; await storeMessage(context.taskId, agentId, userMessage); // Store assistant response if (result.output) { const assistantMessage = { role: 'assistant', content: result.output, iteration, timestamp: new Date().toISOString() }; await storeMessage(context.taskId, agentId, assistantMessage); } console.log(`[agent-executor] Stored messages for iteration ${iteration}`); // Execute CFN Loop protocol (signal completion, report confidence, enter waiting mode) // Iterations are enabled for CFN Loop tasks (indicated by presence of taskId) debugLog('agent-executor: About to execute CFN Protocol', { taskId: context.taskId, agentId, iteration, outputLength: result.output.length }); console.error('[DEBUG] agent-executor: About to execute CFN Protocol...'); console.error(`[DEBUG] agent-executor: taskId=${context.taskId}, agentId=${agentId}, iteration=${iteration}`); try { const maxIterations = 10; // Default max iterations const enableIterations = true; // Enable iterations for all CFN Loop tasks debugLog('agent-executor: Calling executeCFNProtocol', { maxIterations, enableIterations }); console.error('[DEBUG] agent-executor: Calling executeCFNProtocol...'); await executeCFNProtocol(context.taskId, agentId, result.output, iteration, enableIterations, maxIterations); debugLog('agent-executor: ✓ executeCFNProtocol returned successfully'); console.error('[DEBUG] agent-executor: ✓ executeCFNProtocol returned successfully'); } catch (error) { const errorDetails = error instanceof Error ? { message: error.message, stack: error.stack } : { error: String(error) }; debugLog('agent-executor: ✗ executeCFNProtocol threw error', errorDetails); console.error('[DEBUG] agent-executor: ✗ executeCFNProtocol threw error:', error); console.error('[agent-executor] CFN Protocol execution failed:', error); // Don't fail the entire agent execution if CFN protocol fails // This allows agents to complete even if Redis coordination has issues } console.error('[DEBUG] agent-executor: CFN Protocol section complete'); } return { success: result.success, agentId, output: result.output, error: result.error, exitCode: result.success ? 0 : 1 }; } catch (error) { console.error('[agent-executor] API execution failed:', error); return { success: false, agentId, error: error instanceof Error ? error.message : String(error), exitCode: 1 }; } finally{ // v2.0: Clean up command processor if (commandProcessor) { debugLog('executeViaAPI: Stopping command processor'); console.log('[agent-msg] Stopping command processor'); await commandProcessor.stop(); } } } /** * Execute agent using shell script (fallback/simulation) */ async function executeViaScript(definition, prompt, context) { const agentId = getAgentId(definition, context); // BUG #24 FIX: Parse and inject context environment variables if (context.context) { console.log(`[agent-executor] Parsing context: ${context.context}`); const contextEnv = parseContextToEnv(context.context); console.log(`[agent-executor] Injected env vars: ${Object.keys(contextEnv).join(', ')}`); } // Write prompt to temporary file const tmpDir = os.tmpdir(); const promptFile = path.join(tmpDir, `agent-${agentId}-${Date.now()}.md`); await fs.writeFile(promptFile, prompt, 'utf-8'); console.log(`[agent-executor] Executing agent via script: ${definition.name}`); console.log(`[agent-executor] Agent ID: ${agentId}`); console.log(`[agent-executor] Model: ${definition.model}`); console.log(`[agent-executor] Prompt file: ${promptFile}`); return new Promise((resolve)=>{ const scriptPath = path.join(projectRoot, '.claude', 'skills', 'agent-execution', 'execute-agent.sh'); // Build environment variables - WHITELIST ONLY APPROACH // SECURITY FIX: Do not use ...process.env spread which exposes ALL variables including secrets // Instead, explicitly whitelist safe variables to pass to spawned process const safeEnvVars = [ 'CFN_REDIS_HOST', 'CFN_REDIS_PORT', 'CFN_REDIS_PASSWORD', 'CFN_REDIS_URL', 'REDIS_PASSWORD', 'CFN_MEMORY_BUDGET', 'CFN_API_HOST', 'CFN_API_PORT', 'CFN_LOG_LEVEL', 'CFN_LOG_FORMAT', 'CFN_CONTAINER_MODE', 'CFN_DOCKER_SOCKET', 'CFN_NETWORK_NAME', 'CFN_CUSTOM_ROUTING', 'CFN_DEFAULT_PROVIDER', 'NODE_ENV', 'PATH', 'HOME', 'PWD' // Required for working directory context ]; const env = {}; // Add whitelisted variables for (const key of safeEnvVars){ const value = process.env[key]; if (value !== undefined) { env[key] = value; } } // Add API key only when explicitly needed (with strict validation) if (process.env.ANTHROPIC_API_KEY) { if (process.env.ANTHROPIC_API_KEY.match(/^sk-[a-zA-Z0-9-]+$/)) { env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; } } // Add agent execution context (safe values) env.AGENT_TYPE = definition.name; env.AGENT_ID = agentId; env.AGENT_MODEL = definition.model; env.AGENT_TOOLS = definition.tools.join(','); env.TASK_ID = context.taskId || ''; env.ITERATION = String(context.iteration || 1); env.MODE = context.mode || 'cli'; env.PROMPT_FILE = promptFile; // Check if execute script exists fs.access(scriptPath).then(()=>{ // Use execution script const proc = spawn('bash', [ scriptPath ], { env, stdio: 'inherit' }); proc.on('exit', (code)=>{ resolve({ success: code === 0, agentId, exitCode: code || 0 }); }); proc.on('error', (err)=>{ resolve({ success: false, agentId, error: err.message, exitCode: 1 }); }); }).catch(()=>{ // Fallback: Print prompt console.log('\n=== Agent Prompt ==='); console.log(prompt.substring(0, 500) + '...'); console.log('\n[agent-executor] Execution script not found'); console.log('[agent-executor] Using simulation mode\n'); resolve({ success: true, agentId, output: prompt, exitCode: 0 }); }); }); } /** * Main agent execution function */ export async function executeAgent(definition, prompt, context, options = {}) { const method = options.method || 'auto'; debugLog('executeAgent: Starting execution', { agentType: definition.name, method, taskId: context.taskId, iteration: context.iteration, agentId: context.agentId || 'not-set' }); // Auto-select execution method if (method === 'auto') { // Try API execution first, fallback to script if API key not available try { debugLog('executeAgent: Attempting API execution'); return await executeViaAPI(definition, prompt, context); } catch (error) { if (error instanceof Error && error.message.includes('API key not found')) { debugLog('executeAgent: API key not found, falling back to script'); console.log('[agent-executor] API key not found, using script fallback'); return executeViaScript(definition, prompt, context); } debugLog('executeAgent: API execution error', { error: error instanceof Error ? error.message : String(error) }); throw error; } } if (method === 'api') { debugLog('executeAgent: Using API method'); return executeViaAPI(definition, prompt, context); } debugLog('executeAgent: Using script method'); return executeViaScript(definition, prompt, context); } /** * Write agent output to file for debugging */ export async function saveAgentOutput(agentId, output, outputDir = '.claude/tmp/agent-output') { await fs.mkdir(outputDir, { recursive: true }); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `${agentId}-${timestamp}.txt`; const filepath = path.join(outputDir, filename); await fs.writeFile(filepath, output, 'utf-8'); return filepath; } /** * Main entry point when run as a script via tsx */ async function main() { // Parse command line arguments const args = process.argv.slice(2); let agentType; for(let i = 0; i < args.length; i++){ if (args[i] === '--agent-type' && i + 1 < args.length) { agentType = args[i + 1]; break; } } if (!agentType) { console.error('[agent-executor] ERROR: --agent-type is required'); process.exit(1); } // Load agent definition const { parseAgentDefinition } = await import('./agent-definition-parser.js'); const definition = await parseAgentDefinition(agentType); if (!definition) { console.error(`[agent-executor] ERROR: Agent type not found: ${agentType}`); process.exit(1); } // Build task context from environment variables const context = { taskId: process.env.TASK_ID, iteration: process.env.ITERATION ? parseInt(process.env.ITERATION, 10) : 1, mode: process.env.MODE || 'cli', agentId: process.env.AGENT_ID, context: process.env.CONTEXT }; // Get prompt from environment variable or use default const prompt = process.env.PROMPT || `Execute your assigned task for ${agentType}. You are part of a CFN Loop workflow. Review any broadcast messages, complete your work, and report your confidence score.`; console.log(`[agent-executor] Starting agent: ${agentType}`); console.log(`[agent-executor] Task ID: ${context.taskId || 'none'}`); console.log(`[agent-executor] Iteration: ${context.iteration}`); console.log(`[agent-executor] Prompt source: ${process.env.PROMPT ? 'PROMPT env var' : 'default'}`); // Execute agent const result = await executeAgent(definition, prompt, context); // Exit with appropriate code if (!result.success) { console.error(`[agent-executor] Agent execution failed: ${result.error || 'unknown error'}`); process.exit(result.exitCode || 1); } console.log('[agent-executor] Agent execution completed successfully'); process.exit(0); } // Run main if executed as a script if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error)=>{ console.error('[agent-executor] Fatal error:', error); process.exit(1); }); } //# sourceMappingURL=agent-executor.js.map