UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

353 lines (352 loc) 12.1 kB
import { log } from "../util/logging.js"; import { loadConfig, saveConfig } from "../state/config.js"; /** * Advanced tool permission management system inspired by Claude Code * Provides granular control over AI tool usage with security rules */ export class PermissionManager { permissions = new Map(); rules = []; projectPath; constructor() { this.initializeDefaultRules(); } /** * Initialize with project-specific permissions */ async initialize(projectPath) { this.projectPath = projectPath; await this.loadPermissions(); log.info("Permission system initialized"); } /** * Check if a tool is allowed to execute */ async checkPermission(toolName, args, context) { const permission = this.permissions.get(toolName); // If no specific permission found, check rules if (!permission) { return this.evaluateRules(toolName, args, context); } // Check basic permission if (!permission.allowed) { return { allowed: false, reason: `Tool ${toolName} is not allowed in current scope` }; } // Check restrictions if (permission.restrictions) { const restrictionCheck = this.checkRestrictions(permission.restrictions, args, context); if (!restrictionCheck.allowed) { return restrictionCheck; } } return { allowed: true, requiresConfirmation: permission.restrictions?.requireConfirmation }; } /** * Set tool permission */ async setPermission(toolName, allowed, scope = 'project', restrictions) { const permission = { name: toolName, allowed, scope, restrictions }; this.permissions.set(toolName, permission); await this.savePermissions(); log.info(`Tool ${toolName} permission set to ${allowed ? 'allowed' : 'denied'} (scope: ${scope})`); } /** * Get all current permissions */ getPermissions() { return Array.from(this.permissions.values()); } /** * Get permission rules */ getRules() { return [...this.rules]; } /** * Add a permission rule */ addRule(rule) { this.rules.push(rule); this.rules.sort((a, b) => b.priority - a.priority); // Sort by priority (higher first) log.info(`Added permission rule: ${rule.pattern} -> ${rule.action}`); } /** * Remove a permission rule */ removeRule(pattern) { const initialLength = this.rules.length; this.rules = this.rules.filter(rule => rule.pattern !== pattern); const removed = this.rules.length < initialLength; if (removed) { log.info(`Removed permission rule: ${pattern}`); } return removed; } /** * Reset permissions to defaults */ async resetPermissions() { this.permissions.clear(); this.rules = []; this.initializeDefaultRules(); await this.savePermissions(); log.info("Permissions reset to defaults"); } /** * Export permissions for sharing */ exportPermissions() { return JSON.stringify({ permissions: Array.from(this.permissions.entries()), rules: this.rules }, null, 2); } /** * Import permissions from JSON */ async importPermissions(jsonData) { try { const data = JSON.parse(jsonData); // Import permissions if (data.permissions) { this.permissions.clear(); for (const [name, permission] of data.permissions) { this.permissions.set(name, permission); } } // Import rules if (data.rules) { this.rules = data.rules; } await this.savePermissions(); log.info("Permissions imported successfully"); } catch (error) { throw new Error(`Failed to import permissions: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get permission statistics */ getStats() { const permissions = Array.from(this.permissions.values()); const allowed = permissions.filter(p => p.allowed).length; const denied = permissions.length - allowed; const byScope = permissions.reduce((acc, p) => { acc[p.scope] = (acc[p.scope] || 0) + 1; return acc; }, {}); return { totalPermissions: permissions.length, allowedTools: allowed, deniedTools: denied, rulesCount: this.rules.length, byScope }; } /** * Initialize default permission rules */ initializeDefaultRules() { // High-security rules (highest priority) this.addRule({ pattern: "rm.*-rf.*(/|~)", action: 'deny', priority: 100, description: "Prevent dangerous recursive deletions", examples: ["rm -rf /", "rm -rf ~"] }); this.addRule({ pattern: "curl.*\\|.*sh", action: 'deny', priority: 95, description: "Prevent pipe-to-shell downloads", examples: ["curl http://example.com/script.sh | sh"] }); this.addRule({ pattern: "chmod.*777", action: 'confirm', priority: 90, description: "Confirm overly permissive file permissions", examples: ["chmod 777 file.txt"] }); // Development tool rules (medium priority) this.addRule({ pattern: "(npm|yarn|pnpm)\\s+(install|add)", action: 'allow', priority: 50, description: "Allow package manager installations", examples: ["npm install", "yarn add react"] }); this.addRule({ pattern: "(git\\s+(add|commit|push|pull|status|diff))", action: 'allow', priority: 50, description: "Allow basic Git operations", examples: ["git add .", "git commit -m 'message'"] }); // Test and build rules this.addRule({ pattern: "(npm|yarn|pnpm)\\s+(test|run|build)", action: 'allow', priority: 45, description: "Allow test and build commands", examples: ["npm test", "yarn build"] }); // File operation rules (low priority) this.addRule({ pattern: "(ls|cat|head|tail|grep|find)", action: 'allow', priority: 20, description: "Allow basic file operations", examples: ["ls -la", "cat README.md"] }); // Default deny rule (lowest priority) this.addRule({ pattern: ".*", action: 'confirm', priority: 1, description: "Confirm any unmatched commands", examples: ["any unrecognized command"] }); } /** * Evaluate permission rules */ evaluateRules(toolName, args, context) { const commandText = context?.command || `${toolName} ${JSON.stringify(args) || ''}`; for (const rule of this.rules) { const regex = new RegExp(rule.pattern, 'i'); if (regex.test(commandText) || regex.test(toolName)) { switch (rule.action) { case 'allow': return { allowed: true }; case 'deny': return { allowed: false, reason: `Blocked by rule: ${rule.description}` }; case 'confirm': return { allowed: true, requiresConfirmation: true, reason: rule.description }; } } } // Default to confirmation required return { allowed: true, requiresConfirmation: true, reason: "No matching permission rule found" }; } /** * Check permission restrictions */ checkRestrictions(restrictions, args, context) { // Check path restrictions if (restrictions.allowedPaths && context?.path) { const isAllowed = restrictions.allowedPaths.some(allowed => context.path.startsWith(allowed)); if (!isAllowed) { return { allowed: false, reason: `Path not in allowed list: ${context.path}` }; } } if (restrictions.deniedPaths && context?.path) { const isDenied = restrictions.deniedPaths.some(denied => context.path.startsWith(denied)); if (isDenied) { return { allowed: false, reason: `Path is denied: ${context.path}` }; } } // Check command restrictions if (restrictions.allowedCommands && context?.command) { const isAllowed = restrictions.allowedCommands.some(allowed => context.command.includes(allowed)); if (!isAllowed) { return { allowed: false, reason: `Command not in allowed list: ${context.command}` }; } } if (restrictions.deniedCommands && context?.command) { const isDenied = restrictions.deniedCommands.some(denied => context.command.includes(denied)); if (isDenied) { return { allowed: false, reason: `Command is denied: ${context.command}` }; } } return { allowed: true }; } /** * Load permissions from config */ async loadPermissions() { try { const config = await loadConfig(); if (config?.security?.permissions) { // Load permissions if (config.security.permissions.tools) { for (const [name, permission] of Object.entries(config.security.permissions.tools)) { this.permissions.set(name, permission); } } // Load custom rules if (config.security.permissions.rules) { this.rules = [...this.rules, ...config.security.permissions.rules]; this.rules.sort((a, b) => b.priority - a.priority); } } } catch (error) { log.warn("Failed to load permissions from config:", error); } } /** * Save permissions to config */ async savePermissions() { try { const config = await loadConfig() || {}; if (!config.security) { config.security = {}; } if (!config.security.permissions) { config.security.permissions = {}; } // Save permissions config.security.permissions.tools = {}; for (const [name, permission] of this.permissions) { config.security.permissions.tools[name] = permission; } // Save custom rules (filter out default rules) config.security.permissions.rules = this.rules.filter(rule => rule.priority > 10 && rule.priority < 100 // Keep only custom rules ); await saveConfig(config); } catch (error) { log.warn("Failed to save permissions to config:", error); } } } // Export singleton instance export const permissionManager = new PermissionManager();