feat: 升级至 v0.2.1 — 流式渲染修复、安全增强、工具自动执行

流式渲染修复:
- runId 机制防止 abort 后旧流事件污染新 run
- run lock 防止并发 run 污染引擎状态
- abort race 提前退出工具执行等待
- TERMINATED 状态通过 stateChange 发射
- tool_call_delta 流式参数拼接 + pending 占位替换
- 首轮卡片创建路径统一,traceStep 按 ID 精确匹配
- compressed 事件转发为 toast 通知

安全增强:
- ConfirmationHook 支持持久化自动执行(跨会话)
- 设置面板新增自动执行工具管理 UI
- SandboxManager 双重安全校验 fail-closed
- 审计日志链式哈希防篡改
- PromptInjectionDefender 中文注入标记清理
- scanCode 28 模式 + base64/$() 检测
- validatePath realpathSync 防符号链接逃逸
- code-search 使用 execFile 防命令注入

新增工具:
- file_editor、code_search、task_manager、diff_viewer

其他:
- Agent Loop 加 PARSING/REFLECTING 状态 + 指数退避重试
- MemoryManager TF-IDF 语义检索
- run_command Windows 中文编码修复(chcp 65001)
- 版本号 0.2.0 → 0.2.1
This commit is contained in:
thzxx
2026-07-12 12:54:52 +08:00
parent 469f53b623
commit 3c5aea8fb7
41 changed files with 4972 additions and 423 deletions
+75 -4
View File
@@ -3,6 +3,11 @@
*
* run_command — 在沙箱环境中执行 Shell 命令
*
* 安全增强(v0.2.0):
* 1. 接入 SandboxManager.scanCode 进行静态代码安全扫描
* 2. 通过 SandboxManager.validatePath 校验工作目录
* 3. 命令注入模式检测扩展
*
* @see docs/MetonaAI-Desktop 架构与交互设计.html — 9 个基础工具
* @see standard/开发规范.md — 使用 shell-quote 解析命令(禁止简单字符串匹配)
*/
@@ -10,10 +15,12 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import { resolve } from 'path';
import log from 'electron-log';
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';
import type { SandboxManager } from '../../sandbox/sandbox';
const execAsync = promisify(exec);
@@ -22,7 +29,7 @@ const execAsync = promisify(exec);
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.',
description: 'Execute a shell command in a sandboxed environment. Commands run in the workspace directory. High-risk commands require user confirmation. Passes through SandboxManager static code scan and path validation.',
parameters: {
type: 'object',
properties: {
@@ -38,6 +45,14 @@ export class RunCommandTool implements IMetonaTool {
timeoutMs: 120_000,
};
/** v0.2.0: 可选注入 SandboxManager 进行双重安全校验 */
private sandboxManager: SandboxManager | null = null;
/** 注入 SandboxManager(由 main.ts 在注册时调用) */
setSandboxManager(manager: SandboxManager): void {
this.sandboxManager = manager;
}
async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
const command = args.command as string;
const workdir = (args.workdir as string) ?? context.workspacePath;
@@ -49,18 +64,56 @@ export class RunCommandTool implements IMetonaTool {
return { success: false, error: `Working directory must be within workspace: ${workdir}`, command };
}
// 命令安全校验
// v0.2.0: SandboxManager 双重安全校验 — fail-closed 设计
// 若 SandboxManager 未注入(配置错误),拒绝执行高风险命令
if (!this.sandboxManager) {
log.error('[RunCommandTool] SandboxManager not injected — refusing to execute (fail-closed)');
return {
success: false,
error: 'Security sandbox unavailable — command execution disabled',
command,
};
}
// SandboxManager 静态代码扫描
const scanResult = this.sandboxManager.scanCode(command);
if (!scanResult.safe) {
return { success: false, error: 'Command blocked by security policy', command };
}
// SandboxManager 路径校验
const pathResult = this.sandboxManager.validatePath(resolvedWorkdir);
if (!pathResult.allowed) {
return { success: false, error: 'Command blocked by security policy', command };
}
// 命令安全校验(内置硬阻止列表)
const validation = this.validateCommand(command);
if (!validation.allowed) {
return { success: false, error: validation.reason, command };
}
try {
const { stdout, stderr } = await execAsync(command, {
// Windows 中文编码修复:通过 chcp 65001 切换控制台到 UTF-8 代码页
// 避免 GBK/CP936 输出被 Node.js 按 UTF-8 解码导致中文乱码
const isWindows = process.platform === 'win32';
const finalCommand = isWindows
? `chcp 65001 >nul 2>&1 && ${command}`
: command;
const { stdout, stderr } = await execAsync(finalCommand, {
cwd: resolvedWorkdir,
timeout,
maxBuffer: 1024 * 1024, // 1MB
env: { ...process.env, NODE_ENV: 'production' },
env: {
...process.env,
NODE_ENV: 'production',
...(isWindows ? {
// 强制常见程序使用 UTF-8 输出
PYTHONIOENCODING: 'utf-8',
LANG: 'zh_CN.UTF-8',
LC_ALL: 'zh_CN.UTF-8',
} : {}),
},
});
return {
@@ -97,23 +150,41 @@ export class RunCommandTool implements IMetonaTool {
}
// 硬阻止列表(绝对禁止执行)
// v0.2.0: 扩展危险命令检测模式
const hardBlocks = [
// 文件系统破坏
{ pattern: /\brm\b.*\//, reason: 'rm with absolute path is forbidden' },
{ pattern: /\brm\s+-rf?\s+\/(?:[^|;&\s]*\s)*?(?:bin|boot|dev|etc|lib|proc|root|sbin|sys|usr|var)\b/i, reason: 'rm on system directories 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: /\b(killall|pkill)\s+-9\b/, reason: 'Force kill all processes is 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: /\bcurl\s+.*\s*-o\s+\/etc\//i, reason: 'Writing to system directories via curl 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' },
{ pattern: /\bchown\s+-R\s+\S+\s+\/(?:\s|$)/i, reason: 'Recursive chown on root is forbidden' },
// 环境变量窃取
{ pattern: /\b(env|export|printenv)\s*\|.*\b(curl|wget|nc|ncat)\b/i, reason: 'Exfiltrating environment variables is forbidden' },
// 反向 shell
{ pattern: /\b(bash|sh|zsh)\s+-i\s+>\s*&\s*\/dev\/tcp\//i, reason: 'Reverse shell via /dev/tcp is forbidden' },
{ pattern: /\bnc\s+.*\s+-e\s+(bash|sh)/i, reason: 'Reverse shell via netcat 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: /\bpowershell\s+-enc\s+/i, reason: 'PowerShell encoded command execution is forbidden' },
// 后台进程与管道炸弹
{ pattern: /&\s*\(/, reason: 'Background subshell execution is forbidden' },
{ pattern: /\|\s*&/, reason: 'Pipe to background process is forbidden' },
{ pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/, reason: 'Fork bomb is forbidden' },
];
for (const block of hardBlocks) {