alfred-agent/src/services/permissions.js

357 lines
11 KiB
JavaScript
Raw Normal View History

/**
*
* 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<boolean> (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 };