Files
metona-ai-desktop/electron/harness/sandbox/permissions.ts
T

268 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 — 子任务委派(启动 SubAgentEXTERNAL_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<string, PermissionPolicy> = new Map();
/** v0.3.0: 工具调用频率追踪 — 工具名 -> 调用时间戳列表 */
private callFrequency: Map<string, number[]> = 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<string, unknown>): {
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 已做内联清理,
// 该方法属于死代码,删除以减少维护负担
}