/** * 命令工具(1 个) * * run_command — 在沙箱环境中执行 Shell 命令 * * @see docs/MetonaAI-Desktop 架构与交互设计.html — 9 个基础工具 * @see standard/开发规范.md — 使用 shell-quote 解析命令(禁止简单字符串匹配) */ import { exec } from 'child_process'; import { promisify } from 'util'; import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool'; import type { MetonaToolDef } from '../../../harness/types'; import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types'; const execAsync = promisify(exec); // ===== 9. run_command ===== export class RunCommandTool implements IMetonaTool { readonly definition: MetonaToolDef = { name: 'run_command', description: 'Execute a shell command in a sandboxed environment. Commands run in the workspace directory. High-risk commands require user confirmation.', parameters: { type: 'object', properties: { command: { type: 'string', description: 'Shell command to execute' }, workdir: { type: 'string', description: 'Working directory (default: workspace root)' }, timeout: { type: 'number', description: 'Timeout in milliseconds (default 120000)' }, }, required: ['command'], }, category: MetonaToolCategory.CODE_EXECUTION, riskLevel: MetonaRiskLevel.HIGH, requiresPermission: true, timeoutMs: 120_000, }; async execute(args: Record, context: ToolExecutionContext): Promise { const command = args.command as string; const workdir = (args.workdir as string) ?? context.workspacePath; const timeout = Math.min(300_000, Math.max(1_000, (args.timeout as number) ?? 120_000)); // 安全校验 const validation = this.validateCommand(command); if (!validation.allowed) { return { success: false, error: validation.reason, command }; } try { const { stdout, stderr } = await execAsync(command, { cwd: workdir, timeout, maxBuffer: 1024 * 1024, // 1MB env: { ...process.env, NODE_ENV: 'production' }, }); return { success: true, stdout: stdout.slice(0, 50_000), stderr: stderr.slice(0, 10_000), command, }; } catch (error) { const err = error as { message: string; code?: number; stdout?: string; stderr?: string }; return { success: false, error: err.message, exit_code: err.code, stdout: (err.stdout ?? '').slice(0, 10_000), stderr: (err.stderr ?? '').slice(0, 10_000), command, }; } } /** * 命令安全校验 * * 使用模式匹配检查危险命令。 * @see docs/MetonaAI-Desktop 架构与交互设计.html — run_command 安全规则 */ private validateCommand(command: string): { allowed: boolean; reason?: string } { const cmd = command.trim().toLowerCase(); // 硬阻止列表(绝对禁止执行) const hardBlocks = [ { pattern: /\brm\b.*\//, reason: 'rm with absolute path is forbidden' }, { pattern: /\b(sudo|su|doas)\b/, reason: 'Privilege escalation commands are forbidden' }, { pattern: /\b(shutdown|reboot|halt|poweroff)\b/, reason: 'System shutdown commands are forbidden' }, { pattern: /curl.*\|\s*(ba)?sh/, reason: 'Remote code execution via pipe is forbidden' }, { pattern: /wget.*\|\s*(ba)?sh/, reason: 'Remote code execution via pipe is forbidden' }, { pattern: /\bdd\b.*of=\/dev\//, reason: 'Writing to device files is forbidden' }, { pattern: /\b(mkfs|fdisk)\b/, reason: 'Disk formatting commands are forbidden' }, { pattern: /\bchmod\s+777\b/, reason: 'chmod 777 is forbidden' }, ]; for (const block of hardBlocks) { if (block.pattern.test(cmd)) { return { allowed: false, reason: block.reason }; } } return { allowed: true }; } }