第十章讨论了 agent 如何跨进程、跨版本找回过去。恢复要求边界稳定,而扩展恰恰会不断引入新的指令、工具、流程和状态。 这形成了一组天然的张力:一个 harness 如果完全封闭,很快会跟不上真实业务;如果允许扩展随意进入 LOOP,又会失去安全、确定性和可恢复性。 Codex 的答案不是设计一个无所不能的“插件接口”,而是开放一组深浅不同、权力不同的扩展点:skills 教模型怎样做事,MCP 把外部服务接成工具,hooks 在生命周期边界介入,dynamic tools 把前端能力交给模型,plugin 负责打包和分发,Extension API 则让受信任的第一方组件参与更深的运行时生命周期。 本章不只回答“有哪些扩展方式”,更要回答四个问题:扩展改变什么、代码在哪里执行、何时对 LOOP 生效、谁有权限制它。
11.1 可扩展性不是“什么都能插”
很多系统把“可扩展”简单理解成:提供一个 plugin 目录,让第三方代码加载进主进程。这个方案容易实现,也最容易制造长期问题:
- plugin 可以改共享内存,任何 bug 都可能拖垮整个 Session;
- plugin 可以在采样中途修改工具清单,模型看到的 spec 与真正执行的 runtime 不再一致;
- plugin 状态随意进入上下文,token 成本和提示注入风险失去边界;
- plugin 更新以后,旧 rollout 不知道该按哪个版本恢复;
- plugin 代码和 harness 共享全部权限,沙箱与审批可能被绕到背后。
Codex 采用的是相反的思路:
不允许扩展任意介入 LOOP,而只开放一组输入、输出、触发时机和权限边界都明确的扩展点。
第五章说过,LOOP 本身应该尽量“笨”:读取历史、调用模型、执行工具、回灌结果、判断是否继续。复杂能力都挂在边界上。可扩展性就是这条原则的工程化:扩展不能在循环里随便插一段代码,只能通过既定契约贡献某种东西。
常见贡献大致分为六类:
| 扩展想改变什么 | 对应机制 | 典型例子 |
|---|---|---|
| agent 的工作方法 | skills | 教模型按团队规范排查线上故障 |
| agent 能调用的外部能力 | MCP | 接入日历、数据库、代码平台 |
| 生命周期中的检查与自动化 | hooks | 工具执行前审查,停止前检查测试 |
| 前端程序独有的能力 | dynamic tools | 读取 IDE 当前选区、操作编辑器标签页 |
| 一组能力的安装与分发 | plugin | 把 skill、MCP、hooks 和界面信息打成一个包 |
| harness 内部的深层行为 | Extension API | 贡献上下文、工具、生命周期处理器和 scoped state |
这里已经能看到一个重要区别:plugin 不是最深的扩展层,甚至不是一种新的执行机制。 它主要是一个打包和分发单元;真正干活的仍然是 skill、MCP、hook 等已有机制。能深入参与 thread、turn、step 生命周期的,是由前端在构建运行时时注册的 Extension API。
把所有东西都叫“插件”,会掩盖三个决定安全边界的事实:
- 有的扩展只是文本,有的会执行代码;
- 有的代码运行在 harness 进程里,有的运行在外部服务或前端里;
- 有的只给模型建议,有的可以阻止工具或否决停止。
所以讨论可扩展性,第一步不是问“支不支持 plugin”,而是问:这个扩展拿到了哪一种权力?
11.2 一张扩展地图:深度、执行位置与控制权
可以把 Codex 的扩展机制画成从外到内的同心层。越靠内,越接近 LOOP,能力越强,信任要求也越高。
flowchart TB
subgraph DIST["分发与配置层"]
PL["plugin / marketplace<br/>打包、安装、版本与来源"]
CFG["配置分层 / requirements<br/>选择、覆盖与硬约束"]
end
subgraph DECL["声明式扩展层"]
SK["skills<br/>贡献 instruction 与资源"]
HK["hooks<br/>在生命周期边界介入"]
MCP["MCP<br/>贡献外部 tools / resources"]
DT["dynamic tools<br/>贡献前端程序能力"]
end
subgraph CORE["运行时装配层"]
EXT["Extension API<br/>贡献 context / tool / lifecycle / state"]
ASM["统一装配与生命周期扩展点"]
end
subgraph LOOP["稳定内核"]
L["LOOP<br/>历史 → 采样 → 行动 → 回灌"]
end
PL --> SK
PL --> HK
PL --> MCP
CFG --> SK
CFG --> HK
CFG --> MCP
CFG --> DT
SK --> ASM
HK --> ASM
MCP --> ASM
DT --> ASM
EXT --> ASM
ASM --> L
这张图表达的是集成深度,不是调用顺序。每一层解决的问题不同:
- 分发与配置层回答“装什么、从哪里来、是否启用、上限是什么”;
- 声明式扩展层回答“向系统贡献哪类能力”;
- Extension API让受信任组件直接贡献强类型的生命周期行为;
- 两类能力最终汇入同一套运行时装配、工具和上下文边界;
- LOOP只消费最终结果,不需要知道能力来自内置实现、plugin 还是前端。
再换一个角度,按代码真正运行的位置看:
| 机制 | 主要内容 | 执行位置 | 能否直接改变外部世界 |
|---|---|---|---|
| skill | Markdown instruction、脚本与参考资料 | instruction 由模型读取;脚本经工具系统执行 | instruction 不能,脚本可以 |
| MCP | 远端或本地 server 提供的工具和资源 | MCP server 进程或网络服务 | 可以 |
| command hook | 一条受配置约束的命令 | harness 拉起的子进程 | 可以 |
| MCP hook | 对某个 MCP tool 的调用 | MCP server | 可以 |
| dynamic tool | spec + 前端实现 | 前端程序 | 可以 |
| plugin | manifest 与资源集合 | 自身通常不执行,所含能力各自执行 | 取决于所含能力 |
| Extension API | 强类型 contributor | 与 harness 一起构建的受信任运行时 | 可以深度参与内核 |
这张表提醒我们:扩展的权限边界必须跟着执行位置走。 harness 的文件沙箱能约束自己启动的命令,却不能自动约束一个远端 MCP server,也不能约束 IDE 前端收到 dynamic tool 请求后做了什么。第八章的安全策略不是一张覆盖全世界的网;每个执行域都要有自己的鉴权、审批与审计。
11.3 三个阶段:发现、激活、冻结
可扩展系统最容易出现的一类 bug,是把“系统知道某项能力存在”“这次会话允许使用它”“模型这一轮可以调用它”混成同一件事。
Codex 把它们分成三个阶段:
11.3.1 Discovery:系统知道“可能有什么”
discovery 负责扫描和读取候选能力:
- 在多个目录中发现 skills;
- 从 marketplace 或本地目录发现 plugins;
- 从配置中发现 MCP servers 和 hooks;
- 从前端的
thread/start请求中接收 dynamic tools; - 从运行时构建结果中拿到 Extension contributors。
此时只是建立 catalog。一个 skill 被扫描到,不代表已进入模型上下文;一个 plugin 出现在 marketplace,不代表已安装;一个 MCP server 有配置,不代表连接已建立。
discovery 必须有硬边界。目录递归深度、扫描目录数、条目数、单个资源大小、描述长度都要封顶。否则“扫描扩展”本身就可能变成无界 I/O,或让几千个 skill 的描述挤满上下文。
11.3.2 Activation:这次运行“准备用什么”
activation 把 catalog 与当前环境结合:
- plugin 是否已安装、是否启用;
- skill 是否被 policy 禁用;
- project 配置是否因目录不可信而被忽略;
- MCP server 是否允许启动、认证是否完成;
- hook 是否已被用户信任;
- 当前模型是否支持所需的工具形态;
- 当前 thread、agent 身份和权限是否允许这项能力。
activation 是配置、信任和能力协商的交汇点。它回答的不是“存在吗”,而是“在这里可以用吗”。
11.3.3 Snapshot:这一轮模型“实际看到了什么”
activation 仍然不是最终执行边界。真正进入一次采样之前,harness 会在 step 边界冻结:
- 本轮模型和推理参数;
- 本轮可见工具 spec;
- 工具名到 runtime 的路由;
- MCP client、tool metadata、timeout 与 catalog revision;
- 世界状态和扩展贡献的上下文。
flowchart LR
D["Discovery<br/>候选 catalog"] --> A["Activation<br/>配置 + 信任 + 能力"]
A --> S["Step Snapshot<br/>本轮冻结视图"]
S --> M["模型采样"]
M --> C["工具调用"]
C -->|"只按冻结绑定执行"| R["结果回灌"]
R --> N["下一个 step<br/>重新观察变化"]
这个三段式解决了一个关键一致性问题:假设模型刚看到 calendar.create_event,MCP server 随即刷新目录并把它换成了另一个版本。调用时如果去查“最新目录”,模型依据旧 spec 生成的参数可能被新 runtime 接收,后果不可预测。
Codex 的做法是把一次调用绑定到模型当时看到的 client、metadata、timeout 和 catalog revision。目录若在调用准备后发生变化,旧调用会被明确拒绝,而不是悄悄交给新版本执行。变化等到下一个 step 再进入快照。
这和第五章“变化只发生在边界”、第六章“spec 与 runtime 必须一致”是同一条原则:
支持动态刷新,不等于允许采样中的世界动态突变。
11.4 Skills:扩展的不是手,而是做事方法
skill 最容易被误解成“一个装着脚本的 plugin”。实际上,它首先是一份写给 agent 的方法说明:什么情况下使用、应该按什么步骤做、哪些资料按需读取、哪些脚本可以辅助执行。
一个典型 skill 可以包含:
SKILL.md:名称、描述、触发条件和完整工作流;references/:规范、API 说明、领域知识;scripts/:可复用的确定性操作;assets/:模板和产物素材。
它扩展的核心是模型的 procedural knowledge(程序性知识):不是让模型多一只手,而是教它现有的手该按什么顺序使用。
11.4.1 Progressive disclosure:先给目录,再给正文
如果系统有一百个 skills,把一百份完整 SKILL.md 都塞进每次请求,第四章的上下文窗口会立刻被吃掉。Codex 使用 progressive disclosure:
- 初始只注入 skill 的名称、简短描述和 locator;
- 模型或 harness 判断某个 skill 相关时,再读取完整
SKILL.md; - skill 引用的 reference、script、asset 继续按需读取。
flowchart LR
C["Skill catalog<br/>名称 + 描述 + locator"] -->|"判断相关"| M["读取 SKILL.md"]
M -->|"工作流需要"| R["读取 reference"]
M -->|"需要确定性操作"| S["运行 script"]
M -->|"生成产物"| A["使用 asset"]
这和第六章 deferred tools 是同一种上下文经济学:
- 先用一小段“广告”告诉模型能力存在;
- 只有真正需要时才支付完整 token 成本;
- 对目录、描述和单个资源设置硬上限。
区别在于,deferred tool 最终给模型一份可调用 spec;skill 最终给模型一份工作方法。一个解决“能做什么”,一个解决“应该怎么做”。
11.4.2 Skill 来源不是简单覆盖关系
skills 可以来自 repo、user、system、admin、plugin、executor、orchestrator 或运行时附加目录。来源越多,同名冲突就越常见。
一种危险做法是“高优先级目录静默覆盖低优先级目录”。这会让 $deploy 在不同机器上指向不同内容,用户却看不出差异。Codex 更强调来源和 locator:同名 skill 只有在能唯一确定时才适合按名字调用;有歧义时,应保留来源信息并要求明确选择。
例如,用户目录里有一个名为 deploy 的通用 skill,内容是“构建镜像并部署到 Kubernetes”;后来安装的公司发布 plugin 也带了一个 deploy skill,要求“创建发布单、等待审批,再通过内部平台上线”。两者名字相同,但流程、权限和副作用完全不同。
如果采用静默覆盖,用户输入 $deploy 后,实际执行哪套流程将取决于目录优先级:换台机器、进入另一个项目或调整 plugin 顺序,都可能让行为悄悄改变。保留来源和 locator 后,catalog 可以把它们表示为两个不同候选项:
deploy(来源:用户目录)
deploy(来源:company-release plugin)
此时 $deploy 不能被自动解释成其中任意一个。系统不应猜测,而应保留两个候选项,由调用方通过具体 path/locator 选定目标;选定后再读取对应的 SKILL.md。
以本地 skill 为例,前端通常先通过 skills/list 取得每个候选项的真实路径,再在 turn/start 中同时发送用户可见文本和结构化 skill item:
{
"method": "turn/start",
"id": 33,
"params": {
"threadId": "thr_123",
"input": [
{
"type": "text",
"text": "$deploy 发布当前版本"
},
{
"type": "skill",
"name": "deploy",
"path": "/resolved/company-release/skills/deploy/SKILL.md"
}
]
}
}
这里的 text 表达用户意图,skill item 则给 harness 一个不可歧义的选择。当前 app-server 协议中的字段名是 path;locator 是 catalog 中更宽泛的概念,还可以表示由 executor 或 orchestrator 管理的 package。harness 会优先按结构化 item 的 path 匹配已加载且启用的 skill,而不是仅凭 name 猜测。若 path 无效或对应 skill 已禁用,本次结构化选择会失效,也不会再偷偷退回同名 $deploy。
名称负责让人记住能力,path/locator 才负责精确定位能力。
这里的原则与工具命名空间一致:扩展名称不只是显示文本,也是路由地址。地址有歧义,就不能假装它唯一。
11.4.3 Instruction 仍然是不可信输入
skill 是 instruction,不是系统权限。它可以建议模型运行某条命令,却不能绕过工具系统的路由、hook、审批和沙箱;skill 里的脚本也必须通过正常执行入口运行。
这点非常重要。若“安装 skill”就等于“允许其中脚本在主进程任意执行”,skill 会从知识包变成远程代码注入。Codex 把“读懂一个方法”和“执行一个动作”分开:前者进入上下文,后者仍要走第六章的六道关卡。
skills 因此是一种低耦合、高影响的扩展:它几乎不碰内核,却能显著改变 agent 行为。代价是效果依赖模型理解,不能像强类型程序一样保证每一步都执行。需要确定性约束时,应把规则放进 hook、policy 或工具 runtime,而不是只在 skill 里写一句“必须”。
11.5 MCP:把外部系统接到统一工具面
如果 skill 教 agent“怎样办理请假”,MCP 则真正提供“查询余额、创建审批、读取状态”的外部能力。
MCP 的价值不只是统一了工具调用格式,还统一了外部服务接入时的一组工程问题:
- transport:本地 stdio 或远端 streamable HTTP;
- authentication:OAuth、bearer token 或产品身份;
- lifecycle:启动、连接复用、断线与重连;
- discovery:tools、resources 与 templates;
- control:启用/禁用、超时、tool allowlist/denylist;
- interaction:elicitation,即外部服务反过来向用户索取信息;
- provenance:能力来自哪个 server、哪个 plugin、哪个执行环境。
11.5.1 MCP server 不是“一个大工具”
一个 MCP server 更像一个能力域。它可以提供几十个 tools,也可以暴露 resources。harness 需要在几个粒度上分别做控制:
| 粒度 | 可以控制什么 |
|---|---|
| server | 是否启用、是否 required、如何启动、如何认证 |
| catalog | 哪些 tools 对当前环境可见 |
| tool | timeout、审批策略、是否允许并发 |
| model exposure | direct、deferred、namespace 或隐藏 |
| call binding | 本次调用绑定的 client 与 catalog revision |
这解释了为什么“已连接 MCP”不等于“把全部工具塞给模型”。server 可能已启动,runtime 也已注册,但低频工具仍然 deferred,只有 tool_search 命中后才展开完整 spec。
11.5.2 Required 的含义是失败要响亮
有些 MCP 只是锦上添花:连接失败时少几个工具,任务仍可继续。有些则是业务前提,例如企业工单系统,没有它就不应假装能完成任务。
required 的价值不在于多重试几次,而在于改变失败语义:
- optional server 启动失败,可以降级并告知模型能力不可用;
- required server 启动失败,应阻止相关 Session 正常开始。
这是第一章“失败响亮”的扩展版。系统必须区分“能力暂时少了一项”和“运行前提根本不成立”,否则 agent 会在缺少关键能力时用猜测填空。
11.5.3 外部能力需要独立信任边界
MCP tool 的副作用发生在 server 一侧。即使 harness 自己处于只读沙箱,一个远端数据库工具仍可能执行写操作。因此 MCP 的安全不能只依赖本地沙箱,至少需要:
- server 身份与 URL/command 约束;
- tool allowlist/denylist;
- per-tool approval;
- 凭证最小权限;
- 返回内容按外部不可信上下文处理;
- 调用和结果的独立审计。
第八章说过,安全的两条轴是“能不能做”和“要不要问”。MCP 把这两条轴延伸到了进程之外:本地 harness 负责是否把请求发出去,远端服务仍要负责收到请求后允许做到什么。
11.6 Hooks:在生命周期边界上加入规则
skill 给模型建议,hook 则在确定的生命周期点运行。它适合处理那些不能只靠模型“记得做”的事情:
- 每次命令执行前做合规检查;
- 工具结束后记录审计信息;
- 压缩前保存额外状态;
- Session 开始时注入环境说明;
- 模型准备停止时检查测试是否完成;
- 子 agent 启动或结束时更新外部任务状态。
Codex 提供的 hook 事件覆盖了主要生命周期边界:
| 生命周期点 | hook | 典型用途 |
|---|---|---|
| Session 开始/结束 | SessionStart / SessionEnd |
初始化、清理、记录 |
| 用户提交输入 | UserPromptSubmit |
补充上下文、输入检查 |
| 工具执行前后 | PreToolUse / PostToolUse |
拦截、改写、审计 |
| 请求权限 | PermissionRequest |
自动化审批或附加策略 |
| 压缩前后 | PreCompact / PostCompact |
保存或恢复扩展状态 |
| 子 agent 开始/停止 | SubagentStart / SubagentStop |
编排与约束 |
| 根 agent 准备停止 | Stop |
完成条件检查 |
11.6.1 Hook 是流程控制,不是任意插桩
每个 hook 都有固定输入和固定输出。以 PreToolUse 为例,它可以放行、拦截或按契约修改参数;Stop 可以放行、否决并提供续跑指令,或者要求收尾。它不能取得整个 Session 的可变引用,然后随意改历史。
这就是受控扩展点的价值:能力虽然有限,但影响范围可以推理、可以测试、可以持久化。
同一个事件的多个同步 handlers 可以并发运行,最后统一归并结果。这样独立的审计、策略和补充上下文不必串行等待。但归并必须有明确规则:冲突时谁优先、多个否决如何合并、多个参数修改能否共存,不能依赖异步完成顺序。
11.6.2 Sync 与 async 的权力不同
hook 可以同步等待,也可以作为后台任务运行:
- sync hook 位于控制路径上,调用方会等它完成,因此可以阻止、修改或影响本次流程;
- async hook 只适合通知、上报和低耦合自动化,不能在后台任务结束几秒后再“撤销”已经发生的工具调用。
这条限制看似保守,实际上是在保护因果关系。一个 hook 若要影响决策,就必须在决策发生前给出结果;错过边界后,它只能记录事实,不能改写过去。
11.6.3 信任必须由更高权限的层授予
command hook 会执行代码,MCP hook 会调用外部服务,都比纯 instruction 风险更高。因此 hook 不应“随配置出现就自动获得信任”。
Codex 会区分:
- hook 的声明来自哪里;
- 用户是否启用;
- command 内容是否与受信任 hash 一致;
- policy 是否只允许 managed hooks;
- required managed hook 是否成功加载。
尤其重要的是:project 或 plugin 可以声明自己需要哪些 hooks,却不应自己修改“用户已经信任它”的状态。否则一个刚下载的仓库只要附带配置,就能同时提出命令并批准自己执行。
这与浏览器扩展安装时展示权限清单是同一个道理:能力声明和授权决定必须分属不同主体。
11.7 Dynamic tools:让前端程序成为执行者
有些能力只存在于前端:
- IDE 当前选中了哪段代码;
- 用户正在看的 diff 是哪一个;
- 哪个编辑器 tab 处于激活状态;
- 前端保存的本地草稿或设计选项;
- 某个桌面应用提供的专属交互。
把这些能力复制进 harness 不现实,也会让内核依赖具体 UI。dynamic tools 提供了另一种结构:
- 前端在启动 thread 时声明 tool spec;
- harness 把 spec 注册进该 thread 的工具箱;
- 模型发起调用;
- harness 通过 app-server 向前端发送反向请求;
- 前端执行并返回 text、image 或 audio;
- harness 把结果归一化成工具结果 item,重新提交给 LOOP。
sequenceDiagram
participant F as 前端程序
participant A as app-server
participant H as harness
participant M as 模型
F->>A: thread/start(dynamicTools)
A->>H: 创建 thread,注册 spec
H->>M: 本 step 工具清单
M-->>H: 调用 editor.readSelection
H->>A: item/tool/call(反向请求)
A->>F: 请求执行
F-->>A: text / image / audio
A-->>H: dynamic tool response
H->>M: 工具结果回灌
这是一种能力归前端所有、LOOP 仍由 harness 驱动的反向 RPC。前端不用实现 agent 循环,harness 也不用理解编辑器内部对象;双方只在 tool spec 与结果内容上达成契约。
11.7.1 一个实际例子:让 agent 查询前端已登录的工单系统
假设 IDE 已经登录公司工单系统,登录凭证只保存在 IDE 中。我们希望 agent 能查询工单,但不希望把 IDE 的认证状态和业务 SDK 搬进 harness。
前端先开启 experimental API capability,然后在创建 thread 时声明一个 tickets.lookup_ticket 工具。下面省略了与例子无关的 thread/start 字段:
{
"method": "thread/start",
"id": 10,
"params": {
"dynamicTools": [
{
"type": "namespace",
"name": "tickets",
"description": "查询当前用户有权限查看的工单",
"tools": [
{
"type": "function",
"name": "lookup_ticket",
"description": "根据工单编号查询标题、状态和负责人",
"deferLoading": false,
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "工单编号,例如 ABC-123"
}
},
"required": ["id"],
"additionalProperties": false
}
}
]
}
]
}
}
harness 只保存并注册这份 spec,不知道工单 API 地址、认证方式或查询代码。用户随后说“查看 ABC-123 现在由谁处理”,模型根据 spec 生成对 tickets.lookup_ticket 的调用。app-server 不会自己访问工单系统,而是向声明该能力的前端发送反向 JSON-RPC 请求:
{
"method": "item/tool/call",
"id": 60,
"params": {
"threadId": "thr_123",
"turnId": "turn_123",
"callId": "call_123",
"namespace": "tickets",
"tool": "lookup_ticket",
"arguments": {
"id": "ABC-123"
}
}
}
前端收到请求后,用自己的 SDK 和登录态查询工单系统,再用同一个 JSON-RPC id 返回结果:
{
"id": 60,
"result": {
"contentItems": [
{
"type": "inputText",
"text": "ABC-123:支付回调超时;状态为处理中;负责人是李明。"
}
],
"success": true
}
}
harness 将这段内容转换成标准工具结果 item,写入历史并交给下一轮 LOOP。模型随后可以回答:“ABC-123 正在处理中,负责人是李明。”
整个过程中三方职责是清楚的:
| 参与方 | 负责什么 | 不需要知道什么 |
|---|---|---|
| 模型 | 根据 spec 决定是否调用,并生成参数 | 工单 SDK、认证 token、前端实现 |
| harness | 冻结 spec、路由请求、关联 callId、回灌和持久化结果 |
工单系统的内部协议 |
| 前端 | 校验参数、使用当前登录态查询、返回结果 | LOOP、上下文组装和模型流 |
app-server 还会围绕这次反向请求发出 item/started 和 item/completed,因此 UI 能显示“正在查询工单”以及最终成功或失败状态。若前端查询失败,它仍应返回 success: false 和可解释的文本;这会作为一次失败观察回灌给模型,而不是让整个 thread 崩溃。
这个例子说明 dynamic tool 的关键不是“动态生成了一段代码”,而是:前端在运行时声明一份工具契约,并成为该工具的执行者;harness 继续负责 LOOP 和结果协议。
11.7.2 为什么 dynamic tools 属于 thread
dynamic tool 在 thread/start 时声明,而不是每次 turn 临时附带。原因有三点:
- 工具身份在整个对话中保持稳定,模型不会这一轮看到、下一轮无故消失;
- 定义可以写入 Session 元数据,resume 时知道旧 thread 依赖过哪些前端能力;
- thread 是前端订阅、反向请求和历史恢复的共同坐标。
但“定义被持久化”不代表“执行者也被恢复”。进程重启后,旧前端连接已经不存在。resume 只能重建 spec 和依赖事实,真正调用前仍要确认当前连接具备对应能力。
这正是第十章的边界:恢复契约,不复活连接。
11.7.3 前端是独立执行域
dynamic tool 不在 harness 内执行,所以本地沙箱无法观察它的副作用。安全责任要拆开:
- harness 控制哪些 dynamic tools 对模型可见;若能力有高风险副作用,产品层还要显式增加 hook、policy 或审批;
- app-server 负责反向请求的身份、关联 ID 和取消语义,调用层还要设置合理的 timeout;
- 前端验证参数并限制实际操作;
- 用户信任的是“这个前端提供这项能力”,不是只信任一段 tool 描述。
如果前端不响应,又没有 timeout 或断线处理,工具 future 就可能一直等待。因此反向请求不能静默丢失:传输层要有有界队列、过载错误、断线处理和轮次结束时的明确取消。第二章的双向协议在这里不再只是 UI 通信,而是扩展 runtime 的一部分。
11.7.4 同一套扩展语义,三种前端形态
dynamic tools 依赖 app-server 的双向协议,但 app-server 并不绑定某一种 UI 或传输。它可以服务三类前端:
| 前端形态 | 传输 | 适合场景 | 额外边界 |
|---|---|---|---|
| 进程内前端 | bounded memory channel | TUI、与 Codex 一同交付的桌面程序 | 无网络开销,但仍保留 typed request 和 JSON-RPC response envelope |
| 本地进程前端 | stdio JSONL | IDE 插件、本地自动化 | 生命周期随子进程,天然单机 |
| 网络前端 | WebSocket / local socket | 远程 UI、多客户端控制面 | 握手、订阅、鉴权、背压和断线恢复 |
三种形态共享 thread、turn、item、反向请求和 dynamic tool 的语义。进程内模式也不会因为“大家都在同一个进程”就绕开协议直接改 Session;它只是把 socket 换成内存通道。这样 TUI、IDE 和远程前端对生命周期的理解不会分叉。
连接建立时,前端先通过 initialize handshake 声明身份与 capability。实验性 API 按连接 gating:一个前端明确表示能够理解某项实验字段后,服务端才向它开放。可扩展性因此不只是服务端“多提供一个方法”,还包括前端是否有能力正确处理它。
这里也能准确区分两种常被混淆的 API:
- Extension API在受信任运行时内部注册 contributor,参与 context、tool 和 lifecycle;
- app-server API让进程内或进程外前端通过稳定协议控制 thread,并用 dynamic tools 反向提供能力。
前者扩展“harness 内部如何工作”,后者扩展“哪些前端可以驱动 harness、前端能向模型提供什么”。dynamic tools 正是两者之间的桥:它从 app-server 协议进入,最终落到统一工具系统中执行和回灌。
11.8 Plugin:能力包,而不是内核代码注入
现在可以准确解释 plugin 了。一个 Codex plugin 主要包含:
- skills;
- MCP server 声明;
- apps 或连接器信息;
- hooks;
- 面向 UI 的名称、描述和元数据。
它的 manifest 负责说明“这个包里有哪些能力、资源在哪里”,marketplace 负责发现、安装和更新,配置负责启用与裁剪。plugin 自身通常不获得一个“在主进程任意执行代码”的入口。
flowchart LR
MP["Marketplace<br/>发现与版本"] --> I["安装 plugin"]
I --> MF["读取 manifest"]
MF --> SK["Skill roots"]
MF --> MC["MCP servers"]
MF --> HK["Hooks"]
MF --> AP["Apps / UI metadata"]
SK --> ACT["按配置与信任激活"]
MC --> ACT
HK --> ACT
AP --> ACT
ACT --> NT["通常从新 thread 生效"]
这样的设计牺牲了一部分“想怎么改就怎么改”的自由,换来四个重要收益。
11.8.1 能力可枚举
安装前可以回答:这个 plugin 带来哪些 skills、要连接哪些 MCP servers、会注册哪些 hooks。安全审查面对的是结构化清单,不是一段可能在加载时做任何事情的初始化代码。
11.8.2 权限可分开授予
用户可以启用 plugin,但禁用某个 MCP server;可以阅读其中的 skill,却不信任 command hook;组织 policy 还可以要求某些 managed hooks 必须存在。安装不再等于全权授权。
11.8.3 能力走统一管线
plugin 带来的 MCP tool 仍走工具注册、审批、超时和结果回灌;skill 仍受上下文预算约束;hook 仍受 lifecycle 契约与信任策略约束。plugin 不会因为“来自一个包”就获得旁路。
11.8.4 来源可以追踪
同一个 MCP server 或 skill 可能来自用户目录、project 或 plugin。保留 provenance 后,冲突提示、UI 展示、审计和卸载都能回答“这项能力从哪里来”。卸载 plugin 时也可以只移除它贡献的部分,而不误伤同名的用户能力。
plugin 因此更像一个声明式依赖包:它组合能力,却不重新定义能力的执行规则。这也是 plugin 与 Extension API 最重要的区别。
11.9 Extension API:真正深入生命周期的受信任接口
声明式机制覆盖了大多数生态扩展,但 harness 自身和第一方前端仍需要更深的组合能力,例如:
- 在每个 step 注入结构化世界状态;
- 为 thread 或 turn 创建私有状态;
- 贡献内置级工具;
- 观察 tool lifecycle;
- 在 turn 开始、输入进入、item 完成时参与处理;
- 贡献 MCP server 或 token usage;
- 识别 skill invocation;
- 在审批前做自动 review。
这些能力由 Extension API 提供。它不是运行时下载一段未知代码,而是在构建运行时时注册一组强类型 contributors。Registry 建好后保持不可变,后续 Session 只使用这份固定能力集合。
11.9.1 Contributor:一次只贡献一种职责
Extension API 没有一个万能的 on_everything(session)。不同职责由不同 contributor 表达:
| Contributor | 参与的职责 |
|---|---|
ContextContributor |
向模型上下文贡献受预算约束的片段 |
ToolContributor |
提供 session/thread/step 范围的工具 |
ToolLifecycleContributor |
观察或介入工具生命周期 |
ThreadLifecycleContributor |
处理 thread 创建、恢复与结束 |
TurnLifecycleContributor |
处理 turn 开始、完成与中断 |
TurnInputContributor |
处理进入 turn 的输入 |
TurnItemContributor |
观察形成的 item |
McpServerContributor |
动态贡献 MCP server 配置 |
ConfigContributor |
参与运行时配置构造 |
TokenUsageContributor |
观察和聚合 token 用量 |
SkillInvocationContributor |
识别和记录 skill 调用 |
ApprovalReviewContributor |
对需要审批的动作做 review |
职责拆开有两个好处:
- 扩展只拿到完成该职责所需的最小上下文;
- 执行顺序和归并语义可以针对每类贡献点单独定义。
有的 contributor 是按注册顺序累积,有的是 first-claim(第一个明确处理者接管),有的是 later-wins(后来的配置覆盖前者)。顺序本身是 API 契约,不能依赖 HashMap 遍历或异步完成先后。
11.9.2 Scoped state:状态属于生命周期,不属于全局单例
扩展经常需要状态。例如 skills 扩展要记住上一步模型看过哪个 catalog revision,工具扩展可能要记录 thread 级缓存,turn hook 需要保存当前轮次的临时判断。
把这些都放进全局单例会制造串话、泄漏和恢复困难。Extension API 按生命周期提供 scoped state:
- session scope;
- thread scope;
- turn scope;
- step scope。
每个扩展以自己的类型作为 key,读写自己的 typed data;harness 拥有 scope 的创建和销毁时机。扩展不需要把私有字段塞进核心 Session 类型,核心也不需要理解每个扩展的内部结构。
flowchart TD
S["Session scope<br/>跨 thread 的共享状态"] --> T["Thread scope<br/>一段对话的状态"]
T --> U["Turn scope<br/>一次用户任务"]
U --> P["Step scope<br/>一次采样 + 行动"]
P --> X["边界结束后销毁"]
这种状态模型的关键不是“方便存数据”,而是明确寿命:
- step 缓存不能误活到下一个 step;
- turn 判断不会污染下一次用户任务;
- thread 状态不能假装旧内存还在,resume 时要按扩展契约重建;
- 扩展之间通过类型隔离,避免字段名冲突。
11.9.3 为什么 Extension API 不等于 plugin API
Extension API 与 harness 同进程、同信任域,能够直接影响上下文、工具与生命周期。一处 panic、死锁或无界输出都可能伤及内核,因此它适合:
- Codex 自带的第一方模块;
- 与前端一同交付、经过编译和测试的受信任扩展;
- 需要强类型、低延迟和深层生命周期参与的能力。
它不适合直接作为互联网 marketplace 的任意代码加载接口。公开生态优先使用 plugin 的声明式组合;只有当现有扩展机制确实表达不了需求,才应该增加新的强类型 contributor。
这是一条很重要的 API 演进原则:
先扩展能力模型,再扩展任意代码权限。
11.10 配置分层:谁覆盖谁,谁又不能被覆盖
扩展越多,配置来源越多:
- 产品随包默认值;
- 系统级配置;
- 企业托管配置;
- 用户配置;
- profile;
- project 配置;
- 本次 Session 的 flags;
- 兼容旧系统的 managed overrides。
Codex 会按层合并配置,高优先级覆盖低优先级,同时保留每个字段的来源和每层 fingerprint。概念上的顺序可以画成:
flowchart BT
D["Packaged defaults"] --> S["System"]
S --> E["Enterprise managed"]
E --> U["User"]
U --> P["Selected profile"]
P --> R["Project"]
R --> F["Session flags"]
F --> L["Legacy managed overrides"]
箭头越往上,普通覆盖优先级越高。但这张图只描述“最后值从哪里来”,还没有描述“哪些值根本不允许”。
11.10.1 Overlay 解决偏好,requirements 解决边界
配置与 requirements 是两套正交机制:
- 配置表达“我想怎么运行”;
- requirements表达“最多允许怎么运行”。
例如用户配置想启用一个 HTTP MCP server,普通 overlay 可以决定 URL、timeout 和 enabled;企业 requirements 可以进一步规定:
- 只允许连接指定域名;
- 本地 server 只能执行特定 command;
- 必须使用某种身份;
- 某些 hooks 必须启用;
- 某类能力完全禁止。
最终有效值不是“最高配置层获胜”,而是:
有效能力 = 合并后的配置 ∩ requirements 允许的范围
requirements 通常只能收紧,不能被 project、profile 或 Session flag 放宽。否则所谓企业策略只是一份更低优先级的建议。
11.10.2 Project 配置先过信任门
project 配置来自当前仓库,而仓库内容可能刚从互联网下载。若进入目录就自动执行其中声明的 hook、MCP command 或脚本,打开项目本身就变成了代码执行。
因此 project 配置只有在目录被信任后才生效;即使可信,某些高风险字段仍不应由 project 改写,例如模型服务地址、认证目标和本地通知命令。
这里再次出现“声明与授权分离”:
- project 可以说“这个项目建议使用某个 skill”;
- 用户决定是否信任 project 配置;
- 更高层 requirements 决定即使用户信任,也有哪些事情绝对不能做。
11.10.3 来源信息是一等数据
只给 UI 一个最终布尔值 enabled=true 不够。用户需要知道:
- 是哪个配置层启用了它;
- 哪个 requirements 又把它禁用了;
- plugin、project 还是用户目录提供了这项能力;
- 当前值为什么无法修改。
因此配置系统要保留 origin、disabled reason 和 layer fingerprint。可解释性不是附加功能,而是多层控制系统能被人正确使用的前提。第九章说人的注意力是一种预算;最浪费注意力的 UI,就是让用户在十层配置里猜一个开关为什么不生效。
11.11 同一项能力,必须穿过四道门并留下回执
把前面的机制合起来,一项扩展能力要真正产生副作用,至少要过四道门;执行之后,还必须沿统一路径留下结果:
flowchart LR
P["Provenance<br/>它从哪里来?"] --> T["Trust<br/>是否信任并启用?"]
T --> V["Visibility<br/>本 step 模型看得到吗?"]
V --> E["Execution<br/>调用是否获批并受约束?"]
E --> O["Observation<br/>结果如何回灌与审计?"]
11.11.1 Provenance:来源边界
系统必须知道能力来自内置模块、用户目录、project、plugin、MCP server 还是前端程序。没有来源,就无法处理重名、卸载、审计和信任。
11.11.2 Trust:激活边界
存在不等于启用。project 是否可信、hook 是否授权、MCP 身份是否满足 requirements,都在这一层决定。
11.11.3 Visibility:模型边界
启用不等于每一步都展示。工具可能 deferred,skill 只展示 metadata,某些能力只对根 agent 或特定模型可见。这里控制 token 成本、选择复杂度和最小权限。
11.11.4 Execution:副作用边界
模型看见并调用后,仍要经过 hook、policy、approval、sandbox 或外部服务鉴权。dynamic tool 还要经过前端自己的验证。
最后还有 Observation:执行结果必须归一化成 item、进入历史、持久化并留下 telemetry。一个扩展若只会“做事”却不提供稳定结果和生命周期信号,就无法被 LOOP 正确回灌,也无法在第十章的 replay 中解释。
这四道边界构成了一条“最小权力链”。每一层都只回答一个问题,任何一层都不能替代其他层:
- 来自官方 marketplace,不代表每次调用都无需审批;
- 用户启用了 plugin,不代表其中所有 tools 都应直接暴露;
- tool 没展示给模型,不代表历史里的旧调用不需要兼容执行;
- 本地沙箱允许,不代表远端服务一定授权;
- 调用成功,不代表结果可以不受预算地塞进上下文。
11.12 更新、恢复与兼容:扩展不能只考虑“现在能跑”
第十章提出了一个尖锐问题:旧 rollout 恢复时,今天的运行时应该如何理解昨天的扩展?
扩展系统至少要区分三类东西:
| 内容 | 是否适合持久化 | resume 时怎么处理 |
|---|---|---|
| 已发生的输入、调用和结果 item | 是 | 原样 replay,不重新执行 |
| 当时依赖的能力定义和关键元数据 | 视需要保存 | 重建上下文与兼容判断 |
| client、连接、future、进程句柄 | 否 | 创建新运行时,旧对象作废 |
11.12.1 更新不能改写正在采样的 step
skill 文件监听、plugin 更新、MCP catalog 刷新都可以实时发生,但不能直接改变当前 step snapshot。合理的生效边界通常是:
- skill catalog 变化:下一次上下文贡献时更新;
- MCP tool catalog 变化:下一个 step 重新冻结;
- plugin 安装或卸载:新 thread 最清晰,必要时显式刷新已有 Session;
- hook 配置变化:在确定的生命周期边界重新装载;
- Extension Registry:运行时启动后保持不可变,更新需要重建运行时。
越深的扩展,更新边界越保守。因为深层变化影响的不只是“多一个工具”,还可能改变状态布局和生命周期语义。
11.12.2 Replay 旧调用,不代表重新拥有旧能力
历史里可能有一个已经卸载 plugin 提供的工具调用。replay 只需要把“当时调用过什么、结果是什么”恢复进历史,不需要重新执行,也不要求当前 runtime 还保留该工具。
但如果模型在 resume 后想再次调用它,必须按当前 catalog 判断:
- 当前仍可用:按新 step snapshot 正常调用;
- 已卸载或被 policy 禁用:返回明确的不可用结果;
- spec 已不兼容:不能把旧参数静默交给新 runtime。
这和第六章 Hidden 工具的演进姿态相呼应:必要时可以保留旧 runtime 处理兼容调用,但不再向模型展示。历史兼容与未来可见性是两件事。
11.12.3 扩展状态必须选择持久化承诺
Extension scoped state 默认是内存状态,不会因为用了 typed store 就自动可恢复。每个扩展都必须明确:
- 这是可丢弃缓存,resume 时重算即可;
- 这是可从 rollout 推导的 projection;
- 这是必须写成 canonical fact 的业务状态;
- 这是外部系统状态,只能重新查询;
- 这是带副作用的中间态,需要幂等键或人工确认。
最危险的状态是“看起来重要,却没有恢复契约”的内存字段。它在正常运行时一切顺利,一旦进程重启就让行为悄悄改变。
因此 Extension API 的 state scope 解决的是隔离与寿命,持久化协议解决的是跨进程语义,两者不能混为一谈。
11.13 一个完整例子:给 Codex 安装“线上故障处理能力”
假设团队想让 Codex 协助处理线上告警。需求包括:
- 按团队 runbook 排查;
- 查询监控、日志和发布记录;
- 执行高风险操作前必须走审批;
- 停止前确认已留下事故记录;
- IDE 中可以把当前分析结果附到事件面板。
不要把这些需求塞进一个万能 plugin runtime。按职责拆分:
| 需求 | 扩展机制 | 原因 |
|---|---|---|
| runbook 与排查步骤 | skill | 它是工作方法和领域知识 |
| 查询监控、日志、发布记录 | MCP | 能力属于外部平台 |
| 高风险操作审批 | MCP per-tool policy + PermissionRequest hook |
这是确定性安全约束 |
| 停止前检查事故记录 | Stop hook |
必须卡在结束边界 |
| 附到 IDE 事件面板 | dynamic tool | 能力由前端程序拥有 |
| 一键安装整套能力 | plugin | 负责组合、元数据和分发 |
| 企业强制域名与身份 | requirements | 用户和 project 都不能放宽 |
安装后的完整路径如下:
sequenceDiagram
participant U as 用户
participant P as Plugin / 配置
participant H as harness
participant M as 模型
participant X as MCP 平台
participant F as IDE 前端
U->>P: 安装并启用 incident plugin
P->>H: 贡献 skill、MCP 与 hooks
F->>H: thread/start 时声明 dynamic tool
H->>H: requirements + trust + capability 检查
U->>H: “调查这次告警”
H->>M: skill catalog + deferred tool namespaces
M->>H: 读取 incident-response skill
M->>H: tool_search("query deployment and logs")
H->>M: 返回命中的 MCP tool specs
M->>H: 调用日志与发布查询
H->>X: 按冻结 MCP binding 执行
X-->>H: 结构化结果
H->>M: 结果回灌
M->>H: 请求回滚发布
H->>U: 审批请求
U-->>H: 批准一次
H->>X: 执行回滚
M->>H: 提议停止
H->>H: Stop hook 检查事故记录
H->>M: 否决停止:“请先生成并关联事故记录”
M->>H: 调用 IDE attachIncidentReport
H->>F: 反向请求
F-->>H: 已附加
H->>M: 结果回灌
M->>H: 最终总结
这个例子揭示了可扩展设计的真正目标:不是让每个扩展包办全流程,而是让不同机制在同一组边界上组合。
- skill 负责“会不会做”;
- MCP 负责“有没有手”;
- hook 负责“哪些关口不能忘”;
- dynamic tool 负责“前端独有动作”;
- plugin 负责“一起交付”;
- requirements 负责“无论谁配置都不能越过的线”;
- LOOP 仍然只做采样、行动和回灌。
11.14 常见失败模式
| 失败模式 | 表面现象 | 根因 | 更好的做法 |
|---|---|---|---|
| 把 plugin 当任意代码注入 | 一个扩展就能拖垮或绕过整个 harness | 没有能力分层和信任域 | plugin 声明式组合,深层代码只走受信任 Extension API |
| 发现即启用 | 打开项目就执行 hook 或启动服务 | 混淆 catalog 与 activation | discovery、trust、activation 分离 |
| 启用即全量暴露 | 工具 spec 撑爆上下文,模型选错工具 | 没有 visibility 层 | Direct / Deferred / Hidden 分层,skills progressive disclosure |
| 采样中途热替换工具 | 参数按旧 spec 生成,却交给新 runtime | 没有 step snapshot | catalog 更新下一 step 生效,调用绑定 revision |
| 只用 skill 写强制规则 | 模型偶尔忘记审批或漏做收尾 | 把建议当成确定性控制 | 方法写 skill,硬约束写 hook、policy 或 runtime |
| 安装等于授权全部能力 | plugin 中任一 hook/MCP 都自动获得最高权限 | 声明者同时给自己授权 | 按能力分别启用和信任,requirements 再封顶 |
| 把本地沙箱当全局安全边界 | MCP 或前端照样产生高风险副作用 | 忽略执行位置 | 每个执行域独立鉴权、审批和审计 |
| 异步 hook 试图改变过去 | 工具已经执行,后台检查才返回拒绝 | 控制结果错过生命周期边界 | 需要控制就同步等待;async 只做通知和上报 |
| 同名扩展静默覆盖 | 同一 $skill 或工具在不同环境含义不同 |
把名称当展示文本而非地址 | 保留 provenance,冲突显式化 |
| 扩展状态只放内存 | resume 后行为悄悄改变 | 没有持久化承诺 | 明确缓存、projection、canonical fact 或外部状态 |
| 更新后重放旧副作用 | plugin 升级后重复发起历史操作 | 混淆 replay 与 re-execute | 历史只重放结果,新调用按当前能力重新判断 |
| 只给最终配置,不给来源 | 用户无法理解某开关为何无效 | 丢失 layer 与 disabled reason | origin、fingerprint、约束原因一并暴露 |
这些失败看似分散,根因其实相同:没有把“能力存在、能力获准、模型可见、动作执行、结果留痕”拆成不同阶段。
11.15 更深一层:可扩展性是一种治理能力
当一个 harness 只有内置工具时,开发者既是能力提供者,也是规则制定者。引入 plugin、MCP、skills 和 hooks 后,参与者变多了:
- 产品团队提供内置运行时;
- 企业管理员提供 requirements 和 managed hooks;
- 用户安装 plugin、配置 MCP;
- project 提供本地 instruction;
- 前端程序提供 dynamic tools;
- 外部 server 真正执行副作用;
- 模型根据当前可见面选择行动。
这已经不是一个“插件加载器”,而是一个小型治理系统。它必须回答:
- 谁可以声明能力?
- 谁可以授权能力?
- 谁决定模型是否看见?
- 谁执行副作用?
- 谁保存事实和承担审计责任?
- 当这些主体意见冲突时,谁有最终否决权?
Codex 的整体答案可以概括为:
低层可以提供默认值,
高层可以表达用户意图,
requirements 可以收紧边界,
step snapshot 冻结本轮事实,
执行域负责最终副作用,
rollout 保存已经发生的结果。
这里最值得注意的是:扩展点不是越多越好,越稳定才越有价值。
每新增一个 hook event 或 contributor,harness 就承诺了一个长期生命周期语义:
- 它在什么时刻触发;
- 此时哪些状态已经写入历史;
- 失败会阻止流程还是只告警;
- 多个处理器如何排序和归并;
- 中断、重试、resume 时是否再次触发;
- 输出是否进入上下文和 rollout。
一个模糊的扩展点看似灵活,实际会把内部实现细节永久冻结;一个职责单一、边界清晰的扩展点能力较窄,却能跨版本稳定。
因此设计新扩展机制时,应该按这个顺序提问:
- 现有 skill、MCP、hook、dynamic tool 是否已经能表达?
- 能否通过新增一个结构化事件或 tool spec 解决?
- 是否真的需要新的 Extension contributor?
- 如果必须深入内核,它的输入、输出、顺序、预算、失败和恢复语义是什么?
可扩展性的成熟标志,不是“任何地方都能插代码”,而是大多数需求都能落在少数稳定扩展点上,而且每项权力都能解释、限制和回放。
11.16 小结:可扩展性的七条设计原则
-
开放扩展点,不开放任意控制权。 LOOP 保持极简,扩展通过 skill、MCP、hook、dynamic tool 和强类型 contributor 进入固定边界。能表达的权力越具体,影响范围越可推理。
-
先区分发现、激活与冻结。 discovery 建 catalog,activation 结合配置、信任和能力决定是否可用,step snapshot 冻结模型本轮真正看到的 spec 与 runtime。动态刷新只影响未来边界,不改写在途采样。
-
不同机制承担不同职责。 skill 教方法,MCP 接外部能力,hook 保证生命周期规则,dynamic tool 委托前端执行,plugin 负责组合与分发,Extension API 服务受信任的深层集成。不要用一个万能 plugin 模糊所有边界。
-
声明与授权分离,配置与 requirements 正交。 project 或 plugin 可以声明能力,用户和管理策略决定是否信任;配置表达偏好,requirements 给能力封顶。任何低权限来源都不能通过更高优先级配置自行扩大权力。
-
执行位置决定安全边界。 本地命令、MCP server、前端 dynamic tool 和同进程 Extension 处在不同信任域。沙箱、审批、鉴权和审计必须覆盖真实执行者,不能假设 harness 的本地沙箱能约束远端世界。
-
上下文、名称和状态都要有预算与归属。 skills progressive disclosure、tools deferred exposure、扫描与输出硬上限共同控制 token 和资源;名称保留 provenance,冲突显式处理;scoped state 明确寿命,跨进程状态另行定义持久化承诺。
-
扩展也必须可恢复、可兼容、可观测。 replay 旧 item 不重新执行旧能力,连接和 future 在 resume 时重建;更新在稳定边界生效;hook、tool 和 contributor 的触发、耗时、失败、来源与版本都应留下可解释信号。
留给读者思考的几个问题:
- skill 依赖模型遵循 instruction,hook 提供确定性控制。一个规则从“建议”升级为“强制”时,应该如何判断它该从 skill 移到 hook 或 policy?
- MCP server 与 dynamic tool 都在 harness 外执行。两者的身份、审批、超时、重试和审计协议是否应该完全统一?哪些差异来自网络服务与交互式前端的本质不同?
- plugin 更新后,已有 thread 应继续使用旧能力快照,还是尽快切到新版本?若要做到真正可复现,是否需要把 plugin 版本和 skill 内容摘要写入 rollout?
- 多个 hooks 同时修改一个工具调用时,应该按顺序叠加、冲突即拒绝,还是只允许第一个认领?哪种语义最容易测试和向用户解释?
- Extension Registry 启动后不可变,换来的是确定性;但长时间运行的 app-server 又希望在线升级能力。应该重建进程、迁移 Session,还是引入版本化 Registry?每种方案会破坏哪些不变量?
- deferred tools 和 progressive disclosure 都依赖“模型知道自己该搜索”。当能力目录越来越大时,发现质量应该由关键词索引、语义检索、规则路由还是另一个 agent 负责?
- 扩展来源、激活结果、step snapshot、实际调用和结果回灌构成了一条完整因果链。要定位“为什么模型没用某个工具”,可观测系统至少要记录这条链上的哪些节点?
下一章我们进入可观测性:tracing、metrics 与回放如何把一次请求从前端、LOOP、模型流、hook、工具、MCP 一直串到结果 item;当扩展越来越多时,系统如何回答“慢在哪里、谁做了决定、为什么这项能力没有生效”。