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
JavaScript
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
};