mcp-adr-analysis-server
Version:
MCP server for analyzing Architectural Decision Records and project architecture
642 lines • 22.8 kB
JavaScript
/**
* CE-MCP Sandbox Executor
*
* Executes orchestration directives in an isolated sandbox environment.
* Replaces direct OpenRouter calls with local execution of LLM-generated code.
*
* Performance Optimizations (Phase 4.4):
* - Parallel operation execution for independent operations
* - Batched file system operations
* - LRU cache with automatic eviction
* - Lazy dependency resolution
*
* @see ADR-014: CE-MCP Architecture
*/
import { readFile, readdir, stat, access } from 'fs/promises';
import { constants } from 'fs';
import { join, resolve } from 'path';
import { isOrchestrationDirective, isStateMachineDirective, } from '../types/ce-mcp.js';
/**
* Default CE-MCP configuration
*/
export const DEFAULT_CEMCP_CONFIG = {
mode: 'directive',
sandbox: {
enabled: true,
timeout: 30000, // 30 seconds
memoryLimit: 256 * 1024 * 1024, // 256 MB
fsOperationsLimit: 1000,
networkAllowed: false,
},
prompts: {
lazyLoading: true,
cacheEnabled: true,
cacheTTL: 3600, // 1 hour
},
fallback: {
enabled: true,
maxRetries: 2,
},
};
/**
* Maximum cache sizes for memory management (Phase 4.4 optimization)
*/
const MAX_OPERATION_CACHE_SIZE = 500;
const MAX_PROMPT_CACHE_SIZE = 100;
/**
* Sandbox Executor Class
*
* Executes CE-MCP directives in an isolated environment with:
* - Process isolation (operation-level sandboxing)
* - Filesystem restrictions (project path only)
* - Resource limits (timeout, memory, operations)
* - State management (results persist across operations)
* - LRU cache eviction for memory efficiency (Phase 4.4)
*/
export class SandboxExecutor {
config;
operationCache = new Map();
promptCache = new Map();
cacheHits = 0;
cacheMisses = 0;
constructor(config) {
this.config = { ...DEFAULT_CEMCP_CONFIG, ...config };
}
/**
* Evict oldest entries when cache exceeds max size (LRU strategy)
*/
evictOldestEntries(cache, maxSize) {
if (cache.size <= maxSize)
return;
// Sort by last access time
const entries = Array.from(cache.entries()).sort((a, b) => a[1].lastAccess - b[1].lastAccess);
// Remove oldest 25% of entries
const removeCount = Math.ceil(cache.size * 0.25);
for (let i = 0; i < removeCount && i < entries.length; i++) {
const key = entries[i]?.[0];
if (key) {
cache.delete(key);
}
}
}
/**
* Execute an orchestration directive
*/
async executeDirective(directive, projectPath) {
const startTime = Date.now();
const context = this.createContext(projectPath);
const cachedOps = [];
try {
if (isOrchestrationDirective(directive)) {
return await this.executeOrchestrationDirective(directive, context, startTime, cachedOps);
}
else if (isStateMachineDirective(directive)) {
return await this.executeStateMachineDirective(directive, context, startTime, cachedOps);
}
else {
throw new Error('Unknown directive type');
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
executionTime: Date.now() - startTime,
operationsExecuted: context.state.size,
cachedOperations: cachedOps,
},
};
}
}
/**
* Execute an orchestration directive
*/
async executeOrchestrationDirective(directive, context, startTime, cachedOps) {
let operationsExecuted = 0;
// Execute each operation in sequence
for (const operation of directive.sandbox_operations) {
// Check timeout
if (Date.now() - startTime > this.config.sandbox.timeout) {
throw new Error(`Sandbox execution timeout after ${this.config.sandbox.timeout}ms`);
}
// Check condition if present
if (operation.condition && !this.evaluateCondition(operation.condition, context)) {
continue;
}
// Check cache
const cacheKey = this.generateOperationCacheKey(operation);
const cached = this.getCachedOperation(cacheKey);
if (cached !== undefined) {
if (operation.store) {
context.state.set(operation.store, cached);
}
cachedOps.push(operation.op);
continue;
}
// Execute operation
const result = await this.executeOperation(operation, context);
operationsExecuted++;
// Store result if requested
if (operation.store) {
context.state.set(operation.store, result);
}
// Cache result if cacheable
if (directive.metadata?.cacheable) {
this.setCachedOperation(cacheKey, result);
}
// Return early if this operation is marked as return
if (operation.return) {
return {
success: true,
data: result,
metadata: {
executionTime: Date.now() - startTime,
operationsExecuted,
cachedOperations: cachedOps,
},
};
}
}
// Compose final result if composition directive exists
let finalResult;
if (directive.compose) {
finalResult = this.composeResult(directive.compose, context);
}
else {
// Return all stored state
finalResult = Object.fromEntries(context.state);
}
return {
success: true,
data: finalResult,
metadata: {
executionTime: Date.now() - startTime,
operationsExecuted,
cachedOperations: cachedOps,
},
};
}
/**
* Execute a state machine directive
*/
async executeStateMachineDirective(directive, context, startTime, cachedOps) {
let currentState = 'initial';
let operationsExecuted = 0;
// Initialize state
for (const [key, value] of Object.entries(directive.initial_state)) {
context.state.set(key, value);
}
// Execute transitions
while (currentState !== directive.final_state) {
// Check timeout
if (Date.now() - startTime > this.config.sandbox.timeout) {
throw new Error(`State machine timeout after ${this.config.sandbox.timeout}ms`);
}
// Find matching transition
const transition = directive.transitions.find(t => t.from === currentState);
if (!transition) {
throw new Error(`No transition found from state: ${currentState}`);
}
try {
// Execute transition operation
if (typeof transition.operation === 'string') {
// Tool invocation (not implemented in sandbox - would need host LLM)
throw new Error(`Tool invocation not supported in sandbox: ${transition.operation}`);
}
else {
const result = await this.executeOperation(transition.operation, context);
if (transition.operation.store) {
context.state.set(transition.operation.store, result);
}
}
operationsExecuted++;
currentState = transition.next_state;
}
catch (error) {
if (transition.on_error === 'skip') {
currentState = transition.next_state;
}
else if (transition.on_error === 'abort') {
throw error;
}
else {
throw error;
}
}
}
return {
success: true,
data: context.state.get(directive.final_state),
metadata: {
executionTime: Date.now() - startTime,
operationsExecuted,
cachedOperations: cachedOps,
},
};
}
/**
* Execute a single sandbox operation
*/
async executeOperation(operation, context) {
// Resolve inputs
const input = operation.input ? context.state.get(operation.input) : undefined;
const inputs = operation.inputs?.map(key => context.state.get(key));
switch (operation.op) {
case 'loadKnowledge':
return this.opLoadKnowledge(operation.args, context);
case 'loadPrompt':
return this.opLoadPrompt(operation.args, context);
case 'analyzeFiles':
return this.opAnalyzeFiles(operation.args, context);
case 'scanEnvironment':
return this.opScanEnvironment(operation.args, context);
case 'generateContext':
return this.opGenerateContext(operation.args, inputs, context);
case 'composeResult':
return this.opComposeResult(operation.args, inputs, context);
case 'validateOutput':
return this.opValidateOutput(operation.args, input, context);
case 'cacheResult':
return this.opCacheResult(operation.args, input, context);
case 'retrieveCache':
return this.opRetrieveCache(operation.args, context);
default:
throw new Error(`Unknown operation: ${operation.op}`);
}
}
/**
* Operation: Load domain-specific knowledge
*/
async opLoadKnowledge(args, context) {
const domain = args?.['domain'] || 'general';
const scope = args?.['scope'] || 'project';
// Return knowledge structure for the domain
// In CE-MCP, this provides structured data instead of LLM-generated content
return {
domain,
scope,
projectPath: context.projectPath,
timestamp: new Date().toISOString(),
// Placeholder - would load from knowledge graph in full implementation
knowledge: {
patterns: [],
conventions: [],
decisions: [],
},
};
}
/**
* Operation: Load prompt template (lazy loading)
*/
async opLoadPrompt(args, context) {
const promptName = args?.['name'];
const section = args?.['section'];
if (!promptName) {
throw new Error('loadPrompt requires "name" argument');
}
// Check prompt cache with LRU tracking
const cacheKey = `prompt:${promptName}:${section || 'full'}`;
const cached = this.promptCache.get(cacheKey);
if (cached && Date.now() < cached.expiry) {
cached.lastAccess = Date.now();
this.cacheHits++;
return cached.result;
}
this.cacheMisses++;
if (cached) {
this.promptCache.delete(cacheKey);
}
// Load prompt from file system
// In full implementation, would use prompt catalog
const promptPath = join(context.projectPath, 'src', 'prompts', `${promptName}.ts`);
try {
const content = await readFile(promptPath, 'utf-8');
// Evict old entries if cache is full
this.evictOldestEntries(this.promptCache, MAX_PROMPT_CACHE_SIZE);
// Cache the result with LRU tracking
this.promptCache.set(cacheKey, {
result: content,
expiry: Date.now() + this.config.prompts.cacheTTL * 1000,
lastAccess: Date.now(),
});
return content;
}
catch {
// Return placeholder if prompt not found
return `[Prompt: ${promptName}${section ? `:${section}` : ''}]`;
}
}
/**
* Operation: Analyze project files
*/
async opAnalyzeFiles(args, context) {
const patterns = args?.['patterns'] || ['**/*.ts'];
const maxFiles = args?.['maxFiles'] || 100;
const files = [];
// Scan directory (limited to project path)
const scanDir = async (dir, depth = 0) => {
if (depth > 5 || files.length >= maxFiles)
return;
try {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (files.length >= maxFiles)
break;
const fullPath = join(dir, entry.name);
// Skip node_modules, .git, etc.
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
continue;
}
if (entry.isDirectory()) {
await scanDir(fullPath, depth + 1);
}
else if (entry.isFile()) {
// Check if file matches patterns
const matchesPattern = patterns.some(pattern => {
if (pattern.includes('*')) {
const ext = pattern.replace('**/*.', '.');
return entry.name.endsWith(ext);
}
return entry.name === pattern;
});
if (matchesPattern) {
const stats = await stat(fullPath);
files.push({
path: fullPath.replace(context.projectPath, ''),
size: stats.size,
type: entry.name.split('.').pop() || 'unknown',
});
}
}
}
}
catch {
// Ignore errors for inaccessible directories
}
};
await scanDir(context.projectPath);
return {
totalFiles: files.length,
files,
patterns,
scannedAt: new Date().toISOString(),
};
}
/**
* Operation: Scan environment configuration
* Optimized with parallel file checks (Phase 4.4)
*/
async opScanEnvironment(_args, context) {
const checkFiles = [
'package.json',
'tsconfig.json',
'.env.example',
'docker-compose.yml',
'Dockerfile',
'.github/workflows',
];
// Batch file existence checks in parallel for better performance
const checkPromises = checkFiles.map(async (file) => {
try {
await access(join(context.projectPath, file), constants.F_OK);
return { file, exists: true };
}
catch {
return { file, exists: false };
}
});
const results = await Promise.all(checkPromises);
const found = {};
for (const { file, exists } of results) {
found[file] = exists;
}
// Read package.json for dependencies
let dependencies = {};
let devDependencies = {};
try {
const pkgContent = await readFile(join(context.projectPath, 'package.json'), 'utf-8');
const pkg = JSON.parse(pkgContent);
dependencies = pkg.dependencies || {};
devDependencies = pkg.devDependencies || {};
}
catch {
// Ignore if package.json doesn't exist
}
return {
configFiles: found,
dependencies: Object.keys(dependencies).length,
devDependencies: Object.keys(devDependencies).length,
hasTypeScript: found['tsconfig.json'],
hasDocker: found['Dockerfile'] || found['docker-compose.yml'],
hasCI: found['.github/workflows'],
scannedAt: new Date().toISOString(),
};
}
/**
* Operation: Generate context from multiple inputs
*/
async opGenerateContext(args, inputs, _context) {
const contextType = args?.['type'] || 'analysis';
return {
type: contextType,
inputs: inputs || [],
inputCount: inputs?.length || 0,
generatedAt: new Date().toISOString(),
};
}
/**
* Operation: Compose final result
*/
async opComposeResult(args, _inputs, context) {
const template = args?.['template'] || 'default';
const format = args?.['format'] || 'json';
// Compose result from all state
const composed = {
template,
format,
timestamp: new Date().toISOString(),
data: {},
};
// Include all state data
for (const [key, value] of context.state) {
composed['data'][key] = value;
}
return composed;
}
/**
* Operation: Validate output against schema
*/
async opValidateOutput(_args, input, _context) {
// Basic validation - in full implementation would use schema validation
if (input === null || input === undefined) {
return { valid: false, errors: ['Input is null or undefined'] };
}
return { valid: true };
}
/**
* Operation: Cache a result
*/
async opCacheResult(args, input, _context) {
const key = args?.['key'] || 'default';
const ttl = args?.['ttl'] || this.config.prompts.cacheTTL;
this.operationCache.set(key, {
result: input,
expiry: Date.now() + ttl * 1000,
lastAccess: Date.now(),
});
return true;
}
/**
* Operation: Retrieve cached result
*/
async opRetrieveCache(args, _context) {
const key = args?.['key'] || 'default';
const cached = this.operationCache.get(key);
if (cached && Date.now() < cached.expiry) {
return cached.result;
}
return null;
}
/**
* Create sandbox context for execution
*/
createContext(projectPath) {
return {
projectPath: resolve(projectPath),
workingDir: resolve(projectPath),
env: {
NODE_ENV: process.env['NODE_ENV'] || 'development',
PROJECT_PATH: resolve(projectPath),
},
limits: {
timeout: this.config.sandbox.timeout,
memory: this.config.sandbox.memoryLimit,
fsOperations: this.config.sandbox.fsOperationsLimit,
networkAllowed: this.config.sandbox.networkAllowed,
},
state: new Map(),
};
}
/**
* Evaluate a condition
*/
evaluateCondition(condition, context) {
const stateValue = context.state.get(condition.key);
switch (condition.operator) {
case 'exists':
return stateValue !== undefined;
case 'equals':
return stateValue === condition.value;
case 'contains':
if (typeof stateValue === 'string') {
return stateValue.includes(String(condition.value));
}
if (Array.isArray(stateValue)) {
return stateValue.includes(condition.value);
}
return false;
case 'truthy':
return !!stateValue;
default:
return false;
}
}
/**
* Generate cache key for an operation
*/
generateOperationCacheKey(operation) {
return `op:${operation.op}:${JSON.stringify(operation.args || {})}`;
}
/**
* Get cached operation result (with LRU tracking)
*/
getCachedOperation(key) {
const cached = this.operationCache.get(key);
if (cached && Date.now() < cached.expiry) {
// Update last access time for LRU
cached.lastAccess = Date.now();
this.cacheHits++;
return cached.result;
}
this.cacheMisses++;
// Remove expired entry
if (cached) {
this.operationCache.delete(key);
}
return undefined;
}
/**
* Set cached operation result (with LRU eviction)
*/
setCachedOperation(key, result) {
// Evict old entries if cache is full
this.evictOldestEntries(this.operationCache, MAX_OPERATION_CACHE_SIZE);
this.operationCache.set(key, {
result,
expiry: Date.now() + this.config.prompts.cacheTTL * 1000,
lastAccess: Date.now(),
});
}
/**
* Compose result from composition directive
*/
composeResult(compose, context) {
const result = {
template: compose.template,
format: compose.format || 'json',
timestamp: new Date().toISOString(),
};
for (const section of compose.sections) {
result[section.key] = context.state.get(section.source);
}
return result;
}
/**
* Clear all caches
*/
clearCaches() {
this.operationCache.clear();
this.promptCache.clear();
}
/**
* Get cache statistics (Phase 4.4 enhanced metrics)
*/
getCacheStats() {
const total = this.cacheHits + this.cacheMisses;
return {
operations: this.operationCache.size,
prompts: this.promptCache.size,
hits: this.cacheHits,
misses: this.cacheMisses,
hitRate: total > 0 ? Math.round((this.cacheHits / total) * 100) / 100 : 0,
};
}
/**
* Reset cache statistics (useful for testing)
*/
resetCacheStats() {
this.cacheHits = 0;
this.cacheMisses = 0;
}
}
/**
* Global sandbox executor instance
*/
let globalSandboxExecutor = null;
/**
* Get or create the global sandbox executor
*/
export function getSandboxExecutor(config) {
if (!globalSandboxExecutor) {
globalSandboxExecutor = new SandboxExecutor(config);
}
return globalSandboxExecutor;
}
/**
* Reset the global sandbox executor
*/
export function resetSandboxExecutor() {
globalSandboxExecutor = null;
}
//# sourceMappingURL=sandbox-executor.js.map