OpenCode CodeMode:用 Effect 实现受约束的程序化工具编排与 Schema 化代码执行
OpenCode 仓库中的 @opencode-ai/codemode 包提供了一套 Effect 原生的“受限代码执行”(confined code execution)机制:让模型编写一段小型 JavaScript 程序,该程序只能调用宿主显式提供的、由 Schema 描述的工具,而无法获得文件系统、进程、网络或模块等环境权限。本篇基于 packages/codemode/README.md 完整展开其设计目标、公共 API、执行限制、诊断模型与权威边界,并结合 packages/codemode/src 下的源码实现与 packages/codemode/test 下的测试用例,给出可复制的接入方式与原理级佐证。读完后,你将能够在自己的 Agent 宿主中安全地让模型“写代码来编排工具调用”,并理解其隔离机制、资源预算与故障脱敏的完整链路。
一、定位与核心思想
CodeMode 的核心承诺是:程序只能调用宿主注入的工具树,不能获得任何环境权限(ambient authority)。模型写出的程序可以顺序调用、转换纯数据、分支、循环,并把相互独立的调用并行执行,但这一切都发生在一个刻意收窄的 JavaScript 子集中,通过解析器(而非 eval)执行。
从 packages/codemode/src/index.ts 的导出结构看,公共 API 只有四个命名空间/导出:
export * as CodeMode from "./codemode.js"
export * as Tool from "./tool.js"
export * as OpenAPI from "./openapi/index.js"
export { ToolError, toolError } from "./tool-error.js"
包元数据(packages/codemode/package.json)显示它当前为 "private": true,仅供本工作区内部使用,版本 1.18.29,依赖 acorn(解析器)、effect 与 typescript。因此“安装”章节只对本仓库工作区有意义。
二、安装与依赖
在 OpenCode 工作区内部,以 workspace 协议引入:
{
"dependencies": {
"@opencode-ai/codemode": "workspace:*"
}
}
宿主与 CodeMode 的交互全部基于 effect(工具的 run 实现、Effect 类型的返回值),因此宿主应当自行依赖 effect。这一点从 packages/codemode/src/tool.ts 中 Definition.run 的类型签名 Effect.Effect<unknown, unknown, R> 可以直接确认:每个工具本质是一个 Effect,其环境 R 会沿工具树向上传播。
三、Quick Start:定义工具并执行一段程序
基本用法:用 Effect Schema 定义工具,再放入以 tools 暴露给程序的对象树中。
import { CodeMode, Tool } from "@opencode-ai/codemode"
import { Effect, Schema } from "effect"
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" }
`)
关键语义(均在 README 与源码中得到确认):
result永远是CodeMode.Result。程序错误、校验失败、限额超限、工具失败都以诊断(diagnostic)数据返回,而不是让 Effect 失败;宿主的主动中断(interruption)保持为中断。对应 packages/codemode/src/codemode.ts 中Result = Schema.Union([Success, Failure])的定义,execute的错误通道为never。- 成功结果的值是 JSON-safe 数据。程序返回
undefined(包括直接走到末尾没有return)会产出null;嵌套的undefined同样被归一化为null。
四、API 详解
4.1 Tool.make:Schema 化的工具定义
const tool = Tool.make({
description,
input, // Effect Schema (validating) 或 JSON Schema (render-only)
output, // 可选;二选一
run,
})
input 与 output 各自接受两种形态(见 packages/codemode/src/tool.ts 中 SchemaType = Schema.Decoder<unknown> | JsonSchema):
- 可校验的 Effect Schema:输入在进入
run之前完成解码;run返回 Effect Schemaoutput的编码表示,CodeMode 解码并拷贝后才暴露给程序。 - 仅用于渲染的 JSON Schema:这只塑造模型可见的签名,值不做校验直接通过(仍要跨越纯数据边界)。这是 MCP 等“以 JSON Schema 形式交付 schema”的适配器工具的自然形态。
output 可省略;省略时工具签名声明 Promise<unknown>,宿主返回值原样暴露。Tool.make 的 JSDoc 中给出了 JSON Schema 形态的示例:
const fromJsonSchema = Tool.make({
description: "Call an adapter-described tool",
input: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
run: (input) => callHost(input),
})
描述与 schema 共同构成模型可见的工具契约;描述应具体,授权逻辑必须放在 run 或它调用的服务里,而不是指望提示词去约束。公共工具类型统一收敛在 Tool 命名空间下:Tool.Definition、Tool.Options、Tool.SchemaType、Tool.JsonSchema。
4.2 CodeMode.execute:单次执行
const result =
yield *
CodeMode.execute({
tools: { orders: { lookup: lookupOrder } },
code: `return await tools.orders.lookup({ id: "order_42" })`,
limits: { maxToolCalls: 10 },
onToolCallStart: (call) => Effect.logDebug("CodeMode tool started", call),
onToolCallEnd: (call) => Effect.logDebug("CodeMode tool settled", call),
})
从 packages/codemode/src/codemode.ts#L137-L143 看,execute 会对工具树执行 ToolRuntime.assertValidTools 校验,随后委托给 executeWithLimits。Effect 环境由传入的工具推导:CodeMode 不会抹掉工具实现引入的服务依赖——如果某个工具的 run 需要 HttpClient,那么 execute 返回的 Effect 同样需要该服务。
4.3 CodeMode.make:可复用运行时
当工具集与执行策略被复用时使用 make:
const runtime = CodeMode.make({
tools: { orders: { lookup: lookupOrder } },
limits: { timeoutMs: 30_000 },
})
runtime.catalog() // 结构化工具描述
runtime.instructions() // 面向模型的语法与工具指南
runtime.execute(source) // CodeMode.Result
make 内部(packages/codemode/src/codemode.ts#L146-L159)在构造时一次性完成工具树校验、限额解析和 ToolRuntime.prepare(生成目录、指令文本与搜索索引),后续每次 execute 直接复用。
类型层面的组织方式值得注意:CodeMode.Input、CodeMode.Result、CodeMode.Success、CodeMode.Failure、CodeMode.Diagnostic、CodeMode.DiagnosticKind 既是 Effect Schema 也是推导出的 TypeScript 类型,宿主可以把 CodeMode.Input({ code: string })与 CodeMode.Result 组合 runtime.instructions() 和 runtime.execute() 来构建框架特定的 agent 工具。其余类型同样收敛在 CodeMode 命名空间:CodeMode.Options、CodeMode.ExecuteOptions、CodeMode.Runtime、CodeMode.ExecutionLimits、CodeMode.DiscoveryOptions、CodeMode.DataValue、CodeMode.ToolDescription 与各 CodeMode.ToolCall* 观察类型。
4.4 结果结构(Result)
type Result = Success | Failure
interface Success {
readonly ok: true
readonly value: CodeMode.DataValue
readonly logs?: ReadonlyArray<string>
readonly truncated?: boolean
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
}
interface Failure {
readonly ok: false
readonly error: CodeMode.Diagnostic
readonly logs?: ReadonlyArray<string>
readonly truncated?: boolean
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
}
toolCalls记录运行时受理(admitted)的调用名,按调用顺序排列;失败时同样保留,使宿主可以在不暴露输入与宿主失败细节的前提下审计部分执行。truncated在值或日志因maxOutputBytes被截断时出现。
4.5 工具调用钩子
onToolCallStart:在输入解码之后、工具执行之前收到{ index, name, input }。输入是宿主侧解码后的数据,可能包含 schema 转换产生的值;应用不应无差别记录敏感工具参数。onToolCallEnd:在受理的调用落定时收到{ index, name, input, durationMs, outcome, message? }。outcome为"success"或"failure";message是模型安全的失败消息,仅在失败时存在。被中断的调用(如超时触发)不会产生 end 事件。
两个钩子都返回 Effect 且不得失败,这与 packages/codemode/src/tool-runtime.ts#L70-L74 中 ToolCallHooks 的类型 Effect.Effect<void, never, R> 一致——错误通道被显式置为 never。
4.6 OpenAPI 工具:OpenAPI.fromSpec
OpenAPI.fromSpec 把 OpenAPI 3.x 文档转换成工具子树——每个 operation 一个工具:
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(), // 解析后的文档(不支持 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)))
要点(实现位于 packages/codemode/src/openapi):
- 带点号的
operationId形成命名空间(如v2.session.get);缺少 ID 时退化为getUsersById这类 method/path 平铺名,名称会做净化与去重。宿主把子树放在tools树的某个键下,该键即模型可见的命名空间。 fromSpec是同步的,返回{ tools, skipped }。初始适配器支持 query 的form/deepObject、path/header 的simple、JSON 请求体、JSON 与 text 响应;不支持的参数编码、非 JSON 请求体、二进制响应和流式操作进入skipped而不是生成坏工具。operation/path 级 server 优先于文档级 server,除非baseUrl显式覆盖全部。- 工具输入把 path、query、header 与封闭对象体字段压平为一个模型可见对象,但内部保留 HTTP 位置信息;跨位置重名获得位置前缀(如
path_id与query_id);组合/可空/字典/条件必填/非对象 JSON 体保持为body子字段。 - 认证永不模型可见。bearer、basic、header、query 认证遵循 OpenAPI
security语义,由宿主侧的auth.resolve解析——凭据存储、OAuth 流程与令牌刷新不进入编译器;Cookie 认证备选被丢弃,若一个 operation 没有任何受支持备选则整体跳过。完整语义见 packages/codemode/src/openapi/types.ts 中的选项 docstring。 - 生成的工具要求 Effect 环境中存在
HttpClient.HttpClient(来自effect/unstable/http),执行时提供FetchHttpClient.layer或自定义/测试客户端。客户端拥有重定向策略;带凭据的宿主应拒绝重定向或在源变化时剥离凭据。 - 响应上限 50 MiB;非 2xx 响应变为携带状态码与尺寸受限 body 摘要的安全工具失败。延期能力追踪于 packages/codemode/src/openapi/TODO.md。
五、工具发现(Discovery):预算化目录与 search 工具
agent 工具的指令使用预算化目录(budgeted catalog):
- 每个工具命名空间无论预算如何都始终列出,并带工具数量;在估算 token 预算内尽量内联尽可能多的完整、JSDoc 注释化签名(每个带一行描述)。schema 字段描述与标签计入每个签名的成本。
- 选择是跨命名空间轮询(round-robin)以保证公平:每一轮(命名空间按字母序)中,每个仍有未内联工具的命名空间尝试把“下一个最便宜的签名”放入共享预算;下一个签名放不下的命名空间退出,其余继续——因此任何命名空间拿到全部之前,每个命名空间都先有代表。
- 指令会明确说明列表的完整程度:总体(
COMPLETE list或PARTIAL - N of M shown)与每个命名空间((3 tools)、(3 tools, 1 shown)、(3 tools, none shown))。
预算默认值为 2,000 估算 token(字符数除以 4,与 OpenCode 使用的同一启发式;对应 packages/codemode/src/tool-runtime.ts#L22 的 estimateTokens 与 packages/codemode/src/codemode.ts#L19-L23 的 DiscoveryOptions)。它只约束目录中展示的完整工具条目,固定指令与命名空间摘要不计入。可通过构造时覆盖:
const runtime = CodeMode.make({
tools,
discovery: { catalogBudget: 6_000 },
})
预算必须是非负安全整数。
运行时搜索工具始终注册——即使目录被完全内联——从而投机性的 tools.$codemode.search 调用永远不会因未知工具而失败;只有当内联列表不完整时它才会在指令中被宣告:
const matches = await tools.$codemode.search({
query: "order status",
namespace: "orders", // 可选:限定到单一顶层命名空间
limit: 10,
offset: 0,
})
search 执行确定性的加性字段加权匹配(实现见 packages/codemode/src/tool-runtime.ts#L386-L449 的 makeSearchTool):
- 查询先被切词:camelCase 边界拆分;每个非字母数字字符都是分隔符;空词与
*被丢弃。因此resolve-library-id、resolveLibraryId与resolve library id切词结果相同。 - 每个词对每个工具打分:精确路径或路径段匹配(20)、路径子串(8)、描述子串(4)、可搜索文本子串(2)。
- 每个词还携带朴素单数变体(剥离尾部
s/es),字段命中只要任一形态匹配即算过——复数查询词issues仍能找到只写issue的工具,且不改变权重。 - 可搜索文本包含输入 schema 的属性名及其描述字符串,因此点名某个参数即可找到工具;子串匹配意味着部分词也能匹配。
- 分数跨词求和;按分数排序(同分按路径字母序),再从零基
offset(默认 0)切片到limit(默认 10)。remaining统计当前页之后的匹配数;next在有下一页时为{ offset },末页为null;把next展开回原请求即可保留 query、namespace 与 limit:
const request = { query: "order status", namespace: "orders", limit: 10 }
const page = await tools.$codemode.search(request)
const nextPage = page.next ? await tools.$codemode.search({ ...request, ...page.next }) : undefined
每条结果包含路径、描述与内联目录使用的同一份生成的 TypeScript 签名,无需二次查询。签名采用带 JSDoc 的多行形态:每个被描述的输入/输出字段携带 schema description 作为 /** ... */ 注释,TypeScript 无法表达的约束以标签随行(@deprecated、@default、@format、@minItems、@maxItems):
tools.github.list_issues(input: {
/** Repository owner */
owner: string,
/** Cursor from the previous response's pageInfo */
after?: string,
/**
* Results per page
* @default 30
*/
perPage?: number,
}): Promise<unknown>
结果路径渲染为以 tools 为根的 JavaScript 表达式(tools.orders.lookup,非标识符段用 tools.context7["resolve-library-id"] 形式),因此每个 path 可直接用作调用点。空查询按路径字母序浏览目录;配合 namespace({ query: "", namespace: "orders" })可列出该命名空间全部内容。查询恰好点名一个工具路径(canonical 路径、tools. 前缀路径或渲染后的 JavaScript 表达式)时被视为查找(lookup),只返回该工具。
生成的指令是结构化 markdown,按“工作流在上、目录在下”排序:## Workflow(编号步骤:目录不完整时经 search 找工具,完整时从内联列表挑选;按原样调用精确路径;只返回所需字段)、## Rules(仅承载工作流未覆盖的指引:tools 内只存在列表/搜索结果中的工具与内部运行时工具;在代码中过滤聚合集合;运行时收窄 Promise<unknown> 结果;用 Promise.all 并行独立调用;用 Object.keys/for...in 枚举 tools;search 被宣告时浏览命名空间并分页)、简短的 ## Language(声明运行时是受限的 JavaScript 编排语言并点名其主要不可用能力)、以及预算化的 ## Available tools 目录。示例调用形态使用显式 <namespace>.<tool>/<field> 占位符,绝不使用真实或虚构的工具名。
宿主不能自定义 $codemode 顶层命名空间——该名字在 packages/codemode/src/tool-runtime.ts#L85 中作为保留字硬编码。
六、支持的程序子集(Supported Programs)
CodeMode 执行的是一个刻意有界的 JavaScript 子集。支持列表(完整继承自 README):
- 基础:纯数据字面量、属性访问、赋值、解构。
- 控制流:
if、条件表达式、switch、for、for...of(数组、字符串、Map、Set)、for...in(纯对象的自有键、数组的索引串、tools引用的命名空间/工具名——其余情况报错并建议使用for...of或Object.keys,而不是复刻真实 JS 对字符串的怪异索引行为或 Map/Set 的零迭代)、while、do...while。 - 函数:箭头函数与函数声明,支持闭包、默认参数、rest 参数、解构。
- 语法糖:可选链、空值合并、模板串、spread(数组、字符串、Map、Set)、
try/catch。 - 常用内置:常见的数组、字符串、数字、
Object、Math、JSON操作。可变数组方法含push/pop/shift/unshift/splice(就地移除并返回被移除元素)/fill/copyWithin;数组keys/values/entries返回数组(与 Map/Set 约定一致)且可与for...of、spread 配合。字符串方法含localeCompare(忽略 locale/options 参数)、normalize,以及trimLeft/trimRight别名。Object.keys也接受数组(索引串,同 JS)与工具引用:Object.keys(tools)列出顶层命名空间(含$codemode),Object.keys(tools.ns)列出该节点下的名字(可调用工具枚举为[];未知路径报UnknownTool诊断)。对工具引用调用Object.values/Object.entries会失败,并指向Object.keys(tools)与tools.$codemode.search。 - Date:
Date.now()/Date.parse()/Date.UTC()、new Date(...)、getter 方法,以及基于 time value 的日期运算/比较。日期序列化为 ISO(toString亦然,为跨宿主时区确定性)。 - 正则:
/字面量/与new RegExp(...),test/exec(g标志下保持lastIndex状态),字符串match/matchAll/replace/replaceAll/split/search支持 pattern。匹配结果是携带index与命名字段groups自有属性的数组(省略input)。replace/replaceAll接受函数替换器(捕获组、偏移、输入、命名字段),回调顺序执行、可以 await 工具调用,结果被强制转为字符串。非法 pattern、非法 flag、缺g的调用失败为可捕获错误,并说明错在哪、怎么改(转义提示、该写的确切/pattern/g)。pattern 在宿主引擎上运行,因此病态回溯只受执行超时限约束。 - Map 与 Set:由 entries/数组/字符串构造,
get/set/add/has/delete/clear/size/forEach,keys/values/entries返回数组(而非迭代器)。 - URL 辅助:
URL解析与变更、关联的URLSearchParams、URL.canParse/URL.parse、URI 与 URI 组件的编码/解码、查询参数构造/查找/变更/排序/回调/物化。URLSearchParams 迭代方法同样返回数组,与 Map/Set 约定一致。 - 一等 Promise:未被 await 的
tools.ns.tool(...)是一个 Promise 值,其调用立即在受监督的 fiber 上启动;await解析它(await 非 Promise 值是空操作;return tools.ns.tool(...)与异步函数 return 一样解析)。Promise.all/Promise.allSettled/Promise.race接受任意混合 Promise 与纯值的数组(内联构造、预先构造或 spread);Promise.resolve/Promise.reject构造已结算的 Promise。allSettled的拒绝原因与catch绑定看到的是同一份纯{ name?, message }数据;Promise.race会中断落败方仍在飞行的调用。至多 8 个工具调用并发。程序结束时仍在运行的未 await 调用会先被 await 完再结束执行;从未被 await 的调用失败会以未处理拒绝诊断浮出。 - throw 与 Error 体系:
throw value与throw new Error(message)表示显式程序失败。Error(以及TypeError/RangeError/SyntaxError/ReferenceError/EvalError/URIError)是真实构造函数,带不带new都可调用;错误值是额外满足instanceof Error的纯{ name, message }数据(具体类型匹配自身与Error,同 JS)。所有被捕获的失败——抛出的错误、解释器运行时错误、工具失败——在catch块中都instanceof Error;抛出非错误值(throw "text")则不是,与 JS 一致。捕获的失败携带等效真实 JS 失败的name:JSON.parse与非法正则 pattern 产生SyntaxError(满足instanceof SyntaxError)、未知标识符ReferenceError、给常量赋值TypeError、非法normalize形式RangeError;无具体对应的失败(包括工具失败)命名为"Error"。instanceof还识别Date、RegExp、Map、Set、URL、URLSearchParams、Array、Object、Promise;其他右侧操作数是可捕获错误。
数据边界的活值规则:程序内部,标准库值处处保持“存活”——内部数据检查点(Object.* 辅助、spread、强制转换输入)保留实例,因此 Object.values({ d: date })[0].getTime() 与持有 Map 的对象 spread 副本都继续可用。只有宿主边界(最终结果、工具参数、JSON.stringify)才按 JSON.stringify 语义精确序列化:Date 与 URL 变为字符串(非法 Date 变为 null),RegExp、Map、Set、URLSearchParams 变为 {}。Promise 值永不跨越数据边界:结果或工具参数里出现未 await 的 Promise 会产生“请 await 它”的诊断,而不是序列化成 {}。这一机制在 packages/codemode/src/tool-runtime.ts#L171-L295 的 copyIn/copyBounded 中可见:沙箱值类型(SandboxDate 等)在宿主边界按 toJSON 语义转换,SandboxPromise 直接抛带修复提示的 InvalidDataValue。
明确不支持:eval、动态 import、模块、class、generator、定时器、宿主全局、原型变更、自定义 Promise 构造函数(new Promise)、Promise 链(.then/.catch/.finally——await 配 try/catch 是受支持风格),以及任意方法调用。不支持的语法返回带源码位置(可得时)的 UnsupportedSyntax 诊断。
一句话总结:CodeMode 是编排语言,不是一般的 JavaScript 运行时。
七、执行限制(Execution Limits)
限额只有三个旋钮:
| 限制 | 默认值 | 约束对象 |
|---|---|---|
timeoutMs |
无——不超时 | 墙上时钟执行时间 |
maxToolCalls |
无——不限 | 一次执行中受理的工具调用数 |
maxOutputBytes |
无——不截断 | 面向模型的输出:序列化结果值加捕获日志 |
没有任何限额有默认值是刻意为之:执行预算是宿主策略,不是库策略。能中断执行 fiber 的宿主(如 OpenCode 在用户取消时)可以不设超时;有自己工具输出截断机制的宿主(如 OpenCode 有)可以留空 maxOutputBytes。两者都没有的宿主应当设置 maxOutputBytes,否则超大结果会静默淹没模型上下文。
只传需要覆盖的项:
const runtime = CodeMode.make({
tools,
limits: {
maxToolCalls: 20,
timeoutMs: 60_000,
},
})
源码侧验证逻辑见 packages/codemode/src/codemode.ts#L119-L134 的 validateLimit:限额必须是安全整数,timeoutMs 至少为 1,其余可为 0;非法配置在调用 CodeMode.make 或 CodeMode.execute 时抛 RangeError;显式 undefined 等同于未设置。
行为语义:
- 超过
maxOutputBytes从不使执行失败。超大的结果值被替换为截断后的序列化文本加解释性标记;日志从头保留到剩余预算耗尽(末尾加一行标记说明被截);结果携带truncated: true。 - 配置了超时后,超时会中断在飞行的工具 Effect,包括程序尚未 await 的、已抢先启动的调用(其 fiber 由执行受监督)。解释器在步骤之间协作式让出,因此超时也能中断纯忙循环(
while (true) {})——不存在独立的工作预算。工具实现自身负责让其外部操作可中断或有独立边界。 - 两个解释器内部常数是固定常量而非旋钮:至多 8 个工具调用并发;跨越数据边界的值最深嵌套 32 层(更深的值以
InvalidDataValue失败,比原生栈溢出错误更易读)。两者都不属于公共契约。32 层上限对应 packages/codemode/src/tool-runtime.ts#L117-L122 中的MAX_VALUE_DEPTH = 32。
八、诊断(Diagnostics):失败即数据
| 种类 | 含义 |
|---|---|
ParseError |
源码为空或无法解析 |
UnsupportedSyntax |
解析出的 JavaScript 超出受支持子集 |
UnknownTool |
程序引用了宿主未提供的工具 |
InvalidToolInput |
工具输入未通过 schema 解码或安全数据拷贝 |
InvalidToolOutput |
工具输出未通过 schema 解码或安全数据拷贝 |
InvalidDataValue |
程序数据违反纯数据契约(深度、循环、被阻断属性、非数据值) |
ToolCallLimitExceeded |
调用数超过 maxToolCalls |
TimeoutExceeded |
执行超过 timeoutMs |
ToolFailure |
工具拒绝或失败 |
ExecutionFailure |
程序抛出或其他执行错误 |
Diagnostic 的 schema(packages/codemode/src/codemode.ts#L77-L84)包含 kind、message、可选的 location: { line, column } 与可选的 suggestions: string[]——诊断本身就是可跨 agent 工具边界安全返回的结构化数据。
宿主失败脱敏:未知宿主失败、缺陷(defect)、非法输出与拷贝失败一律被净化。要返回一个安全的操作拒绝,请用 toolError 失败:
import { toolError } from "@opencode-ai/codemode"
run: ({ id }) => (authorized(id) ? loadOrder(id) : Effect.fail(toolError("Order is unavailable")))
只有提供的 message 是模型可见的;可选的 cause 永不返回在 CodeMode.Result 中——宿主应在跨越这条边界之前完成所需内部日志。toolError 返回 ToolError(带 message 与可选 cause: Defect,见 packages/codemode/src/tool-error.ts)。运行时的净化逻辑在 packages/codemode/src/tool-runtime.ts#L143-L149:中断保留为中断;ToolError 原样透传;其他一切异常被包装为 toolError("Tool execution failed", error)。
测试用例 packages/codemode/test/codemode.test.ts 用真实断言印证了这条边界:把 Authorization: Bearer typed-secret、postgres://user:defect-secret@... 这样的敏感信息放入宿主异常或缺陷,最终 result.error 只得到 { kind: "ToolFailure", message: "Tool execution failed" },且 JSON.stringify(result) 不含任何密钥;非法工具输出(包括在拷贝过程中抛异常的恶意 Proxy)同样被净化为不含秘密的 InvalidToolOutput。
九、权威边界(Authority Boundary)
CodeMode 把程序限制在所提供的工具树内,但它不决定这些工具能做什么:
宿主拥有:
- 认证与授权;
- 工具选择与不可变的作用域;
- 凭据与网络客户端;
- 持久化、幂等性、审批与持久副作用;
- 日志与脱敏策略。
CodeMode 拥有:
- 不用
eval解析并解释受支持子集; - 工具调用周围的 schema 边界;
- 纯数据拷贝与被阻断的原型成员(
__proto__、constructor、prototype,见 packages/codemode/src/tool-runtime.ts#L152-L154); - 资源限制、调用计数与归一化诊断;
- 面向模型的工具发现与指令。
程序无法通过提示词或生成的代码获得权限;它只能行使已存在于所提供工具中的权限。不要暴露一个宽泛工具然后指望提示词去限制它。
十、契约定律(Laws)与目标外事项(Non-Goals)
公共契约受以下等价关系指引:
CodeMode.execute({ ...options, code })等价于CodeMode.make(options).execute(code)。- 除非输入解码成功,工具实现不会被调用。
- 除非输出解码并成功跨越纯数据边界,工具结果对程序不可见。
- 未知宿主失败不会变成模型可见诊断;
ToolError是显式的安全消息通道。 - 宿主中断保持为中断,而不是
CodeMode.Failure。
非目标(明确不做的事,同样重要):
- 通用权限提示或审批工作流;
- 持久暂停/恢复、回放或存储适配器;
- 恰好一次的外部副作用;
- 应用授权或产品策略;
- 任意 JavaScript 的文件系统/进程沙箱;
- 与完整 JavaScript 语言或 npm 生态的兼容。
需要审批或持久后果的应用应在 CodeMode 之上建模这些能力,只暴露当前已授权的工具。
十一、测试入口
在包目录(packages/codemode)下:
bun test
bun run typecheck
typecheck 对应 packages/codemode/package.json 中的 tsgo --noEmit。直接测试套件覆盖:公共投影(public projections)、发现(discovery)、schema 边界、诊断脱敏、资源限额、工具调用观察与中断。测试文件位于 packages/codemode/test,其中 codemode.test.ts 以“宿主失败边界”为主题组织了上述脱敏断言,另有 enumeration.test.ts、openapi.test.ts、parity.test.ts、signature.test.ts、stdlib.test.ts、promise.test.ts 分别验证工具枚举、OpenAPI 适配器、签名生成、标准库与 Promise 语义。
十二、小结
@opencode-ai/codemode 展示了在 Agent 基础设施层做“代码执行安全化”的一种完整工程范式:用受限解释器替代 eval 消除任意代码风险,用 Effect Schema 在工具输入/输出两侧建立可校验的边界,用纯数据拷贝与固定深度上限阻断原型攻击与栈溢出,用“失败即数据 + 宿主失败脱敏”保证模型上下文不被内部秘密污染,用预算化目录加确定性 search 工具解决大规模工具集下的发现问题,并用无默认的三旋钮限额把资源策略交还宿主。对于要在 OpenCode 或同类 Agent 宿主中集成“让模型写程序编排工具”这一模式的人,packages/codemode/README.md 与 packages/codemode/AGENTS.md 是权威文档,packages/codemode/src/codemode.ts、packages/codemode/src/tool-runtime.ts 与 packages/codemode/test 则是逐条可核验的实现与行为证据。
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 StartedRust0624
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