UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

490 lines (483 loc) 17.5 kB
import { logger } from '../logger.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; const execAsync = promisify(exec); /** * OS-level sandboxing manager with platform-specific implementations * Provides secure execution environment with resource limits and access controls */ export class SandboxManager { platform; activeSandboxes = new Map(); constructor() { this.platform = this.detectPlatform(); logger.info(`Sandbox manager initialized for platform: ${this.platform}`); } /** * Create a new sandbox instance */ async createSandbox(config) { const sandboxId = this.generateSandboxId(); let sandbox; switch (this.platform) { case 'macos': sandbox = new MacOSSandbox(config, sandboxId); break; case 'linux': sandbox = new LinuxSandbox(config, sandboxId); break; case 'windows': sandbox = new WindowsSandbox(config, sandboxId); break; default: throw new Error(`Unsupported platform: ${this.platform}`); } await sandbox.validatePath(config.workspaceRoot); this.activeSandboxes.set(sandboxId, sandbox); logger.info(`Created sandbox: ${sandboxId} (${config.mode} mode)`); return sandbox; } /** * Get sandbox by ID */ getSandbox(sandboxId) { return this.activeSandboxes.get(sandboxId); } /** * Cleanup all active sandboxes */ async cleanupAll() { const cleanupPromises = Array.from(this.activeSandboxes.values()).map(sandbox => sandbox.cleanup()); await Promise.all(cleanupPromises); this.activeSandboxes.clear(); logger.info('All sandboxes cleaned up'); } /** * Get status of all active sandboxes */ getOverallStatus() { const sandboxes = Array.from(this.activeSandboxes.values()); const byMode = { 'read-only': 0, 'workspace-write': 0, 'full-access': 0, }; const byPlatform = { windows: 0, macos: 0, linux: 0 }; sandboxes.forEach(sandbox => { const status = sandbox.getStatus(); byMode[status.mode]++; byPlatform[status.platform]++; }); return { total: sandboxes.length, byMode, byPlatform, }; } detectPlatform() { const platform = os.platform(); switch (platform) { case 'darwin': return 'macos'; case 'linux': return 'linux'; case 'win32': return 'windows'; default: logger.warn(`Unknown platform ${platform}, defaulting to linux`); return 'linux'; } } generateSandboxId() { return `sandbox_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } } /** * Base sandbox implementation with common functionality */ class BaseSandbox { config; sandboxId; startTime; constructor(config, sandboxId) { this.config = config; this.sandboxId = sandboxId; this.startTime = Date.now(); } /** * Validate if a path is allowed by sandbox restrictions */ async validatePath(filePath) { try { const absolutePath = path.resolve(filePath); const workspaceRoot = path.resolve(this.config.workspaceRoot); // Check if path is within workspace for workspace-write mode if (this.config.mode === 'workspace-write') { if (!absolutePath.startsWith(workspaceRoot)) { logger.warn(`Path outside workspace blocked: ${absolutePath}`); return false; } } // Check allowed paths if (this.config.restrictions.allowedPaths.length > 0) { const isAllowed = this.config.restrictions.allowedPaths.some(allowedPath => { const resolvedAllowed = path.resolve(allowedPath); return absolutePath.startsWith(resolvedAllowed); }); if (!isAllowed) { logger.warn(`Path not in allowed list: ${absolutePath}`); return false; } } // For read-only mode, check if path exists and is readable if (this.config.mode === 'read-only') { try { await fs.access(absolutePath, fs.constants.R_OK); } catch { logger.warn(`Path not readable in read-only mode: ${absolutePath}`); return false; } } return true; } catch (error) { logger.error(`Path validation error for ${filePath}:`, error); return false; } } /** * Check if command is blocked */ isCommandBlocked(command) { const baseCommand = command.split(' ')[0].toLowerCase(); // Always block dangerous commands const dangerousCommands = [ 'rm', 'del', 'format', 'fdisk', 'mkfs', 'chmod', 'chown', 'sudo', 'su', 'wget', 'curl', 'nc', 'netcat', 'python', 'node', 'powershell', 'cmd', ]; if (dangerousCommands.includes(baseCommand)) { // Allow some commands in full-access mode if (this.config.mode === 'full-access') { const allowedInFullAccess = ['python', 'node', 'wget', 'curl']; if (!allowedInFullAccess.includes(baseCommand)) { return true; } } else { return true; } } // Check user-defined blocked commands return this.config.restrictions.blockedCommands.includes(baseCommand); } /** * Get current sandbox status */ getStatus() { return { active: true, mode: this.config.mode, platform: this.config.platform, resourceUsage: { memory: 0, // To be implemented by platform-specific classes cpu: 0, }, restrictions: { pathsAllowed: this.config.restrictions.allowedPaths.length, commandsBlocked: this.config.restrictions.blockedCommands.length, }, }; } } /** * macOS sandbox implementation using sandbox-exec */ class MacOSSandbox extends BaseSandbox { sandboxProfile; constructor(config, sandboxId) { super(config, sandboxId); this.sandboxProfile = this.generateSandboxProfile(); } async executeCommand(command, context) { const startTime = Date.now(); // Check if command is blocked if (this.isCommandBlocked(command)) { return { success: false, stdout: '', stderr: 'Command blocked by sandbox policy', exitCode: 1, duration: Date.now() - startTime, blocked: true, blockReason: 'Command not allowed in current sandbox mode', }; } try { // Write sandbox profile to temporary file const profilePath = `/tmp/sandbox_${this.sandboxId}.sb`; await fs.writeFile(profilePath, this.sandboxProfile); // Execute with sandbox-exec const sandboxedCommand = `sandbox-exec -f ${profilePath} ${command}`; const { stdout, stderr } = await execAsync(sandboxedCommand, { timeout: this.config.restrictions.resourceLimits.timeout, maxBuffer: 1024 * 1024, // 1MB buffer cwd: context?.workingDirectory || this.config.workspaceRoot, env: { ...process.env, ...context?.environment }, }); // Cleanup profile file await fs.unlink(profilePath).catch(() => { // Ignore errors }); return { success: true, stdout, stderr, exitCode: 0, duration: Date.now() - startTime, }; } catch (error) { return { success: false, stdout: '', stderr: error.message || 'Command execution failed', exitCode: error.code || 1, duration: Date.now() - startTime, }; } } generateSandboxProfile() { const allowedPaths = this.config.restrictions.allowedPaths .map(p => `(allow file-read* (subpath "${path.resolve(p)}"))`) .join('\n'); const workspaceAccess = this.config.mode === 'read-only' ? `(allow file-read* (subpath "${this.config.workspaceRoot}"))` : `(allow file-read* file-write* (subpath "${this.config.workspaceRoot}"))`; return ` (version 1) (debug deny) (deny default) ; Basic system access (allow process-exec (literal "/bin/sh") (literal "/usr/bin/env")) (allow file-read* (literal "/dev/null") (literal "/dev/zero")) (allow file-read* (subpath "/usr/lib") (subpath "/System/Library")) ; Workspace access ${workspaceAccess} ; User-defined allowed paths ${allowedPaths} ; Network restrictions (${this.config.mode === 'full-access' ? 'allowed' : 'blocked'}) ${this.config.mode === 'full-access' ? '(allow network*)' : '(deny network*)'} `; } async cleanup() { // Cleanup any temporary files const profilePath = `/tmp/sandbox_${this.sandboxId}.sb`; await fs.unlink(profilePath).catch(() => { }); // Ignore errors logger.debug(`macOS sandbox ${this.sandboxId} cleaned up`); } } /** * Linux sandbox implementation using namespaces and cgroups */ class LinuxSandbox extends BaseSandbox { cgroupPath; constructor(config, sandboxId) { super(config, sandboxId); this.cgroupPath = `/sys/fs/cgroup/codecrucible/${sandboxId}`; } async executeCommand(command, context) { const startTime = Date.now(); // Check if command is blocked if (this.isCommandBlocked(command)) { return { success: false, stdout: '', stderr: 'Command blocked by sandbox policy', exitCode: 1, duration: Date.now() - startTime, blocked: true, blockReason: 'Command not allowed in current sandbox mode', }; } try { // Setup cgroup for resource limits await this.setupCgroup(); // Use unshare for namespace isolation const namespacedCommand = this.buildNamespacedCommand(command, context); const { stdout, stderr } = await execAsync(namespacedCommand, { timeout: this.config.restrictions.resourceLimits.timeout, maxBuffer: 1024 * 1024, // 1MB buffer cwd: context?.workingDirectory || this.config.workspaceRoot, env: { ...process.env, ...context?.environment }, }); return { success: true, stdout, stderr, exitCode: 0, duration: Date.now() - startTime, }; } catch (error) { return { success: false, stdout: '', stderr: error.message || 'Command execution failed', exitCode: error.code || 1, duration: Date.now() - startTime, }; } } buildNamespacedCommand(command, context) { const nsFlags = [ '--mount', // Mount namespace '--pid', // PID namespace '--ipc', // IPC namespace '--uts', // UTS namespace ]; // Add network namespace unless full-access mode if (this.config.mode !== 'full-access') { nsFlags.push('--net'); } // Build bind mounts for allowed paths const bindMounts = this.config.restrictions.allowedPaths.map(p => `--bind ${p} ${p}`).join(' '); const workspaceMount = this.config.mode === 'read-only' ? `--bind-ro ${this.config.workspaceRoot} ${this.config.workspaceRoot}` : `--bind ${this.config.workspaceRoot} ${this.config.workspaceRoot}`; return `unshare ${nsFlags.join(' ')} --map-root-user cgexec -g memory,cpu:codecrucible/${this.sandboxId} -- ${workspaceMount} ${bindMounts} ${command}`; } async setupCgroup() { try { // Create cgroup directory await fs.mkdir(this.cgroupPath, { recursive: true }); // Set memory limit const memoryLimitBytes = this.config.restrictions.resourceLimits.memory * 1024 * 1024; await fs.writeFile(path.join(this.cgroupPath, 'memory.max'), memoryLimitBytes.toString()); // Set CPU limit const cpuQuota = Math.floor(this.config.restrictions.resourceLimits.cpu * 1000); // Convert percentage to quota await fs.writeFile(path.join(this.cgroupPath, 'cpu.max'), `${cpuQuota} 100000`); logger.debug(`Linux cgroup setup complete: ${this.cgroupPath}`); } catch (error) { logger.warn(`Failed to setup cgroup (continuing without resource limits):`, error); } } async cleanup() { try { // Remove cgroup await fs.rmdir(this.cgroupPath); logger.debug(`Linux sandbox ${this.sandboxId} cleaned up`); } catch (error) { logger.warn(`Failed to cleanup cgroup ${this.cgroupPath}:`, error); } } } /** * Windows sandbox implementation using process job objects and restricted tokens */ class WindowsSandbox extends BaseSandbox { jobObject; constructor(config, sandboxId) { super(config, sandboxId); this.jobObject = `codecrucible_${sandboxId}`; } async executeCommand(command, context) { const startTime = Date.now(); // Check if command is blocked if (this.isCommandBlocked(command)) { return { success: false, stdout: '', stderr: 'Command blocked by sandbox policy', exitCode: 1, duration: Date.now() - startTime, blocked: true, blockReason: 'Command not allowed in current sandbox mode', }; } try { // Use PowerShell with execution policy and constrained session const psCommand = this.buildPowerShellCommand(command, context); const { stdout, stderr } = await execAsync(psCommand, { timeout: this.config.restrictions.resourceLimits.timeout, maxBuffer: 1024 * 1024, // 1MB buffer cwd: context?.workingDirectory || this.config.workspaceRoot, env: { ...process.env, ...context?.environment }, }); return { success: true, stdout, stderr, exitCode: 0, duration: Date.now() - startTime, }; } catch (error) { return { success: false, stdout: '', stderr: error.message || 'Command execution failed', exitCode: error.code || 1, duration: Date.now() - startTime, }; } } buildPowerShellCommand(command, context) { // Create a restricted PowerShell session const sessionConfig = this.generateSessionConfiguration(); const allowedPaths = this.config.restrictions.allowedPaths .map(p => `"${path.resolve(p)}"`) .join(','); const workspaceAccess = this.config.mode === 'read-only' ? 'Read' : 'ReadWrite'; // PowerShell command with restricted execution policy return `powershell.exe -ExecutionPolicy Restricted -NoProfile -Command " $AllowedPaths = @(${allowedPaths}); $WorkspaceRoot = '${this.config.workspaceRoot}'; $AccessMode = '${workspaceAccess}'; # Validate paths and execute command try { ${command} } catch { Write-Error $_.Exception.Message; exit 1; } "`; } generateSessionConfiguration() { return ` # Session configuration for CodeCrucible sandbox # Mode: ${this.config.mode} # Generated: ${new Date().toISOString()} # Restricted execution policy Set-ExecutionPolicy -Scope Process -ExecutionPolicy Restricted -Force # Memory and CPU limits (basic PowerShell implementation) $Host.UI.RawUI.Title = "CodeCrucible Sandbox - ${this.sandboxId}" `; } async cleanup() { // Windows-specific cleanup would go here // For now, just log the cleanup logger.debug(`Windows sandbox ${this.sandboxId} cleaned up`); } } //# sourceMappingURL=sandbox-manager.js.map