根因:DEFAULT_POLICIES 只配置了10个工具的策略,file_editor、code_search、 diff_viewer、task_manager、delegate_task 这5个工具未配置策略, PolicyEngine.checkAuthorization 对无策略工具返回 authorized: false, 导致 "Blocked: No policy configured for tool" 错误。 新增策略(按工具风险级别配置): - file_editor: WRITE + deniedPatterns(系统目录/MEMORY.md) + requireConfirmation + maxFrequency 10 - code_search: READ(只读搜索) - diff_viewer: READ(只读对比) - task_manager: WRITE(数据库读写,低风险) - delegate_task: EXTERNAL_ACTION + maxFrequency 5(启动 SubAgent)
221 lines
8.7 KiB
TypeScript
221 lines
8.7 KiB
TypeScript
/**
|
||
* 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 字符串中后跟引号的情况)
|
||
{ toolName: 'read_file', requiredLevel: PermissionLevel.READ, deniedPatterns: [/\/etc(?:\/|["'\s,}]|$)/, /\/proc(?:\/|["'\s,}]|$)/, /C:\\Windows\\/i, /C:\\System32\\/i, /MEMORY\.md/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, /MEMORY\.md/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, /MEMORY\.md/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 },
|
||
];
|
||
|
||
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;
|
||
} {
|
||
const policy = this.policies.get(toolName);
|
||
|
||
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 已做内联清理,
|
||
// 该方法属于死代码,删除以减少维护负担
|
||
}
|