bash tool
设计挑战
核心实现
输入 Schema
const BashInputSchema = z.object({
command: z.string(),
cwd: z.string().optional(),
explanation: z.string().optional(),
timeout: z.number().optional(),
});const BashInputSchema = z.object({
command: z.string(),
cwd: z.string().optional(),
explanation: z.string().optional(),
timeout: z.number().optional(),
});async execute(input: BashInput, context: ToolContext) {
// 1. 命令分类
const classification = classifyCommand(input.command);
// 2. 权限检查
const permission = await checkBashPermission(
input.command,
classification,
context
);
if (!permission.allowed) {
return {
success: false,
error: `Permission denied: ${permission.reason}`,
};
}
// 3. 执行命令
const result = await executeCommand(input.command, {
cwd: input.cwd || context.cwd,
timeout: input.timeout || 30000,
});
return {
success: true,
output: result.stdout + result.stderr,
};
}// src/tools/BashTool/bashPermissions.ts
function classifyCommand(command: string): CommandClassification {
// 危险命令
if (/rm\s+-rf\s+\//.test(command)) {
return { level: 'dangerous', reason: 'Deletes root directory' };
}
// 写操作
if (/^(echo|cat)\s+>/.test(command)) {
return { level: 'write', reason: 'Writes to file' };
}
// 只读操作
if (/^(ls|cat|grep|find)/.test(command)) {
return { level: 'read', reason: 'Read-only operation' };
}
// 网络操作
if (/^(curl|wget|ssh)/.test(command)) {
return { level: 'network', reason: 'Network access' };
}
// 未知命令
return { level: 'unknown', reason: 'Unknown command' };
}async function checkBashPermission(
command: string,
classification: CommandClassification,
context: ToolContext
): Promise<PermissionResult> {
const mode = context.state.permissions.mode;
// Auto 模式
if (mode === 'auto') {
if (classification.level === 'read') {
return { allowed: true };
}
if (classification.level === 'write') {
// 使用分类器判断
return await classifierApprove(command, context);
}
if (classification.level === 'dangerous') {
return { allowed: false, reason: 'Dangerous command' };
}
}
// Manual 模式
if (mode === 'manual') {
return await askUser(command, classification);
}
// Supervised 模式
if (mode === 'supervised') {
const result = await executeDryRun(command);
return await askUserWithPreview(command, result);
}
}async function executeCommand(
command: string,
options: ExecOptions
): Promise<ExecResult> {
return new Promise((resolve, reject) => {
const child = spawn(command, {
shell: true,
cwd: options.cwd,
timeout: options.timeout,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', data => {
stdout += data.toString();
});
child.stderr.on('data', data => {
stderr += data.toString();
});
child.on('close', code => {
resolve({ code, stdout, stderr });
});
child.on('error', reject);
});
}// controlPwshProcess 工具
async function startBackgroundProcess(
command: string,
cwd: string
): Promise<string> {
const terminalId = generateId();
const child = spawn(command, {
shell: true,
cwd: cwd,
detached: true,
});
// 保存进程引用
backgroundProcesses.set(terminalId, {
pid: child.pid,
command: command,
startTime: Date.now(),
});
return terminalId;
}function adaptCommandForWindows(command: string): string {
// 替换 Unix 命令为 Windows 等价命令
return command
.replace(/^ls\b/, 'dir')
.replace(/^cat\b/, 'type')
.replace(/^rm\b/, 'del')
.replace(/&&/g, ';'); // PowerShell 使用分号
}function detectShell(): 'bash' | 'zsh' | 'fish' | 'powershell' {
if (process.platform === 'win32') {
return 'powershell';
}
const shell = process.env.SHELL || '';
if (shell.includes('zsh')) return 'zsh';
if (shell.includes('fish')) return 'fish';
return 'bash';
}function handleSedCommand(command: string): string {
if (process.platform === 'darwin') {
// macOS 需要 -i '' 而不是 -i
return command.replace(/sed\s+-i\b/, 'sed -i ""');
}
return command;
}function parsePipelineCommand(command: string): string[] {
// 分解管道命令
return command.split('|').map(cmd => cmd.trim());
}
async function executePipeline(commands: string[]): Promise<string> {
let output = '';
for (const cmd of commands) {
output = await executeCommand(cmd, { input: output });
}
return output;
}const SAFE_COMMANDS = new Set([
'ls', 'cat', 'grep', 'find', 'head', 'tail',
'git status', 'git log', 'git diff',
'npm list', 'npm outdated',
]);
function isSafeCommand(command: string): boolean {
const base = command.split(' ')[0];
return SAFE_COMMANDS.has(base) || SAFE_COMMANDS.has(command);
}const DANGEROUS_PATTERNS = [
/rm\s+-rf\s+\//, // 删除根目录
/chmod\s+777/, // 修改权限
/sudo\s+/, // 提权
/curl\s+.*\|\s*bash/, // 下载并执行
/>\s*\/dev\/sd[a-z]/, // 直接写入磁盘
];
function isDangerous(command: string): boolean {
return DANGEROUS_PATTERNS.some(pattern => pattern.test(command));
}async function executeSandboxed(
command: string,
cwd: string
): Promise<ExecResult> {
// 使用 Docker 容器隔离
const sandboxCommand = `
docker run --rm \
-v ${cwd}:/workspace \
-w /workspace \
--network none \
alpine:latest \
sh -c "${escapeCommand(command)}"
`;
return executeCommand(sandboxCommand);
}const MAX_OUTPUT_SIZE = 50000;
function truncateOutput(output: string): string {
if (output.length <= MAX_OUTPUT_SIZE) {
return output;
}
const head = output.slice(0, MAX_OUTPUT_SIZE * 0.8);
const tail = output.slice(-MAX_OUTPUT_SIZE * 0.2);
return `${head}\n\n[... ${output.length - MAX_OUTPUT_SIZE} characters truncated ...]\n\n${tail}`;
}function intelligentPrune(
output: string,
explanation: string
): string {
// 根据 explanation 保留相关内容
const keywords = extractKeywords(explanation);
const lines = output.split('\n');
const relevant = lines.filter(line =>
keywords.some(kw => line.includes(kw))
);
if (relevant.length < lines.length * 0.3) {
// 相关内容太少,返回完整输出
return output;
}
return relevant.join('\n');
}