首页
/ OpenCode CodeMode:用 Effect 实现受约束的程序化工具编排与 Schema 化代码执行

OpenCode CodeMode:用 Effect 实现受约束的程序化工具编排与 Schema 化代码执行

2026-09-06 22:17:08作者:咎竹峻Karen

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(解析器)、effecttypescript。因此“安装”章节只对本仓库工作区有意义。

二、安装与依赖

在 OpenCode 工作区内部,以 workspace 协议引入:

{
  "dependencies": {
    "@opencode-ai/codemode": "workspace:*"
  }
}

宿主与 CodeMode 的交互全部基于 effect(工具的 run 实现、Effect 类型的返回值),因此宿主应当自行依赖 effect。这一点从 packages/codemode/src/tool.tsDefinition.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.tsResult = 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,
})

inputoutput 各自接受两种形态(见 packages/codemode/src/tool.tsSchemaType = Schema.Decoder<unknown> | JsonSchema):

  1. 可校验的 Effect Schema:输入在进入 run 之前完成解码;run 返回 Effect Schema output 的编码表示,CodeMode 解码并拷贝后才暴露给程序。
  2. 仅用于渲染的 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.DefinitionTool.OptionsTool.SchemaTypeTool.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.InputCodeMode.ResultCodeMode.SuccessCodeMode.FailureCodeMode.DiagnosticCodeMode.DiagnosticKind 既是 Effect Schema 也是推导出的 TypeScript 类型,宿主可以把 CodeMode.Input{ code: string })与 CodeMode.Result 组合 runtime.instructions()runtime.execute() 来构建框架特定的 agent 工具。其余类型同样收敛在 CodeMode 命名空间:CodeMode.OptionsCodeMode.ExecuteOptionsCodeMode.RuntimeCodeMode.ExecutionLimitsCodeMode.DiscoveryOptionsCodeMode.DataValueCodeMode.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-L74ToolCallHooks 的类型 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_idquery_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 listPARTIAL - 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#L22estimateTokenspackages/codemode/src/codemode.ts#L19-L23DiscoveryOptions)。它只约束目录中展示的完整工具条目,固定指令与命名空间摘要不计入。可通过构造时覆盖:

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-L449makeSearchTool):

  • 查询先被切词:camelCase 边界拆分;每个非字母数字字符都是分隔符;空词与 * 被丢弃。因此 resolve-library-idresolveLibraryIdresolve 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、条件表达式、switchforfor...of(数组、字符串、Map、Set)、for...in(纯对象的自有键、数组的索引串、tools 引用的命名空间/工具名——其余情况报错并建议使用 for...ofObject.keys,而不是复刻真实 JS 对字符串的怪异索引行为或 Map/Set 的零迭代)、whiledo...while
  • 函数:箭头函数与函数声明,支持闭包、默认参数、rest 参数、解构。
  • 语法糖:可选链、空值合并、模板串、spread(数组、字符串、Map、Set)、try/catch
  • 常用内置:常见的数组、字符串、数字、ObjectMathJSON 操作。可变数组方法含 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
  • DateDate.now()/Date.parse()/Date.UTC()new Date(...)、getter 方法,以及基于 time value 的日期运算/比较。日期序列化为 ISO(toString 亦然,为跨宿主时区确定性)。
  • 正则/字面量/new RegExp(...)test/execg 标志下保持 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/forEachkeys/values/entries 返回数组(而非迭代器)。
  • URL 辅助URL 解析与变更、关联的 URLSearchParamsURL.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 valuethrow new Error(message) 表示显式程序失败。Error(以及 TypeError/RangeError/SyntaxError/ReferenceError/EvalError/URIError)是真实构造函数,带不带 new 都可调用;错误值是额外满足 instanceof Error 的纯 { name, message } 数据(具体类型匹配自身与 Error,同 JS)。所有被捕获的失败——抛出的错误、解释器运行时错误、工具失败——在 catch 块中都 instanceof Error;抛出非错误值(throw "text")则不是,与 JS 一致。捕获的失败携带等效真实 JS 失败的 nameJSON.parse 与非法正则 pattern 产生 SyntaxError(满足 instanceof SyntaxError)、未知标识符 ReferenceError、给常量赋值 TypeError、非法 normalize 形式 RangeError;无具体对应的失败(包括工具失败)命名为 "Error"instanceof 还识别 DateRegExpMapSetURLURLSearchParamsArrayObjectPromise;其他右侧操作数是可捕获错误。

数据边界的活值规则:程序内部,标准库值处处保持“存活”——内部数据检查点(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-L295copyIn/copyBounded 中可见:沙箱值类型(SandboxDate 等)在宿主边界按 toJSON 语义转换,SandboxPromise 直接抛带修复提示的 InvalidDataValue

明确不支持eval、动态 import、模块、class、generator、定时器、宿主全局、原型变更、自定义 Promise 构造函数(new Promise)、Promise 链(.then/.catch/.finally——awaittry/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-L134validateLimit:限额必须是安全整数,timeoutMs 至少为 1,其余可为 0;非法配置在调用 CodeMode.makeCodeMode.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)包含 kindmessage、可选的 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-secretpostgres://user:defect-secret@... 这样的敏感信息放入宿主异常或缺陷,最终 result.error 只得到 { kind: "ToolFailure", message: "Tool execution failed" },且 JSON.stringify(result) 不含任何密钥;非法工具输出(包括在拷贝过程中抛异常的恶意 Proxy)同样被净化为不含秘密的 InvalidToolOutput

九、权威边界(Authority Boundary)

CodeMode 把程序限制在所提供的工具树内,但它不决定这些工具能做什么

宿主拥有:

  • 认证与授权;
  • 工具选择与不可变的作用域;
  • 凭据与网络客户端;
  • 持久化、幂等性、审批与持久副作用;
  • 日志与脱敏策略。

CodeMode 拥有:

  • 不用 eval 解析并解释受支持子集;
  • 工具调用周围的 schema 边界;
  • 纯数据拷贝与被阻断的原型成员(__proto__constructorprototype,见 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.tsopenapi.test.tsparity.test.tssignature.test.tsstdlib.test.tspromise.test.ts 分别验证工具枚举、OpenAPI 适配器、签名生成、标准库与 Promise 语义。

十二、小结

@opencode-ai/codemode 展示了在 Agent 基础设施层做“代码执行安全化”的一种完整工程范式:用受限解释器替代 eval 消除任意代码风险,用 Effect Schema 在工具输入/输出两侧建立可校验的边界,用纯数据拷贝与固定深度上限阻断原型攻击与栈溢出,用“失败即数据 + 宿主失败脱敏”保证模型上下文不被内部秘密污染,用预算化目录加确定性 search 工具解决大规模工具集下的发现问题,并用无默认的三旋钮限额把资源策略交还宿主。对于要在 OpenCode 或同类 Agent 宿主中集成“让模型写程序编排工具”这一模式的人,packages/codemode/README.mdpackages/codemode/AGENTS.md 是权威文档,packages/codemode/src/codemode.tspackages/codemode/src/tool-runtime.tspackages/codemode/test 则是逐条可核验的实现与行为证据。

登录后查看全文
热门项目推荐
相关项目推荐