CODING AGENT HARNESS · SOURCE AUDITREPORT 07 / 18
07

OpenCode

持久化 message-parts 状态机与插件生态很强;默认宿主执行是最明显安全缺口。

TypeScript · Persistent Session PlatformMITdev
SOURCE
VERIFIED
Repository
anomalyco/opencode
Commit
284e97068c987a7a2e863430e07e2f1ad7d4be46
Commit date
2026-07-28T01:16:18Z
Findings
28
Citations
86
Tracked files
6,332
EXECUTIVE READING

先给结论,再进入源码

核心机制

持久化消息状态驱动;每 step 重建 agent/tools/system

上下文

可用输入窗阈值;摘要保尾;工具输出后台 prune

安全边界

默认 ask 权限,但 shell 直接宿主执行

适用建设

多前端、插件市场、持久任务与可回退编辑

值得借鉴

  • 消息部件事件模型细
  • Shell 权限解析扎实
  • 影子 Git 可回退

需要警惕

  • 无内建 OS 沙箱
  • 插件进程内高权限
  • 远程 MCP 缺 SSRF 私网拦截

直接带走

  • message-part 事件模型
  • doom-loop 二次确认
  • step 前后 shadow Git
00 · METHOD

研究口径:先锁提交,再沿运行链读代码

README POLICY

README 只用于产品入口定位;核心结论来自 session loop、stream processor、compaction、permission、tools、MCP、plugin、storage 与测试。

FACT POLICY

以 packages/opencode 的 Effect 服务实现为主,同时跟到 packages/core 的数据库/后台任务合同;UI 能力不替代执行层证据。

INFERENCE POLICY

明确区分权限审批、宿主进程执行与 OS 沙箱;实验能力按 flag 和测试覆盖标注,不当作默认稳定能力。

L1运行实现
L2接口契约
L3测试证明
L4文档佐证
L5明确推断

本页引用 27 个不同源码/测试文件;证据角色分布:实现 74 · 配置 3 · 契约 6 · 测试 3。代码块是固定提交中的原文截取,长区间仅在中部折叠,首尾行号保持真实。

01 · TECHNICAL MAPS

架构总图与单轮执行链路

两张图均由本页证据账本生成,并通过 Archify showcase 9 项校验(0 error / 0 warning)。图可单独打开、搜索、缩放和追踪关系。

FIGURE 01OpenCode Harness 架构图全屏打开 ↗
FIGURE 02用户输入到工具回写的技术链路全屏打开 ↗
02 · COVERAGE MAP

审计维度与证据等级

入口、会话与主循环 verified L1 / L2 / L3

已审计持久消息驱动 loop、任务选择、流事件处理、退出、重试和 cleanup。

Provider、流式与重试 verified L1 / L2 / L3

已审计 AI SDK provider 装载、协议变换、参数/headers hooks、SSE timeout 和 usage。

上下文、压缩与记忆 verified L1 / L2 / L3

已审计 overflow、摘要、近期尾部保留、工具输出裁剪、overflow replay 和自动续跑。

工具分发与结果治理 verified L1 / L2 / L3

已审计 builtins/custom/plugin/MCP 动态工具、schema 转换、hooks、truncation 和 snapshots。

执行环境与沙箱 verified L1 / L2

shell 是宿主 child process;没有内建 OS/container sandbox,主要是 permission gate。

权限与安全 verified L1 / L2 / L3

已审计 last-match rules、once/always/reject、external directory、doom loop 和敏感文件默认策略。

指令与 Prompt verified L1 / L2 / L3

已审计 provider prompt、AGENTS/CLAUDE 层级指令、remote instructions、skills 和 compaction hooks。

工具、连接器与插件 verified L1 / L2 / L3

已审计 MCP local/remote/OAuth/resources/prompts、npm/file plugins、custom tools 和 tool hooks。

子 Agent 与协作 verified L1 / L2 / L3

已审计 child session、depth、权限继承、resume、foreground/background 和递归取消。

持久化与观测 verified L1 / L2 / L3

消息/part/event、成本/token、git snapshot/patch、fork/revert、可选 OTEL 与 share sync。

测试、评测与成熟度 partial L2 / L3

大量单元/集成/recorded stream 测试;仓库未见统一 Coding Agent 成功率 benchmark。

01
DIMENSION · ENTRY-SESSION-LOOP

入口、会话与主循环

本章共 3 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

01
L1事实opencode-loop-001

主循环由持久化消息状态驱动,而不是一次性的 while(tool_call)

源码事实

runLoop 每步从数据库重建过滤后的消息视图,找最后 user/assistant/finished 与待处理 subtask/compaction;只有已完成、无工具调用且顺序闭合才退出,否则继续调度。

白话解释

它每一轮都重新看账本决定“接下来做什么”,所以进程中断、工具异步完成和压缩都能落在统一状态机里。

对自研 Harness 的含义

消息/part 是事实源,loop 是其投影;这比仅在内存追加数组更利于恢复和多客户端。

关键源码 · 实现
packages/opencode/src/session/prompt.ts · L1081–L1130
 1081      const runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.run")(
 1082        function* (sessionID: SessionID) {
 1083          const ctx = yield* InstanceState.context
 1084          let structured: unknown
 1085          let step = 0
 1086          const session = yield* sessions.get(sessionID).pipe(Effect.orDie)
 1087  
 1088          while (true) {
 1089            yield* status.set(sessionID, { type: "busy" })
 1090            yield* Effect.logInfo("loop", { "session.id": sessionID, step })
 1091  
 1092            let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe(
 1093              Effect.provideService(Database.Service, database),
 1094            )
 1095  
 1096            const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs)
 1097  
 1098            if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
 1099  
 1100            const lastAssistantMsg = msgs.findLast(
 1101              (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
 1102            )
 1103            // Some providers return "stop" even when the assistant message contains
 1104            // tool calls. Keep the loop running so tool results can be sent back to
      … 16 lines omitted; exact range 1081–1130 …
 1121                yield* Effect.logWarning("loop exit with orphaned interrupted tool", {
 1122                  "session.id": sessionID,
 1123                  messageID: lastAssistant.id,
 1124                  tool: orphan.tool,
 1125                  callID: orphan.callID,
 1126                })
 1127              }
 1128              yield* Effect.logInfo("exiting loop", { "session.id": sessionID })
 1129              break
 1130            }
查看全部 2 处证据
  • 实现 packages/opencode/src/session/prompt.ts:1081–1130 loop、状态恢复与退出判定。
  • 实现 packages/opencode/src/session/message-v2.ts:578–600 按单调 ID 找最新消息和未处理任务。
02
L1事实opencode-loop-002

每一步动态重建 Agent、工具、系统上下文和模型请求

源码事实

loop 解析 agent/model,应用 reminders,创建 assistant record,按当前 permission/provider 解析工具,再并行装配 skills、environment、instructions、MCP instructions 和 compacted model messages。

白话解释

不是开会前一次性发完所有资料;每走一步都按当前身份、模型和权限重新整理桌面。

对自研 Harness 的含义

支持运行中配置和权限变化,但每步装配链更复杂、需要缓存与测试。

关键源码 · 实现
packages/opencode/src/session/prompt.ts · L1170–L1241
 1170            const agent = yield* agents.get(lastUser.agent)
 1171            if (!agent) {
 1172              const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
 1173              const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
 1174              const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` })
 1175              yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() })
 1176              throw error
 1177            }
 1178            const maxSteps = agent.steps ?? Infinity
 1179            const isLastStep = step >= maxSteps
 1180            msgs = yield* SessionReminders.apply({ messages: msgs, agent, session }).pipe(
 1181              Effect.provideService(RuntimeFlags.Service, flags),
 1182              Effect.provideService(FSUtil.Service, fsys),
 1183              Effect.provideService(Session.Service, sessions),
 1184            )
 1185  
 1186            const msg: SessionV1.Assistant = {
 1187              id: MessageID.ascending(),
 1188              parentID: lastUser.id,
 1189              role: "assistant",
 1190              mode: agent.name,
 1191              agent: agent.name,
 1192              variant: lastUser.model.variant,
 1193              path: { cwd: ctx.directory, root: ctx.worktree },
      … 38 lines omitted; exact range 1170–1241 …
 1232                messages: msgs,
 1233                promptOps,
 1234              }).pipe(
 1235                Effect.provideService(Plugin.Service, plugin),
 1236                Effect.provideService(Permission.Service, permission),
 1237                Effect.provideService(ToolRegistry.Service, registry),
 1238                Effect.provideService(MCP.Service, mcp),
 1239                Effect.provideService(Truncate.Service, truncate),
 1240                Effect.provideService(RuntimeFlags.Service, flags),
 1241              )
查看全部 2 处证据
  • 实现 packages/opencode/src/session/prompt.ts:1170–1241 Agent、assistant message、processor 与工具装配。
  • 实现 packages/opencode/src/session/prompt.ts:1252–1286 系统上下文与模型调用。
03
L1事实opencode-retry-001

重试、拒绝、上下文溢出和中断有不同终态

源码事实

stream 应用 provider-aware retry policy;拒绝默认让 loop 停止,可配置继续;ContextOverflow 在 auto compact 开启时转为 compact,其他错误进入 session error;cleanup 会把残留 tool call 标为 interrupted error。

白话解释

网络抖动会重试,权限拒绝会刹车,记忆塞满会整理,进程被打断则把未完成工具明确标成中止,不会都混成一个“失败”。

对自研 Harness 的含义

错误分类是自治可靠性的核心,而不只是 UI 文案。

关键源码 · 实现
packages/opencode/src/session/processor.ts · L539–L597
  539        const cleanup = Effect.fn("SessionProcessor.cleanup")(function* () {
  540          if (ctx.snapshot) {
  541            const patch = yield* snapshot.patch(ctx.snapshot)
  542            if (patch.files.length) {
  543              yield* session.updatePart({
  544                id: PartID.ascending(),
  545                messageID: ctx.assistantMessage.id,
  546                sessionID: ctx.sessionID,
  547                type: "patch",
  548                hash: patch.hash,
  549                files: patch.files,
  550              })
  551            }
  552            ctx.snapshot = undefined
  553          }
  554  
  555          if (ctx.currentText) {
  556            const end = Date.now()
  557            ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end }
  558            yield* session.updatePart(ctx.currentText)
  559            ctx.currentText = undefined
  560          }
  561  
  562          for (const part of Object.values(ctx.reasoningMap)) {
      … 25 lines omitted; exact range 539–597 …
  588                error: "Tool execution aborted",
  589                metadata: { ...metadata, interrupted: true },
  590                time: { start: "time" in part.state ? part.state.time.start : end, end },
  591              },
  592            })
  593          }
  594          ctx.toolcalls = {}
  595          ctx.assistantMessage.time.completed = Date.now()
  596          yield* session.updateMessage(ctx.assistantMessage)
  597        })
查看全部 3 处证据
  • 实现 packages/opencode/src/session/processor.ts:539–597 cleanup 中止未决工具。
  • 实现 packages/opencode/src/session/processor.ts:599–625 overflow 与普通错误分流。
  • 实现 packages/opencode/src/session/processor.ts:627–681 retry、compaction 与终态。
02
DIMENSION · PROVIDER-STREAMING

Provider、流式与重试

本章共 3 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

04
L1事实opencode-stream-001

stream processor 把 reasoning、text、tool、usage、patch 全部事件化持久

源码事实

processor 为 reasoning/text/tool call 建 part,tool result 归一化附件;step-finish 记录 finish、cost、tokens、snapshot 和 patch,并异步更新 session summary。

白话解释

模型的思考、文字、每次工具起止和文件变化都不是终端里一闪而过,而是独立可回放的事件。

对自研 Harness 的含义

TUI、Web、Desktop、ACP 可以消费同一事件模型。

关键源码 · 实现
packages/opencode/src/session/processor.ts · L315–L413
  315            case "tool-input-start":
  316              if (ctx.assistantMessage.summary) {
  317                throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
  318              }
  319              yield* ensureToolCall(value)
  320              return
  321  
  322            case "tool-input-delta":
  323              yield* ensureToolCall(value)
  324              return
  325  
  326            case "tool-input-end": {
  327              yield* ensureToolCall(value)
  328              return
  329            }
  330  
  331            case "tool-call": {
  332              if (ctx.assistantMessage.summary) {
  333                throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
  334              }
  335              yield* ensureToolCall(value)
  336              const input = isRecord(value.input) ? value.input : { value: value.input }
  337              yield* updateToolCall(value.id, (match) => ({
  338                ...match,
      … 65 lines omitted; exact range 315–413 …
  404              const output = {
  405                ...rawOutput,
  406                output:
  407                  omitted === 0
  408                    ? rawOutput.output
  409                    : `${rawOutput.output}\n\n[${omitted} image${omitted === 1 ? "" : "s"} omitted: could not be resized below the image size limit.]`,
  410                attachments: attachments.length ? attachments : undefined,
  411              }
  412              yield* completeToolCall(value.id, output)
  413              return
查看全部 3 处证据
  • 实现 packages/opencode/src/session/processor.ts:315–413 tool call/result 状态机。
  • 实现 packages/opencode/src/session/processor.ts:424–483 step、usage、snapshot 与 patch。
  • 实现 packages/opencode/src/session/processor.ts:486–531 增量文本持久化和 plugin complete hook。
05
L1事实opencode-provider-001

Provider 层是 AI SDK 适配矩阵,不只兼容 OpenAI API

源码事实

内置动态加载 Anthropic、OpenAI/Azure、Google/Vertex、Bedrock、xAI、Mistral、Groq、Cohere、OpenRouter、GitHub Copilot 等 SDK,并为不同 provider 决定 responses/chat/messages、认证和区域模型规则。

白话解释

它不是把所有厂商硬塞成同一种 HTTP;每家方言由独立适配器翻译。

对自研 Harness 的含义

模型覆盖广,但 provider 特例数量大,回归测试成本高。

关键源码 · 实现
packages/opencode/src/provider/provider.ts · L101–L145
  101  type BundledSDK = {
  102    languageModel(modelId: string): LanguageModelV3
  103    chat?: (modelId: string) => LanguageModelV3
  104    responses?: (modelId: string) => LanguageModelV3
  105  }
  106  
  107  const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>> = {
  108    "@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock),
  109    "@ai-sdk/amazon-bedrock/mantle": () => import("@ai-sdk/amazon-bedrock/mantle").then((m) => m.createBedrockMantle),
  110    "@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
  111    "@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
  112    "@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
  113    "@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex),
  114    "@ai-sdk/google-vertex/anthropic": () =>
  115      import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic),
  116    "@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI),
  117    "@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible),
  118    "@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter),
  119    "@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai),
  120    "@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral),
  121    "@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq),
  122    "@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra),
  123    "@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras),
  124    "@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere),
      … 11 lines omitted; exact range 101–145 …
  136  type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>, model?: Model) => Promise<any>
  137  type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
  138  type CustomDiscoverModels = () => Promise<Record<string, Model>>
  139  type CustomLoader = (provider: Info) => Effect.Effect<{
  140    autoload: boolean
  141    getModel?: CustomModelLoader
  142    vars?: CustomVarsLoader
  143    options?: Record<string, any>
  144    discoverModels?: CustomDiscoverModels
  145  }>
查看全部 3 处证据
  • 实现 packages/opencode/src/provider/provider.ts:101–145 bundled provider SDK 列表和 loader 合同。
  • 实现 packages/opencode/src/provider/provider.ts:154–238 endpoint 选择与 OpenAI/xAI/Copilot 特例。
  • 实现 packages/opencode/src/provider/provider.ts:294–368 Bedrock 凭据链与自定义 endpoint。
06
L1事实opencode-provider-002

请求准备层统一合并 prompt、variant、provider options 与 hooks

源码事实

system 由 agent/provider prompt、环境/项目指令和 user system 合成;参数按 provider base→model→agent→variant 合并,plugins 可改 system、params、headers;工具按 permission 和本轮 user.tools 过滤后排序。

白话解释

模型请求像一张多层样式表:厂商默认、模型设置、Agent 设置、当前档位和插件逐层覆盖。

对自研 Harness 的含义

可扩展性强,但必须记录最终展开结果才能排查“为什么这轮表现不同”。

关键源码 · 实现
packages/opencode/src/session/llm/request.ts · L56–L100
   56  export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: PrepareInput) {
   57    const isOpenaiOauth = input.provider.id === "openai" && input.auth?.type === "oauth"
   58    const system = [
   59      [
   60        ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
   61        ...input.system,
   62        ...(input.user.system ? [input.user.system] : []),
   63      ]
   64        .filter((x) => x)
   65        .join("\n"),
   66    ]
   67  
   68    const header = system[0]
   69    yield* input.plugin.trigger(
   70      "experimental.chat.system.transform",
   71      { sessionID: input.sessionID, model: input.model },
   72      { system },
   73    )
   74    if (system.length > 2 && system[0] === header) {
   75      const rest = system.slice(1)
   76      system.length = 0
   77      system.push(header, rest.join("\n"))
   78    }
   79  
      … 11 lines omitted; exact range 56–100 …
   91    const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), input.agent.options), variant)
   92    if (
   93      input.model.api.npm === "@ai-sdk/azure" &&
   94      (input.provider.options.useCompletionUrls || input.model.options.useCompletionUrls || options.useCompletionUrls)
   95    ) {
   96      delete options.reasoningSummary
   97      delete options.include
   98    }
   99    if (isOpenaiOauth) options.instructions = system.join("\n")
  100  
查看全部 3 处证据
  • 实现 packages/opencode/src/session/llm/request.ts:56–100 system 与 options 合并。
  • 实现 packages/opencode/src/session/llm/request.ts:114–158 参数、headers hooks 与 tool schema 兼容。
  • 实现 packages/opencode/src/session/llm/request.ts:181–213 session headers 与工具过滤。
03
DIMENSION · CONTEXT-COMPACTION-MEMORY

上下文、压缩与记忆

本章共 4 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

07
L1事实opencode-context-001

overflow 阈值为可用输入窗口,而非模型总窗口

源码事实

usable 优先使用 model.limit.input;否则从 context 中扣除最大输出 token,reserved 默认最多 20k。token total 达到 usable 才判 overflow,auto=false 时关闭。

白话解释

它会先给模型回答预留座位,再判断历史是否坐满,避免输入刚好塞满后没有空间输出。

对自研 Harness 的含义

比简单按 context×百分比更贴合各 provider 的 input/output 限制。

关键源码 · 实现
packages/opencode/src/session/overflow.ts · L8–L33
    8  const COMPACTION_BUFFER = 20_000
    9  
   10  export function usable(input: { cfg: ConfigV1.Info; model: Provider.Model; outputTokenMax?: number }) {
   11    const context = input.model.limit.context
   12    if (context === 0) return 0
   13  
   14    const reserved =
   15      input.cfg.compaction?.reserved ??
   16      Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax))
   17    return input.model.limit.input
   18      ? Math.max(0, input.model.limit.input - reserved)
   19      : Math.max(0, context - ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax))
   20  }
   21  
   22  export function isOverflow(input: {
   23    cfg: ConfigV1.Info
   24    tokens: SessionV1.Assistant["tokens"]
   25    model: Provider.Model
   26    outputTokenMax?: number
   27  }) {
   28    if (input.cfg.compaction?.auto === false) return false
   29    if (input.model.limit.context === 0) return false
   30  
   31    const count =
   32      input.tokens.total || input.tokens.input + input.tokens.output + input.tokens.cache.read + input.tokens.cache.write
   33    return count >= usable(input)
查看全部 2 处证据
  • 实现 packages/opencode/src/session/overflow.ts:8–33 可用窗口和溢出公式。
  • 实现 packages/opencode/src/session/processor.ts:435–482 用真实 step usage 触发压缩。
08
L1事实opencode-context-002

压缩保留近期原文尾部,而不是只剩一段摘要

源码事实

默认尝试保留最近 2 个 user turn,预算为 usable 的 25%、上下限 2k–8k;若整轮放不下可从轮中切分。模型视图被重排为 compaction marker、summary、retained tail、continue。

白话解释

老故事写成摘要,最近几轮尽量保留原话;必要时甚至保留半个超长回合。

对自研 Harness 的含义

降低摘要丢失当前执行细节的风险,但消息顺序投影更复杂。

关键源码 · 配置
packages/opencode/src/session/compaction.ts · L28–L35
   28  export const PRUNE_MINIMUM = 20_000
   29  export const PRUNE_PROTECT = 40_000
   30  const TOOL_OUTPUT_MAX_CHARS = 2_000
   31  const PRUNE_PROTECTED_TOOLS = ["skill"]
   32  const DEFAULT_TAIL_TURNS = 2
   33  const MIN_PRESERVE_RECENT_TOKENS = 2_000
   34  const MAX_PRESERVE_RECENT_TOKENS = 8_000
   35  type Turn = {
查看全部 3 处证据
  • 配置 packages/opencode/src/session/compaction.ts:28–35 tail 与 preserve 默认值。
  • 实现 packages/opencode/src/session/compaction.ts:180–239 按 token 选择近期尾部和轮内切分。
  • 实现 packages/opencode/src/session/message-v2.ts:521–571 compacted 模型视图重排。
09
L1事实opencode-context-003

摘要前先去媒体、限制工具输出,失败时可 replay 原请求

源码事实

compaction 对历史去除媒体并把单个工具输出限制 2000 字符;可把前次 summary 带入增量摘要,plugins 可替换 prompt/加 context。若 provider 因大媒体直接 overflow,会回退到之前 user turn,压缩后以文字占位重放。

白话解释

整理书桌前先把大图片和长日志搬走;如果一张附件让请求根本进不了门,就去掉附件后把原问题再问一次。

对自研 Harness 的含义

覆盖了“请求发送前就超限”和“响应后发现临界”两类路径。

关键源码 · 实现
packages/opencode/src/session/compaction.ts · L289–L354
  289      const processCompaction = Effect.fn("SessionCompaction.process")(function* (input: {
  290        parentID: MessageID
  291        messages: SessionV1.WithParts[]
  292        sessionID: SessionID
  293        auto: boolean
  294        overflow?: boolean
  295      }) {
  296        const parent = input.messages.findLast((m) => m.info.id === input.parentID)
  297        if (!parent || parent.info.role !== "user") {
  298          throw new Error(`Compaction parent must be a user message: ${input.parentID}`)
  299        }
  300        const userMessage = parent.info
  301        const compactionPart = parent.parts.find((part): part is SessionV1.CompactionPart => part.type === "compaction")
  302  
  303        let messages = input.messages
  304        let replay:
  305          | {
  306              info: SessionV1.User
  307              parts: SessionV1.Part[]
  308            }
  309          | undefined
  310        if (input.overflow) {
  311          const idx = input.messages.findIndex((m) => m.info.id === input.parentID)
  312          for (let i = idx - 1; i >= 0; i--) {
      … 32 lines omitted; exact range 289–354 …
  345          { sessionID: input.sessionID },
  346          { context: [], prompt: undefined },
  347        )
  348        const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context })
  349        const msgs = structuredClone(selected.head)
  350        yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
  351        const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
  352          stripMedia: true,
  353          toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
  354        })
查看全部 3 处证据
  • 实现 packages/opencode/src/session/compaction.ts:289–354 overflow replay、增量摘要和媒体/工具输出裁剪。
  • 实现 packages/opencode/src/session/compaction.ts:383–413 无工具 compaction 请求与二次溢出终止。
  • 实现 packages/opencode/src/session/compaction.ts:422–503 replay 与自动 continue。
10
L1事实opencode-context-004

工具输出先独立裁剪,旧输出还能在后台 prune

源码事实

普通工具输出默认上限 2000 行/50KiB,全文保存到临时文件 7 天并给 Grep/Read 或 subagent 指引;compaction prune 在保护最近 40k token 后,累计可释放超过 20k 才标记旧 tool output compacted,skill 输出受保护。

白话解释

长日志不会直接淹没聊天:模型看摘要,全文留在旁边可查;更老的工具输出再逐步从上下文退场。

对自研 Harness 的含义

上下文治理覆盖生成前、工具后和会话历史三层。

关键源码 · 契约
packages/opencode/src/tool/truncate.ts · L13–L44
   13  const RETENTION = Duration.days(7)
   14  
   15  export const MAX_LINES = 2000
   16  export const MAX_BYTES = 50 * 1024
   17  export const DIR = TRUNCATION_DIR
   18  export const GLOB = path.join(TRUNCATION_DIR, "*")
   19  
   20  export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string }
   21  
   22  export interface Options {
   23    maxLines?: number
   24    maxBytes?: number
   25    direction?: "head" | "tail"
   26  }
   27  
   28  function hasTaskTool(agent?: Agent.Info) {
   29    if (!agent?.permission) return false
   30    return evaluate("task", "*", agent.permission).action !== "deny"
   31  }
   32  
   33  export interface Interface {
   34    readonly cleanup: () => Effect.Effect<void>
   35    readonly write: (text: string) => Effect.Effect<string>
   36    /**
   37     * Returns output unchanged when it fits within the limits, otherwise writes the full text
   38     * to the truncation directory and returns a preview plus a hint to inspect the saved file.
   39     */
   40    readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
   41    /**
   42     * Resolved truncation limits: values from `tool_output` in opencode config, or MAX_LINES / MAX_BYTES if unset.
   43     */
   44    readonly limits: () => Effect.Effect<{ maxLines: number; maxBytes: number }>
查看全部 3 处证据
  • 契约 packages/opencode/src/tool/truncate.ts:13–44 输出限额、保留期与服务合同。
  • 实现 packages/opencode/src/tool/truncate.ts:85–140 保存全文与上下文节约提示。
  • 实现 packages/opencode/src/session/compaction.ts:241–287 旧工具输出 prune。
04
DIMENSION · PERMISSIONS-SECURITY

权限与安全

本章共 4 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

11
L1事实opencode-permission-001

权限采用 last-match wildcard 规则,默认 ask 而非默认 allow

源码事实

evaluate 从所有 ruleset 尾部找最后匹配 permission+pattern 的规则,无匹配返回 ask。ask 可 deny、直接 allow 或发事件等待 reply;always approval 只在当前 instance state 追加规则并自动解开同 session 匹配请求。

白话解释

越靠后的规则优先;没写明能不能做时先问人。点“始终允许”会记住本次运行,但不是永久改配置。

对自研 Harness 的含义

顺序非常重要,配置合并必须可解释;临时批准不会静默写回磁盘。

关键源码 · 实现
packages/opencode/src/permission/index.ts · L28–L37
   28  export function evaluate(permission: string, pattern: string, ...rulesets: PermissionV1.Ruleset[]): PermissionV1.Rule {
   29    return (
   30      rulesets
   31        .flat()
   32        .findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) ?? {
   33        action: "ask",
   34        permission,
   35        pattern: "*",
   36      }
   37    )
查看全部 3 处证据
  • 实现 packages/opencode/src/permission/index.ts:28–37 last-match 与默认 ask。
  • 实现 packages/opencode/src/permission/index.ts:67–107 ask/deny/allow/deferred。
  • 实现 packages/opencode/src/permission/index.ts:109–166 reject/once/always 与同 session pending 处理。
12
L1事实opencode-permission-002

默认 build 可执行,但敏感文件、外部目录和死循环会询问

源码事实

默认权限总体 allow;doom_loop 与 external_directory 为 ask,.env/.env.* 为 ask、.env.example allow。build 开放 question/plan_enter;plan 禁 edit,仅允许计划文件;explore 只开放搜索/读取/web/bash 等白名单。

白话解释

日常改项目尽量顺畅,碰到密钥文件、项目外路径或重复工具循环才踩刹车;规划和探索 Agent 的能力更窄。

对自研 Harness 的含义

Agent profile 本身就是 capability bundle。

关键源码 · 实现
packages/opencode/src/agent/agent.ts · L98–L155
   98      const state = yield* InstanceState.make<State>(
   99        Effect.fn("Agent.state")(function* (ctx) {
  100          const cfg = yield* config.get()
  101          const skillDirs = yield* skill.dirs()
  102          const referenceDirs = Object.keys(cfg.references ?? cfg.reference ?? {}).length
  103            ? yield* Effect.gen(function* () {
  104                yield* (yield* PluginV2.Service).wait(PluginV2.ID.make("core/config-reference"))
  105                return (yield* (yield* Reference.Service).list()).map((reference) => reference.path)
  106              }).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
  107            : []
  108          const whitelistedDirs = [
  109            Truncate.GLOB,
  110            path.join(Global.Path.tmp, "*"),
  111            ...skillDirs.map((dir) => path.join(dir, "*")),
  112            ...referenceDirs.map((dir) => path.join(dir, "*")),
  113          ]
  114          const readonlyExternalDirectory = {
  115            "*": "ask",
  116            ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
  117          } satisfies Record<string, "allow" | "ask" | "deny">
  118  
  119          const defaults = Permission.fromConfig({
  120            "*": "allow",
  121            doom_loop: "ask",
      … 24 lines omitted; exact range 98–155 …
  146                defaults,
  147                Permission.fromConfig({
  148                  question: "allow",
  149                  plan_enter: "allow",
  150                }),
  151                user,
  152              ),
  153              mode: "primary",
  154              native: true,
  155            },
查看全部 3 处证据
  • 实现 packages/opencode/src/agent/agent.ts:98–155 默认安全规则和 build。
  • 实现 packages/opencode/src/agent/agent.ts:156–180 plan 的 edit 限制。
  • 实现 packages/opencode/src/agent/agent.ts:182–218 general/explore 子 Agent 能力。
13
L1事实opencode-permission-003

连续相同工具调用触发 doom-loop 二次确认

源码事实

processor 检查最近固定阈值的 parts;若工具名和 JSON 输入全部相同且非 pending,就请求 doom_loop 权限。

白话解释

模型若一直用相同参数撞同一扇门,系统不会无限烧 token,而是停下来问人。

对自研 Harness 的含义

这是运行时行为检测,不依赖模型自觉。

关键源码 · 实现
packages/opencode/src/session/processor.ts · L331–L380
  331            case "tool-call": {
  332              if (ctx.assistantMessage.summary) {
  333                throw new Error(`Tool call not allowed while generating summary: ${value.name}`)
  334              }
  335              yield* ensureToolCall(value)
  336              const input = isRecord(value.input) ? value.input : { value: value.input }
  337              yield* updateToolCall(value.id, (match) => ({
  338                ...match,
  339                tool: value.name,
  340                state:
  341                  match.state.status === "running"
  342                    ? { ...match.state, input }
  343                    : {
  344                        status: "running",
  345                        input,
  346                        time: { start: Date.now() },
  347                      },
  348                metadata: match.metadata?.providerExecuted
  349                  ? { ...value.providerMetadata, providerExecuted: true }
  350                  : value.providerMetadata,
  351              }))
  352  
  353              const parts = yield* MessageV2.parts(ctx.assistantMessage.id).pipe(
  354                Effect.provideService(Database.Service, database),
      … 16 lines omitted; exact range 331–380 …
  371              const agent = yield* agents.get(ctx.assistantMessage.agent)
  372              yield* permission.ask({
  373                permission: "doom_loop",
  374                patterns: [value.name],
  375                sessionID: ctx.assistantMessage.sessionID,
  376                metadata: { tool: value.name, input },
  377                always: [value.name],
  378                ruleset: agent.permission,
  379              })
  380              return
查看全部 1 处证据
  • 实现 packages/opencode/src/session/processor.ts:331–380 相同工具+输入检测与权限请求。
14
L1事实opencode-mcp-002

MCP OAuth 有 state 校验,但远程连接没有内建 SSRF 私网拦截

源码事实

OAuth state 用 32 随机字节并在 callback 后比对,错配报潜在 CSRF;remoteURL 只验证 URL 可解析,StreamableHTTP/SSE 直接使用目标 URL,未见 private/loopback/metadata IP guard。

白话解释

登录回调防伪造做了,但“这个 MCP 地址是不是公司内网或云元数据地址”没有额外门卫。

对自研 Harness 的含义

在服务端/共享环境运行时,应由网络策略或 URL guard 补上 SSRF 边界。

关键源码 · 实现
packages/opencode/src/mcp/index.ts · L123–L125
  123  function remoteURL(value: string) {
  124    if (URL.canParse(value)) return new URL(value)
  125  }
查看全部 3 处证据
  • 实现 packages/opencode/src/mcp/index.ts:123–125 remote URL 仅做可解析检查。
  • 实现 packages/opencode/src/mcp/index.ts:806–849 OAuth state 与 transport 创建。
  • 实现 packages/opencode/src/mcp/index.ts:898–915 callback state/CSRF 校验。
05
DIMENSION · EXECUTION-SANDBOX

执行环境与沙箱

本章共 1 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

15
L1限制opencode-sandbox-001

shell 最终直接启动宿主子进程,没有内建 OS 沙箱

源码事实

ShellTool 用真实环境变量和 plugin env,在当前/指定 cwd 通过 ChildProcess 启动系统 shell;超时或 abort 后 kill。源码未施加容器、namespace、seccomp、Seatbelt 或 Windows job sandbox。

白话解释

权限门像门卫,但命令一旦放行就在你的真实机器上跑,并不是在一次性隔离箱里。

对自研 Harness 的含义

全自动或不可信仓库需要外层 VM/container;permission 不应被宣传成 sandbox。

关键源码 · 实现
packages/opencode/src/tool/shell.ts · L293–L309
  293  function cmd(shell: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
  294    if (process.platform === "win32" && Shell.ps(shell)) {
  295      return ChildProcess.make(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command], {
  296        cwd,
  297        env,
  298        stdin: "ignore",
  299        detached: false,
  300      })
  301    }
  302  
  303    return ChildProcess.make(command, [], {
  304      shell,
  305      cwd,
  306      env,
  307      stdin: "ignore",
  308      detached: process.platform !== "win32",
  309    })
查看全部 3 处证据
  • 实现 packages/opencode/src/tool/shell.ts:293–309 系统 shell child process。
  • 实现 packages/opencode/src/tool/shell.ts:416–425 继承宿主环境并允许 plugin 注入。
  • 实现 packages/opencode/src/tool/shell.ts:481–559 真实进程、流输出、超时和 kill。
06
DIMENSION · TOOL-DISPATCH

工具分发与结果治理

本章共 3 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

16
L1事实opencode-shell-001

shell 权限不是简单字符串前缀,而是 Bash/PowerShell 语法树扫描

源码事实

tree-sitter 解析 Bash/PowerShell,逐 command 生成 permission pattern 和 arity-based always pattern;对 rm/cp/mv/cat/PowerShell 文件 cmdlet 解析路径,项目外目录单独请求 external_directory。

白话解释

它会拆开一条复杂命令,分别看里面有哪些子命令和文件路径,而不是只看第一个单词。

对自研 Harness 的含义

比 startsWith 白名单稳健,但动态变量/命令替换的静态路径推断仍有天然边界。

关键源码 · 实现
packages/opencode/src/tool/shell.ts · L257–L291
  257  const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boolean) {
  258    const tree = yield* Effect.promise(() => parser().then((p) => (ps ? p.ps : p.bash).parse(command)))
  259    if (!tree) throw new Error("Failed to parse command")
  260    return tree
  261  })
  262  
  263  const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan, input: { command: string }) {
  264    if (scan.dirs.size > 0) {
  265      const directories = Array.from(scan.dirs)
  266      const globs = directories.map((dir) => {
  267        if (process.platform === "win32") return FSUtil.normalizePathPattern(path.join(dir, "*"))
  268        return path.join(dir, "*")
  269      })
  270      yield* ctx.ask({
  271        permission: "external_directory",
  272        patterns: globs,
  273        always: globs,
  274        metadata: {
  275          command: input.command,
  276          directories,
  277          patterns: globs,
  278        },
  279      })
  280    }
      … 1 lines omitted; exact range 257–291 …
  282    if (scan.patterns.size === 0) return
  283    yield* ctx.ask({
  284      permission: ShellID.ToolID,
  285      patterns: Array.from(scan.patterns),
  286      always: Array.from(scan.always),
  287      metadata: {
  288        command: input.command,
  289      },
  290    })
  291  })
查看全部 3 处证据
  • 实现 packages/opencode/src/tool/shell.ts:257–291 AST parse 后双层权限询问。
  • 实现 packages/opencode/src/tool/shell.ts:378–413 command/path/arity 收集。
  • 实现 packages/opencode/src/tool/shell.ts:597–640 执行前完成 parse/collect/ask。
17
L1事实opencode-tool-001

工具注册表统一 builtin、项目脚本和 npm/file plugin 工具

源码事实

registry 初始化 read/grep/glob/edit/write/apply_patch/shell/task/web/skill/LSP 等 builtins,扫描 config directories 的 tool(s)/*.js|ts,再合并 plugin.tool;所有 plugin 输出走统一 truncation 与 tracing。

白话解释

内置扳手、项目自制工具和插件工具最后都进同一个工具箱,走同一套执行上下文。

对自研 Harness 的含义

能力面一致,但本地 JS/TS plugin 是可执行代码,信任边界等同于宿主进程。

关键源码 · 实现
packages/opencode/src/tool/registry.ts · L86–L175
   86  const layer = Layer.effect(
   87    Service,
   88    Effect.gen(function* () {
   89      const config = yield* Config.Service
   90      const plugin = yield* Plugin.Service
   91      const agents = yield* Agent.Service
   92      const truncate = yield* Truncate.Service
   93      const flags = yield* RuntimeFlags.Service
   94      const mcp = yield* MCP.Service
   95  
   96      const invalid = yield* InvalidTool
   97      const task = yield* TaskTool
   98      const read = yield* ReadTool
   99      const question = yield* QuestionTool
  100      const todo = yield* TodoWriteTool
  101      const lsptool = yield* LspTool
  102      const plan = yield* PlanExitTool
  103      const webfetch = yield* WebFetchTool
  104      const websearch = yield* WebSearchTool
  105      const shell = yield* ShellTool
  106      const globtool = yield* GlobTool
  107      const writetool = yield* WriteTool
  108      const edit = yield* EditTool
  109      const greptool = yield* GrepTool
      … 56 lines omitted; exact range 86–175 …
  166                  Effect.withSpan("Tool.execute", {
  167                    attributes: {
  168                      "tool.name": id,
  169                      "session.id": toolCtx.sessionID,
  170                      "message.id": toolCtx.messageID,
  171                      ...(toolCtx.callID ? { "tool.call_id": toolCtx.callID } : {}),
  172                    },
  173                  }),
  174                ),
  175            }
查看全部 3 处证据
  • 实现 packages/opencode/src/tool/registry.ts:86–175 plugin tool 适配、权限桥与 truncation。
  • 实现 packages/opencode/src/tool/registry.ts:178–244 项目/插件发现和 builtin 列表。
  • 实现 packages/opencode/src/tool/registry.ts:286–334 按 provider/model 选择工具并开放定义 hook。
18
L1事实opencode-edit-001

Edit 在写前生成 diff、请求权限,写后格式化并回送 LSP 错误

源码事实

每文件 semaphore 防并发覆盖;路径先做 external-directory check,精确/容错 replacer 计算新内容和 diff,permission metadata 携带 diff;写入保持 BOM/换行,formatter 后重新读,最后触发 file events 和 LSP diagnostics。

白话解释

先把拟修改内容展示给门卫,再落盘;落盘后自动格式化,并立刻告诉模型有没有新语法错误。

对自研 Harness 的含义

编辑是有事务边界感的工具,而非裸 fs.writeFile。

关键源码 · 实现
packages/opencode/src/tool/edit.ts · L35–L56
   35  const locks = new Map<string, Semaphore.Semaphore>()
   36  
   37  function lock(filePath: string) {
   38    const resolvedFilePath = FSUtil.resolve(filePath)
   39    const hit = locks.get(resolvedFilePath)
   40    if (hit) return hit
   41  
   42    const next = Semaphore.makeUnsafe(1)
   43    locks.set(resolvedFilePath, next)
   44    return next
   45  }
   46  
   47  export const Parameters = Schema.Struct({
   48    filePath: Schema.String.annotate({ description: "The absolute path to the file to modify" }),
   49    oldString: Schema.String.annotate({ description: "The text to replace" }),
   50    newString: Schema.String.annotate({
   51      description: "The text to replace it with (must be different from oldString)",
   52    }),
   53    replaceAll: Schema.optional(Schema.Boolean).annotate({
   54      description: "Replace all occurrences of oldString (default false)",
   55    }),
   56  })
查看全部 4 处证据
  • 实现 packages/opencode/src/tool/edit.ts:35–56 文件锁和 schema。
  • 实现 packages/opencode/src/tool/edit.ts:79–119 外部路径、新文件 diff、ask 与写入。
  • 实现 packages/opencode/src/tool/edit.ts:123–171 已有文件替换、diff、格式化与 events。
  • 实现 packages/opencode/src/tool/edit.ts:175–211 统计、metadata 和 LSP diagnostics。
07
DIMENSION · INSTRUCTIONS-PROMPTS

指令与 Prompt

本章共 2 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

19
L1事实opencode-instruction-001

指令既有全局/项目 system 层,也有读文件时的局部层

源码事实

system 搜索全局 AGENTS.md 或 ~/.claude/CLAUDE.md,并在项目树中选择首类 AGENTS/CLAUDE/CONTEXT;config 还能引用 glob 与 URL。Read 某文件时沿目录向上找更近指令,每条 assistant message 只附一次,已在未压缩 Read 中加载的也不重复。

白话解释

公司总规章开会前发,走进某个子目录时再补当地规章;同一轮不会反复念。

对自研 Harness 的含义

层级指令与代码位置绑定,适合 monorepo,但 remote instruction URL 引入供应链和可用性风险。

关键源码 · 实现
packages/opencode/src/session/instruction.ts · L60–L89
   60      const globalFiles = [
   61        path.join(global.config, "AGENTS.md"),
   62        ...(!flags.disableClaudeCodePrompt ? [path.join(global.home, ".claude", "CLAUDE.md")] : []),
   63      ]
   64      const instructionFiles = [
   65        "AGENTS.md",
   66        ...(!flags.disableClaudeCodePrompt ? ["CLAUDE.md"] : []),
   67        "CONTEXT.md", // deprecated
   68      ]
   69  
   70      const state = yield* InstanceState.make(
   71        Effect.fn("Instruction.state")(() =>
   72          Effect.succeed({
   73            // Track which instruction files have already been attached for a given assistant message.
   74            claims: new Map<MessageID, Set<string>>(),
   75          }),
   76        ),
   77      )
   78  
   79      const relative = Effect.fnUntraced(function* (instruction: string) {
   80        const ctx = yield* InstanceState.context
   81        if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
   82          return yield* fs
   83            .globUp(instruction, ctx.directory, ctx.worktree)
   84            .pipe(Effect.catch(() => Effect.succeed([] as string[])))
   85        }
   86        return yield* fs
   87          .globUp(instruction, global.config, global.config)
   88          .pipe(Effect.catch(() => Effect.succeed([] as string[])))
   89      })
查看全部 3 处证据
  • 实现 packages/opencode/src/session/instruction.ts:60–89 全局/项目指令候选和向上搜索。
  • 实现 packages/opencode/src/session/instruction.ts:110–169 system paths、URL 与并发装载。
  • 实现 packages/opencode/src/session/instruction.ts:179–220 按 read path 局部发现和去重。
20
L1事实opencode-skill-001

Skills 支持 OpenCode、Claude、agents 目录与远程 discovery

源码事实

扫描 ~/.claude/.agents 及项目祖先的 skills/**/SKILL.md、各 config dir 的 skill(s)、显式 paths 和 URLs;解析 frontmatter,重复 name 后加载者覆盖并告警,available 再按 agent skill permission 过滤。

白话解释

它能复用多种 Agent 生态的技能目录,也能从远程拉技能;最终只有当前 Agent 有权用的技能会出现。

对自研 Harness 的含义

兼容性强,但远程技能内容属于 prompt 供应链,应配合 pin/hash/审计。

关键源码 · 契约
packages/opencode/src/skill/index.ts · L21–L43
   21  const CLAUDE_EXTERNAL_DIR = ".claude"
   22  const AGENTS_EXTERNAL_DIR = ".agents"
   23  const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
   24  const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
   25  const SKILL_PATTERN = "**/SKILL.md"
   26  
   27  // Built-in skill that ships with opencode. The model's intuition for what an
   28  // opencode.json should look like is often wrong, and opencode hard-fails on
   29  // invalid config, so users hit cryptic startup errors. Loading this skill
   30  // when the model is asked to touch opencode's own config files gives it the
   31  // actual schemas instead of guesses.
   32  const CUSTOMIZE_OPENCODE_SKILL_NAME = "customize-opencode"
   33  const CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION =
   34    "Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself."
   35  const CUSTOMIZE_OPENCODE_SKILL_BODY = SkillPlugin.CustomizeOpencodeContent
   36  
   37  export const Info = Schema.Struct({
   38    name: Schema.String,
   39    description: Schema.optional(Schema.String),
   40    location: Schema.String,
   41    content: Schema.String,
   42  })
   43  export type Info = Schema.Schema.Type<typeof Info>
查看全部 3 处证据
  • 契约 packages/opencode/src/skill/index.ts:21–43 多生态目录和 skill shape。
  • 实现 packages/opencode/src/skill/index.ts:105–140 frontmatter、重复名和覆盖。
  • 实现 packages/opencode/src/skill/index.ts:173–232 全局/项目/config/path/URL discovery。
08
DIMENSION · TOOLS-CONNECTORS-PLUGINS

工具、连接器与插件

本章共 2 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

21
L1事实opencode-mcp-001

MCP 同时支持 stdio、Streamable HTTP、SSE、OAuth、prompts 和 resources

源码事实

local MCP 直接启动 configured command;remote 依次尝试 StreamableHTTP/SSE,支持 headers、timeout 和 OAuth。连接后缓存 tool defs/instructions,并暴露 prompts/resources/templates/readResource。

白话解释

既能在本机拉起一个工具进程,也能连远程工具站;不只会调函数,还能取提示模板和资料。

对自研 Harness 的含义

连接器覆盖完整,但 local MCP 与 plugin 一样拥有宿主代码执行权。

关键源码 · 契约
packages/opencode/src/mcp/index.ts · L164–L198
  164  export interface Interface {
  165    readonly status: () => Effect.Effect<Record<string, Status>>
  166    readonly clients: () => Effect.Effect<Record<string, MCPClient>>
  167    readonly instructions: () => Effect.Effect<ServerInstructions[]>
  168    readonly tools: () => Effect.Effect<Record<string, McpTool>>
  169    readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
  170    readonly resources: (clientName?: string) => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
  171    readonly resourceTemplates: (
  172      clientName?: string,
  173    ) => Effect.Effect<Record<string, ResourceTemplateInfo & { client: string }>>
  174    readonly add: (name: string, mcp: ConfigMCPV1.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
  175    readonly connect: (name: string) => Effect.Effect<void, NotFoundError>
  176    readonly disconnect: (name: string) => Effect.Effect<void, NotFoundError>
  177    readonly getPrompt: (
  178      clientName: string,
  179      name: string,
  180      args?: Record<string, string>,
  181    ) => Effect.Effect<Awaited<ReturnType<MCPClient["getPrompt"]>> | undefined>
  182    readonly readResource: (
  183      clientName: string,
  184      resourceUri: string,
  185    ) => Effect.Effect<Awaited<ReturnType<MCPClient["readResource"]>> | undefined>
  186    readonly startAuth: (
  187      mcpName: string,
      … 1 lines omitted; exact range 164–198 …
  189    readonly authenticate: (
  190      mcpName: string,
  191      onAuthorization?: (authorizationUrl: string) => void,
  192    ) => Effect.Effect<Status, NotFoundError>
  193    readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status, NotFoundError>
  194    readonly removeAuth: (mcpName: string) => Effect.Effect<void>
  195    readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean, NotFoundError>
  196    readonly hasStoredTokens: (mcpName: string) => Effect.Effect<boolean>
  197    readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus>
  198  }
查看全部 4 处证据
  • 契约 packages/opencode/src/mcp/index.ts:164–198 MCP 完整服务面。
  • 实现 packages/opencode/src/mcp/index.ts:236–338 remote transport 与 OAuth 状态。
  • 实现 packages/opencode/src/mcp/index.ts:340–370 local stdio 进程。
  • 实现 packages/opencode/src/mcp/index.ts:666–738 namespaced tools、prompts 和 resources。
22
L1事实opencode-plugin-001

插件是进程内代码,可改 prompt、请求、工具定义和执行结果

源码事实

loader 可按 npm/file spec 安装、检查版本兼容并 dynamic import;运行时 hooks 包括 system/messages/params/headers、compaction、tool.definition、tool.execute.before/after、text.complete、shell.env 等。

白话解释

插件不是只提供一段文字,而是能伸手进模型请求和工具执行链,因此能力很强、信任也很高。

对自研 Harness 的含义

需要插件来源签名、版本 pin、allowlist 与隔离策略;当前 dynamic import 与主进程同权限。

关键源码 · 实现
packages/opencode/src/plugin/loader.ts · L76–L144
   76    // Normalize a config item into the loader's internal representation.
   77    function plan(item: ConfigPluginV1.Spec): Plan {
   78      const spec = ConfigPlugin.pluginSpecifier(item)
   79      return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
   80    }
   81  
   82    // Resolve a configured plugin into a concrete entrypoint that can later be imported.
   83    //
   84    // The stages here intentionally separate install/target resolution, entrypoint detection,
   85    // and compatibility checks so callers can report the exact reason a plugin was skipped.
   86    export async function resolve(
   87      plan: Plan,
   88      kind: PluginKind,
   89    ): Promise<
   90      | { ok: true; value: Resolved }
   91      | { ok: false; stage: "missing"; value: Missing }
   92      | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
   93    > {
   94      // First make sure the plugin exists locally, installing npm plugins on demand.
   95      let target = ""
   96      try {
   97        target = await resolvePluginTarget(plan.spec)
   98      } catch (error) {
   99        return { ok: false, stage: "install", error }
      … 35 lines omitted; exact range 76–144 …
  135    // Import the resolved module only after all earlier validation has succeeded.
  136    export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> {
  137      let mod
  138      try {
  139        mod = await import(row.entry)
  140      } catch (error) {
  141        return { ok: false, error }
  142      }
  143      if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) }
  144      return { ok: true, value: { ...row, mod } }
查看全部 4 处证据
  • 实现 packages/opencode/src/plugin/loader.ts:76–144 resolve、compatibility 与 dynamic import。
  • 实现 packages/opencode/src/session/llm/request.ts:68–78 system transform hook。
  • 实现 packages/opencode/src/session/tools.ts:92–129 tool before/after hooks。
  • 实现 packages/opencode/src/session/compaction.ts:342–350 compaction prompt/context hook。
09
DIMENSION · SUBAGENTS-COLLABORATION

子 Agent 与协作

本章共 2 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

23
L1事实opencode-subagent-001

子 Agent 是独立持久 session,可恢复、限深度并继承关键 deny

源码事实

TaskTool 可用 task_id 恢复 child session;新 child 写 parentID、独立 agent/model/permission。默认 subagent_depth=1;child 继承 parent session 的所有 deny 和 external_directory 规则,默认禁 todowrite/task,除非子 Agent 自己显式声明。

白话解释

子任务有自己的聊天记录,不是父对话里的一段临时函数;父亲的禁区会传下去,默认也不能无限生孩子。

对自研 Harness 的含义

上下文隔离、恢复和权限边界都较完整。

关键源码 · 实现
packages/opencode/src/agent/subagent-permissions.ts · L4–L26
    4  /**
    5   * Build the `permission` ruleset for a subagent's session when it's spawned
    6   * via the task tool. Combines:
    7   *
    8   * 1. The parent session's deny rules and external_directory rules.
    9   *    Parent agent restrictions only govern that agent; the subagent's own
   10   *    permissions determine its capabilities.
   11   * 2. Default `todowrite` and `task` denies if the subagent's own ruleset
   12   *    doesn't already permit them.
   13   */
   14  export function deriveSubagentSessionPermission(input: {
   15    parentSessionPermission: PermissionV1.Ruleset
   16    subagent: Agent.Info
   17  }): PermissionV1.Ruleset {
   18    const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
   19    const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
   20    return [
   21      ...input.parentSessionPermission.filter(
   22        (rule) => rule.permission === "external_directory" || rule.action === "deny",
   23      ),
   24      ...(canTodo ? [] : [{ permission: "todowrite" as const, pattern: "*" as const, action: "deny" as const }]),
   25      ...(canTask ? [] : [{ permission: "task" as const, pattern: "*" as const, action: "deny" as const }]),
   26    ]
查看全部 3 处证据
  • 实现 packages/opencode/src/agent/subagent-permissions.ts:4–26 child 权限继承与默认 deny。
  • 实现 packages/opencode/src/tool/task.ts:104–142 depth、task permission、resume 和 child rule。
  • 实现 packages/opencode/src/tool/task.ts:143–172 child session 与 primary tool deny。
24
L1事实opencode-subagent-002

子 Agent 支持 foreground/background、结果自动注回和递归取消

源码事实

foreground 等待 child job,background 由实验 flag 开启后立即返回;完成/失败会以 synthetic parent prompt 自动注回。父执行 abort 同时 cancel child prompt/job,删除 parent session 也递归删除 children。

白话解释

前台像打电话等对方答完,后台像发工单继续做别的;工单结束会主动回报,父任务取消时孩子也停。

对自研 Harness 的含义

已经具备实用并行协作骨架,但 background 仍是实验能力。

关键源码 · 契约
packages/opencode/src/tool/task.ts · L24–L62
   24  const id = "task"
   25  const BACKGROUND_DESCRIPTION = [
   26    "Background mode: background=true launches the subagent asynchronously and returns immediately.",
   27    "Foreground is the default; use it when you need the result before continuing.",
   28    "Use background only for independent work that can run while you continue elsewhere.",
   29    "You will be notified automatically when it finishes.",
   30  ].join(" ")
   31  const BACKGROUND_STARTED = [
   32    "The task is working in the background. You will be notified automatically when it finishes.",
   33    "DO NOT sleep, poll for progress, ask the task for status, or duplicate this task's work — avoid working with the same files or topics it is using.",
   34    "Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.",
   35  ].join("\n")
   36  const BACKGROUND_UPDATED = [
   37    "Additional context sent to the running background task.",
   38    "The task is still working in the background. You will be notified automatically when it finishes.",
   39    "DO NOT sleep, poll for progress, ask the task for status, or duplicate this task's work — avoid working with the same files or topics it is using.",
   40    "Work on non-overlapping tasks, or briefly tell the user what you sent and end your response.",
   41  ].join("\n")
   42  
   43  const BaseParameterFields = {
   44    description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }),
   45    prompt: Schema.String.annotate({ description: "The task for the agent to perform" }),
   46    subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
   47    task_id: Schema.optional(Schema.String).annotate({
      … 5 lines omitted; exact range 24–62 …
   53  
   54  const BaseParameters = Schema.Struct(BaseParameterFields)
   55  
   56  export const Parameters = Schema.Struct({
   57    ...BaseParameterFields,
   58    background: Schema.optional(Schema.Boolean).annotate({
   59      description:
   60        "Run the agent in the background. You will be notified when it completes. DO NOT sleep, poll, or proactively check on its progress",
   61    }),
   62  })
查看全部 4 处证据
  • 契约 packages/opencode/src/tool/task.ts:24–62 foreground/background 语义和实验参数。
  • 实现 packages/opencode/src/tool/task.ts:200–253 child run 与 synthetic result 注回。
  • 实现 packages/opencode/src/tool/task.ts:273–347 background start、wait 和取消。
  • 实现 packages/opencode/src/session/session.ts:608–629 删除 session 递归取消/删除 children。
10
DIMENSION · OBSERVABILITY-PERSISTENCE

持久化与观测

本章共 3 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

25
L1事实opencode-observe-001

session 持久化 agent/model/permission/cost/tokens/summary/revert 与 parent

源码事实

Session row 包含 parent、agent/model、permission、input/output/reasoning/cache tokens、cost、文件统计、share、revert snapshot 和 timestamps;messages/parts 通过事件桥更新并写数据库。

白话解释

一条任务不只存聊天文本,还存它用了哪个 Agent/模型、花了多少钱、改了哪些文件、能否回退以及是谁的子任务。

对自研 Harness 的含义

为恢复、审计、成本和协作提供统一数据底座。

关键源码 · 实现
packages/opencode/src/session/session.ts · L120–L158
  120  export function toRow(info: Info) {
  121    return {
  122      id: info.id,
  123      project_id: info.projectID,
  124      workspace_id: info.workspaceID,
  125      parent_id: info.parentID,
  126      slug: info.slug,
  127      directory: info.directory,
  128      path: info.path,
  129      title: info.title,
  130      agent: info.agent,
  131      model: info.model,
  132      version: info.version,
  133      share_url: info.share?.url,
  134      summary_additions: info.summary?.additions,
  135      summary_deletions: info.summary?.deletions,
  136      summary_files: info.summary?.files,
  137      summary_diffs: info.summary?.diffs,
  138      metadata: info.metadata,
  139      cost: info.cost ?? 0,
  140      tokens_input: (info.tokens ?? EmptyTokens).input,
  141      tokens_output: (info.tokens ?? EmptyTokens).output,
  142      tokens_reasoning: (info.tokens ?? EmptyTokens).reasoning,
  143      tokens_cache_read: (info.tokens ?? EmptyTokens).cache.read,
      … 5 lines omitted; exact range 120–158 …
  149            snapshot: info.revert.snapshot,
  150            diff: info.revert.diff,
  151          }
  152        : null,
  153      permission: info.permission,
  154      time_created: info.time.created,
  155      time_updated: info.time.updated,
  156      time_compacting: info.time.compacting,
  157      time_archived: info.time.archived,
  158    }
查看全部 3 处证据
  • 实现 packages/opencode/src/session/session.ts:120–158 Session row 完整投影。
  • 契约 packages/opencode/src/session/session.ts:224–244 Session schema。
  • 实现 packages/opencode/src/session/session.ts:631–666 message/part event 与读取。
26
L1事实opencode-snapshot-001

每个模型 step 前后用影子 Git 仓库生成可回退 patch

源码事实

processor 在 stream 前预抓 snapshot,step-finish 后 track 并 patch;Snapshot 使用独立 gitdir,复用源仓对象库/索引,忽略规则同步,跳过大于 2MiB 的未跟踪文件并只 stage 候选路径。

白话解释

每走一步都拍“修改前后”照片,照片存进旁边的 Git 仓库,不污染用户当前分支。

对自研 Harness 的含义

支持精确 diff/revert;非 Git 项目或 snapshot=false 时能力降级。

关键源码 · 实现
packages/opencode/src/session/processor.ts · L98–L114
   98      const create = Effect.fn("SessionProcessor.create")(function* (input: Input) {
   99        // Pre-capture snapshot before the LLM stream starts. The AI SDK
  100        // may execute tools internally before emitting start-step events,
  101        // so capturing inside the event handler can be too late.
  102        const initialSnapshot = yield* snapshot.track()
  103        const ctx: ProcessorContext = {
  104          assistantMessage: input.assistantMessage,
  105          sessionID: input.sessionID,
  106          model: input.model,
  107          toolcalls: {},
  108          shouldBreak: false,
  109          snapshot: initialSnapshot,
  110          blocked: false,
  111          needsCompaction: false,
  112          currentText: undefined,
  113          reasoningMap: {},
  114        }
查看全部 5 处证据
  • 实现 packages/opencode/src/session/processor.ts:98–114 LLM stream 前预抓 snapshot。
  • 实现 packages/opencode/src/session/processor.ts:435–469 step 后 patch part。
  • 配置 packages/opencode/src/snapshot/index.ts:23–27 保留期、大小和 Git 参数。
  • 实现 packages/opencode/src/snapshot/index.ts:195–232 复用对象库和索引。
  • 实现 packages/opencode/src/snapshot/index.ts:235–298 候选、ignore、大文件和 scoped stage。
27
L1风险opencode-share-001

Share 是显式远程同步会话、消息、parts、diff 和模型

源码事实

创建 share 后,event subscribers 将 session/message/part/session_diff/model 聚合并发到 opncd.ai 或组织 console;可用 OPENCODE_DISABLE_SHARE 全局关闭,配置还支持 share=auto。

白话解释

分享链接不是只上传一张截图,而是持续同步完整任务数据;若开了自动分享,必须按数据出境功能治理。

对自研 Harness 的含义

企业默认应关闭或接自有 console,并在 UI 明确展示共享状态与数据范围。

关键源码 · 契约
packages/opencode/src/share/share-next.ts · L23–L72
   23  const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1"
   24  
   25  export type Api = {
   26    create: string
   27    sync: (shareID: string) => string
   28    remove: (shareID: string) => string
   29    data: (shareID: string) => string
   30  }
   31  
   32  export type Req = {
   33    headers: Record<string, string>
   34    api: Api
   35    baseUrl: string
   36  }
   37  
   38  const ShareSchema = Schema.Struct({
   39    id: Schema.String,
   40    url: Schema.String,
   41    secret: Schema.String,
   42  })
   43  export type Share = typeof ShareSchema.Type
   44  
   45  type State = {
   46    queue: Map<SessionID, Map<string, Data>>
      … 16 lines omitted; exact range 23–72 …
   63      }
   64    | {
   65        type: "session_diff"
   66        data: SDK.SnapshotFileDiff[]
   67      }
   68    | {
   69        type: "model"
   70        data: SDK.Model[]
   71      }
   72  
查看全部 4 处证据
  • 契约 packages/opencode/src/share/share-next.ts:23–72 关闭开关和同步数据类型。
  • 实现 packages/opencode/src/share/share-next.ts:124–200 事件驱动增量同步。
  • 实现 packages/opencode/src/share/share-next.ts:247–298 远程 flush 和 full sync 数据范围。
  • 配置 packages/opencode/src/config/config.ts:575–577 autoshare 到 share=auto 兼容。
11
DIMENSION · TESTS-EVALS-MATURITY

测试、评测与成熟度

本章共 1 个可定位结论;结论按“实现事实 → 白话解释 → 工程影响 → 源码摘录”展开。

28
L3限制opencode-maturity-001

关键机制测试密集,但缺统一任务成功率评测闭环

源码事实

仓库有 compaction、processor、recorded provider loop、permission、shell、task/background、MCP lifecycle/OAuth、plugin、snapshot、server/ACP 等测试;未发现统一基准将真实 coding task、成本、时延和回归 verdict 连成 eval pipeline。

白话解释

零件台架测试很丰富,但还没有一张公开的整车赛道成绩单。

对自研 Harness 的含义

建设自有 Agent 时应保留其机制级测试,同时补 golden workflow 和端到端 regression harness。

关键源码 · 测试
packages/opencode/test/session/compaction.test.ts · L920–L1045
  920        }
  921      }),
  922    )
  923  
  924    itCompaction.instance(
  925      "persists tail_start_id for retained recent turns",
  926      Effect.gen(function* () {
  927        const ssn = yield* SessionNs.Service
  928        const session = yield* ssn.create({})
  929        yield* createUserMessage(session.id, "first")
  930        const keep = yield* createUserMessage(session.id, "second")
  931        yield* createUserMessage(session.id, "third")
  932        yield* createSummaryCompaction(session.id)
  933  
  934        const msgs = yield* ssn.messages({ sessionID: session.id })
  935        const parent = msgs.at(-1)?.info.id
  936        expect(parent).toBeTruthy()
  937        yield* SessionCompaction.use.process({
  938          parentID: parent!,
  939          messages: msgs,
  940          sessionID: session.id,
  941          auto: false,
  942        })
  943  
      … 92 lines omitted; exact range 920–1045 …
 1036      },
 1037      { git: true },
 1038    )
 1039  
 1040    itCompaction.instance(
 1041      "retains a split turn suffix when a later message fits the preserve token budget",
 1042      () => {
 1043        const stub = llm()
 1044        let captured = ""
 1045        stub.push(reply("summary", (input) => (captured = JSON.stringify(input.messages))))
查看全部 3 处证据
  • 测试 packages/opencode/test/session/compaction.test.ts:920–1045 tail、预算和 compaction 组合测试。
  • 测试 packages/opencode/test/tool/task.test.ts:630–690 background child 执行测试。
  • 测试 packages/opencode/test/tool/shell.test.ts:920–1005 external directory 与 always pattern 测试。
APPENDIX · SOURCE INDEX

本报告引用过的实现文件

这是一份代码阅读索引,不是仓库文件总表。机器候选扫描覆盖整个仓库;进入结论的文件必须经人工沿调用链复核。

  1. 01packages/opencode/src/session/prompt.tsL1081–1130, 1170–1241, 1252–1286
  2. 02packages/opencode/src/session/message-v2.tsL578–600, 521–571
  3. 03packages/opencode/src/session/processor.tsL315–413, 424–483, 486–531, 539–597, 599–625, 627–681, 435–482, 331–380, 98–114, 435–469
  4. 04packages/opencode/src/provider/provider.tsL101–145, 154–238, 294–368
  5. 05packages/opencode/src/session/llm/request.tsL56–100, 114–158, 181–213, 68–78
  6. 06packages/opencode/src/session/overflow.tsL8–33
  7. 07packages/opencode/src/session/compaction.tsL28–35, 180–239, 289–354, 383–413, 422–503, 241–287, 342–350
  8. 08packages/opencode/src/tool/truncate.tsL13–44, 85–140
  9. 09packages/opencode/src/permission/index.tsL28–37, 67–107, 109–166
  10. 10packages/opencode/src/agent/agent.tsL98–155, 156–180, 182–218
  11. 11packages/opencode/src/tool/shell.tsL293–309, 416–425, 481–559, 257–291, 378–413, 597–640
  12. 12packages/opencode/src/tool/registry.tsL86–175, 178–244, 286–334
  13. 13packages/opencode/src/tool/edit.tsL35–56, 79–119, 123–171, 175–211
  14. 14packages/opencode/src/session/instruction.tsL60–89, 110–169, 179–220
  15. 15packages/opencode/src/skill/index.tsL21–43, 105–140, 173–232
  16. 16packages/opencode/src/mcp/index.tsL164–198, 236–338, 340–370, 666–738, 123–125, 806–849, 898–915
  17. 17packages/opencode/src/agent/subagent-permissions.tsL4–26
  18. 18packages/opencode/src/tool/task.tsL104–142, 143–172, 24–62, 200–253, 273–347
  19. 19packages/opencode/src/session/session.tsL608–629, 120–158, 224–244, 631–666
  20. 20packages/opencode/src/snapshot/index.tsL23–27, 195–232, 235–298
  21. 21packages/opencode/src/share/share-next.tsL23–72, 124–200, 247–298
  22. 22packages/opencode/src/config/config.tsL575–577
  23. 23packages/opencode/src/plugin/loader.tsL76–144
  24. 24packages/opencode/src/session/tools.tsL92–129
  25. 25packages/opencode/test/session/compaction.test.tsL920–1045
  26. 26packages/opencode/test/tool/task.test.tsL630–690
  27. 27packages/opencode/test/tool/shell.test.tsL920–1005