来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
本集属于技术侧(S2)模块 T2「解剖 Grok Build:Rust 写的生产级 Coding Agent」,取材自 2 节课:
这两条内容放在一起不是凑数。它们各自解决一个不同的外部扩展面:MCP 解决工具从哪来,插件市场解决能力包怎么安装和获准执行。但把源码摆平看,两边的工程重心完全同构——都不在传输或解析本身,而在连接之后的纪律:命名、可见性、身份、状态合并、恢复退避,以及来源、启用、信任三道门。
一句话概括:连上是起点,不是终点;出现在目录里是可发现,不是可执行。
读完本集(听完音频),你应该能够:
| 能力 | 具体表现 |
|---|---|
| 判定协议角色 | 从「谁发起初始化 / 工具列表 / 调用」判断客户端与服务端,不把内部 Hub Server 当成通用 MCP Server |
| 复述授权链路 | 说清「复用刷新 → 浏览器授权 → 回调换令牌 → 锁定写入」四步,以及每步失败后的走向 |
| 处理凭据并发 | 知道多进程场景下令牌为何必须用文件锁 + 原子保存写回 |
| 设计工具命名 | 用 服务端名 + 保留分隔符 + 原始工具名 构造全局唯一名,并理解「分隔符恰好出现一次」的原因 |
| 区分三类受众 | 说清 disabled / model-visible / UI-only 三条去向各自的落点 |
| 解释按需检索 | 说明为什么要维护元数据快照,以及 mcp_initialized 标记解决了什么歧义 |
| 画出恢复状态机 | 列出 Initializing / Ready / NeedsAuth / Unavailable / Disabled,并标出转换条件 |
| 设计抖动吸收 | 用五十毫秒窗口做 last-write-wins 合并,估算高频事件的实际推送次数 |
| 防串连接 | 用 client_id 护栏丢弃迟到事件,避免旧连接的善后逻辑删掉新连接 |
| 分开重启策略 | 说明 stdio 与 HTTP 两种不同的退避与护栏检查 |
| 拆开 Marketplace 与运行时 | 分清「哪些可以装」与「哪些会被加载」两条链 |
| 排序发现来源 | 按优先级复述五种来源,并说出来源如何影响初始信任 |
| 走完三道门 | 逐项检查来源与路径、启用状态、执行信任 |
| 读懂未信任矩阵 | 知道 skill / agent / hook / MCP / script 在未信任状态下的处理差异 |
| 坚持 fail closed | 能解释为什么路径规范化失败必须落到「不信任」,而不是放行 |
SOURCE CONFIRMED —— Grok Build 是 MCP 客户端。
判定依据只有一条:看调用方向。McpClient 启动 stdio 或 Streamable HTTP 连接,执行初始化、list_tools 与 call_tool。Computer Hub 的 MCP Adapter 的描述也是「把 MCP Server 的工具桥接进 Hub 路由」——方向同样是往里接。
保守口径(重要):当前源码快照没有找到把 Grok Build 自身通过 MCP 传输暴露给任意 MCP Client 的入口。仓库里的 Hub Server 属于 xAI Computer Hub 协议,不能作为「Grok Build 也是通用 MCP Server」的证据。
这条结论听着像抠字眼,实际影响很大。如果你按「它同时也是服务端」去做对接设计,做出来的东西没有源码支撑,一次版本更新就可能无声崩塌。
| 判断项 | 源码事实 | 证据强度 |
|---|---|---|
Grok Build 发起 list_tools / call_tool | 是 | 源码确认 |
| Computer Hub Adapter 桥接外部 MCP 工具进 Hub 路由 | 是 | 源码确认 |
| Grok Build 自身作为通用 MCP Server 对外暴露 | 未发现入口 | 未证实 |
| Hub Server 可当作通用 MCP Server 证据 | 不可以 | 协议不同 |
1 · 复用或刷新 先读磁盘凭据,尝试 token refresh
2 · 浏览器授权 需要交互时启动用户同意流程
3 · 回调换令牌 授权码交换访问令牌与刷新令牌
4 · 锁定写入 文件锁 + 原子保存,支持多进程
配置侧只有三个字段需要关心:
oauth_client_idoauth_client_secret_env_var —— 注意后缀是 env_var,存的是环境变量名字,不是密钥本体oauth_scopes凭据落点是本地 JSON 文件,路径为 grok_home 下的 mcp_credentials.json。读写顺序为 lock + load + insert + atomic save。
第 4 步「锁定写入」是工程上最容易省略的一步。它的必要性不在单进程,而在多进程:两个进程各自刷新令牌,后写的覆盖先写的,前一个进程手里就握着一张已经失效的令牌,而错误往往要等到下一次调用才暴露——排查成本极高。
不要凭感觉设 refresh 提前量。先量两个数:令牌有效期和本进程的多实例启动频率。刷新太激进会被服务端限流,太保守就整天弹授权页。这两个数取到之后,阈值是算出来的,不是猜出来的。同理,「文件锁 + 原子保存」不是一个可选的优化项,而是多进程写入的前提条件——只要你的架构里可能出现两个进程同时操作同一份凭据文件,这一步就是必做。
命名空间(NAMESPACE)
注册名由三段构成:服务端名 + 保留分隔符 __ + 原始工具名。源码要求完整名称中分隔符恰好出现一次(into_registration)。
为什么这么严?因为一旦分隔符出现两次,切分位置就产生了歧义——服务端自己的名字里如果带下划线,就会和分隔符打架,解析器无法判断从哪儿切开。
这条约定顺带解决了冲突问题:两个不同 Server 提供同名工具时,拼出的完整名不同,ToolId 也不同,模型侧不会串。
两类受众(TWO AUDIENCES)
工具不是「进来就等于能用」,它有三个去向:
disabled_tool_registrations,保留元数据,不进执行链ui.resourceUri → 可单独进入 UI 通知,给上层应用看常见错误是把它当成单一开关。三者是正交的:一个工具可以「对模型不可见、对 UI 可见、且未被禁用」。
接了五六个 MCP Server 之后,工具数轻易上百,全塞进提示词既贵又乱。源码的做法是维护一份 ToolMetadataSnapshot:
pub struct ToolMetadataSnapshot {
pub tools: Vec<ToolMetadata>,
pub servers: Vec<ServerMetadata>,
pub mcp_initialized: bool,
}
其中 mcp_initialized 这个布尔标记是关键。没有它,搜索层分不清两种完全不同的状态:「暂时搜不到」和「还没初始化」。前者是结果,后者是状态,二者给出不同的用户提示与重试策略。
检索侧建的是 BM25 索引,支持两种命中方式:
ToolId支持裸名的理由很实际:模型和用户不一定记得住完整前缀,裸名先把候选捞出来,再由调用层消歧。
取舍提示:按需检索省下提示词空间,代价是模型可能压根不知道某个能力存在(尤其是调用期才被发现的能力)。这套设计隐含一个要求——工具名必须足够自解释,否则裸名检索救不回来。命名偷懒,检索就是摆设。
连接状态共五个:
| 状态 | 含义 |
|---|---|
Initializing | 开始握手 |
Ready | 能力可用 |
NeedsAuth | 等待授权 |
Unavailable | 连接中断 |
Disabled | 配置关闭 |
枚举本身很简单,复杂度全在状态之间的事件流。三个机制必须配套:
① 五十毫秒合并(50 MS COALESCE)
mcp_dispatcher 以 (server_name, event_kind) 为键,在 50 ms tumbling window 内 last-write-wins。
课堂中的估算练习:模拟 100 条 tools/list_changed,写出合并后的预期通知数。答案不是 100,也不是固定常数——它取决于这批事件分布在多少个 50 ms 窗口内。理解这一点比记住数字重要:合并的收益来自「同一窗口内重复事件的折叠率」。
② 身份护栏(IDENTITY GUARD)
移除 dead client 之前比较 client_id。如果断线事件属于已被替换的旧客户端,则保留当前客户端,丢弃过期状态。
这条护栏防的是一类极难复现的线上 bug:旧连接的善后逻辑把新连接删了。表现为「偶发自动断线」,日志里看不出因果,因为没有 client_id 比较就无法区分事件的归属代际。
③ 重启策略(RESTART POLICY)
两种传输不同处理:
| 传输 | 恢复动作 | 退避 | 护栏检查 |
|---|---|---|---|
| stdio | 自动重启子进程 | 固定 1s → 4s → 16s | 正在关闭 / 已被禁用 / 配置已被移除 |
| HTTP | 先尝试客户端内恢复 | 独立退避 | —— |
三道护栏检查不到位,最典型的症状就是:一个已被用户禁用的服务在后台反复拉起。
无论哪种传输,重连成功后都必须重新做能力发现与工具注册,随后刷新快照。 跳过这一步,连接是活的,工具列表却是旧的——这类不一致最难定位。
marketplace-root/
├── .grok-plugin/
│ ├── marketplace.json
│ └── plugin-index.json
├── plugins/
│ └── sample-plugin/
└── default-skills/
扫描器先读索引,索引缺失或无效时才回退去逐个扫 plugins/*/。这个顺序有性能原因:目录大时全盘扫很慢,索引是正常路径上的快通道。default-skills 可作为虚拟插件加入结果。
单个插件的结构:
sample-plugin/
├── plugin.json # 首选 manifest
├── skills/*/SKILL.md
├── commands/
├── agents/
├── hooks/hooks.json
├── .mcp.json
└── scripts/
plugin.json 是首选 manifest,后备位置两处:.grok-plugin/plugin.json 与 .claude-plugin/plugin.json。PluginManifest 可覆盖 skills、commands、agents、hooks、MCP 与 LSP 六类路径。
关键实现细节:解析完 override 之后,还要验证这些路径仍包含在插件根目录内。少了这次校验,一个恶意 manifest 能把某类资源的路径指到别处去(例如把 skills 指向 .ssh)。
| 职责 | 归属包 | 回答的问题 |
|---|---|---|
| 目录、扫描、安装 | xai-grok-plugin-marketplace | 有哪些东西可以装 |
| 运行时发现、去重、名称冲突、启用、信任 | xai-grok-agent::plugins | 哪些东西真的会被加载 |
核心区分:Marketplace 里出现一条记录,不代表组件会立即执行。 中间隔着来源判定、启用状态与信任三重检查。
发现来源按优先级从高到低:
--plugin-dir —— 最高来源优先级.grok/plugins(兼容 .claude)$GROK_HOME/plugins —— 已安装插件[plugins].paths —— 位置影响信任来源不只决定谁覆盖谁,它直接决定初始信任判断。
第一道门 · 来源与路径
MarketplaceRelativePath 拒绝三类输入:绝对路径、父目录穿越、越界 join。远程条目可用 git ref 或 SHA 定位内容。
远程条目既然支持 SHA 定位,就默认锁 SHA 而不是锁分支。分支是可变的,意味着你今天审计过的插件,明天内容可能已经不同。锁 SHA 换来的是可复现性——这是插件生态里最值得花的五个字符。
第二道门 · 启用状态
发现配置维护 enabled 与 disabled 两个列表。默认值有明确偏向:
| 来源范围 | 默认进入 |
|---|---|
| 项目范围 | disabled |
| 用户范围 | disabled |
| CLI override | enabled |
| Config path | enabled |
这个偏向是有设计意图的:越靠近「别人给你的代码」,默认越保守。用户可显式调整。
第三道门 · 执行信任
粒度不是单个功能模块,而是单个插件根(canonical plugin root)。信任记录写入 ~/.grok/trusted-plugins。
判定逻辑如下:
match dunce::canonicalize(plugin_root) {
Ok(path) => self.trusted.contains(&path),
Err(_) => false, // ← FAIL CLOSED
}
Err(_) => false 这一行是本集最值得记住的一行代码。 路径解析失败 → 落到「不信任」,而不是「信任」。若反过来写(Err 也放行),攻击者只需制造一次解析失败就能绕过整道门——这道门就彻底失效了。
初始信任的来源影响:
| 来源 | 初始判断 |
|---|---|
| CLI override | 源码中标记为 trusted |
| 用户范围 | 源码中标记为 trusted |
| 项目范围 | 要求显式信任 |
| Config path | 位于用户 home 下自动信任,其他位置仍需授权 |
未信任并不等于完全不可见。源码保留了元数据级发现,只在执行面收口:
| 组件 | 发现 | 未信任时的执行 |
|---|---|---|
| Skills / Agents | 可列出元数据 | 保持元数据级发现 |
| Hooks | 可从 manifest 识别 | 阻止加载执行 |
| MCP Servers | 可从配置路径识别 | 阻止启动命令 |
| Scripts | 属于插件内容 | 阻止执行 |
一句话:可读性保留,可执行性收走。
安装器会为每个已安装插件写入一条 provenance:本地 Marketplace 通过 managed install storage 安装,远程条目通过 Git URL、ref、SHA 与 subdir 定位,InstallRegistry 记录下来。
这条记录的价值体现在事后:出事时你能回答两个问题——这个插件从哪来,以及它锁在哪个提交上。没有 provenance,事故复盘就只能停在「装过一个插件」。
落地走查时,按顺序逐项打勾:
MCP 侧
oauth_client_secret_env_var)ToolMetadataSnapshot 含 mcp_initialized,搜索层能区分「未初始化」与「无结果」client_id 护栏已实现,旧连接事件不会删掉新连接插件侧
canonicalize 失败 → 不信任)如果你要在一个团队里落地上面的清单,三件事投入产出比最高:① 把 client_id 护栏和重连后重新发现写成两条必须通过的集成测试(分别覆盖 stdio 与 HTTP);② 在 CI 里加一个路径约束测试:构造含绝对路径、.. 穿越、越界 join 的 manifest,断言全部被拒;③ 信任相关测试必须包含错误分支——刻意让 canonicalize 失败,断言结果是拒绝而非放行。这三条覆盖的正是最贵、最难事后补的三类缺陷。
本页严格遵守以下约束,也请你在引用时保持同样的克制:
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版