Skip to content

Commit e5cacbc

Browse files
committed
refactor(security): 重构权限系统,移除工具内置确认逻辑
重构确认流程,完全由权限系统管理工具调用确认 新增 PatternAbstractor 实现权限规则智能抽象 优化权限模式行为,支持 DEFAULT/AUTO_EDIT/YOLO 三种模式 更新相关文档和测试用例
1 parent 358fe81 commit e5cacbc

37 files changed

Lines changed: 1406 additions & 580 deletions

.blade/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
{
2-
"permissions": {}
2+
"permissions": {
3+
"allow": [],
4+
"ask": [],
5+
"deny": []
6+
}
37
}

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"Bash(npm test:*)",
4141
"Bash(npm run test:integration:*)",
4242
"Bash(blade mcp:*)",
43-
"Bash(test:*)"
43+
"Bash(test:*)",
44+
"Bash(npm run test:unit:*)"
4445
],
4546
"deny": [],
4647
"ask": [],

docs/development/architecture/confirmation-flow.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# 用户确认流程文档
22

3+
> **⚠️ 文档部分过时**: 本文档描述的 `requiresConfirmation` 工具字段和 `shouldConfirm()` 方法已被废弃。
4+
>
5+
> **当前实现**: Blade 的确认逻辑完全由权限系统管理(PermissionStage),不再依赖工具自身的确认配置。
6+
>
7+
> - 确认行为由 `settings.json` 中的权限规则控制(allow/ask/deny)
8+
> - 权限模式(DEFAULT/AUTO_EDIT/YOLO)决定自动批准策略
9+
> - 工具不再有 `requiresConfirmation` 字段
10+
>
11+
> 请参考 [权限系统文档](../../public/configuration/permissions.md) 了解当前的确认机制。
12+
313
## 概述
414

515
Blade 实现了一个完整的用户确认流程,用于在执行潜在危险操作前获取用户明确同意。该流程集成在工具执行管道(ExecutionPipeline)中,提供了安全、直观的交互体验。

docs/development/architecture/tool-system.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ export interface Tool<TParams = unknown> {
103103
readonly category?: string;
104104
readonly tags: string[];
105105

106-
// 安全控制
107-
readonly requiresConfirmation: boolean;
108-
109106
// 方法
110107
getFunctionDeclaration(): FunctionDeclaration;
111108
getMetadata(): Record<string, unknown>;

docs/public/configuration/permissions.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,168 @@ Blade 支持三级权限控制:
208208
}
209209
```
210210

211+
## 智能模式抽象
212+
213+
从 v0.0.10 开始,Blade 实现了**智能权限模式抽象**机制,自动将具体的工具调用转换为模式规则。
214+
215+
### 工作原理
216+
217+
当你在确认提示中选择 **"Yes, don't ask again this session"**(记住本项目会话)时,Blade 不会保存精确的参数,而是根据工具类型自动生成模式规则:
218+
219+
**优化前(旧行为)**
220+
```json
221+
{
222+
"permissions": {
223+
"allow": [
224+
"Bash(command:cd /path && npm run typecheck)",
225+
"Bash(command:cd /path && npm test)",
226+
"Edit(file_path:/path/src/a.ts, old_string:..., new_string:...)",
227+
"Edit(file_path:/path/src/b.ts, old_string:..., new_string:...)"
228+
]
229+
}
230+
}
231+
```
232+
233+
**优化后(新行为)**
234+
```json
235+
{
236+
"permissions": {
237+
"allow": [
238+
"Bash(command:*npm*)", // 覆盖所有 npm 命令
239+
"Edit(file_path:**/*.ts)" // 覆盖所有 TS 文件编辑
240+
]
241+
}
242+
}
243+
```
244+
245+
### 抽象策略
246+
247+
不同工具类型采用不同的抽象策略:
248+
249+
#### Bash 命令
250+
- **安全命令**(cd/ls/pwd)→ `Bash(command:*)`
251+
- **包管理器**(npm/pnpm/yarn)→ `Bash(command:*npm*)`
252+
- **Git 命令**(git status)→ `Bash(command:git status*)`
253+
- **开发工具**(test/build/lint)→ `Bash(command:*)`
254+
- **其他命令**`Bash(command:<主命令>*)`
255+
256+
#### 文件操作(Read/Edit/Write)
257+
- **有扩展名**`Tool(file_path:**/*.ext)`
258+
- **源码目录**`Tool(file_path:**)`
259+
- **其他目录**`Tool(file_path:<dir>/*)`
260+
261+
#### 搜索操作(Grep/Glob)
262+
- **有类型限制**`Grep(pattern:*, type:ts)`
263+
- **有 glob 限制**`Grep(pattern:*, glob:*.ts)`
264+
- **有路径限制**`Grep(pattern:*, path:**/*.ts)`
265+
- **默认**`Grep(pattern:*)`
266+
267+
#### Web 请求(WebFetch)
268+
- **按域名分组**`WebFetch(domain:api.github.com)`
269+
270+
### 优势
271+
272+
1. **减少权限文件大小**:从数百条规则减少到几条模式
273+
2. **避免重复确认**:相似操作自动复用权限
274+
3. **更符合直觉**:"记住"意味着"信任这类操作",而非"仅记住此确切操作"
275+
276+
### 规则覆盖与清理
277+
278+
当添加新的模式规则时,Blade 会自动检测并移除被覆盖的旧规则:
279+
280+
```
281+
添加 "Edit(file_path:**/*.ts)" 后:
282+
✅ 保留:Read(file_path:**/*.json) (不冲突)
283+
❌ 删除:Edit(file_path:/path/a.ts) (被覆盖)
284+
❌ 删除:Edit(file_path:/path/b.ts) (被覆盖)
285+
```
286+
287+
## 权限模式
288+
289+
Blade 提供了三种权限模式,控制工具调用的默认行为。
290+
291+
### 模式对比
292+
293+
| 模式 | Read | Search | Edit | Write | Bash | 其他 | 适用场景 |
294+
|------|------|--------|------|-------|------|------|----------|
295+
| **DEFAULT** ||||||| 日常开发(默认) |
296+
| **AUTO_EDIT** ||||||| 频繁修改代码 |
297+
| **YOLO** ||||||| 演示/高度信任 |
298+
299+
✅ = 自动批准 | ❌ = 需要确认
300+
301+
### DEFAULT 模式(默认)
302+
303+
自动批准只读操作,其他操作需要确认。
304+
305+
**设计理念**
306+
- Read/Search 是只读操作,不会修改文件,默认安全
307+
- Edit/Write/Bash 可能修改系统,需要用户确认
308+
309+
**适用场景**
310+
- 日常开发任务
311+
- 不确定 AI 行为时的保守策略
312+
313+
### AUTO_EDIT 模式
314+
315+
在 DEFAULT 基础上,额外自动批准 Edit 操作。
316+
317+
**设计理念**
318+
- 信任 AI 的文件修改能力
319+
- 减少频繁确认,提升效率
320+
- 仍需确认 Write(创建新文件)和 Bash(执行命令)
321+
322+
**适用场景**
323+
- 重构代码
324+
- 批量修改文件
325+
- 频繁的编辑任务
326+
327+
**配置方式**
328+
```json
329+
{
330+
"permissionMode": "autoEdit"
331+
}
332+
```
333+
334+
或通过命令行:
335+
```bash
336+
blade --permission-mode autoEdit
337+
```
338+
339+
### YOLO 模式
340+
341+
自动批准所有工具调用,完全信任 AI。
342+
343+
**⚠️ 警告**
344+
- 跳过所有确认,AI 可以执行任何操作
345+
- 可能删除文件、执行危险命令
346+
- 仅在高度可控的环境使用
347+
348+
**适用场景**
349+
- 演示场景
350+
- 测试环境
351+
- 一次性任务
352+
353+
**配置方式**
354+
```json
355+
{
356+
"permissionMode": "yolo"
357+
}
358+
```
359+
360+
### 权限规则优先级
361+
362+
权限模式与权限规则的优先级关系:
363+
364+
```
365+
DENY 规则 > ALLOW 规则 > 权限模式 > ASK(默认)
366+
```
367+
368+
**示例**
369+
- 即使在 YOLO 模式下,`deny` 规则仍然生效
370+
- `allow` 规则可以覆盖模式的 ASK 行为
371+
- 权限模式只影响未匹配任何规则的工具调用
372+
211373
## /permissions 命令
212374

213375
交互式管理项目本地权限规则(`.blade/settings.local.json`)。

src/config/ConfigManager.ts

Lines changed: 137 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,9 @@ export class ConfigManager {
457457

458458
/**
459459
* 将权限规则追加到项目本地 settings.local.json 的 allow 列表
460+
* 增强功能:
461+
* 1. 去重:检查规则是否已存在
462+
* 2. 清理:移除被新规则覆盖的旧规则
460463
*/
461464
async appendLocalPermissionAllowRule(rule: string): Promise<void> {
462465
const localSettingsPath = path.join(process.cwd(), '.blade', 'settings.local.json');
@@ -475,19 +478,37 @@ export class ConfigManager {
475478
permissions.ask = Array.isArray(permissions.ask) ? permissions.ask : [];
476479
permissions.deny = Array.isArray(permissions.deny) ? permissions.deny : [];
477480

478-
if (!permissions.allow.includes(rule)) {
479-
permissions.allow = [...permissions.allow, rule];
480-
existingSettings.permissions = permissions;
481+
// 检查新规则是否已存在
482+
if (permissions.allow.includes(rule)) {
483+
return; // 规则已存在,无需重复添加
484+
}
481485

482-
await fs.writeFile(
483-
localSettingsPath,
484-
JSON.stringify(existingSettings, null, 2),
485-
{ mode: 0o600, encoding: 'utf-8' }
486+
// 移除被新规则覆盖的旧规则
487+
const originalCount = permissions.allow.length;
488+
permissions.allow = permissions.allow.filter((oldRule: string) => {
489+
return !this.isRuleCoveredBy(oldRule, rule);
490+
});
491+
492+
const removedCount = originalCount - permissions.allow.length;
493+
if (removedCount > 0 && this.config?.debug) {
494+
console.log(
495+
`[ConfigManager] 新规则 "${rule}" 覆盖了 ${removedCount} 条旧规则,已自动清理`
486496
);
487497
}
488498

489-
if (this.config && !this.config.permissions.allow.includes(rule)) {
490-
this.config.permissions.allow = [...this.config.permissions.allow, rule];
499+
// 添加新规则
500+
permissions.allow.push(rule);
501+
existingSettings.permissions = permissions;
502+
503+
await fs.writeFile(
504+
localSettingsPath,
505+
JSON.stringify(existingSettings, null, 2),
506+
{ mode: 0o600, encoding: 'utf-8' }
507+
);
508+
509+
// 更新内存配置
510+
if (this.config) {
511+
this.config.permissions.allow = [...permissions.allow];
491512
}
492513
} catch (error) {
493514
console.error('[ConfigManager] Failed to append local permission rule:', error);
@@ -497,6 +518,113 @@ export class ConfigManager {
497518
}
498519
}
499520

521+
/**
522+
* 判断 rule1 是否被 rule2 覆盖
523+
* 使用 PermissionChecker 的匹配逻辑来判断
524+
* @param rule1 旧规则
525+
* @param rule2 新规则
526+
*/
527+
private isRuleCoveredBy(rule1: string, rule2: string): boolean {
528+
try {
529+
// 动态导入 PermissionChecker
530+
const { PermissionChecker } = require('./PermissionChecker.js');
531+
532+
// 创建只包含新规则的 checker
533+
const checker = new PermissionChecker({
534+
allow: [rule2],
535+
ask: [],
536+
deny: [],
537+
});
538+
539+
// 从 rule1 解析出工具名和参数
540+
const toolName = this.extractToolNameFromRule(rule1);
541+
if (!toolName) return false;
542+
543+
const params = this.extractParamsFromRule(rule1);
544+
545+
// 构造描述符
546+
const descriptor = {
547+
toolName,
548+
params,
549+
affectedPaths: [],
550+
};
551+
552+
// 检查 rule1 是否匹配 rule2
553+
const checkResult = checker.check(descriptor);
554+
return checkResult.result === 'allow';
555+
} catch (error) {
556+
// 解析失败,保守处理:不删除
557+
console.warn(
558+
`[ConfigManager] 无法判断规则覆盖关系: ${error instanceof Error ? error.message : '未知错误'}`
559+
);
560+
return false;
561+
}
562+
}
563+
564+
/**
565+
* 从规则字符串中提取工具名
566+
*/
567+
private extractToolNameFromRule(rule: string): string | null {
568+
const match = rule.match(/^([A-Za-z0-9_]+)(\(|$)/);
569+
return match ? match[1] : null;
570+
}
571+
572+
/**
573+
* 从规则字符串中提取参数
574+
*/
575+
private extractParamsFromRule(rule: string): Record<string, unknown> {
576+
const match = rule.match(/\((.*)\)$/);
577+
if (!match) return {};
578+
579+
const paramString = match[1];
580+
const params: Record<string, unknown> = {};
581+
582+
// 简单解析参数(key:value 格式)
583+
const parts = this.smartSplitParams(paramString);
584+
for (const part of parts) {
585+
const colonIndex = part.indexOf(':');
586+
if (colonIndex > 0) {
587+
const key = part.slice(0, colonIndex).trim();
588+
const value = part.slice(colonIndex + 1).trim();
589+
params[key] = value;
590+
}
591+
}
592+
593+
return params;
594+
}
595+
596+
/**
597+
* 智能分割参数字符串(处理嵌套括号)
598+
*/
599+
private smartSplitParams(str: string): string[] {
600+
const result: string[] = [];
601+
let current = '';
602+
let braceDepth = 0;
603+
let parenDepth = 0;
604+
605+
for (let i = 0; i < str.length; i++) {
606+
const char = str[i];
607+
608+
if (char === '{') braceDepth++;
609+
else if (char === '}') braceDepth--;
610+
else if (char === '(') parenDepth++;
611+
else if (char === ')') parenDepth--;
612+
613+
if (char === ',' && braceDepth === 0 && parenDepth === 0) {
614+
result.push(current.trim());
615+
current = '';
616+
} else {
617+
current += char;
618+
}
619+
}
620+
621+
if (current) {
622+
result.push(current.trim());
623+
}
624+
625+
return result;
626+
}
627+
500628
/**
501629
* 设置权限模式
502630
* @param mode 目标权限模式

0 commit comments

Comments
 (0)