/** * Policy Engine — 权限策略引擎 * * 三级权限模型:Read / Write / External Action * * v0.3.0 增强: * - 实现 maxFrequency 频率限制(滑动窗口算法) * - 添加 checkFrequency 和 recordCall 方法 * - 添加 cleanupFrequencyRecords 防止内存泄漏 * * @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第十章 */ export enum PermissionLevel { READ = 'read', WRITE = 'write', EXTERNAL_ACTION = 'external', } export interface PermissionPolicy { toolName: string; requiredLevel: PermissionLevel; allowedPatterns?: RegExp[]; deniedPatterns?: RegExp[]; maxFrequency?: number; requireConfirmation?: boolean; } export const DEFAULT_POLICIES: PermissionPolicy[] = [ // v0.3.0 修复:deniedPatterns 使用 (?:\/|["'\s,}]|$) 匹配, // 覆盖 /etc/ 和 /etc(无尾斜杠,在 JSON 字符串中后跟引号的情况) // H-5 修复: 移除 /MEMORY\.md/i 粗粒度正则 — 之前会误拦子目录的 MEMORY.md // 改为在 engine.ts executeToolSafely 中进行精确的根目录校验(仅保护 workspacePath/MEMORY.md) // @see project_memory.md — Only the MEMORY.md in the workspace root directory is protected { toolName: 'read_file', requiredLevel: PermissionLevel.READ, deniedPatterns: [/\/etc(?:\/|["'\s,}]|$)/, /\/proc(?:\/|["'\s,}]|$)/, /C:\\Windows\\/i, /C:\\System32\\/i] }, { toolName: 'web_search', requiredLevel: PermissionLevel.READ, maxFrequency: 10 }, { toolName: 'list_directory', requiredLevel: PermissionLevel.READ }, { toolName: 'search_files', requiredLevel: PermissionLevel.READ }, { toolName: 'memory_search', requiredLevel: PermissionLevel.READ }, { toolName: 'write_file', requiredLevel: PermissionLevel.WRITE, deniedPatterns: [/\/etc(?:\/|["'\s,}]|$)/, /\/proc(?:\/|["'\s,}]|$)/, /\/System(?:\/|["'\s,}]|$)/, /C:\\Windows\\/i, /C:\\System32\\/i], requireConfirmation: true, maxFrequency: 5 }, { toolName: 'memory_store', requiredLevel: PermissionLevel.WRITE }, { toolName: 'run_command', requiredLevel: PermissionLevel.EXTERNAL_ACTION, deniedPatterns: [/MEMORY\.md/i], requireConfirmation: true, maxFrequency: 10 }, { toolName: 'web_fetch', requiredLevel: PermissionLevel.READ }, // web_browser — 统一浏览器工具(合并自 9 个独立 browser_* 工具) // 由于该工具可执行 JS、点击元素等高风险操作,统一设为 EXTERNAL_ACTION { toolName: 'web_browser', requiredLevel: PermissionLevel.EXTERNAL_ACTION, requireConfirmation: true, maxFrequency: 20 }, // v0.3.0 修复: 补全缺失的工具策略 — 之前这5个工具未配置策略,导致被 PolicyEngine 拦截 // file_editor — 精准文件编辑(WRITE),与 write_file 同级安全约束 { toolName: 'file_editor', requiredLevel: PermissionLevel.WRITE, deniedPatterns: [/\/etc(?:\/|["'\s,}]|$)/, /\/proc(?:\/|["'\s,}]|$)/, /\/System(?:\/|["'\s,}]|$)/, /C:\\Windows\\/i, /C:\\System32\\/i], requireConfirmation: true, maxFrequency: 10 }, // code_search — 基于 ripgrep 的只读搜索(READ) { toolName: 'code_search', requiredLevel: PermissionLevel.READ }, // diff_viewer — 文件/文本差异对比(只读,READ) { toolName: 'diff_viewer', requiredLevel: PermissionLevel.READ }, // task_manager — 任务管理(数据库读写,低风险 WRITE) { toolName: 'task_manager', requiredLevel: PermissionLevel.WRITE }, // delegate_task — 子任务委派(启动 SubAgent,EXTERNAL_ACTION) { toolName: 'delegate_task', requiredLevel: PermissionLevel.EXTERNAL_ACTION, requireConfirmation: false, maxFrequency: 5 }, // C-7 修复: MCP 工具通配符策略 — MCP 工具名称动态生成(mcp_{serverName}_{toolName}) // 无法预先配置精确策略,使用 mcp_* 通配符匹配所有 MCP 工具 // @see project_memory.md — All tools must have a configured policy in DEFAULT_POLICIES { toolName: 'mcp_*', requiredLevel: PermissionLevel.EXTERNAL_ACTION, requireConfirmation: true, maxFrequency: 20 }, // v0.3.1: Git 工具集(4 个) { toolName: 'git_status', requiredLevel: PermissionLevel.READ }, { toolName: 'git_diff', requiredLevel: PermissionLevel.READ }, { toolName: 'git_log', requiredLevel: PermissionLevel.READ }, { toolName: 'git_commit', requiredLevel: PermissionLevel.WRITE, requireConfirmation: true, maxFrequency: 10 }, // v0.3.1: 开发工具集(3 个) { toolName: 'lint_code', requiredLevel: PermissionLevel.READ }, { toolName: 'run_tests', requiredLevel: PermissionLevel.READ }, { toolName: 'project_info', requiredLevel: PermissionLevel.READ }, // v0.3.1: HTTP 请求工具(1 个) { toolName: 'http_request', requiredLevel: PermissionLevel.READ, maxFrequency: 20 }, // v0.3.1: 结构化思考工具(1 个)— 无副作用 { toolName: 'think', requiredLevel: PermissionLevel.READ }, // v0.3.1: 图片查看工具(1 个)— 只读 { toolName: 'view_image', requiredLevel: PermissionLevel.READ }, // v0.3.2: 文件删除工具(1 个)— 破坏性操作,必须确认 { toolName: 'delete_file', requiredLevel: PermissionLevel.WRITE, requireConfirmation: true, maxFrequency: 30 }, // v0.3.3: 文件移动/重命名工具(1 个)— 可能覆盖目标,需确认 { toolName: 'file_move', requiredLevel: PermissionLevel.WRITE, requireConfirmation: true, maxFrequency: 30 }, // v0.3.3: 文件信息查询工具(1 个)— 只读 { toolName: 'file_info', requiredLevel: PermissionLevel.READ }, ]; export class PolicyEngine { private policies: Map = new Map(); /** v0.3.0: 工具调用频率追踪 — 工具名 -> 调用时间戳列表 */ private callFrequency: Map = new Map(); /** v0.3.0: 频率限制的时间窗口(1分钟 = 60秒) */ private readonly FREQ_WINDOW_MS = 60_000; /** * v0.3.0 修复:customPolicies 与 DEFAULT_POLICIES 合并而非完全覆盖 * * 合并策略:customPolicies 中的字段覆盖默认策略的同名字段, * 未指定的字段保留默认值(如 deniedPatterns 等安全配置不会被丢失) */ constructor(customPolicies: PermissionPolicy[] = []) { for (const policy of DEFAULT_POLICIES) { this.policies.set(policy.toolName, { ...policy }); } for (const policy of customPolicies) { const existing = this.policies.get(policy.toolName); if (existing) { // v0.3.0 修复:合并而非替换,保留默认的安全配置(如 deniedPatterns) this.policies.set(policy.toolName, { ...existing, ...policy }); } else { this.policies.set(policy.toolName, policy); } } } checkAuthorization(toolName: string, args: Record): { authorized: boolean; reason?: string; level: PermissionLevel; requiresConfirmation: boolean; } { let policy = this.policies.get(toolName); // C-7 修复: 支持通配符策略匹配(如 mcp_* 匹配所有 MCP 工具) // MCP 工具名称动态生成(mcp_{serverName}_{toolName}),无法预先配置精确策略 if (!policy) { for (const [pattern, p] of this.policies) { if (pattern.endsWith('*') && toolName.startsWith(pattern.slice(0, -1))) { policy = p; break; } } } if (!policy) { return { authorized: false, reason: `No policy configured for tool: ${toolName}`, level: PermissionLevel.EXTERNAL_ACTION, requiresConfirmation: true, }; } // v0.3.0 修复:使用 try-catch 防止循环引用导致 JSON.stringify 抛错 let argsStr: string; try { argsStr = JSON.stringify(args); } catch { // 循环引用等异常情况,降级为 toString argsStr = String(args); } // v0.2.0: allowedPatterns 白名单校验 — 若定义了白名单,参数必须匹配其中之一 if (policy.allowedPatterns && policy.allowedPatterns.length > 0) { let matchedAllowed = false; for (const pattern of policy.allowedPatterns) { if (pattern.test(argsStr)) { matchedAllowed = true; break; } } if (!matchedAllowed) { return { authorized: false, reason: 'Arguments do not match any allowed pattern', level: policy.requiredLevel, requiresConfirmation: policy.requireConfirmation ?? false, }; } } if (policy.deniedPatterns) { for (const pattern of policy.deniedPatterns) { if (pattern.test(argsStr)) { return { authorized: false, reason: 'Command blocked by security policy', level: policy.requiredLevel, requiresConfirmation: false, }; } } } // v0.3.0: 频率限制检查 if (policy.maxFrequency !== undefined) { const freqCheck = this.checkFrequency(toolName, policy.maxFrequency); if (!freqCheck.allowed) { return { authorized: false, reason: freqCheck.reason, level: policy.requiredLevel, requiresConfirmation: false, }; } } return { authorized: true, level: policy.requiredLevel, requiresConfirmation: policy.requireConfirmation ?? false, }; } /** * v0.3.0: 频率限制检查(滑动窗口算法) * * 检查指定工具在时间窗口内的调用次数是否超过限制。 * 注意:此方法仅检查,不记录调用。调用成功后需调用 recordCall()。 * * v0.3.0 修复: * - 将 validCalls 写回 Map,避免 callFrequency 数组无限增长(内存泄漏) * * @param toolName 工具名称 * @param maxFreq 最大频率(每分钟) * @returns 检查结果 */ checkFrequency(toolName: string, maxFreq?: number): { allowed: boolean; reason?: string } { const policy = this.policies.get(toolName); const limit = maxFreq ?? policy?.maxFrequency; if (limit === undefined) return { allowed: true }; const now = Date.now(); const calls = this.callFrequency.get(toolName) ?? []; // 移除时间窗口外的调用记录 const validCalls = calls.filter((t) => now - t < this.FREQ_WINDOW_MS); // v0.3.0 修复:将清理后的 validCalls 写回 Map,避免数组无限增长 if (validCalls.length !== calls.length) { this.callFrequency.set(toolName, validCalls); } if (validCalls.length >= limit) { return { allowed: false, reason: `Rate limit exceeded for ${toolName}: max ${limit} calls per minute (current: ${validCalls.length})`, }; } return { allowed: true }; } /** * v0.3.0: 记录工具调用(工具成功执行后调用) * * v0.3.0 修复:同时清理过期记录,防止数组无限增长 * * @param toolName 工具名称 */ recordCall(toolName: string): void { const now = Date.now(); const calls = this.callFrequency.get(toolName) ?? []; // v0.3.0 修复:记录新调用时同时清理过期记录 const validCalls = calls.filter((t) => now - t < this.FREQ_WINDOW_MS); validCalls.push(now); this.callFrequency.set(toolName, validCalls); } // v0.3.0 修复: cleanupFrequencyRecords 已删除 — checkFrequency 和 recordCall 已做内联清理, // 该方法属于死代码,删除以减少维护负担 }