Files
metona-ai-desktop/electron/harness/tools/built-in/command.ts
T
thzxx 6f08759f63 v0.1.2: 全项目代码审查修复 + 子代理编排器 + 上下文压缩 + 并行工具执行
后端修复: Ollama adapter 移除死代码/修复超时硬编码/iteration硬编码/pullModel无超时/流异常断开补发DONE; SSE解析器支持非字符串arguments; Sandbox fail-closed安全加固; IPC移除未使用变量

新增功能: TaskOrchestrator子任务编排器; DelegateTaskTool委派工具; Agent Loop并行工具执行; 上下文自动压缩; 记忆检索注入System Prompt

前端修复: useAgentStream text_delta thought累积bug; 工具调用状态正确流转; 修复重复stateChange事件; TraceStep.thought正确填充
2026-07-06 22:48:33 +08:00

128 lines
5.2 KiB
TypeScript

/**
* 命令工具(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 { resolve } from 'path';
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';
import { commandTouchesProtectedFile, isPathWithinWorkspace } from './file-guard';
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<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
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));
// 安全校验:workdir 必须在工作空间内
const resolvedWorkdir = resolve(context.workspacePath, workdir);
if (!isPathWithinWorkspace(workdir, context.workspacePath)) {
return { success: false, error: `Working directory must be within workspace: ${workdir}`, command };
}
// 命令安全校验
const validation = this.validateCommand(command);
if (!validation.allowed) {
return { success: false, error: validation.reason, command };
}
try {
const { stdout, stderr } = await execAsync(command, {
cwd: resolvedWorkdir,
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();
// 受保护文件检查:禁止通过命令行读写工作空间根目录的 MEMORY.md
if (commandTouchesProtectedFile(command)) {
return { allowed: false, reason: 'Access denied: MEMORY.md is managed by the memory system and cannot be accessed via command execution' };
}
// 硬阻止列表(绝对禁止执行)
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' },
// Windows 危险命令
{ pattern: /\b(format|diskpart)\b/i, reason: 'Disk formatting commands are forbidden' },
{ pattern: /\bshutdown\s*\//i, reason: 'System shutdown commands are forbidden' },
{ pattern: /\breg\s+(add|delete|import|restore)/i, reason: 'Registry modification commands are forbidden' },
{ pattern: /\b(taskkill|kill)\s*\//i, reason: 'Process termination with system flags is forbidden' },
// 后台进程与管道炸弹
{ pattern: /&\s*\(/, reason: 'Background subshell execution is forbidden' },
{ pattern: /\|\s*&/, reason: 'Pipe to background process is forbidden' },
];
for (const block of hardBlocks) {
if (block.pattern.test(cmd)) {
return { allowed: false, reason: block.reason };
}
}
return { allowed: true };
}
}