为什么要有插件
Codex 是一个 AI coding agent。它的核心 loop 是:接收用户指令 → LLM 推理 → 调用工具 → 返回结果。
那问题来了,工具从哪来?
最初,工具是硬编码的:shell、file read/write、apply_patch。这时的AI可以写代码,但完全不可扩展,如果你想接 GitHub API、连接数据库,每次改一个能力,都要硬编码发布新版本。
MCP的出现解决了这个问题,它定义了一个通用协议,任何进程只要实现 initialize → list_tools → call_tool 这套接口,就能把自己的能力暴露给 agent。
这解决了接入标准化的问题——你不用再改核心代码了,写一个 MCP server 就行。就像 USB 协议让你不用为每种外设改主板一样。
但 MCP 只解决了"怎么连上",没解决"怎么管理"。用户怎么知道有哪些 MCP server 可用?怎么一键安装而不是手动写配置文件?两个 server 名字冲突了怎么办?怎么限制团队成员只能用审核过的 server?一个扩展同时提供 tools、skills、app 连接器,怎么统一组织?
这就是插件系统要做的事,建立在MCP之上,本质就是让第三方能力以标准化方式接入 agent 的工具调用链。

一个插件是什么
在 Codex 里,一个"插件"不是一个可执行文件,而是一个声明式的目录结构,通过一个 manifest 文件描述自己能提供什么能力。
在codex-rs/plugin/中:

插件的入口是lib.rs


PluginCapabilitySummary 是给 LLM "看"的。agent 的 system prompt 里会包含其中已激活插件的摘要,让 LLM 知道有哪些工具可用。
了解插件的体系,需要知道四个核心概念。
1. PluginId

插件的标识id,格式为:plugin_name@marketplace_name,例如 github-tools@openai-curated。
为什么需要 marketplace_name?
因为不同来源(OpenAI 官方 marketplace、社区 marketplace、本地目录)可能有同名插件。@marketplace 后缀消除歧义,就像 npm 的 @scope/package。
PluginId有验证规则,保证了它可以用作文件系统路径,直接做缓存目录用。

2. PluginManifest
Manifest 是插件的"自我描述文件",对应磁盘上的 plugin.json。

在 Rust 里,
// 定义:Dapeng 是泛型参数名(必须大写开头,但叫什么都行)
structWrapper<Dapeng> {
value: Dapeng,
}
// 使用时,把 Dapeng 具体化为 String
leta: Wrapper<String> = Wrapper { value: "hello".to_string() };
// → 此时 value 的类型是 String
// 使用时,把 Dapeng 具体化为 i32
letb: Wrapper<i32> = Wrapper { value: 42 };
// → 此时 value 的类型是 i32和写死 String 的区别:String 是确定的类型;Dapeng(或叫 Resource、T)是"等使用时再决定"的类型占位符。PluginManifest<Resource> 也一样——Resource 就是那个占位符,只不过起了个有实际含义的名字。
PluginManifest 里嵌套了 PluginManifestPaths,后者自己也带着<Resource>:

当外层 PluginManifest<Resource> 的 Resource 被确定为某个具体类型(比如 AbsolutePathBuf),里面所有层级的 Resource 都自动变成同一个类型:

这样就用一个泛型参数统一控制了整棵结构体树里所有"涉及路径"的字段。如果不用泛型,你得为三个阶段各写一份几乎一模一样的结构体:

有了泛型,一份定义覆盖三个阶段。以后加字段只改一处,三个阶段自动都有:

那 Resource 什么时候被确定?在不同的生命周期阶段,它会被具体化为不同的类型:

同一份 manifest 数据,三个阶段用的是同一个结构体定义(PluginManifest),只是泛型参数不同。
那怎么从一个阶段转到下一个阶段?源码里有一个方法负责这件事——它遍历整棵结构体树里所有 Resource 类型的字段,逐一执行你给的转换函数:

实际使用时

再回到PluginManifestPaths中看,一个插件声明了什么内容

一个插件可以同时提供:
• Skills:注入 agent 的 system prompt 上下文 • MCP Servers:启动外部进程提供工具 • Apps:声明与第三方服务的集成 • Hooks:在特定事件(如 session start)时触发的脚本
3. PluginProvider

Trait 是 Rust 里的"接口合同",它定义了"你得能做什么",但不管"你具体怎么做"。如果你熟悉其他语言:Java、go 里叫 interface,Python 里类似抽象基类(ABC)。
上面这段代码说的是:任何声称自己是 PluginProvider 的类型,都必须实现一个 resolve 方法。
实际源码里只有一个实现者:ExecutorPluginProvider。它通过 EnvironmentManager 拿到目标环境的文件系统抽象,本地环境就读磁盘,远程环境就走远程文件系统接口
使用 trait 而不是直接写函数的好处:上层代码只调 resolve(),不关心底层到底是读本地磁盘还是走网络。以后如果新增一种环境类型(比如容器内的文件系统),只要实现同一个 trait 就能无缝接入,上层不需要修改。
resolve 方法的输入是去哪里找,输出是该插件是否找到。

每一个资源路径都必须绑定一个"所有者"。你不能说"读 /home/user/plugin/skill.md",你必须说"读 env-123 环境的 /home/user/plugin/skill.md"。
因为 Codex 支持远程执行环境。一个路径在不同环境里指向不同的文件。Authority 绑定确保不会"借用"另一个环境的文件系统。
4. PluginLoadOutcome

这是"加载完成后的快照"。泛型 M 是 MCP server 配置类型——在 core-plugins 会被具体化为 McpServerConfig。
LoadedPlugin 包含一个插件从磁盘加载后的全部信息:插件是否启用,以及插件中的skill、mcp、app、hook等。
本篇内容介绍了一个插件的设计:有哪些组成成分,系统又是怎么唯一标识一个插件的,以及加载一个插件后,系统拿到了什么。
下一篇,我们进入 core-plugins,看看一个插件是怎么从"marketplace 上的一个名字"变成磁盘上可用的 LoadedPlugin 的。

夜雨聆风