opencode CodeMode:受限代码执行引擎的设计边界与实现原则
本文围绕 @opencode-ai/codemode 包的设计约束文档 packages/codemode/AGENTS.md 展开,系统讲解这个受限执行引擎的四个核心授权原则、OpenAPI 适配器的五条设计红线,以及面向未来能力扩展(输出通道、错误分类、二进制边界、宿主能力)的设计笔记。结合 README、设计文档 与源码实现,读者将理解"模型生成代码"在 opencode 中如何被安全地限定在宿主显式提供的工具树内运行,以及如何在自研 Agent 中正确地为它设置边界。
包定位:谁拥有什么权力
packages/codemode/AGENTS.md 开篇即给出本包的四条总纲,它们是理解 CodeMode 所有 API 设计的前提:
- 本包只拥有"对显式 schema 描述工具的受限执行"(confined execution over explicit schema-described tools)。认证、授权、持久化、外部权限、工具特定投递语义全部归宿主应用所有。
- 不要添加臆测性的通用权限/审批策略。宿主应通过"不暴露某工具"来表达限制,并在每个提供的工具内部执行领域授权,而不是依赖 CodeMode 提供一层通用审批。
- 保持 CodeMode 对宿主会话、频道、会话模型无知。由宿主应用在它外围提供可信的执行作用域。
- 工具 schema 就是面向模型的接口。参数应保持最小且贴合操作本身,绝不要塞入无关 ID 当作"环境能力令牌"(ambient capability token)使用。
这些原则在 README 的 Authority Boundary 一节 中被展开为一张责任对照表:宿主负责认证与授权、工具选择与不可变作用域、凭据与网络客户端、持久化/幂等/审批/持久副作用、日志与脱敏策略;CodeMode 只负责不用 eval 地解析并解释执行受支持子集、工具调用周围的 schema 边界、纯数据复制与原型成员屏蔽、资源限制与调用计数、面向模型的工具发现与指令。README 还特别强调了一条反直觉但关键的结论:"程序不能通过散文或生成的代码获得权力,它只能行使所提供的工具中已存在的权力。不要暴露宽泛工具再指望提示词去限制它。"
从源码结构看,这个定位是"硬编码"的而非口号。入口 src/index.ts 只导出 CodeMode、Tool、OpenAPI 命名空间与 ToolError/toolError——没有任何会话、上下文或权限服务的抽象泄漏进公共 API。工具的唯一定义入口 Tool.make 接收 description、input、可选 output 和 run 四个字段,run 是一个返回 Effect 的函数,其类型参数 R 允许工具声明自己的服务需求(如 HttpClient.HttpClient),CodeMode 会原样透传这些服务需求而不去"替宿主决策":
const lookupOrder = Tool.make({
description: "Look up an order by ID",
input: Schema.Struct({ id: Schema.String }),
output: Schema.Struct({ id: Schema.String, status: Schema.String }),
run: ({ id }) => Effect.succeed({ id, status: "open" }),
})
const runtime = CodeMode.make({ tools: { orders: { lookup: lookupOrder } } })
const result =
yield * runtime.execute(`
const order = await tools.orders.lookup({ id: "order_42" })
return { id: order.id, needsAttention: order.status !== "complete" }
`)
程序里通过 tools.<namespace>.<tool>(...) 调用工具,能拿到的就是宿主放入这棵对象树的东西——这就是第 1 条原则的最小实现形态。
OpenAPI 适配器的五条设计红线
AGENTS.md 的 ## OpenAPI 小节把 OpenAPI 适配器(把 OpenAPI 3.x 文档转成 CodeMode 工具子树)的纪律浓缩为五条规则,每一条都对应一类真实事故模式:
- 只有传输语义被支持时才生成操作,否则返回精确的
skipped原因。src/openapi/types.ts 中Skipped类型被定义为{ method, path, reason },fromSpec同步返回{ tools, skipped }——被跳过的操作不是静默消失,而是携带可审计的理由。 - 绝不猜测参数序列化或畸形的 security 语义。不支持的序列化走
skipped,畸形的安全定义则"fail closed"(直接失败而非降级)。 - 无法解析的 schema 构造渲染为
unknown,绝不发明 TypeScript 名字——避免模型基于虚构类型生成错误调用。 - 网络读取必须有界,并把预期的编码、传输、解码失败映射为对模型安全的
ToolError值。 - 测试直接覆盖支持的行为,不要在测试中复刻适配器算法本身——防止测试与实现"同步腐烂"。
README 的 OpenAPI 工具一节 给出了这些规则下的实际能力面:OpenAPI.fromSpec({ spec, auth, baseUrl?, headers? }) 接受已解析的 JSON 文档(YAML 需宿主自行解析),一个 operation 生成一个工具;带点的 operationId(如 v2.session.get)形成命名空间,缺失 ID 时回退到 getUsersById 这类 method/path 扁平命名并做去重。首个适配器支持 query 的 form/deepObject、path/header 的 simple 风格、JSON 请求体、JSON 与文本响应;不支持的参数编码、非 JSON 请求体、二进制响应、流式操作全部落入 skipped。操作级与路径级 server 优先于文档级 server,除非 baseUrl 显式覆盖全部。
认证语义值得单独展开。openapi/types.ts 中凭据材料是宿主解析器在调用时通过 auth.resolve 提供的联合类型(bearer / basic / apiKey / header),解析器返回 undefined 表示"该方案不可用、尝试下一个 OR 备选",而解析失败会中止调用而不是静默跳过;cookie 认证的备选被直接丢弃,某操作若无任何受支持的备选则整体进入 skipped。这正体现了红线第 2 条:"认证绝不进入模型可见面",凭据存储、OAuth 流程、令牌刷新永远留在编译器之外。响应限制在 50 MiB,非 2xx 响应变成携带状态码与截断 body 摘要的安全工具失败;生成的工具在执行期需要 HttpClient.HttpClient(来自 effect/unstable/http),宿主提供 FetchHttpClient.layer 或自定义测试客户端,重定向策略归客户端所有——带凭据的宿主应当拒绝重定向或在源变化时剥离凭据。未完成的适配能力记录在 src/openapi/TODO.md。
import { CodeMode, OpenAPI } from "@opencode-ai/codemode"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
const api = OpenAPI.fromSpec({
spec: await Bun.file("openapi.json").json(), // parsed document (no YAML)
auth: {
resolve: ({ name, scopes, operation }) =>
name === "BearerAuth" ? Effect.succeed({ type: "bearer", token }) : Effect.succeed(undefined),
},
})
const runtime = CodeMode.make({ tools: { opencode: api.tools } })
const result = await Effect.runPromise(runtime.execute(code).pipe(Effect.provide(FetchHttpClient.layer)))
未来设计笔记:六条尚未动工的设计储备
AGENTS.md 的 ## Future Design Notes 是文档中最容易被忽视、但最能反映设计者思考深度的部分。它们不是待办清单,而是"如果将来要做 X,必须遵守的前提":
- 输出通道(output channel):若未来恢复用户可见的输出通道(早期
output.text/output.file/output.imageAPI 已在 v1 中移除),必须保持output这个名字,并与程序返回值严格区分:return永远是给模型的结构化结果,output.*描述宿主在执行后可能渲染进会话或 UI 的产物;保持宿主中立,v1 的做法是宿主在沙箱外自行收集媒体。这与 codemode.md 中"文件与附件内容留在解释器之外,宿主可在子工具执行期间收集并挂到外层结果"的现状一致。 - 改进沙箱失败分类:区分解析/编译错误、不支持语法、用户抛出错误、非法返回数据、工具拒绝、工具内部故障、超时与真正的运行时缺陷,让 Agent 能精确恢复,而不是把所有东西都当成泛化执行失败。当前 DiagnosticKind 已经实现了十个稳定类别(
ParseError、UnsupportedSyntax、UnknownTool、InvalidToolInput、InvalidToolOutput、InvalidDataValue、ToolCallLimitExceeded、TimeoutExceeded、ToolFailure、ExecutionFailure),该笔记指向进一步细化的方向。 - 保持公共/私有错误分割:工具作者应能返回一条对模型安全的可见消息,同时保留一个私有 cause 供宿主诊断;未知宿主故障必须默认被消毒。这正是 toolError 的实现契约——
toolError(message, cause?)创建的ToolError中只有message是模型可见的,cause永远不会出现在CodeMode.Result里:
import { toolError } from "@opencode-ai/codemode"
run: ({ id }) => (authorized(id) ? loadOrder(id) : Effect.fail(toolError("Order is unavailable")))
- 二进制边界要审慎设计:在允许
Blob、File、ArrayBuffer、流或类型化数组越过边界之前,先用显式的 tagged 数据形态和清晰的尺寸限制,而不是依赖宿主运行时的环境序列化。当前程序结果与工具参数是 JSON 风格数据:Date 在宿主边界变成 ISO 字符串、RegExp/Map/Set/URLSearchParams 变成{}(与JSON.stringify一致),Promise 值永远不能跨边界——未 await 的 promise 出现在结果或工具参数里会得到一条"请 await 它"的诊断。 - 宿主能力必须显式化:
fetch、crypto、文件句柄、额外模块或网络客户端应当是带明显策略默认值的 opt-in 运行时能力,而不是环境权力;默认不可用,除非宿主刻意提供。 fetch若落地则建模为宿主提供的出站能力,带策略控制:允许的源、方法、头、响应大小、超时,以及响应体是返回、发出还是仅通过工具摘要。
设计文档中的 Intentionally Unsupported 一节 与这些笔记互为镜像:环境文件系统、进程、环境、网络、凭据或应用访问;模块、动态导入、eval、任意宿主全局、npm 包、原型变更;通用权限提示、持久暂停/恢复、重放、恰好一次外部副作用——这些被明确定义为"产品边界而非 DSL 待办"。
源码佐证:原则如何落成实现
把 AGENTS.md 的四条总纲与实现对照,可以看到每条原则都有具体的落点:
受限执行通过自有的树遍历解释器实现。 interpreter/runtime.ts 的 parseProgram 先用 TypeScript 编译器 API 把源码包进一个 async function 转译掉 TS 语法(失败即 ParseError 诊断),再用 Acorn 解析出 AST,随后由自有的树遍历解释器执行——全程没有 eval。受支持子集是有意的编排语言:字面量、属性访问、解构、if/switch/各类循环、带闭包的箭头函数与函数声明、可选链与空值合并、模板串、spread、try/catch,以及数组/字符串/数字/Object/Math/JSON 的常用操作、Date、正则、Map/Set、URL 工具与一等 Promise(Promise.all/allSettled/race,最多 8 个工具调用并发,未 await 的调用在程序完成前排空)。不支持的语法返回带源码位置的 UnsupportedSyntax 诊断。
结果与失败都是数据。 CodeMode.execute/CodeMode.make(...).execute 返回的 CodeMode.Result 是一个联合 schema:成功为 { ok: true, value, logs?, truncated?, toolCalls },失败为 { ok: false, error: Diagnostic, logs?, truncated?, toolCalls }(见 Success/Failure schema 定义)。程序失败、校验失败、限额失败、工具失败都作为诊断返回而非使 Effect 失败;只有宿主中断保持为 Effect 中断。toolCalls 在失败时也被保留,让宿主可以审计部分执行而不暴露输入或宿主故障。
资源预算没有默认值,是刻意为之。 codemode.ts 的 validateLimit 在 make/execute 调用时对 timeoutMs(至少 1)、maxToolCalls(可 0)、maxOutputBytes(可 0)做安全整数校验,非法配置抛 RangeError。三个旋钮都没有默认值,因为"执行预算是宿主策略而非库策略":能中断执行 fiber 的宿主(如 OpenCode 在用户取消时所做)可以不设超时,有自己工具输出截断的宿主可以不设 maxOutputBytes——但两者都没有的宿主应当设置 maxOutputBytes,否则超大结果会无声地灌满模型上下文。超限的 maxOutputBytes 不会使执行失败:值被替换为截断的序列化文本加解释标记、日志从头部保留到预算耗尽、结果携带 truncated: true。
工具发现是预算化的渐进披露。 tool-runtime.ts 定义了保留命名空间 $codemode(宿主不得占用)、默认 2,000 估算 token 的目录预算(字符数除以 4)、默认搜索页大小 10。指令按"每个命名空间始终列出其工具计数 → 在预算内尽量多地内联完整带 JSDoc 的签名(跨命名空间轮转,避免大命名空间饿死小命名空间)→ 显式声明列表是 COMPLETE 还是 PARTIAL"的结构组织。tools.$codemode.search 始终注册(即使在目录全量内联时),保证模型的投机式搜索调用永远不会以"未知工具"失败;它做确定性加权匹配(精确路径 20 分、路径子串 8 分、描述子串 4 分、可搜索文本 2 分),支持命名空间浏览、精确路径查找与分页。
公开契约以"定律"形式固化。 README 的 Laws 一节 列出了五条等价式:CodeMode.execute({ ...options, code }) 等价于 CodeMode.make(options).execute(code);工具实现只在输入成功解码后才被调用;工具结果只在输出解码并越过纯数据边界后才对程序可见;未知宿主失败不会变成模型可见诊断(ToolError 是显式的安全消息通道);宿主中断保持为中断而非 CodeMode.Failure。
验证方式
AGENTS.md 对 OpenAPI 测试的要求("直接测试支持的行为,不要复刻适配器算法")与 README 的 Testing 一节共同给出验证入口。在包目录下运行:
bun test
bun run typecheck
直接测试套件(packages/codemode/test/)覆盖公共投影、发现机制、schema 边界、诊断消毒、资源限额、工具调用观察与中断;openapi.test.ts 与 parity.test.ts 则分别对应适配器行为与跨投影一致性。需要注意的前提:该包目前是工作区私有包(workspace:* 依赖),宿主需自行依赖 effect 并在执行期提供 HttpClient 等工具实现所需的服务层。
小结
packages/codemode/AGENTS.md 的价值在于它用极短的篇幅划定了三层边界:对宿主——CodeMode 只拥有受限执行,授权、持久化、投递语义归应用;对模型——工具 schema 即接口,失败以分类诊断数据的形式返回且保持可恢复;对适配器——不确定就 skipped 或 fail closed,绝不猜测。结合 README 的 API 文档、codemode.md 的决策表与 src/ 下的实现源码,这套"边界即契约"的设计可以作为构建 Agent 受限代码执行层时的参照:先把权力收敛到显式工具树,再把预算、诊断与发现机制建立在它之上。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00