import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import type { BashPermissionConfig, PermissionContext, PermissionCheckResult, PermissionDecision, PermissionRule, } from '../types.js'; import type { PermissionChecker } from './base.js'; import { matchRulesAsync } from '../wildcard.js'; import { parseBashCommand } from '../bash-parser.js'; const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant'); const PERMISSION_FILE = path.join(CONFIG_DIR, 'permissions.json'); // 默认权限配置 const DEFAULT_CONFIG: BashPermissionConfig = { rules: [ // 默认允许的安全命令 { pattern: 'ls *', action: 'allow' }, { pattern: 'cat *', action: 'allow' }, { pattern: 'head *', action: 'allow' }, { pattern: 'tail *', action: 'allow' }, { pattern: 'grep *', action: 'allow' }, { pattern: 'find *', action: 'allow' }, { pattern: 'echo *', action: 'allow' }, { pattern: 'pwd', action: 'allow' }, { pattern: 'which *', action: 'allow' }, { pattern: 'type *', action: 'allow' }, { pattern: 'git status', action: 'allow' }, { pattern: 'git log *', action: 'allow' }, { pattern: 'git diff *', action: 'allow' }, { pattern: 'git branch *', action: 'allow' }, { pattern: 'npm list *', action: 'allow' }, { pattern: 'npm run *', action: 'ask' }, // 需要确认的命令 { pattern: 'git push *', action: 'ask' }, { pattern: 'git commit *', action: 'ask' }, { pattern: 'git checkout *', action: 'ask' }, { pattern: 'git reset *', action: 'ask' }, { pattern: 'npm install *', action: 'ask' }, { pattern: 'npm uninstall *', action: 'ask' }, { pattern: 'yarn *', action: 'ask' }, { pattern: 'pnpm *', action: 'ask' }, // 危险命令 - 默认拒绝 { pattern: 'rm -rf *', action: 'deny' }, { pattern: 'rm -r *', action: 'ask' }, { pattern: 'sudo *', action: 'deny' }, { pattern: 'chmod 777 *', action: 'deny' }, { pattern: 'mkfs *', action: 'deny' }, { pattern: 'dd *', action: 'deny' }, { pattern: '> /dev/*', action: 'deny' }, ], externalDirectory: 'ask', default: 'ask', }; /** * Bash 命令权限检查器 * 使用 tree-sitter 解析命令并检查权限 */ export class BashPermissionChecker implements PermissionChecker { readonly name = 'bash'; private config: BashPermissionConfig; private projectRoot: string; private askCallback?: (ctx: PermissionContext) => Promise; private sessionPermissions = new Map(); constructor(projectRoot: string = process.cwd()) { this.projectRoot = path.resolve(projectRoot); this.config = this.loadConfig(); } /** * 设置权限询问回调 */ setAskCallback(callback: (ctx: PermissionContext) => Promise): void { this.askCallback = callback; } /** * 加载权限配置 */ private loadConfig(): BashPermissionConfig { try { if (fs.existsSync(PERMISSION_FILE)) { const content = fs.readFileSync(PERMISSION_FILE, 'utf-8'); const stored = JSON.parse(content) as Partial; return { rules: stored.rules || DEFAULT_CONFIG.rules, externalDirectory: stored.externalDirectory || DEFAULT_CONFIG.externalDirectory, default: stored.default || DEFAULT_CONFIG.default, }; } } catch { // 忽略加载错误 } return { ...DEFAULT_CONFIG }; } /** * 保存权限配置 */ saveConfig(): void { if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } fs.writeFileSync(PERMISSION_FILE, JSON.stringify(this.config, null, 2)); } /** * 添加权限规则 */ addRule(rule: PermissionRule): void { const existingIndex = this.config.rules.findIndex(r => r.pattern === rule.pattern); if (existingIndex >= 0) { this.config.rules[existingIndex] = rule; } else { this.config.rules.unshift(rule); } this.saveConfig(); } /** * 检查路径是否在项目目录内 */ private isInProjectDirectory(targetPath: string): boolean { const resolved = path.resolve(targetPath); return resolved.startsWith(this.projectRoot + path.sep) || resolved === this.projectRoot; } /** * 从解析后的命令中提取可能的外部路径 */ private async extractPathsFromCommand(command: string, workdir: string): Promise { const externalPaths: string[] = []; const parseResult = await parseBashCommand(command); const pathCommands = new Set(['cd', 'rm', 'cp', 'mv', 'mkdir', 'touch', 'chmod', 'chown', 'cat', 'ls']); for (const cmd of parseResult.commands) { if (!pathCommands.has(cmd.name)) continue; const pathsToCheck = [cmd.subcommand, ...cmd.args].filter(Boolean) as string[]; for (const arg of pathsToCheck) { if (arg.startsWith('-')) continue; let resolved: string | null = null; if (arg.startsWith('/')) { resolved = arg; } else if (arg.startsWith('~')) { resolved = arg.replace('~', os.homedir()); } else if (arg.includes('..') || arg.includes('/')) { resolved = path.resolve(workdir, arg); } if (resolved && !this.isInProjectDirectory(resolved)) { externalPaths.push(resolved); } } } return [...new Set(externalPaths)]; } /** * 检查命令权限 */ async check(ctx: PermissionContext): Promise { const { command, workdir } = ctx; // 1. 使用 tree-sitter 解析命令并检查权限 const parseResult = await matchRulesAsync(command, this.config.rules, this.config.default); // 2. 检查会话级别的临时权限 for (const pattern of parseResult.askPatterns) { const sessionPerm = this.sessionPermissions.get(pattern); if (sessionPerm === 'deny') { return { allowed: false, action: 'deny', reason: `本次会话已拒绝此类命令: ${pattern}`, }; } if (sessionPerm === 'allow') { const allAllowed = parseResult.askPatterns.every(p => this.sessionPermissions.get(p) === 'allow'); if (allAllowed && parseResult.action === 'ask') { return { allowed: true, action: 'allow', reason: '本次会话已允许此类命令', }; } } } // 3. 检查外部目录访问 const externalPaths = await this.extractPathsFromCommand(command, workdir); if (externalPaths.length > 0) { const extAction = this.config.externalDirectory; if (extAction === 'deny') { return { allowed: false, action: 'deny', reason: `命令访问项目目录外的路径: ${externalPaths.join(', ')}`, }; } if (extAction === 'ask') { if (!this.askCallback) { return { allowed: false, action: 'ask', needsConfirmation: true, reason: `命令访问项目目录外的路径`, patterns: externalPaths, }; } const decision = await this.askCallback({ ...ctx, externalPaths, }); if (!decision.allow) { return { allowed: false, action: 'deny', reason: '用户拒绝访问外部目录', }; } } } // 4. 根据解析结果处理权限 const { action, matchedPattern, allCommands, askPatterns } = parseResult; if (action === 'allow') { return { allowed: true, action: 'allow', reason: matchedPattern ? `匹配规则: ${matchedPattern}` : '默认允许', }; } if (action === 'deny') { return { allowed: false, action: 'deny', reason: matchedPattern ? `匹配规则: ${matchedPattern}` : `包含被拒绝的命令: ${allCommands.map(c => c.name).join(', ')}`, }; } // action === 'ask' if (!this.askCallback) { return { allowed: false, action: 'ask', needsConfirmation: true, patterns: askPatterns, }; } const decision = await this.askCallback({ ...ctx, patterns: askPatterns, }); if (decision.remember) { for (const pattern of askPatterns) { this.sessionPermissions.set(pattern, decision.allow ? 'allow' : 'deny'); } } return { allowed: decision.allow, action: decision.allow ? 'allow' : 'deny', reason: decision.allow ? '用户允许' : '用户拒绝', }; } /** * 清除会话权限 */ clearSessionPermissions(): void { this.sessionPermissions.clear(); } /** * 获取当前配置 */ getConfig(): BashPermissionConfig { return { ...this.config }; } }