From 70394ed06cd6dffa1cfc26de21a0799f5e121898 Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 16 Dec 2025 11:23:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E4=BF=AE=E5=A4=8D=20Plan=20Agent?= =?UTF-8?q?=20=E9=80=9A=E8=BF=87=20heredoc=20=E7=BB=95=E8=BF=87=E5=86=99?= =?UTF-8?q?=E5=85=A5=E9=99=90=E5=88=B6=E7=9A=84=E5=AE=89=E5=85=A8=E6=BC=8F?= =?UTF-8?q?=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重排 bash 权限规则顺序,deny 规则置于 allow 规则之前 - 添加 heredoc 重定向检测规则 (* << *) - 新增 checkRedirectInRawCommand 预检函数,在 tree-sitter 解析前检测重定向 - 禁用 Plan Agent 的 tool_search 工具,防止动态发现写入工具 - 添加更多危险命令: ln, install, truncate, dd, tee --- packages/core/src/agent/presets/plan.ts | 80 ++++++++++++++---------- packages/core/src/permission/wildcard.ts | 41 ++++++++++++ 2 files changed, 89 insertions(+), 32 deletions(-) diff --git a/packages/core/src/agent/presets/plan.ts b/packages/core/src/agent/presets/plan.ts index 23fddaa..78b8780 100644 --- a/packages/core/src/agent/presets/plan.ts +++ b/packages/core/src/agent/presets/plan.ts @@ -37,6 +37,8 @@ export const planAgent: Omit = { 'checkpoint_create', 'checkpoint_restore', 'undo', + // 工具发现(Plan 模式不应动态发现新工具) + 'tool_search', ], }, permission: { @@ -49,6 +51,52 @@ export const planAgent: Omit = { bash: { enabled: true, rules: [ + // ============================================================ + // 重要:deny 规则必须放在 allow 规则之前,确保优先匹配! + // ============================================================ + + // ============ 危险命令 - 拒绝 ============ + { pattern: 'rm *', action: 'deny' }, + { pattern: 'rmdir *', action: 'deny' }, + { pattern: 'mv *', action: 'deny' }, + { pattern: 'cp *', action: 'deny' }, + { pattern: 'mkdir *', action: 'deny' }, + { pattern: 'touch *', action: 'deny' }, + { pattern: 'chmod *', action: 'deny' }, + { pattern: 'chown *', action: 'deny' }, + { pattern: 'sudo *', action: 'deny' }, + { pattern: 'su *', action: 'deny' }, + { pattern: 'ln *', action: 'deny' }, + { pattern: 'install *', action: 'deny' }, + { pattern: 'truncate *', action: 'deny' }, + { pattern: 'dd *', action: 'deny' }, + { pattern: 'tee *', action: 'deny' }, + + // ============ 重定向操作 - 拒绝(必须在 cat/echo 等允许规则之前)============ + { pattern: '* > *', action: 'deny' }, + { pattern: '* >> *', action: 'deny' }, + { pattern: '* << *', action: 'deny' }, // heredoc 重定向 + + // ============ Git 写操作 - 拒绝 ============ + { pattern: 'git add *', action: 'deny' }, + { pattern: 'git commit *', action: 'deny' }, + { pattern: 'git push *', action: 'deny' }, + { pattern: 'git pull *', action: 'deny' }, + { pattern: 'git checkout *', action: 'deny' }, + { pattern: 'git reset *', action: 'deny' }, + { pattern: 'git rebase *', action: 'deny' }, + { pattern: 'git merge *', action: 'deny' }, + { pattern: 'git stash *', action: 'deny' }, + { pattern: 'git clean *', action: 'deny' }, + + // ============ find 危险操作 - 拒绝 ============ + { pattern: 'find * -delete*', action: 'deny' }, + { pattern: 'find * -exec*', action: 'deny' }, + + // ============================================================ + // 以下为只读操作的 allow 规则 + // ============================================================ + // ============ 文件查看 - 允许 ============ { pattern: 'ls', action: 'allow' }, { pattern: 'ls *', action: 'allow' }, @@ -100,38 +148,6 @@ export const planAgent: Omit = { { pattern: 'git blame *', action: 'allow' }, { pattern: 'git ls-files*', action: 'allow' }, { pattern: 'git rev-parse *', action: 'allow' }, - - // ============ 危险命令 - 拒绝 ============ - { pattern: 'rm *', action: 'deny' }, - { pattern: 'rmdir *', action: 'deny' }, - { pattern: 'mv *', action: 'deny' }, - { pattern: 'cp *', action: 'deny' }, - { pattern: 'mkdir *', action: 'deny' }, - { pattern: 'touch *', action: 'deny' }, - { pattern: 'chmod *', action: 'deny' }, - { pattern: 'chown *', action: 'deny' }, - { pattern: 'sudo *', action: 'deny' }, - { pattern: 'su *', action: 'deny' }, - - // ============ Git 写操作 - 拒绝 ============ - { pattern: 'git add *', action: 'deny' }, - { pattern: 'git commit *', action: 'deny' }, - { pattern: 'git push *', action: 'deny' }, - { pattern: 'git pull *', action: 'deny' }, - { pattern: 'git checkout *', action: 'deny' }, - { pattern: 'git reset *', action: 'deny' }, - { pattern: 'git rebase *', action: 'deny' }, - { pattern: 'git merge *', action: 'deny' }, - { pattern: 'git stash *', action: 'deny' }, - { pattern: 'git clean *', action: 'deny' }, - - // ============ find 危险操作 - 拒绝 ============ - { pattern: 'find * -delete*', action: 'deny' }, - { pattern: 'find * -exec*', action: 'deny' }, - - // ============ 重定向操作 - 拒绝 ============ - { pattern: '* > *', action: 'deny' }, - { pattern: '* >> *', action: 'deny' }, ], default: 'ask', // 其他命令询问用户 }, diff --git a/packages/core/src/permission/wildcard.ts b/packages/core/src/permission/wildcard.ts index e0eb5ee..f87c533 100644 --- a/packages/core/src/permission/wildcard.ts +++ b/packages/core/src/permission/wildcard.ts @@ -95,6 +95,35 @@ export function matchRules( return matchSingleCommand(parsed, rules, defaultAction); } +/** + * 检查原始命令字符串是否包含危险的重定向操作 + * 这是一个快速预检,在 tree-sitter 解析之前进行 + */ +function checkRedirectInRawCommand( + command: string, + rules: PermissionRule[] +): { action: PermissionAction; matchedPattern?: string } | null { + // 检查重定向相关的 deny 规则 + const redirectPatterns = ['* > *', '* >> *', '* << *']; + + for (const rule of rules) { + if (rule.action === 'deny' && redirectPatterns.includes(rule.pattern)) { + // 根据模式检查原始命令 + if (rule.pattern === '* > *' && / > /.test(command) && !/ >> /.test(command)) { + return { action: 'deny', matchedPattern: rule.pattern }; + } + if (rule.pattern === '* >> *' && / >> /.test(command)) { + return { action: 'deny', matchedPattern: rule.pattern }; + } + if (rule.pattern === '* << *' && / << /.test(command)) { + return { action: 'deny', matchedPattern: rule.pattern }; + } + } + } + + return null; +} + /** * 使用 tree-sitter 解析并检查所有命令的权限(异步版本) * 返回最严格的权限要求 @@ -109,6 +138,18 @@ export async function matchRulesAsync( allCommands: ParsedCommand[]; askPatterns: string[]; }> { + // 首先检查原始命令中的重定向操作(这是安全关键检查,必须在解析之前进行) + const redirectCheck = checkRedirectInRawCommand(command, rules); + if (redirectCheck) { + // 如果检测到重定向被拒绝,直接返回 + return { + action: redirectCheck.action, + matchedPattern: redirectCheck.matchedPattern, + allCommands: [parseCommandSimple(command)], + askPatterns: [], + }; + } + const result = await parseBashCommand(command); // 如果解析失败,降级到简单解析