From b6468d22b531fde5da427baa52300f9624b755a1 Mon Sep 17 00:00:00 2001 From: 01luyicheng Date: Sun, 1 Feb 2026 19:07:52 +0800 Subject: [PATCH] fix: prevent Windows reserved device names from being added to directory checks and simplify bash description parameter --- packages/opencode/src/tool/bash.ts | 61 +++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 5d073fd681..b825b237cf 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -1,3 +1,29 @@ +/** + * 文件用途:Bash工具实现,提供命令执行、权限管理、输出截断等功能 + * 作者:TRAE + * 创建日期:2026-02-01 + * + * 输入输出签名: + * - 输入:command(命令字符串)、timeout(超时毫秒)、workdir(工作目录)、description(命令描述) + * - 输出:{ title, metadata: { output, exit, description }, output } + * + * 依赖列表: + * - zod@latest + * - web-tree-sitter@latest + * - tree-sitter-bash@latest + * - bun@latest + * + * 与其他模块交互方式: + * - 调用Instance.directory获取项目根目录 + * - 调用ctx.ask()请求权限(external_directory、bash) + * - 调用ctx.metadata()更新元数据 + * - 调用ctx.abort监听中止事件 + * - 调用Shell.killTree()终止进程树 + * + * 其他备注: + * - 相关文件:bash.txt(工具描述模板) + * - 支持Windows、Linux、macOS多平台 + */ import z from "zod" import { spawn } from "child_process" import { Tool } from "./tool" @@ -22,6 +48,35 @@ const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 export const log = Log.create({ service: "bash-tool" }) +const WINDOWS_RESERVED_DEVICE_NAMES = new Set([ + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", +]) + +/** + * 实现说明:Windows保留设备名称检查 + * + * Windows系统保留以下设备名称,不能作为文件名使用: + * - CON, PRN, AUX, NUL(标准设备) + * - COM1-9(串行端口) + * - LPT1-9(并行端口) + * + * 检查逻辑: + * 1. 提取路径的基本名称(去除目录部分) + * 2. 转换为大写(Windows文件系统不区分大小写) + * 3. 检查是否在保留名称集合中 + * + * 注意事项: + * - 此检查应在调用realpath之前进行,避免无效命令 + * - 只检查基本名称,不检查路径中间的保留名称 + * - 适用于Windows平台(process.platform === 'win32') + */ +const isWindowsReservedDeviceName = (name: string): boolean => { + const baseName = path.basename(name).toUpperCase() + return WINDOWS_RESERVED_DEVICE_NAMES.has(baseName) +} + const resolveWasm = (asset: string) => { if (asset.startsWith("file://")) return fileURLToPath(asset) if (asset.startsWith("/") || /^[a-z]:/i.test(asset)) return asset @@ -71,7 +126,7 @@ export const BashTool = Tool.define("bash", async () => { description: z .string() .describe( - "Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'", + "Clear, concise description of what this command does in 5-10 words", ), }), async execute(params, ctx) { @@ -115,6 +170,10 @@ export const BashTool = Tool.define("bash", async () => { if (["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown", "cat"].includes(command[0])) { for (const arg of command.slice(1)) { if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue + if (process.platform === "win32" && isWindowsReservedDeviceName(arg)) { + log.info("skipping Windows reserved device name", { arg }) + continue + } const resolved = await $`realpath ${arg}` .cwd(cwd) .quiet()