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.
389 lines (385 loc) • 14.3 kB
JavaScript
/**
* Agent Spawning CLI - Direct agent process spawning
*
* Usage:
* npx cfn-spawn agent <type> [options]
* npx cfn-spawn <type> [options] (agent is implied)
*
* Examples:
* npx cfn-spawn agent researcher --task-id task-123 --iteration 1
* npx cfn-spawn researcher --task-id task-123 --iteration 1
*/ import { spawn, execFileSync } from 'child_process';
/**
* SECURITY: Parameter validation to prevent command injection (CVSS 8.9)
* Validates all parameters that will be passed to execFileSync()
*/ /**
* Validates taskId format to prevent command injection attacks
* Pattern: optional namespace prefix (e.g., "cli:", "task:"), alphanumeric, underscore, hyphen, dot (max 64 chars)
* Examples: "task-123", "cli:task-456", "orchestrator:batch-789"
*/ function validateTaskId(taskId) {
if (typeof taskId !== 'string' || taskId.length === 0) {
return {
valid: false,
error: 'Task ID must be a non-empty string'
};
}
// Allow optional namespace prefix (lowercase letters + colon), followed by alphanumeric/underscore/hyphen/dot
const taskIdPattern = /^([a-z]+:)?[a-zA-Z0-9_.-]{1,64}$/;
if (!taskIdPattern.test(taskId)) {
return {
valid: false,
error: 'Invalid task ID format - must contain optional namespace prefix (e.g., "cli:") and alphanumeric characters, underscores, hyphens, or dots (max 64 chars)'
};
}
return {
valid: true
};
}
/**
* Validates Redis host to prevent command injection
* Allows: hostnames, domain names, localhost, IPv4, IPv6 (::1)
*/ function validateRedisHost(host) {
if (typeof host !== 'string' || host.length === 0) {
return {
valid: false,
error: 'Redis host must be a non-empty string'
};
}
// Pattern: alphanumeric, hyphens, dots (for domains), plus special IPv6 loopback
const hostPattern = /^[a-zA-Z0-9.-]+$|^::1$/;
if (!hostPattern.test(host)) {
return {
valid: false,
error: 'Invalid Redis host format'
};
}
return {
valid: true
};
}
/**
* Validates Redis port to prevent command injection
* Range: 1-65535
*/ function validateRedisPort(port) {
if (typeof port !== 'string' || port.length === 0) {
return {
valid: false,
error: 'Redis port must be a non-empty string'
};
}
const portNum = parseInt(port, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return {
valid: false,
error: 'Invalid Redis port - must be between 1 and 65535'
};
}
return {
valid: true
};
}
/**
* Safely retrieves context from Redis using execFileSync()
* Prevents command injection by using array-based arguments instead of template literals
*/ function getRedisContextSafely(taskId, redisHost, redisPort, contextKey) {
try {
// Validate all parameters BEFORE executing
const taskIdValidation = validateTaskId(taskId);
if (!taskIdValidation.valid) {
console.warn(`[cfn-spawn] Invalid task ID: ${taskIdValidation.error}`);
return '';
}
const hostValidation = validateRedisHost(redisHost);
if (!hostValidation.valid) {
console.warn(`[cfn-spawn] Invalid Redis host: ${hostValidation.error}`);
return '';
}
const portValidation = validateRedisPort(redisPort);
if (!portValidation.valid) {
console.warn(`[cfn-spawn] Invalid Redis port: ${portValidation.error}`);
return '';
}
// All parameters validated - now execute safely with execFileSync()
// Using array arguments prevents shell interpolation of metacharacters
const redisKey = `swarm:${taskId}:${contextKey}`;
const result = execFileSync('redis-cli', [
'-h',
redisHost,
'-p',
redisPort,
'get',
redisKey
], {
encoding: 'utf8'
});
const trimmed = result.trim();
return trimmed === '(nil)' ? '' : trimmed;
} catch (e) {
// Redis not available or key doesn't exist - fail silently
return '';
}
}
/**
* Parse command line arguments for agent spawning
*/ export function parseAgentArgs(args) {
// Handle both "agent <type>" and "<type>" patterns
let agentType;
let optionArgs;
if (args[0] === 'agent') {
agentType = args[1];
optionArgs = args.slice(2);
} else {
agentType = args[0];
optionArgs = args.slice(1);
}
// Validate agent type exists and is not a flag
if (!agentType || agentType.startsWith('--')) {
console.error('Error: Agent type is required');
console.error('Usage: cfn-spawn agent <type> [options]');
return null;
}
const options = {
agentType
};
// Parse optional parameters
for(let i = 0; i < optionArgs.length; i += 2){
const key = optionArgs[i];
const value = optionArgs[i + 1];
switch(key){
case '--agent-id':
options.agentId = value;
break;
case '--task-id':
options.taskId = value;
break;
case '--iteration':
options.iteration = parseInt(value, 10);
break;
case '--context':
options.context = value;
break;
case '--mode':
options.mode = value;
break;
case '--priority':
options.priority = parseInt(value, 10);
break;
case '--parent-task':
case '--parent-task-id':
options.parentTaskId = value;
break;
default:
console.warn(`Unknown option: ${key}`);
}
}
return options;
}
/**
* Spawn an agent process using npx claude-flow-novice agent
*
* This is a wrapper/alias for the existing claude-flow-novice agent spawning mechanism
* Provides the cfn-spawn naming pattern while delegating to the working implementation
*/ export async function spawnAgent(options) {
const { agentType, agentId, taskId, iteration, context, mode, priority, parentTaskId } = options;
console.log(`[cfn-spawn] Spawning agent: ${agentType}`);
if (agentId) console.log(`[cfn-spawn] Agent ID: ${agentId}`);
if (taskId) console.log(`[cfn-spawn] Task ID: ${taskId}`);
if (iteration) console.log(`[cfn-spawn] Iteration: ${iteration}`);
if (context) console.log(`[cfn-spawn] Context: ${context}`);
if (mode) console.log(`[cfn-spawn] Mode: ${mode}`);
// Build command arguments for npx claude-flow-novice agent
const claudeArgs = [
'claude-flow-novice',
'agent',
agentType
];
// Add optional parameters
if (agentId) {
claudeArgs.push('--agent-id', agentId);
}
if (taskId) {
claudeArgs.push('--task-id', taskId);
}
if (iteration) {
claudeArgs.push('--iteration', iteration.toString());
}
if (context) {
claudeArgs.push('--context', context);
}
if (mode) {
claudeArgs.push('--mode', mode);
}
if (priority) {
claudeArgs.push('--priority', priority.toString());
}
if (parentTaskId) {
claudeArgs.push('--parent-task-id', parentTaskId);
}
// Fetch epic context from Redis if available
let epicContext = '';
let phaseContext = '';
let successCriteria = '';
if (taskId) {
// SECURITY FIX (CVSS 8.9): Use safe parameter validation and execFileSync()
// Prevent command injection by validating all parameters BEFORE execution
// FIX: Default to 'localhost' for CLI mode (host execution), not 'cfn-redis' (Docker)
const redisHost = process.env.CFN_REDIS_HOST || 'localhost';
const redisPort = process.env.CFN_REDIS_PORT || '6379';
// Validate Redis connection parameters
const hostValidation = validateRedisHost(redisHost);
const portValidation = validateRedisPort(redisPort);
if (hostValidation.valid && portValidation.valid) {
// Try to read epic-level context from Redis
epicContext = getRedisContextSafely(taskId, redisHost, redisPort, 'epic-context');
// Try to read phase-specific context
phaseContext = getRedisContextSafely(taskId, redisHost, redisPort, 'phase-context');
// Try to read success criteria
successCriteria = getRedisContextSafely(taskId, redisHost, redisPort, 'success-criteria');
if (epicContext) {
console.log(`[cfn-spawn] Epic context loaded from Redis`);
}
} else {
console.warn(`[cfn-spawn] Invalid Redis configuration - skipping context load`);
if (!hostValidation.valid) console.warn(`[cfn-spawn] ${hostValidation.error}`);
if (!portValidation.valid) console.warn(`[cfn-spawn] ${portValidation.error}`);
}
}
// Add environment variables for agent context - 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
];
// Build whitelist-only env object
const env = {};
// Add whitelisted CFN 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) {
// Validate format: should start with "sk-" or "sk-ant-"
if (process.env.ANTHROPIC_API_KEY.match(/^sk-[a-zA-Z0-9-]+$/)) {
env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
} else {
console.warn('[cfn-spawn] Warning: ANTHROPIC_API_KEY format invalid, not passing to agent');
}
}
// Add task and execution context variables
env.AGENT_TYPE = agentType;
env.TASK_ID = taskId || '';
env.ITERATION = iteration?.toString() || '1';
env.CONTEXT = context || '';
env.MODE = mode || 'cli';
env.PRIORITY = priority?.toString() || '5';
env.PARENT_TASK_ID = parentTaskId || '';
// Epic-level context from Redis (sanitized)
env.EPIC_CONTEXT = epicContext;
env.PHASE_CONTEXT = phaseContext;
env.SUCCESS_CRITERIA = successCriteria;
console.log(`[cfn-spawn] Executing: npx ${claudeArgs.join(' ')}`);
// Spawn the claude-flow-novice agent process
const agentProcess = spawn('npx', claudeArgs, {
stdio: 'inherit',
env,
cwd: process.cwd()
});
// Handle process exit
agentProcess.on('exit', (code, signal)=>{
if (code === 0) {
console.log(`[cfn-spawn] Agent ${agentType} completed successfully`);
} else {
console.error(`[cfn-spawn] Agent ${agentType} exited with code ${code}, signal ${signal}`);
}
process.exit(code || 0);
});
// Handle process errors
agentProcess.on('error', (err)=>{
console.error(`[cfn-spawn] Failed to spawn agent ${agentType}:`, err.message);
process.exit(1);
});
// Cleanup on parent exit
process.on('SIGINT', ()=>{
console.log('\n[cfn-spawn] Received SIGINT, terminating agent...');
agentProcess.kill('SIGINT');
});
process.on('SIGTERM', ()=>{
console.log('\n[cfn-spawn] Received SIGTERM, terminating agent...');
agentProcess.kill('SIGTERM');
});
}
/**
* Build task description for the agent
*/ export function buildTaskDescription(agentType, taskId, iteration, context) {
let desc = `Execute task as ${agentType} agent`;
if (taskId) desc += ` for task ${taskId}`;
if (iteration !== undefined) desc += ` (iteration ${iteration})`;
if (context) desc += `: ${context}`;
return desc;
}
/**
* Main CLI entry point
*/ export async function main(args = process.argv.slice(2)) {
// Show help if requested
if (args.includes('--help') || args.includes('-h')) {
console.log(`
cfn-spawn - Claude Flow Novice Agent Spawner
Usage:
cfn-spawn agent <type> [options]
cfn-spawn <type> [options] (agent is implied)
Options:
--agent-id <id> Explicit agent identifier (overrides auto-generation)
--task-id <id> Task identifier
--iteration <n> Iteration number
--context <text> Context description
--mode <mode> Execution mode (cli, api, hybrid)
--priority <1-10> Task priority
--parent-task-id <id> Parent task identifier
Examples:
cfn-spawn agent researcher --task-id task-123 --iteration 1
cfn-spawn coder --task-id auth-impl --context "Implement JWT auth"
cfn-spawn reviewer --task-id auth-impl --iteration 2 --mode cli
cfn-spawn tester --agent-id tester-1-1 --task-id test-phase --iteration 1
`);
return;
}
// Parse arguments
const options = parseAgentArgs(args);
if (!options) {
process.exit(1);
}
// Spawn the agent
await spawnAgent(options);
}
// Run if called directly
// ES module check - compare import.meta.url with the executed file
const isMainModule = import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, '/') || '');
if (isMainModule) {
main().catch((err)=>{
console.error('[cfn-spawn] Fatal error:', err);
process.exit(1);
});
}
//# sourceMappingURL=agent-spawn.js.map