第 7 章 · 02 Extension Protocol v1 sidecar 与拦截器


文档摘要

第 7 章 · 02 Extension Protocol v1 sidecar 与拦截器 本节摘要:本节精读比 MCP 更强的扩展——Extension Protocol v1 sidecar( ,约 20K 行)。它是 Reasonix(host)与跑在进程外的 code extension(sidecar)之间的稳定线协议( )。和 MCP 只贡献工具不同,Extension 能做四件 MCP 做不到的事:拦截运行时事件(interceptor)、贡献流式 Provider、提供结构化 UI、拥有替换策略槽(replace slot)。

第 7 章 · 02 Extension Protocol v1 sidecar 与拦截器

本节摘要:本节精读比 MCP 更强的扩展——Extension Protocol v1 sidecar(internal/extension,约 20K 行)。它是 Reasonix(host)与跑在进程外的 code extension(sidecar)之间的稳定线协议(reasonix.extension.v1)。和 MCP 只贡献工具不同,Extension 能做四件 MCP 做不到的事:拦截运行时事件(interceptor)贡献流式 Provider提供结构化 UI拥有替换策略槽(replace slot)。本节讲清 Extension 的传输(NDJSON over stdio)、生命周期(spawn → initialize → initialized → shutdown)、内容引用(>64KiB 外存)、17 个冻结的拦截钩子点、五种拦截决策(continue/block/replace/allow/deny)、十个替换策略槽、流式 Provider 协议、结构化 UI,以及与 MCP 的能力边界差异。最后讲 Plugin Manifest v1(apiVersion: reasonix.io/plugin/v1)版本化插件包的分发安装,和"code extension 是 full trust"的安全模型。这是学习"如何设计一个既能拦截宿主事件、又能贡献核心能力、还保持稳定 ABI 的插件协议"的优质真实案例。

内容来源:原项目源码 internal/extension/(intercept.go、replace.go、sidecar/、protocol/、uihub/、providerext/)、docs/EXTENSION_PROTOCOL.mddocs/PLUGIN_PACKAGES.md,精读并套用体系化模板。

⚠️ 注意:Extension Protocol v1 是冻结协议——major version 1 内只允许"新增可选字段/新增枚举值/新增方法",现有的必需字段、方向、限制、错误原因、语义永不改变。CI 用确定性生成测试(TestGeneratedArtifactsAreDeterministicAndCommitted)强制守这条线。改协议要先想清楚"这是不是破坏性变更"——破坏性变更必须升 major version。

学习目标

阅读完本节,你应当能够:

  1. 说清 Extension Protocol v1 与 MCP 的能力边界差异(Extension 能做哪四件 MCP 做不到的事)。
  2. 讲清 Extension 的传输:NDJSON over stdio、8 MiB 帧上限、整数 request id。
  3. 背诵 Extension 生命周期五步:spawn → initialize → initialized → shutdown → crash 行为。
  4. 解释内容引用(Content Reference):>64KiB 外存、256 KiB 分块、8 MiB 单对象上限。
  5. 列举 17 个拦截钩子点中的关键几类(input/tool/permission/session/context/compaction/system_prompt)。
  6. 说出五种拦截决策(continue/block/replace/allow/deny)各自语义,以及 allow 的特殊地位。
  7. 讲清替换策略槽(replace slot)的"唯一 owner"规则与十个槽名。
  8. 解释流式 Provider 的 chunk 序列号机制和结构化 UI 的"structured only"约束。
  9. 说清 Plugin Manifest v1 的 apiVersioncontributesruntime 三块,以及"full trust"安全模型。

一、Extension 与 MCP 的能力边界

上一节讲了 MCP 客户端——它只贡献 tools/prompts/resources。Extension Protocol v1 sidecar 要强得多。docs/EXTENSION_PROTOCOL.md 开宗明义:

The Extension Protocol is the stable wire contract between Reasonix (the host) and code extensions running as out-of-process sidecars. It is how an installed plugin with a runtime block intercepts runtime events, owns replacement strategies, contributes streaming model providers, and publishes structured UI — without ever linking into the host binary.

翻译:Extension Protocol 是 Reasonix(host)和跑在进程外的 code extension(sidecar)之间的稳定线协议。它是一个带 runtime block 的已安装插件拦截运行时事件、拥有替换策略、贡献流式模型 Provider、发布结构化 UI 的方式——而永远不需要链进宿主二进制

用一张表对比 Extension 和 MCP 的能力:

能力 MCP Extension Protocol v1
贡献工具 是(也可拦截 tool:<name>)
贡献 prompts/resources 是(prompts via UI)
拦截运行时事件 (17 个冻结钩子点)
拥有替换策略槽 (10 个 slot,唯一 owner)
贡献流式 Provider (plugin/<plugin>/<provider>/<model>)
结构化 UI (status/card/form/notification)
影响权限决策 (只能被 permission 管) (permission.decision 的 allow 能盖过 host deny)

一句话总结:MCP 是"被动贡献工具"的;Extension 是"主动参与宿主运行时"的。MCP 服务器只能等 agent 来调它的工具;Extension 能在工具执行前/后、权限决策时、系统提示构建时、上下文压缩时主动介入。这就是为什么 Extension 的协议比 MCP 复杂得多——它要表达"拦截""替换""阻断"这些主动语义。

二、传输与帧:NDJSON over stdio

EXTENSION_PROTOCOL.md 的 Transport 节:

  • Strict JSON-RPC 2.0 over NDJSON: one complete JSON object per line on stdin/stdout. stderr belongs to the extension for diagnostics; the host captures a bounded, credential-redacted tail for errors.
  • Frames are capped at 8 MiB in both directions; oversized frames are a connection-fatal frame_too_large error.
  • Request IDs are integers. params must be an object. Unknown members are tolerated at the frame level; DTO decoding is strict (unknown fields are rejected) so typos surface immediately.

和 MCP 一样是 JSON-RPC 2.0,但有几点更严格。

第一,NDJSON(换行分隔 JSON)。一行一个完整 JSON 对象,走 stdin/stdout。这和 MCP stdio 一样。stderr 属于 extension 做诊断;host 捕获一个有界、脱敏凭据的尾部用于错误显示。注意"credential-redacted"——host 会对 extension 的 stderr 做凭据脱敏,防止 extension 不小心把密钥打到日志里。

第二,8 MiB 帧上限。双向都有 8 MiB 的帧大小上限,超了是 connection-fatal 的 frame_too_large 错误(直接断连)。这是为了防止一个失控的 extension 发巨型帧把 host 撑爆。大内容用下一节的"内容引用"外存,不直接塞帧。

第三,DTO 严格解码。帧级别对未知成员宽容(允许协议演进),但 DTO(Data Transfer Object)解码严格——未知字段直接拒绝。注释:"typos surface immediately"(拼错立刻暴露)。这是个有意的严格:协议能演进(帧宽容),但每个方法的具体参数结构必须精确(DTO 严格),避免"拼错字段名导致静默失效"。

三、生命周期:五步

EXTENSION_PROTOCOL.md 的 Lifecycle 节定义了完整的五步:

逐步拆解。

第一步,spawn。host 用 exec form, no shell 启动 sidecar(不用 shell 解释,直接 exec 可执行文件 + 参数)。这避免了 shell 注入风险。一次 runtime generation 内,host 最多并行启动 4 个 sidecar,共享 30 秒启动预算。

第二步,initialize。host 先发 extension/initialize,params 带着 manifest 期望:这个 sidecar 将拦截哪些事件、替换哪些 slot、贡献哪些 provider、提供哪些 UI action。这是 host 把"我在 manifest 里批准了你做什么"告诉 sidecar。

第三步,sidecar 回答 + host 校验。sidecar 回答自己的 declaration。host 校验:协议 major 版本必须精确匹配,而且 sidecar 声明的每个 subscription/replacement slot/provider/UI action 必须是 plugin manifest 的子集。任何超出 manifest 的声明,握手失败,错误是 capability_not_declared。这是个关键的安全闸——sidecar 不能"声明 manifest 没批准的能力"。

第四步,initialized。host 发 extension/initialized。注释强调:"Any extension-to-host traffic before this point poisons the connection."(在这之前的任何 extension→host 流量都会毒化连接。)也就是说,sidecar 在收到 initialized 之前不能主动发消息给 host,否则连接作废。

第五步,shutdown + crash 行为。关闭是有界的:发 extension/shutdown(带 timeout),然后关 stdin,再不退出就 kill 进程树。crash 行为很严格:sidecar 死了,取消它所有 pending RPC;如果它拥有当前选中的 provider 或某个 replacement slot,当前操作显式失败——host 绝不静默 fallback 到别的模型或策略。注释:"A crashed sidecar is only restarted by an idle-time runtime reload."(崩溃的 sidecar 只在空闲时的 runtime reload 才重启。)这是"显式失败优于静默降级"的设计——宁可让用户看到错误,也不悄悄换一个行为可能不同的 provider。

四、内容引用:大对象外存

Extension 协议处理大对象(比如一个超长的工具输出或上下文)用"内容引用"机制,不直接塞进 JSON 帧。EXTENSION_PROTOCOL.md 的 Content references 节:

Payload fields marked externalizable that exceed 64 KiB are offloaded into the host content store: the frame carries an ExternalizedField descriptor (JSON pointer, content ref, byte count, SHA-256) and a null placeholder. The peer pages the bytes back with host/content/read in 256 KiB chunks, verifying byte count and hash. A single content object is capped at 8 MiB. Unknown or expired refs fail with content_ref_expired.

机制:标记为 externalizable 的字段,如果超过 64 KiB,就外存到 host 的 content store。帧里只带一个 ExternalizedField 描述符(JSON pointer 指明它在原 payload 的位置、content ref、字节数、SHA-256)和一个 null 占位。对端用 host/content/read256 KiB 一块分页拉回字节,每块校验字节数和 hash。单个 content 对象上限 8 MiB。过期/未知的 ref 报 content_ref_expired

这套机制让 Extension 协议既能传大对象(不像 MCP 帧有上限就传不了),又不会让大对象撑爆帧(帧里只有描述符)。SHA-256 校验保证完整性。这是处理"大对象 + 协议帧"这对矛盾的标准工程方案。

五、拦截:17 个冻结钩子点与五种决策

拦截(interception)是 Extension 最核心的能力。EXTENSION_PROTOCOL.md 的 Interception 节:

Seventeen frozen hook points (see the generated index). extension/intercept is blocking; extension/event is fire-and-forget observation of the same points.

17 个冻结的钩子点(具体名单在 docs/EXTENSION_PROTOCOL.generated.md 生成的索引里)。两种调用方式:extension/intercept阻塞的(等 extension 决策),extension/eventfire-and-forget 观察(同一钩子点的只读通知)。

事件投递用有界非阻塞 writer 队列:队列饱和时丢弃这次观察并警告,而不是阻塞 Agent。这保证了"观察型 extension 出问题不能拖垮主循环"。

拦截器顺序

Ordinary interceptors run sequentially in a deterministic order: priority ascending (manifest priority, -1000..1000, default 0), then plugin ID, then registration order.

普通拦截器按确定性顺序串行执行:先按 manifest 的 priority(-1000..1000,默认 0)升序,再按 plugin ID,最后按注册顺序。"确定性顺序"很关键——拦截器的副作用必须可复现,不能这次先 A 后 B、下次先 B 后 A。

五种决策

每个拦截调用的决策有五种:

决策 语义 适用范围
continue 放行,把 payload 传下去 所有点
block 中止操作,带面向用户的理由 所有点
replace 替换 payload(host 重新校验 DTO 和 schema) 所有点
allow 全信任放行,能盖过 host deny,被审计 permission.decision
deny 拒绝 permission.decision

allow/deny 只在 permission.decision 这个钩子点合法——这是 Extension 能影响权限决策的体现。一个 full-trust extension 的 allow盖过 host 的 deny,且会被审计。这就是上一节说的"Extension 能影响权限"——MCP 工具只能被 permission 管,Extension 能反过来盖过 permission 的 deny。这是极大的权力,所以只给 full-trust extension,且有审计。

替换策略槽(replace slot)

Replacement strategy slots (system_prompt, context, provider_request, provider_response, compaction, session_policy, permission, frontend_events, tool:<name>, provider:<ref>) have exactly one owner across all installed plugins. The chain runs first; the slot owner gets the final say. A strategy owner's timeout or error always fails the operation.

十个替换策略槽:system_prompt(系统提示)、context(上下文)、provider_request/provider_response(Provider 请求/响应)、compaction(压缩)、session_policy(会话策略)、permission(权限)、frontend_events(前端事件)、tool:<name>(特定工具)、provider:<ref>(特定 Provider)。

关键规则:每个 slot 在所有已安装插件中只能有一个 owner。冲突会让 build 失败并点名两个来源。执行顺序:拦截链先跑,然后 slot owner 有最终决定权。slot owner 超时或出错,操作必然失败——和 crash 行为一样,不静默降级。

这十个槽几乎覆盖了 Reasonix 运行时的所有"可替换核心逻辑"。一个 extension 占了 system_prompt slot,它就能完全控制系统提示长什么样(对应第 6 章 boot 的 SystemPrompt 装配,extension 能插手)。占了 compaction slot,就能自定义压缩策略。这是比拦截更强的能力——拦截是"在某点介入",replace slot 是"完全接管某块逻辑"。

💡 契约要点:Extension 的拦截体系是"钩子点 + 决策 + 槽"三层。17 个冻结钩子点是"事件发生地";五种决策是"拦截器在每个点能做什么";十个 replace slot 是"能完全接管的整块逻辑"。MCP 完全没有这三层——它只能被动贡献工具等调用。这就是 Extension 强在哪的本质。代价是:协议复杂得多,且 extension 是 full trust(下节安全模型)。

六、流式 Provider 与结构化 UI

两个高级能力,简述。

流式 Provider

providers 能力的 extension,用 extension/provider/catalog 回答等价于 host provider 的描述符(models/context window/pricing/vision/reasoning/effort)——绝不包含凭据。模型以 plugin/<plugin>/<provider>/<model> 形式出现。

流式协议:extension/provider/stream/openstream/chunkstream/end。chunks 带 1-based 连续序列号,stream/end.lastSeq 冻结终止边界。host 缓冲乱序 chunk、丢弃重复、缺序时以 interrupted 失败并指出缺失的序列号。chunk 类型:text / reasoning(带 signature)/ tool_call_start / tool_call_args_delta / tool_call / usage(含 cache token)/ done / error。Provider 错误由生产者脱敏,host 再防御性脱敏一次。

取消流:取消 stream context 发 stream/cancel,sidecar 必须停止产 chunk。extension 自己读环境和凭据;host 绝不发别的 provider 的 API key 或 header。崩溃的 provider 绝不触发 fallback 到别的模型——和 slot owner 一样,显式失败。

结构化 UI

ui 能力的 extension 发布 status/card/form/notification(host/ui/publish)和提问(host/ui/request:confirm/input/select/multiselect)。关键约束:structured only——没有 HTML/CSS/JavaScript/远程脚本/任意前端组件/不受控 URL;Markdown 走每个前端已有的安全渲染器。每个 surface 更新带 plugin id/surface id/session id/runtime generation;stale-generation 更新被丢弃(防 tab 切换或 reload 后的迟到结果覆盖当前状态)。Action 在 initialize 时声明,命名空间化为 /<plugin>:<action>,通过 extension/ui/action 调用;表单提交通过 extension/ui/submit 到达。

这套"structured only + generation 标记"的设计,让 extension 能影响 UI 但不能注入任意前端代码——安全且可预测。

七、Plugin Manifest v1:版本化插件包

Extension 通过 Plugin Manifest v1 声明。docs/PLUGIN_PACKAGES.md 的 "Manifest v1 (Extensions)" 节。一个插件通过声明 apiVersion 选择 v1:

{ "apiVersion": "reasonix.io/plugin/v1", "name": "example", "version": "1.0.0", "description": "Example extension", "contributes": { "skills": ["skills"], "agents": ["agents"], "commands": ["commands"], "prompts": ["prompts"], "hooks": {}, "mcpServers": {}, "themes": ["themes/*.reasonix-theme"] }, "runtime": { "command": "${REASONIX_PLUGIN_ROOT}/bin/example", "args": [], "env": {}, "required": true, "priority": 0, "intercepts": ["input.receive", "tool.before"], "replaces": ["system_prompt"], "capabilities": ["interceptors", "strategies", "providers", "ui"] } }

manifest 分三块:

  • 顶层 + contributes:声明插件贡献的静态资源(skills/agents/commands/prompts/hooks/mcpServers/themes)。这些是"被动资源",和 MCP 类似。
  • runtime:声明 code extension——一个 sidecar 进程。command/args/envexec form only(command 是可执行文件,不经 shell 解释);${REASONIX_PLUGIN_ROOT} 展开为已安装的插件根。intercepts 列要拦截的事件(如 input.receivetool.beforepermission.decision);replaces 声明要拥有的替换槽(如 system_prompt);capabilities 按特性族门控(interceptors/strategies/providers/ui)。
  • 解析规则:没有 apiVersion 的 manifest 按老格式解析(未知字段忽略);v1 严格——根或 contributes/runtime 下任何未知字段都是错误(点名字段路径);未知 major 版本(reasonix.io/plugin/v2)被拒;v1 可混用 legacy 顶层字段和 contributes(相同路径去重,同 key 两定义是 manifest 错误);所有相对路径和 glob 必须留在插件根内(穿越/绝对路径/逃逸符号链接/非普通 theme 文件都拒)。

runtime block 里 extension 声明的能力必须是它在 intercepts/replaces/capabilities 里声明的子集——握手时 host 校验,超出 manifest 的声明 capability_not_declared 失败。

八、Full Trust 安全模型

Extension 的能力这么大,安全模型必须讲清楚。EXTENSION_PROTOCOL.md 的 Security model 节,以及 PLUGIN_PACKAGES.md 的 "Full trust" 段:

A code extension is full trust: it runs outside the Reasonix sandbox with the unfiltered inherited environment, can read the full session and environment, can bypass permissions, and can operate the machine directly. Installing, updating, replacing, or --linking a plugin with a runtime block is the authorization — there is no second confirmation.

翻译:code extension 是完全信任的:它跑在 Reasonix 沙箱之外,带着未过滤的继承环境,能读完整会话和环境绕过权限直接操作机器。安装/更新/替换/--link 一个带 runtime block 的插件就是授权——没有第二次确认。

几条硬约束。

第一,只有 plugin flow 安装的插件能启动 runtime。"Only plugins installed through the plugin flow (recorded in plugin-packages.json) can start a sidecar; project configuration can never declare one." 项目配置永远不能声明 runtime——必须是用户显式通过 plugin 流程安装的。这避免了"克隆一个恶意项目,它的配置就启动了 sidecar"的攻击。

第二,凭据脱敏。任何 sidecar 诊断、结构化 UI、拦截器理由、provider 错误在到达 UI/日志/错误面前,host 都跑一遍凭据脱敏。普通 provider/model 内容作为产品数据保留。

第三,显眼的 FULL TRUST 提示。install preview、plugin details、capability diagnostics 永远显示 runtime 插件的 FULL TRUST block(运行时命令、拦截器、替换槽、provider/UI 能力)。安装前必须审查这块。

第四,--link 持续信任变化内容--link 链接本地目录(开发模式),会自动信任变化的 content——所以 --link 只能用于你完全信任的本地开发插件,移动/删除该目录会破坏链接插件。

这套"安装即授权 + full trust + 显式提示 + 持续信任 link"的安全模型,把"_extension 能做什么_的决策权完全交给用户安装动作"。Reasonix 不试图在运行时限制 extension(因为 extension 在沙箱外),而是把决定权前移到安装环节,并强制让用户看到"你装的这个东西能干什么"。

九、与 MCP 的关系:不是替代,是分层

最后澄清一个常见误解:Extension 不是 MCP 的替代,两者是分层共存的。

  • MCP 服务器:通过 [[plugin.servers]] 或 manifest 的 mcpServers 声明,只贡献工具/prompts/resources,被 permission 管,适合"我想接一个外部工具源"的场景。
  • Extension sidecar:通过 manifest 的 runtime block 声明,能拦截事件/拥有槽/贡献 provider/提供 UI,full trust,适合"我想深度介入 Reasonix 运行时"的场景。

一个插件可以同时贡献 MCP 服务器(在 contributes.mcpServers)和 Extension sidecar(在 runtime)——它们不冲突。Reasonix 会分别处理:MCP 服务器走 internal/plugin 客户端,Extension sidecar 走 internal/extension host。下一节讲 sdk/go 怎么开发 Extension sidecar。

本节要点回顾

  1. Extension vs MCP 能力差:Extension 能做四件 MCP 做不到的事——拦截运行时事件、拥有替换策略槽、贡献流式 Provider、提供结构化 UI;还能在 permission.decision 用 allow 盖过 host deny。
  2. 传输:NDJSON over stdio(JSON-RPC 2.0),8 MiB 帧上限(connection-fatal),整数 request id,DTO 严格解码(unknown field 拒绝,typo 立刻暴露),stderr 脱敏。
  3. 生命周期五步:spawn(exec form,no shell)→ initialize(host 给 manifest 期望)→ sidecar 回 declaration + host 校验(必须 manifest 子集,超了 capability_not_declared)→ initialized(之前 extension 不能主动发消息)→ shutdown(stdin close → kill);crash 取消所有 pending RPC,owned provider/slot 显式失败不静默 fallback。
  4. 内容引用:>64 KiB 外存,ExternalizedField 描述符(JSON pointer + content ref + 字节数 + SHA-256),host/content/read 256 KiB 分块,单对象 8 MiB 上限。
  5. 17 个冻结钩子点:extension/intercept(阻塞)vs extension/event(fire-and-forget,队列饱和丢弃);串行确定性顺序(priority -1000..1000 → plugin id → 注册顺序)。
  6. 五种决策:continue/block/replace(所有点);allow/deny(仅 permission.decision,full-trust allow 盖过 host deny 并审计)。
  7. 十个 replace slot:system_prompt/context/provider_request/provider_response/compaction/session_policy/permission/frontend_events/tool:<name>/provider:<ref>;每 slot 全局唯一 owner,冲突 build 失败;owner 超时/出错操作必败。
  8. 流式 Provider:plugin/<plugin>/<provider>/<model>,stream/open → chunk(1-based 连续序列号)→ end(lastSeq);chunk 类型 text/reasoning/tool_call_*/usage/done/error;绝不传别的 provider 凭据;崩溃不 fallback。
  9. 结构化 UI:status/card/form/notification + request(confirm/input/select/multiselect);structured only(无 HTML/JS/远程脚本),stale-generation 更新丢弃。
  10. Manifest v1:apiVersion: reasonix.io/plugin/v1,顶层 + contributes(静态资源)+ runtime(exec form command/intercepts/replaces/capabilities);严格解析(unknown field 错误);relpath 必须留在插件根。
  11. Full trust 安全模型:沙箱外、未过滤环境、能读全 session、绕权限、操作机器;安装即授权无二次确认;只有 plugin flow 能启 runtime(项目配置不能);凭据脱敏;FULL TRUST block 显式提示;--link 持续信任。

下一节,我们看 sdk/go——它让第三方用 Go 写 Extension sidecar 变得简单。我们会读 examples/starterextension(最小 sidecar,验证 manifest → sidecar → intercept 全链路)和 examples/fullsidecar(完整参考,演示每种贡献类型),讲 Plugin Manifest v1 的实战和插件分发安装。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U