feat: MetonaAI Desktop 初始项目
- Electron + React + TypeScript 架构 - 三栏布局: Sidebar | ChatPanel | DetailPanel - 9 个内置工具 (文件系统/网络/记忆/命令) - SQLite 持久化 (better-sqlite3) - MUI 暗色/亮色主题系统 - Agent Loop ReAct 状态机引擎 - DeepSeek / Agnes AI / Ollama Provider 适配器 - MCP 协议集成 - 系统托盘 + 全局快捷键 - Tailwind CSS v4 + Tailwind Merge - 修复: Sidebar 缺失 TextField 导入导致黑屏
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 命令工具(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<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));
|
||||
|
||||
// 安全校验
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user