feat: 添加权限管理系统

- 实现 tree-sitter 解析 bash 命令,准确识别管道、&&、子shell 等复杂命令
- 新增权限检查器模式,支持 allow/deny/ask 三级权限控制
- BashPermissionChecker: 支持命令模式匹配和外部目录访问检测
- FilePermissionChecker: 支持文件操作分级(read/write/edit/list/search/delete)
- 敏感路径规则:系统目录拒绝,SSH/AWS 等凭证目录需确认
- 会话级权限记忆,用户决定可在当前会话内生效
- 所有工具(bash、read_file、write_file、edit_file、list_directory、search_files)已集成权限检查
This commit is contained in:
2025-12-10 18:07:50 +08:00
parent af1185c4d7
commit 60a046357b
19 changed files with 1560 additions and 16 deletions
+18 -11
View File
@@ -2,6 +2,7 @@ import { exec } from 'child_process';
import { promisify } from 'util';
import type { Tool, ToolResult } from '../types/index.js';
import { loadDescription } from './load_description.js';
import { getPermissionManager } from '../permission/index.js';
const execAsync = promisify(exec);
@@ -16,7 +17,7 @@ export const bashTool: Tool = {
},
cwd: {
type: 'string',
description: '工作目录(可选)',
description: '工作目录(可选,默认为当前目录',
required: false,
},
},
@@ -24,22 +25,28 @@ export const bashTool: Tool = {
const command = params.command as string;
const cwd = (params.cwd as string) || process.cwd();
// 安全检查:禁止危险命令
const dangerousPatterns = [
/rm\s+-rf\s+\//, // rm -rf /
/mkfs/, // 格式化磁盘
/dd\s+if=.*of=\/dev/, // 直接写入设备
/>\s*\/dev\/sd/, // 重定向到磁盘设备
];
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkBashPermission({
command,
workdir: cwd,
});
for (const pattern of dangerousPatterns) {
if (pattern.test(command)) {
if (!permResult.allowed) {
// 如果需要用户确认但没有设置回调,返回等待确认的状态
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: '检测到危险命令,已阻止执行',
error: `需要用户确认: ${command}\n原因: ${permResult.reason || '需要权限确认'}\n模式: ${permResult.patterns?.join(', ') || ''}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '命令不被允许执行'}`,
};
}
try {
+26 -1
View File
@@ -2,6 +2,7 @@ import * as fs from 'fs/promises';
import * as path from 'path';
import type { Tool, ToolResult } from '../types/index.js';
import { loadDescription } from './load_description.js';
import { getPermissionManager } from '../permission/index.js';
export const editFileTool: Tool = {
name: 'edit_file',
@@ -27,9 +28,33 @@ export const editFileTool: Tool = {
const filePath = params.path as string;
const oldString = params.old_string as string;
const newString = params.new_string as string;
const cwd = process.cwd();
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(process.cwd(), filePath);
: path.join(cwd, filePath);
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkFilePermission({
operation: 'edit',
path: absolutePath,
workdir: cwd,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: 编辑 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '不允许编辑此文件'}`,
};
}
try {
const content = await fs.readFile(absolutePath, 'utf-8');
+26 -1
View File
@@ -2,6 +2,7 @@ import * as fs from 'fs/promises';
import * as path from 'path';
import type { Tool, ToolResult } from '../types/index.js';
import { loadDescription } from './load_description.js';
import { getPermissionManager } from '../permission/index.js';
export const listDirTool: Tool = {
name: 'list_directory',
@@ -15,9 +16,33 @@ export const listDirTool: Tool = {
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const dirPath = params.path as string;
const cwd = process.cwd();
const absolutePath = path.isAbsolute(dirPath)
? dirPath
: path.join(process.cwd(), dirPath);
: path.join(cwd, dirPath);
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkFilePermission({
operation: 'list',
path: absolutePath,
workdir: cwd,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: 列出目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '不允许列出此目录'}`,
};
}
try {
const entries = await fs.readdir(absolutePath, { withFileTypes: true });
+26 -1
View File
@@ -2,6 +2,7 @@ import * as fs from 'fs/promises';
import * as path from 'path';
import type { Tool, ToolResult } from '../types/index.js';
import { loadDescription } from './load_description.js';
import { getPermissionManager } from '../permission/index.js';
export const readFileTool: Tool = {
name: 'read_file',
@@ -15,9 +16,33 @@ export const readFileTool: Tool = {
},
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const filePath = params.path as string;
const cwd = process.cwd();
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(process.cwd(), filePath);
: path.join(cwd, filePath);
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkFilePermission({
operation: 'read',
path: absolutePath,
workdir: cwd,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: 读取 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '不允许读取此文件'}`,
};
}
try {
const content = await fs.readFile(absolutePath, 'utf-8');
+26 -1
View File
@@ -2,6 +2,7 @@ import * as fs from 'fs/promises';
import * as path from 'path';
import type { Tool, ToolResult } from '../types/index.js';
import { loadDescription } from './load_description.js';
import { getPermissionManager } from '../permission/index.js';
export const searchFilesTool: Tool = {
name: 'search_files',
@@ -21,9 +22,33 @@ export const searchFilesTool: Tool = {
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const directory = params.directory as string;
const pattern = params.pattern as string;
const cwd = process.cwd();
const absolutePath = path.isAbsolute(directory)
? directory
: path.join(process.cwd(), directory);
: path.join(cwd, directory);
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkFilePermission({
operation: 'search',
path: absolutePath,
workdir: cwd,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: 搜索目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '不允许搜索此目录'}`,
};
}
const matches: string[] = [];
const regex = new RegExp(
+26 -1
View File
@@ -2,6 +2,7 @@ import * as fs from 'fs/promises';
import * as path from 'path';
import type { Tool, ToolResult } from '../types/index.js';
import { loadDescription } from './load_description.js';
import { getPermissionManager } from '../permission/index.js';
export const writeFileTool: Tool = {
name: 'write_file',
@@ -21,9 +22,33 @@ export const writeFileTool: Tool = {
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
const filePath = params.path as string;
const content = params.content as string;
const cwd = process.cwd();
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(process.cwd(), filePath);
: path.join(cwd, filePath);
// 权限检查
const permissionManager = getPermissionManager();
const permResult = await permissionManager.checkFilePermission({
operation: 'write',
path: absolutePath,
workdir: cwd,
});
if (!permResult.allowed) {
if (permResult.needsConfirmation) {
return {
success: false,
output: '',
error: `需要用户确认: 写入 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
};
}
return {
success: false,
output: '',
error: `权限被拒绝: ${permResult.reason || '不允许写入此文件'}`,
};
}
try {
await fs.mkdir(path.dirname(absolutePath), { recursive: true });