UNPKG

christmas-mcp-copilot-runner

Version:

A Model Copilot Provider (MCP) for safely executing whitelisted system commands across platforms with automatic VS Code setup

316 lines (283 loc) 8.3 kB
const { execSync } = require('child_process'); const fs = require('fs-extra'); const path = require('path'); const os = require('os'); // Platform-specific command mappings const WINDOWS_COMMANDS = { 'ls': 'dir', 'cat': 'type', 'which': 'where', 'grep': 'findstr' }; // Whitelisted base commands (cross-platform) const ALLOWED_COMMANDS = [ 'php', 'curl', 'node', 'npm', 'yarn', 'git', 'python', 'python3', 'pip', 'pip3', 'echo', 'pwd', 'whoami', 'date', 'head', 'tail', 'wc', 'sort', 'uniq', 'find', 'cd', // Allow directory navigation (safe) 'docker', // Docker commands 'docker-compose', // Docker Compose 'playwright', // Playwright testing 'npx', // Node package executor 'pnpm', // Alternative package manager 'bun', // Bun runtime and package manager 'deno', // Deno runtime 'java', // Java runtime 'javac', // Java compiler 'mvn', // Maven 'gradle', // Gradle 'make', // Make build tool 'cmake', // CMake 'go', // Go language 'rust', // Rust language 'cargo', // Rust package manager 'composer', // PHP package manager 'bundle', // Ruby bundler 'gem', // Ruby gems 'ruby', // Ruby runtime 'rails', // Ruby on Rails 'pytest', // Python testing 'jest', // JavaScript testing 'mocha', // JavaScript testing 'cypress', // End-to-end testing 'selenium', // Web automation 'terraform', // Infrastructure as Code 'kubectl', // Kubernetes CLI 'helm', // Kubernetes package manager 'aws', // AWS CLI 'az', // Azure CLI 'gcloud', // Google Cloud CLI 'heroku', // Heroku CLI 'vercel', // Vercel CLI 'netlify', // Netlify CLI // .NET and C# development 'dotnet', // .NET CLI 'nuget', // NuGet package manager 'msbuild', // MSBuild 'csc', // C# compiler 'vbc', // VB.NET compiler // Unix/Linux/macOS specific 'ls', 'cat', 'which', 'grep', 'rm', // Allow safe file deletion // Windows specific 'dir', 'type', 'where', 'findstr', 'powershell', 'cmd', 'del' // Allow safe file deletion on Windows ]; // Dangerous patterns to reject (cross-platform) const DANGEROUS_PATTERNS = [ // Unix/Linux/macOS dangerous commands /rm\s+(-rf|--recursive|-r\s)/i, // More specific rm patterns /sudo/i, /chmod\s+777/i, // Windows dangerous commands - be more specific /del\s+.*[\/\\]s/i, // del with /s flag (recursive) /del\s+.*\*.*[\/\\]s/i, // del with wildcards and /s /rmdir\s+.*[\/\\]s/i, // rmdir with /s flag /format\s+/i, /diskpart/i, // Cross-platform dangerous patterns />/, // File redirection /\|/, // Pipe operations (except simple single pipes) // Note: Removed && restriction - it's safe for command chaining like "cd dir && command" /;.*rm/i, // Command separation with dangerous commands /;.*del/i, // Command separation with dangerous commands /`/, // Command substitution /\$\(/, // Command substitution /eval/i, /exec/i, /system/i, /mkfs/i, /fdisk/i, /deltree/i, // PowerShell specific /Remove-Item.*-Recurse/i, // Dangerous wildcards - be more specific /\*.*\*/, // Multiple wildcards /del\s+\*\.\*$/i, // del *.* type commands (exact match) /rm\s+\*\.\*$/i // rm *.* type commands (exact match) ]; /** * Normalizes commands for cross-platform compatibility * @param {string} cmd - The original command * @returns {string} - The normalized command for the current platform */ function normalizeCommand(cmd) { if (os.platform() === 'win32') { let normalizedCmd = cmd; // Replace Unix commands with Windows equivalents Object.entries(WINDOWS_COMMANDS).forEach(([unixCmd, winCmd]) => { const regex = new RegExp(`^${unixCmd}\\b`, 'i'); normalizedCmd = normalizedCmd.replace(regex, winCmd); }); return normalizedCmd; } return cmd; } /** * Gets platform-specific execution options * @returns {Object} - Execution options for child_process */ function getExecOptions() { const options = { encoding: 'utf8', timeout: 30000, // 30 second timeout maxBuffer: 1024 * 1024 // 1MB max buffer }; // Windows-specific options if (os.platform() === 'win32') { options.shell = true; } return options; } /** * Validates if a command is safe to execute * @param {string} cmd - The command to validate * @returns {boolean} - True if command is safe, false otherwise */ function isCommandSafe(cmd) { if (!cmd || typeof cmd !== 'string') { return false; } // Trim and normalize the command const trimmedCmd = cmd.trim(); if (trimmedCmd.length === 0) { return false; } // Check for dangerous patterns for (const pattern of DANGEROUS_PATTERNS) { if (pattern.test(trimmedCmd)) { return false; } } // Handle compound commands with && (safe command chaining) const commandParts = trimmedCmd.split('&&').map(part => part.trim()); // Validate each part of the compound command for (const commandPart of commandParts) { if (!commandPart) continue; // Extract the base command (first word) const baseCommand = commandPart.split(/\s+/)[0]; // Special handling for 'cd' command - always allow it as it's safe for navigation if (baseCommand === 'cd') { continue; } // Check if base command is whitelisted if (!ALLOWED_COMMANDS.includes(baseCommand)) { return false; } } return true; } /** * Logs command execution to the logs directory * @param {string} cmd - The command that was executed * @param {string} result - The result of the command execution * @param {boolean} success - Whether the command succeeded */ async function logCommand(cmd, result, success) { try { const logsDir = path.join(process.cwd(), 'logs'); await fs.ensureDir(logsDir); const timestamp = new Date().toISOString(); const logEntry = { timestamp, command: cmd, success, result: result.substring(0, 1000), // Limit log size pid: process.pid }; const logFile = path.join(logsDir, `commands-${new Date().toISOString().split('T')[0]}.log`); await fs.appendFile(logFile, JSON.stringify(logEntry) + '\n'); } catch (error) { console.error('Failed to log command:', error.message); } } /** * Executes a whitelisted system command safely * @param {Object} params - Parameters object * @param {string} params.cmd - The command to execute * @returns {Object} - Result object with output or error */ async function runCommand({ cmd }) { try { // Validate input if (!cmd) { const error = 'Command is required'; await logCommand('', error, false); return { success: false, error, timestamp: new Date().toISOString() }; } // Check if command is safe if (!isCommandSafe(cmd)) { const error = `Command rejected: '${cmd}' is not allowed or contains dangerous patterns`; await logCommand(cmd, error, false); return { success: false, error, timestamp: new Date().toISOString() }; } // Normalize command for current platform const normalizedCmd = normalizeCommand(cmd); console.log(`Executing command: ${normalizedCmd} (original: ${cmd})`); // Execute the command with platform-specific options const output = execSync(normalizedCmd, getExecOptions()); const result = { success: true, output: output.toString(), command: cmd, normalizedCommand: normalizedCmd, platform: os.platform(), timestamp: new Date().toISOString() }; await logCommand(cmd, output.toString(), true); return result; } catch (error) { const errorMsg = error.message || 'Unknown error occurred'; const result = { success: false, error: errorMsg, command: cmd, platform: os.platform(), timestamp: new Date().toISOString() }; await logCommand(cmd, errorMsg, false); return result; } } module.exports = { runCommand, isCommandSafe, normalizeCommand, getExecOptions, ALLOWED_COMMANDS, DANGEROUS_PATTERNS, WINDOWS_COMMANDS };