UNPKG

@abix/zisk-dev-cli

Version:

A comprehensive CLI tool for ZisK zkVM development with full macOS support. Features project initialization, building, execution, proof generation, environment diagnostics, and project management. This is a personal tool for testing and learning - not an

726 lines (608 loc) 19.3 kB
/** * Command Execution System * Handles secure command execution with proper error handling and logging */ const { spawn, exec } = require('child_process'); const { promisify } = require('util'); const path = require('path'); const fs = require('fs-extra'); const os = require('os'); const pLimit = require('p-limit'); const { Logger } = require('./logger'); const { ErrorHandler, BuildError, ExecutionError } = require('./errors'); const execAsync = promisify(exec); class CommandExecutor { constructor() { this.logger = new Logger(); this.errorHandler = new ErrorHandler(); this.allowedCommands = new Set([ 'cargo', 'cargo-zisk', 'ziskemu', 'rustc', 'rustup', 'node', 'npm', 'tar', 'curl', 'wget', 'git', 'make', 'gcc', 'clang', 'mpirun', 'nvidia-smi' ]); // Add parallelism control this.processPool = pLimit(this.getMaxConcurrent()); // Safe environment variables this.safeEnvVars = new Set([ 'OMP_NUM_THREADS', 'RAYON_NUM_THREADS', 'RUST_LOG', 'CARGO_TARGET_DIR', 'PATH', 'HOME', 'USER', 'SHELL', 'TMPDIR', 'TMP', 'TEMP' ]); // Add ZISK_* variables this.addZiskEnvVars(); } /** * Get maximum concurrent processes to prevent system overload */ getMaxConcurrent() { const envValue = parseInt(process.env.ZISK_MAX_CONCURRENT); const defaultValue = Math.max(1, Math.floor(os.cpus().length / 2)); const maxAllowed = 16; if (envValue > 0 && envValue <= maxAllowed) { return envValue; } return Math.min(defaultValue, maxAllowed); } /** * Add ZISK_* environment variables to safe list * Use regex pattern and explicitly block dangerous variables */ addZiskEnvVars() { const env = process.env; const ziskPattern = /^ZISK_/; for (const key in env) { if (ziskPattern.test(key)) { this.safeEnvVars.add(key); } } // Explicitly block dangerous variables even if they start with ZISK_ const blockedVars = ['ZISK_PATH', 'ZISK_LD_LIBRARY_PATH', 'ZISK_LD_PRELOAD']; blockedVars.forEach(varName => this.safeEnvVars.delete(varName)); } /** * Security: Validate and normalize file paths to prevent traversal attacks */ validateAndNormalizePath(inputPath, allowAbsolute = false, baseDir = process.cwd()) { if (!inputPath || typeof inputPath !== 'string') { throw new Error('Invalid path provided'); } // Normalize the path const normalized = path.normalize(inputPath); // Check for absolute paths if not allowed if (!allowAbsolute && path.isAbsolute(normalized)) { throw new Error('Absolute paths not allowed'); } // Check for path traversal attempts if (normalized.includes('..') || normalized.includes('~')) { throw new Error('Path traversal detected'); } // Resolve against base directory to ensure it's within bounds const resolved = path.resolve(baseDir, normalized); const baseResolved = path.resolve(baseDir); if (!resolved.startsWith(baseResolved)) { throw new Error('Path outside allowed directory'); } return normalized; } /** * Sanitize environment variables for process execution */ sanitizeEnvironment(env = {}) { const sanitized = {}; for (const [key, value] of Object.entries(env)) { if (this.safeEnvVars.has(key)) { // For paths, validate boundaries instead of aggressive sanitization if (key.includes('PATH') || key.includes('DIR')) { sanitized[key] = this.validatePathBoundaries(value); } else { // Basic sanitization for non-path values sanitized[key] = String(value).replace(/[\x00-\x1F\x7F-\x9F]/g, ''); } } } return sanitized; } /** * Validate path boundaries without over-sanitizing * @param {string} pathValue - Path value to validate * @returns {string} Validated path */ validatePathBoundaries(pathValue) { if (typeof pathValue !== 'string') { return String(pathValue); } // Only remove control characters, preserve legitimate path characters return pathValue.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); } /** * Sanitize string inputs to prevent injection */ sanitizeString(input) { if (typeof input !== 'string') { return String(input); } // Remove control characters and potential injection patterns return input .replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Remove control characters .replace(/[;&|`$(){}[\]\\]/g, '') // Remove shell metacharacters .trim(); } /** * Redact sensitive values from logs * @param {string} message - Log message to redact * @returns {string} Redacted message */ redactSensitiveValues(message) { if (typeof message !== 'string') { return String(message); } // Redact sensitive patterns let redacted = message .replace(/--proving-key\s+\S+/g, '--proving-key [REDACTED]') .replace(/--witness\s+\S+/g, '--witness [REDACTED]') .replace(/--key\s+\S+/g, '--key [REDACTED]') .replace(/--secret\s+\S+/g, '--secret [REDACTED]') .replace(/\/\.ssh\/[^\s]+/g, '/.ssh/[REDACTED]') .replace(/\/\.zisk\/[^\s]+/g, '/.zisk/[REDACTED]') .replace(/password[=:]\s*\S+/gi, 'password=[REDACTED]') .replace(/token[=:]\s*\S+/gi, 'token=[REDACTED]') .replace(/key[=:]\s*\S+/gi, 'key=[REDACTED]'); return redacted; } /** * Validate command arguments */ validateCommandArgs(args) { if (!Array.isArray(args)) { throw new Error('Command arguments must be an array'); } return args.map(arg => { if (typeof arg !== 'string') { throw new Error('All command arguments must be strings'); } return this.sanitizeString(arg); }); } /** * Execute command with full logging and error handling */ async executeCommand(command, args = [], options = {}) { const startTime = Date.now(); const commandId = this.generateCommandId(); // Log command execution this.logger.logCommand(command, args, options); try { // Validate command and arguments this.validateCommand(command); const sanitizedArgs = this.validateCommandArgs(args); // Prepare execution options const execOptions = this.prepareExecutionOptions(options); execOptions.startTime = startTime; // Pass start time for duration calculation // Use process pool to limit concurrency const result = await this.processPool(() => this.executeWithStreaming(command, sanitizedArgs, execOptions) ); // Log command result this.logger.logCommandResult(command, result); // Log performance const duration = Date.now() - startTime; this.logger.logPerformance(`command:${command}`, duration, { commandId, exitCode: result.exitCode }); return result; } catch (error) { // Handle error with context const errorContext = await this.errorHandler.handleError(error, { name: command, args, startTime }, options); throw error; } } /** * Execute cargo-zisk command specifically */ async executeCargoZisk(subcommand, args = [], options = {}) { const { PlatformManager } = require('./platform'); const platform = new PlatformManager(); const libPaths = platform.resolveLibraryPaths(); // Build cargo-zisk command const cargoZiskArgs = [subcommand, ...args]; // Add platform-specific flags const buildFlags = platform.getBuildFlags(); cargoZiskArgs.push(...buildFlags); // Execute with cargo-zisk binary return this.executeCommand(libPaths.cargoZisk, cargoZiskArgs, options); } /** * Execute ziskemu command specifically */ async executeZiskemu(args = [], options = {}) { const { PlatformManager } = require('./platform'); const platform = new PlatformManager(); const libPaths = platform.resolveLibraryPaths(); return this.executeCommand(libPaths.ziskemu, args, options); } /** * Execute command with MPI support */ async executeWithMPI(command, args = [], options = {}) { const { PlatformManager } = require('./platform'); const platform = new PlatformManager(); if (!platform.capabilities.mpiSupport) { throw new Error('MPI support not available on this platform'); } const mpiArgs = [ '--bind-to', 'none', '-np', options.processes || '1', '-x', 'OMP_NUM_THREADS=' + (options.threadsPerProcess || '1'), '-x', 'RAYON_NUM_THREADS=' + (options.threadsPerProcess || '1') ]; const fullArgs = [...mpiArgs, command, ...args]; return this.executeCommand('mpirun', fullArgs, options); } /** * Validate command is allowed */ validateCommand(command) { const baseCommand = command.split(' ')[0]; // Extract just the command name from full path const commandName = path.basename(baseCommand); // Check if the command name is allowed if (!this.allowedCommands.has(commandName)) { // Special case for cargo-zisk binary if (commandName === 'cargo-zisk') { return; // Allow cargo-zisk binary } throw new Error(`Command not allowed: ${baseCommand}`); } } /** * Prepare execution options with security hardening */ prepareExecutionOptions(options) { const defaultOptions = { cwd: process.cwd(), env: this.sanitizeEnvironment(process.env), // Sanitize environment stdio: 'pipe', timeout: this.getTimeoutForOperation(options.operation), // Operation-specific timeouts maxBuffer: 1024 * 1024 * 10 // 10MB }; // Merge with provided options const execOptions = { ...defaultOptions, ...options }; // Sanitize additional environment variables if (options.env) { execOptions.env = { ...execOptions.env, ...this.sanitizeEnvironment(options.env) }; } return execOptions; } /** * Get appropriate timeout for operation type */ getTimeoutForOperation(operation) { const timeouts = { 'prove': 600000, // 10 minutes for proof generation 'build': 120000, // 2 minutes for build 'run': 120000, // 2 minutes for execution 'verify': 60000, // 1 minute for verification 'default': 300000 // 5 minutes default }; return timeouts[operation] || timeouts.default; } /** * Run command with spawn and improved process lifecycle */ async runCommand(command, args, options) { return new Promise((resolve, reject) => { const child = spawn(command, args, options); let stdout = ''; let stderr = ''; let killed = false; let sigtermSent = false; // Security: Improved timeout handling with SIGTERM → SIGKILL escalation const timeout = setTimeout(() => { if (!killed) { killed = true; this.terminateProcess(child, sigtermSent); reject(new Error(`Command timed out after ${options.timeout}ms`)); } }, options.timeout); // Security: Force kill after additional 30 seconds if SIGTERM doesn't work const forceKillTimeout = setTimeout(() => { if (!killed && sigtermSent) { killed = true; child.kill('SIGKILL'); } }, options.timeout + 30000); // Collect stdout child.stdout.on('data', (data) => { stdout += data.toString(); }); // Collect stderr child.stderr.on('data', (data) => { stderr += data.toString(); }); // Handle completion child.on('close', (code) => { clearTimeout(timeout); clearTimeout(forceKillTimeout); if (killed) return; const result = { exitCode: code, stdout: stdout.trim(), stderr: stderr.trim(), duration: Date.now() - (options.startTime || Date.now()) }; if (code === 0) { resolve(result); } else { reject(new ExecutionError(`Command failed with exit code ${code}`, { command, args, result })); } }); // Handle errors child.on('error', (error) => { clearTimeout(timeout); clearTimeout(forceKillTimeout); if (!killed) { reject(new ExecutionError(`Command execution failed: ${error.message}`, { command, args, error })); } }); }); } /** * Terminate process with SIGTERM → SIGKILL escalation * Handles process groups to kill child processes (Windows-compatible) */ terminateProcess(child, sigtermSent) { try { if (process.platform === 'win32') { // Windows: Kill child and its children explicitly this.killProcessTree(child.pid, sigtermSent ? 'SIGKILL' : 'SIGTERM'); } else { // Unix: Kill process group (negative PID) if (!sigtermSent) { if (child.pid) { process.kill(-child.pid, 'SIGTERM'); } else { child.kill('SIGTERM'); } sigtermSent = true; } else { // Force kill process group if (child.pid) { process.kill(-child.pid, 'SIGKILL'); } else { child.kill('SIGKILL'); } } } } catch (error) { // Fallback to killing just the child if process group kill fails try { child.kill(sigtermSent ? 'SIGKILL' : 'SIGTERM'); } catch (fallbackError) { console.warn(`Failed to terminate process ${child.pid}: ${fallbackError.message}`); } } } /** * Windows-compatible process tree termination * @param {number} pid - Process ID to terminate * @param {string} signal - Signal to send */ killProcessTree(pid, signal) { if (process.platform !== 'win32') { // On Unix, use process group kill process.kill(-pid, signal); return; } // Windows: Use taskkill to terminate process tree const { spawn } = require('child_process'); const taskkill = spawn('taskkill', ['/PID', pid.toString(), '/T', '/F'], { stdio: 'pipe' }); taskkill.on('error', (error) => { console.warn(`Failed to kill process tree for PID ${pid}: ${error.message}`); }); } /** * Generate unique command ID */ generateCommandId() { return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Execute command with streaming output */ async executeWithStreaming(command, args = [], options = {}) { return new Promise((resolve, reject) => { const startTime = options.startTime || Date.now(); const child = spawn(command, args, { ...this.prepareExecutionOptions(options), stdio: ['inherit', 'pipe', 'pipe'], detached: true // Create new process group for proper cleanup }); // Note: Not calling unref() because we want to capture logs and wait for completion let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { const output = data.toString(); stdout += output; process.stdout.write(output); }); child.stderr.on('data', (data) => { const output = data.toString(); stderr += output; process.stderr.write(output); }); child.on('close', (code) => { const result = { exitCode: code, stdout: stdout.trim(), stderr: stderr.trim(), duration: Date.now() - startTime }; if (code === 0) { resolve(result); } else { reject(new ExecutionError(`Command failed with exit code ${code}`, { command, args, result })); } }); child.on('error', (error) => { reject(new ExecutionError(`Command execution failed: ${error.message}`, { command, args, error })); }); }); } /** * Execute command with progress tracking */ async executeWithProgress(command, args = [], options = {}) { const ora = require('ora'); const spinner = ora(`Executing ${command}...`).start(); try { const result = await this.executeCommand(command, args, options); spinner.succeed(`${command} completed successfully`); return result; } catch (error) { spinner.fail(`${command} failed: ${error.message}`); throw error; } } /** * Check if command exists */ async commandExists(command) { try { await execAsync(`which ${command}`); return true; } catch { return false; } } /** * Get command version */ async getCommandVersion(command) { try { const { stdout } = await execAsync(`${command} --version`); return stdout.trim(); } catch { return null; } } } /** * ZISK Command Builder * Builds ZISK-specific commands with proper arguments */ class ZiskCommandBuilder { constructor() { this.platform = new PlatformManager(); } /** * Build cargo-zisk build command */ buildBuildCommand(options = {}) { const args = ['build']; if (options.profile) { args.push('--profile', options.profile); } if (options.features && options.features.length > 0) { args.push('--features', options.features.join(',')); } if (options.target) { args.push('--target', options.target); } return { command: 'cargo-zisk', args }; } /** * Build cargo-zisk run command */ buildRunCommand(inputPath, options = {}) { const args = ['run']; if (options.profile) { args.push('--profile', options.profile); } if (inputPath) { args.push('-i', inputPath); } if (options.metrics) { args.push('-m'); } if (options.stats) { args.push('-x'); } return { command: 'cargo-zisk', args }; } /** * Build cargo-zisk prove command */ buildProveCommand(inputPath, options = {}) { const args = ['prove']; if (inputPath) { args.push('-i', inputPath); } if (options.output) { args.push('-o', options.output); } if (options.verify) { args.push('-y'); } if (options.aggregate) { args.push('-a'); } return { command: 'cargo-zisk', args }; } /** * Build cargo-zisk verify command */ buildVerifyCommand(proofPath, options = {}) { const args = ['verify']; if (proofPath) { args.push('-p', proofPath); } return { command: 'cargo-zisk', args }; } /** * Build ziskemu command */ buildZiskemuCommand(elfPath, inputPath, options = {}) { const args = []; if (elfPath) { args.push('-e', elfPath); } if (inputPath) { args.push('-i', inputPath); } if (options.maxSteps) { args.push('-n', options.maxSteps.toString()); } if (options.metrics) { args.push('-m'); } if (options.stats) { args.push('-x'); } return { command: 'ziskemu', args }; } } module.exports = { CommandExecutor, ZiskCommandBuilder };