Files
metona-ai-desktop/electron/harness/sandbox/permissions.ts
T
thzxx 4554177db0 feat: 升级至 v0.3.3 — 新增 delete_file 工具 + 文件工具全面优化
## 主要变更

### 1. 新增 delete_file 工具(26 → 27 个)
- 支持:删除文件/空目录(默认)/递归删除非空目录(recursive: true)
- 5 层安全防护:
  * isPathWithinWorkspace — 路径遍历防护
  * isProtectedWorkspaceFile — MEMORY.md 拦截
  * 工作空间根目录保护 — 禁止删除 workspace 本身
  * 递归删除需显式开启 — 默认仅删空目录
  * riskLevel: HIGH + requiresPermission — 破坏性操作必须确认
- 友好错误处理:ENOTEMPTY 时提示设置 recursive: true

### 2. 文件工具全面优化(6 个工具)
- 抽取共享代码到 file-guard.ts:
  * safeResolvePath — 合并路径遍历 + MEMORY.md 校验
  * matchGlob — 简易通配符匹配
  * extractErrorMessage — 统一错误提取
  * MAX_FILE_SIZE_BYTES (10MB) / FILE_TOOL_TIMEOUT_MS (15s) / MAX_LINE_LENGTH (10000)
- read_file:stat 预检 + 超长行截断 + 二进制检测 + 大小上限
- write_file:原子写入(临时文件+rename)+ 内容大小上限 + append 返回 new_file_size
- list_directory:1000 结果上限 + modified time + include_hidden 参数 + 提前终止优化
- search_files:regex lastIndex 修复 + context_lines + include_hidden + 大文件跳过
- file_editor:dry_run 预览模式 + 文件大小上限

### 3. 审计修复(1 FAIL + 3 WARN)
- FAIL: isBinaryFile 用 bytesRead 限制循环,修复 < 8KB 文本误判为二进制
- WARN-1: file-editor dry_run preview 分模式计算,修复 insert 范围过大
- WARN-2: write_file 允许空字符串创建空文件
- WARN-3: list_directory listDir 提前终止,避免大目录全量遍历
2026-07-14 21:42:46 +08:00

266 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: 3 },
{ 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: TODO 管理工具(1 个)— 内存存储,无持久化副作用
// v0.3.1 修复 WARN-9: 改为 READ(内存操作,非数据库)
{ toolName: 'todo_write', requiredLevel: PermissionLevel.READ },
// 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 },
];
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 已做内联清理,
// 该方法属于死代码,删除以减少维护负担
}