UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

396 lines (341 loc) 9.02 kB
/** * Operation Guard * * Intercepts tool operations and enforces permission rules * based on current permission mode. * * @module permissions/operation-guard * @version 1.0.0 */ const { PermissionMode } = require('./permission-mode'); class OperationGuard { /** * Commands that are always safe (read-only operations) */ static SAFE_COMMANDS = [ // Git read operations 'git status', 'git log', 'git diff', 'git branch', 'git show', 'git ls-files', 'git remote -v', // File system read operations 'ls', 'pwd', 'cat', 'head', 'tail', 'wc', 'find', 'grep', 'which', 'file', 'stat', // Package manager read operations 'npm list', 'npm outdated', 'npm audit', 'npm view', 'npm search', 'yarn list', 'yarn info', 'bun pm ls', // Version checks 'node --version', 'npm --version', 'yarn --version', 'bun --version', 'git --version', 'python --version', 'python3 --version', // System info 'uname', 'whoami', 'hostname', 'date', 'uptime', 'df -h', 'free -h', 'env', 'printenv', // Network read operations 'curl -I', 'ping -c', 'nslookup', 'dig', // Process info 'ps aux', 'top -l 1', 'htop', // gh CLI read operations 'gh auth status', 'gh repo view', 'gh pr list', 'gh pr view', 'gh issue list', 'gh issue view', 'gh api', ]; /** * Patterns that indicate destructive operations */ static DESTRUCTIVE_PATTERNS = [ // File deletion /\brm\s+(-[rf]+\s+)?/, /\brmdir\b/, /\bunlink\b/, // Git destructive operations /\bgit\s+reset\s+--hard\b/, /\bgit\s+push\s+--force\b/, /\bgit\s+push\s+-f\b/, /\bgit\s+clean\s+-[fd]+/, /\bgit\s+checkout\s+\.\s*$/, /\bgit\s+restore\s+\.\s*$/, /\bgit\s+stash\s+drop\b/, /\bgit\s+branch\s+-[dD]\b/, // Database destructive operations /\bDROP\s+(TABLE|DATABASE|INDEX|VIEW)\b/i, /\bDELETE\s+FROM\b/i, /\bTRUNCATE\b/i, /\bALTER\s+TABLE\b.*\bDROP\b/i, // System destructive /\bkill\s+-9\b/, /\bkillall\b/, /\bshutdown\b/, /\breboot\b/, // Package manager destructive /\bnpm\s+uninstall\b/, /\byarn\s+remove\b/, /\bbun\s+remove\b/, ]; /** * Patterns that indicate write operations */ static WRITE_PATTERNS = [ // Redirects /[^<]>/, // > but not <> />>/, // File creation/modification /\bmkdir\b/, /\btouch\b/, /\bmv\b/, /\bcp\b/, /\bln\b/, /\bchmod\b/, /\bchown\b/, // Editors /\bsed\s+-i\b/, /\bawk\s+-i\b/, // Git write operations /\bgit\s+add\b/, /\bgit\s+commit\b/, /\bgit\s+push\b/, /\bgit\s+merge\b/, /\bgit\s+rebase\b/, /\bgit\s+cherry-pick\b/, /\bgit\s+stash\b/, // Package manager write operations /\bnpm\s+install\b/, /\bnpm\s+i\b/, /\bnpm\s+ci\b/, /\byarn\s+add\b/, /\byarn\s+install\b/, /\bbun\s+install\b/, /\bbun\s+add\b/, ]; /** * Create an OperationGuard instance * @param {PermissionMode} permissionMode - Permission mode instance */ constructor(permissionMode = null) { this.permissionMode = permissionMode || new PermissionMode(); this.operationLog = []; } /** * Classify an operation type based on tool and parameters * @param {string} tool - Tool name (Read, Write, Edit, Bash, etc.) * @param {Object} params - Tool parameters * @returns {string} Operation type (read, write, execute, delete) */ classifyOperation(tool, params = {}) { // Read-only tools if (['Read', 'Glob', 'Grep', 'WebFetch', 'WebSearch'].includes(tool)) { return 'read'; } // Write tools if (['Write', 'Edit', 'NotebookEdit'].includes(tool)) { return 'write'; } // Task tool - depends on subagent type if (tool === 'Task') { const readOnlyAgents = ['Explore', 'Plan', 'claude-code-guide']; if (readOnlyAgents.includes(params.subagent_type)) { return 'read'; } return 'execute'; } // Bash needs deeper analysis if (tool === 'Bash') { return this.classifyBashCommand(params.command || ''); } // MCP tools - generally execute if (tool.startsWith('mcp__')) { return 'execute'; } // Default to read (safe) return 'read'; } /** * Classify a bash command * @param {string} command - Bash command string * @returns {string} Operation type */ classifyBashCommand(command) { const normalizedCmd = command.trim().toLowerCase(); // Check safe commands first (most specific match) for (const safe of OperationGuard.SAFE_COMMANDS) { if (normalizedCmd.startsWith(safe.toLowerCase())) { return 'read'; } } // Check destructive patterns for (const pattern of OperationGuard.DESTRUCTIVE_PATTERNS) { if (pattern.test(command)) { return 'delete'; } } // Check write patterns for (const pattern of OperationGuard.WRITE_PATTERNS) { if (pattern.test(command)) { return 'write'; } } // Default unknown bash commands to execute return 'execute'; } /** * Guard an operation - check if it should proceed * @param {string} tool - Tool name * @param {Object} params - Tool parameters * @returns {Promise<Object>} Guard result */ async guard(tool, params = {}) { // Ensure mode is loaded await this.permissionMode.load(); const operation = this.classifyOperation(tool, params); const check = this.permissionMode.canPerform(operation); // Log the operation this._logOperation(tool, params, operation, check); // Operation allowed if (check.allowed === true) { return { proceed: true, operation }; } // Operation blocked if (check.allowed === false) { const modeInfo = this.permissionMode.getModeInfo(); return { proceed: false, blocked: true, operation, message: this._formatBlockedMessage(tool, params, operation, modeInfo), }; } // Operation needs confirmation if (check.allowed === 'confirm') { return { proceed: false, needsConfirmation: true, operation, tool, params, message: this._formatConfirmMessage(tool, params, operation), }; } // Unknown state - block to be safe return { proceed: false, blocked: true, message: 'Unknown permission state', }; } /** * Format blocked message * @private */ _formatBlockedMessage(tool, params, operation, modeInfo) { let detail = ''; if (tool === 'Bash' && params.command) { detail = `\nCommand: \`${params.command.substring(0, 100)}${params.command.length > 100 ? '...' : ''}\``; } else if (params.file_path) { detail = `\nFile: \`${params.file_path}\``; } return `🔒 **Blocked in ${modeInfo.name} Mode** Operation: **${operation}** Tool: \`${tool}\`${detail} **To enable this operation:** - \`*mode ask\` - Confirm before changes - \`*mode auto\` - Full autonomy`; } /** * Format confirmation message * @private */ _formatConfirmMessage(tool, params, operation) { let detail = ''; if (tool === 'Bash' && params.command) { detail = `\n\n\`\`\`bash\n${params.command}\n\`\`\``; } else if (params.file_path) { detail = `\n\nFile: \`${params.file_path}\``; } return `⚠️ **Confirmation Required** Operation: **${operation}** Tool: \`${tool}\`${detail}`; } /** * Log operation for debugging/audit * @private */ _logOperation(tool, params, operation, check) { const entry = { timestamp: new Date().toISOString(), tool, operation, allowed: check.allowed, command: tool === 'Bash' ? params.command?.substring(0, 100) : undefined, file: params.file_path, }; this.operationLog.push(entry); // Keep only last 100 entries if (this.operationLog.length > 100) { this.operationLog = this.operationLog.slice(-100); } } /** * Get operation log * @returns {Array} Operation log entries */ getLog() { return [...this.operationLog]; } /** * Get statistics about operations * @returns {Object} Stats */ getStats() { const stats = { total: this.operationLog.length, byOperation: { read: 0, write: 0, execute: 0, delete: 0 }, byResult: { allowed: 0, blocked: 0, confirmed: 0 }, }; for (const entry of this.operationLog) { stats.byOperation[entry.operation] = (stats.byOperation[entry.operation] || 0) + 1; if (entry.allowed === true) stats.byResult.allowed++; else if (entry.allowed === false) stats.byResult.blocked++; else if (entry.allowed === 'confirm') stats.byResult.confirmed++; } return stats; } } module.exports = { OperationGuard };