Files
metona-ai-desktop/electron/harness/sandbox/permissions.ts
T
thzxx e4d81d8247 feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强
本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题,
并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。

Critical (10/10 完成):
- C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配
  可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过

High (11/11 完成):
- 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等

Medium (55/55 完成):
- 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、
  React 组件 cancelled 标志、类型收窄等

Low (20/20 完成):
- 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等)
- nanoid 统一替代 Date.now()+Math.random()
- confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等

返工审计修复 (10/10 完成):
- L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert
- M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死
- M-42: 脱敏短值(length <= 4)泄露修复
- M-47: tasks:update 补全 title/description 类型校验
- L-9: ollama.adapter 非流式路径 nanoid 统一
- M-45: audit:query limit 策略与 memory:listAll 一致化
- SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup
- L-15: CommandPalette useMemo 补全 sessions 响应式依赖
- useAgentStream 事件类型补全 seq/timestamp 字段

新增依赖: shell-quote + @types/shell-quote
版本号: 0.3.0 -> 0.3.1
2026-07-13 22:36:58 +08:00

239 lines
9.7 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 },
];
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 已做内联清理,
// 该方法属于死代码,删除以减少维护负担
}