Files
metona-ai-desktop/electron/harness/tools/built-in/command.ts
T
thzxx aa13a98f63 feat: 升级至 v0.2.2 — 工作空间路径注入、Provider 切换修复、编码与样式优化
功能增强:
- System Prompt 注入当前工作空间路径到动态区(LLM 可使用绝对路径调用工具)
- ContextBuilder.buildSystemPrompt 新增 workspacePath 参数

Provider 切换修复:
- reloadAdapter 返回 boolean,失败时 sendMessage 中止发送
- 切换 Provider 时自动清空 apiKey(不同 Provider key 不通用)
- apiKey 为空时显示警告提示
- Provider 切换事件加 sessionId 字段

编码修复:
- run_command 使用 encoding: 'buffer' 获取原始字节
- 新增 decodeBuffer 智能解码:UTF-8 严格 → GBK → UTF-8 宽松
- 修复不响应 chcp 的 Windows 命令中文乱码问题

样式优化:
- 代码块底色改用主题变量(灰底主题色文字,双主题适配)
- 表格标题 th/td 底色和文字色改用主题变量
- CodeBlock 组件 pre 背景从 secondary.main 改为 background.default
- 设置面板已自动执行工具行 opacity 0.8→0.15(淡绿底,文字清晰)

版本号 0.2.1 → 0.2.2
2026-07-12 13:46:09 +08:00

222 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 命令工具(1 个)
*
* run_command — 在沙箱环境中执行 Shell 命令
*
* 安全增强(v0.2.0):
* 1. 接入 SandboxManager.scanCode 进行静态代码安全扫描
* 2. 通过 SandboxManager.validatePath 校验工作目录
* 3. 命令注入模式检测扩展
*
* @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 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);
// ===== Windows 编码智能解码 =====
// 某些 Windows 程序(dir、systeminfo、ipconfig 等)不响应 chcp 65001
// 仍按系统代码页(GBK/CP936)输出字节流。
// 策略:先尝试严格 UTF-8 解码,失败则用 GBK 解码,最后降级为 UTF-8 宽松模式。
function decodeBuffer(buf: Buffer): string {
if (buf.length === 0) return '';
try {
// 严格模式:包含非法 UTF-8 字节序列时抛错
return new TextDecoder('utf-8', { fatal: true }).decode(buf);
} catch {
// UTF-8 解码失败 → 用 GBK/CP936 解码(Windows 中文系统默认代码页)
try {
return new TextDecoder('gbk').decode(buf);
} catch {
// 最终降级:UTF-8 宽松模式(用 替换字符 代替非法字节)
return buf.toString('utf-8');
}
}
}
// ===== 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. Passes through SandboxManager static code scan and path validation.',
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,
};
/** 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;
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 };
}
// 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 {
// 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;
// 使用 encoding: 'buffer' 获取原始字节,手动智能解码
// 部分程序(如 dir、systeminfo)不响应 chcp 65001,仍输出 GBK 字节流
const { stdout, stderr } = await execAsync(finalCommand, {
cwd: resolvedWorkdir,
timeout,
maxBuffer: 1024 * 1024, // 1MB
encoding: 'buffer', // 返回 Buffer 而非字符串,便于智能解码
env: {
...process.env,
NODE_ENV: 'production',
...(isWindows ? {
// 强制常见程序使用 UTF-8 输出
PYTHONIOENCODING: 'utf-8',
LANG: 'zh_CN.UTF-8',
LC_ALL: 'zh_CN.UTF-8',
} : {}),
},
});
return {
success: true,
stdout: decodeBuffer(stdout).slice(0, 50_000),
stderr: decodeBuffer(stderr).slice(0, 10_000),
command,
};
} catch (error) {
const err = error as { message: string; code?: number; stdout?: Buffer; stderr?: Buffer };
return {
success: false,
error: err.message,
exit_code: err.code,
stdout: decodeBuffer(err.stdout ?? Buffer.alloc(0)).slice(0, 10_000),
stderr: decodeBuffer(err.stderr ?? Buffer.alloc(0)).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' };
}
// 硬阻止列表(绝对禁止执行)
// 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) {
if (block.pattern.test(cmd)) {
return { allowed: false, reason: block.reason };
}
}
return { allowed: true };
}
}