/** * ═══════════════════════════════════════════════════════════════════════════ * ALFRED AGENT — Permissions & Approval Flow Service * * Tool-level access control with: * - Commander (client_id=33): unrestricted, all tools allowed * - Customer tier: sandboxed, dangerous ops require approval * - Future: per-user rules, time-limited grants * * Pattern: preToolUse hook checks → allow/deny/ask * * Built by Commander Danny William Perez and Alfred. * ═══════════════════════════════════════════════════════════════════════════ */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { join } from 'path'; const PERMISSIONS_DIR = join(process.env.HOME || '/tmp', 'alfred-agent', 'data', 'permissions'); // ── Default Permission Profiles ──────────────────────────────────────── const PROFILES = { commander: { name: 'Commander', description: 'Full unrestricted access — Commander Danny (client_id=33)', allowAll: true, maxMultiplier: 600, maxTokensPerQuery: 128000, canApproveOthers: true, canAccessVault: true, canModifySystem: true, canDeleteFiles: true, canRunBash: true, canRunDestructive: true, canAccessAllDomains: true, }, customer: { name: 'Customer', description: 'Sandboxed access — paying customer with workspace', allowAll: false, maxMultiplier: 120, maxTokensPerQuery: 32000, canApproveOthers: false, canAccessVault: false, canModifySystem: false, canDeleteFiles: false, // Must approve each delete canRunBash: true, // Sandboxed bash only canRunDestructive: false, canAccessAllDomains: false, allowedTools: [ 'read_file', 'write_file', 'edit_file', 'list_dir', 'bash', 'search', 'web_fetch', 'mcp_call', 'agent', 'task_create', 'task_update', 'task_list', ], blockedTools: [ 'vault_read', 'vault_write', 'system_config', 'pm2_control', 'database_admin', ], // Bash command restrictions for customer profile bashAllowPatterns: [ /^(ls|cat|head|tail|wc|grep|find|echo|pwd|date|whoami|node|npm|python3?|php|git)\b/, ], bashBlockPatterns: [ /\brm\s+-rf?\s+\//, // No rm -r / /\bsudo\b/, // No sudo /\bchmod\s+[0-7]*7/, // No world-writable /\bkill\b/, // No kill /\bpkill\b/, /\bsystemctl\b/, /\biptables\b/, /\bcrontab\b/, /\bcurl.*\|\s*bash/, // No curl|bash /\bwget.*\|\s*bash/, /\bdd\s+if=/, // No dd /\bmkfs\b/, ], }, free: { name: 'Free Tier', description: 'Limited access — free account', allowAll: false, maxMultiplier: 30, maxTokensPerQuery: 8192, canApproveOthers: false, canAccessVault: false, canModifySystem: false, canDeleteFiles: false, canRunBash: false, canRunDestructive: false, canAccessAllDomains: false, allowedTools: [ 'read_file', 'write_file', 'edit_file', 'list_dir', 'search', 'web_fetch', ], blockedTools: [ 'bash', 'mcp_call', 'vault_read', 'vault_write', 'system_config', 'pm2_control', 'database_admin', 'agent', 'task_create', ], }, }; // ── Per-User Rule Overrides ──────────────────────────────────────────── /** * Load per-user permission overrides from disk. */ function loadUserRules(clientId) { try { const filePath = join(PERMISSIONS_DIR, `user-${clientId}.json`); if (!existsSync(filePath)) return null; return JSON.parse(readFileSync(filePath, 'utf8')); } catch { return null; } } /** * Save per-user permission overrides. */ function saveUserRules(clientId, rules) { mkdirSync(PERMISSIONS_DIR, { recursive: true }); const filePath = join(PERMISSIONS_DIR, `user-${clientId}.json`); writeFileSync(filePath, JSON.stringify(rules, null, 2)); } // ── Permission Engine ────────────────────────────────────────────────── /** * Create a permission engine for a user session. * * @param {string} profile - 'commander' | 'customer' | 'free' * @param {Object} opts * @param {number} opts.clientId * @param {string} opts.workspaceRoot - Sandbox root path * @param {Function} opts.onApprovalNeeded - Called when user approval is needed * Returns Promise (true = approve, false = deny) */ export function createPermissionEngine(profile = 'commander', opts = {}) { const baseProfile = PROFILES[profile] || PROFILES.customer; const userRules = opts.clientId ? loadUserRules(opts.clientId) : null; const workspaceRoot = opts.workspaceRoot || process.cwd(); const onApprovalNeeded = opts.onApprovalNeeded || (() => Promise.resolve(false)); // Merge user overrides onto base profile const effectiveProfile = { ...baseProfile }; if (userRules) { if (userRules.additionalTools) { effectiveProfile.allowedTools = [ ...(effectiveProfile.allowedTools || []), ...userRules.additionalTools, ]; } if (userRules.maxMultiplier !== undefined) { effectiveProfile.maxMultiplier = Math.min( userRules.maxMultiplier, baseProfile.maxMultiplier, ); } } // Approval log const approvalLog = []; /** * Check if a tool call is permitted. * Returns: { allowed: boolean, reason?: string, needsApproval?: boolean } */ function checkToolPermission(toolName, input = {}) { // Commander gets everything if (effectiveProfile.allowAll) { return { allowed: true }; } // Explicitly blocked tools if (effectiveProfile.blockedTools?.includes(toolName)) { return { allowed: false, reason: `Tool "${toolName}" is blocked for ${effectiveProfile.name} profile`, }; } // Check if tool is in allowed list if (effectiveProfile.allowedTools && !effectiveProfile.allowedTools.includes(toolName)) { return { allowed: false, reason: `Tool "${toolName}" is not in the allowed list for ${effectiveProfile.name}`, }; } // Special checks for bash commands if (toolName === 'bash' && input.command) { return checkBashPermission(input.command); } // File path sandboxing if (['read_file', 'write_file', 'edit_file'].includes(toolName) && input.path) { return checkPathPermission(input.path); } // Delete operations need approval if (toolName === 'write_file' && input.path && !effectiveProfile.canDeleteFiles) { // Writing empty content = effective delete if (!input.content || input.content.trim() === '') { return { allowed: false, needsApproval: true, reason: `Deleting files requires approval for ${effectiveProfile.name}`, }; } } return { allowed: true }; } /** * Check bash command permission. */ function checkBashPermission(command) { if (!effectiveProfile.canRunBash) { return { allowed: false, reason: 'Bash access is not available on your plan' }; } // Check block patterns if (effectiveProfile.bashBlockPatterns) { for (const pattern of effectiveProfile.bashBlockPatterns) { if (pattern.test(command)) { return { allowed: false, reason: `Command matches blocked pattern: ${pattern}`, needsApproval: effectiveProfile.name === 'Customer', }; } } } return { allowed: true }; } /** * Check file path permission (sandboxing). */ function checkPathPermission(filePath) { if (effectiveProfile.canAccessAllDomains) { return { allowed: true }; } // Must be under workspace root const resolved = require('path').resolve(filePath); if (!resolved.startsWith(workspaceRoot)) { return { allowed: false, reason: `Access denied: ${filePath} is outside your workspace (${workspaceRoot})`, }; } // Block access to sensitive files const sensitivePatterns = [ /\.env$/, /credentials/i, /\.key$/, /\.pem$/, /password/i, /secret/i, /\.htaccess$/, ]; for (const pattern of sensitivePatterns) { if (pattern.test(filePath)) { return { allowed: false, needsApproval: true, reason: `Accessing sensitive file requires approval: ${filePath}`, }; } } return { allowed: true }; } /** * Validate multiplier against profile limits. * Returns clamped multiplier. */ function clampMultiplier(requestedMultiplier) { const max = effectiveProfile.maxMultiplier || 30; return Math.min(Math.max(1, requestedMultiplier || 30), max); } /** * Validate max tokens against profile limits. */ function clampMaxTokens(requestedTokens) { const max = effectiveProfile.maxTokensPerQuery || 8192; return Math.min(Math.max(256, requestedTokens || 8192), max); } /** * Request approval for a blocked action. * Records the result for audit. */ async function requestApproval(toolName, input, reason) { const request = { ts: Date.now(), toolName, input: JSON.stringify(input).slice(0, 500), reason, profile: effectiveProfile.name, clientId: opts.clientId, }; try { const approved = await onApprovalNeeded(request); request.result = approved ? 'approved' : 'denied'; approvalLog.push(request); return approved; } catch { request.result = 'error'; approvalLog.push(request); return false; } } /** * Get the effective profile for inspection. */ function getProfile() { return { ...effectiveProfile, // Don't expose regex patterns in JSON bashAllowPatterns: undefined, bashBlockPatterns: undefined, }; } /** * Get the approval audit log. */ function getApprovalLog() { return [...approvalLog]; } return { checkToolPermission, clampMultiplier, clampMaxTokens, requestApproval, getProfile, getApprovalLog, saveUserRules: (rules) => saveUserRules(opts.clientId, rules), }; } export { PROFILES, loadUserRules, saveUserRules };