本文讲 区域 5 · 能力来源:模型在对话里能"动手"的工具从哪来、怎么被描述、怎么被冻结、又怎么在安全的边界内被真正执行------涵盖内置工具、Skill 技能、MCP 外部工具三大类。
0. 先建立心智模型
模型的手,是"能力"
上一篇文章说,模型看到的世界是一张纸(prompt)。但光能看不能动,编码 agent 就只是个聊天机器人。真正让它"干活"的,是能力------模型可以在对话中发起调用的那些工具。
Grodex 里有三大类能力来源:
| 类别 | 是什么 | 例子 |
|---|---|---|
| 内置工具 | 编译进二进制的文件/命令操作 | read_file、exec、edit_file... |
| Skill 技能 | 磁盘上的 Markdown 指令集 | deploy、test、style... |
| MCP 工具 | 外部 MCP 服务器提供的远程操作 | mcp_github_search_issues... |
这一层要回答的问题很实际:
- 模型调用工具时,按哪一版的定义执行(准备时看到的 schema vs 执行时的 schema)?
- 工具在对话中途增删改了(新 MCP 连上、Skill 文件被改),正在进行的回合怎么处理?
- 安全收紧时,怎么让已经冻结的工具调用立刻失效?
- 大工具结果怎么回传、崩溃后怎么区分"只读可重放"和"有副作用要人工裁决"?
答案不是把工具做成一个简单的 name → handler 哈希表,而是把能力的一生管起来:注册 → 冻结 → 派发 → 执行,每一步都带上可审计的凭证。
1. 一条流水线:从"注册"到"执行"
先把三类能力在系统里流转的完整链路看一遍:

- 注册 :内置工具注册进
ToolRegistry;Skill 被SkillCatalog发现并校验;MCP 服务器被适配器包装。三路最终都汇入 loop 层的 CapabilityManager(能力注册表,按代保管); - 冻结 :每个 Turn 开始,
snapshot_base()把当前代的最新工具 schema、运行时句柄、代数号冻结成一份不可变基线(呼应 01 篇 §3 的"冻结"------能力是它冻结的对象之一); - 派发 :模型看到的清单由
effective_specs(base, overlay)算出;真正执行某个调用前,生成PreparedCapabilityCall,把"按哪一版执行"钉死; - 执行:通过权限门 + 沙箱(区域 6)后,调用内置运行时或 MCP 进程。
后面几节分别讲三类来源,再讲"冻结与叠加"和"与安全的接缝"。
2. 内置工具:七件套 + 两阶段
七件套
ToolRegistry::builtin() 注册了 7 个工具,每个都带着完整的元数据(并发等级、副作用等级、默认策略):
| 工具 | 作用 | 并发 | 副作用 | 默认策略 |
|---|---|---|---|---|
read_file |
带行号读文件,支持 offset/limit/字节上限 | Parallel | 只读 | 允许 |
read_artifact |
读被外置的大工具结果 | Parallel | 只读 | 允许 |
write_file |
创建/覆盖文件 | Serial | 非幂等 | 询问 |
edit_file |
精确字符串替换(要求完全匹配) | Serial | 非幂等 | 询问 |
exec |
跑 shell 命令,返回 stdout/stderr/退出码 | Serial | 非幂等 | 询问 |
apply_patch |
结构化文件操作(create/modify/delete/rename,一次多个) | Serial | 非幂等 | 询问 |
process_io |
与后台进程交互(轮询输出、写 stdin、发信号) | Serial | 非幂等 | 允许 |
注意这个分工的保守取向:只读的放 Parallel + 允许(无副作用、可放心并发);凡是碰状态的一律 Serial + 询问(可能产生不可逆副作用,先问用户)。"并发等级"决定工具批次能不能并行(01 篇 §4 说过的整批降级),"副作用等级"是崩溃恢复时判断能不能安全重放的依据,"默认策略"是权限引擎的第一层默认值。
两阶段:prepare 与 execute
内置工具实现了一个两阶段 trait:BuiltInTool 把一次调用拆成 prepare 和 execute:
prepare:不产生副作用,只做参数校验、文件快照、静态检查;execute:真正动手。
拆开的收益是:准备阶段可以被安全地并行、可被审批打断、可被回放验证;而"有副作用"的瞬间被压缩到 execute 一个点。这与权限门(区域 6)"批准的是意图、执行的是动作"是同一套逻辑的两面。
跨工具原语
7 个工具不是各自为政,它们共享一套"地基原语":
- FileSnapshot / ReadRange / HashlineAnchor:读的稳定性。给文件拍快照、按行号锚定,保证"读的时候"和"改的时候"看到的是同一版本;
- ToolResultEnvelope + HeadTailBuffer + TruncationInfo :大结果的处理。长输出只留头尾、中间截断,完整内容外置成 blob,由
read_artifact按需读回; - ProcessHandle + ExecStatus :长进程管理。
exec跑很久的命令时,句柄化交给process_io轮询; - PatchPlan + PatchFile :
apply_patch的原子性。先计划、再落盘,避免半改状态; - StaleFile + AtomicityLevel:版本栅栏 + 原子写。文件在"准备→执行"之间被别人改了,能发现并拒绝(见 §6 的 stale 语义)。

3. Skill:不是代码,是指令集
Skill 是三大能力里最特别的一类------它不执行任何东西 ,它只是给模型看的指令集。一个 Skill 就是磁盘上的一个 Markdown 文件(带可选 YAML frontmatter),比如:
yaml
---
name: deploy
description: Deploy the app
---
# Deploy
Run `deploy.sh`, then verify the health endpoint.
发现与校验
SkillCatalog::discover() 从两个位置找 Skill:项目级 (.grodex/skills/)和用户级 (全局配置目录),项目优先、按名字去重,最后排序。发现时还跑一遍 lint ------有 error 级问题的 Skill 整个被排除出目录,不参与注入。
信任策略:未信任的工作区,只给"目录"
Skill 是否把正文注入 prompt,取决于信任状态:
- 内置和用户级 Skill:永远信任,正文完整注入;
- 项目级 Skill:只有工作区被
--trusted标记才信任; - 未信任 :prompt 里只显示名字 + 描述,并明确告诉模型"内容被扣留了,用
--trusted才能启用"。正文不进 prompt,而不是蒙混过关。
这延续了总览的 fail-closed 原则------别人的工作区里躺着一个 Skill,里面写的可能是"执行 rm -rf /",不能因为发现了一个文件就把它当作神圣指令执行。
按 Turn 冻结
Skill 在 Turn 开始被拍成 SkillSnapshot 冻结 。之后即使磁盘上的文件被改了,进行中的 Turn 仍然用冻结的版本------模型规划时看到的 Skill 和执行的完全一致(01 篇 §3 冻结纪律的又一兑现)。冻结靠 SHA-256 内容哈希做变更检测:哈希变了就 bump skill_generation,但普通变更默认下一 Turn 才采纳。
4. MCP:外部世界怎么接进来
MCP(Model Context Protocol)是让 Grodex 连上外部工具的协议。Grodex 把 MCP 服务器跑成独立的子进程,通过 stdio 走 JSON-RPC 通信。
命名空间:避免和内置工具打架
MCP 工具的名字被加前缀:mcp_{server}_{tool} 。比如 GitHub 服务器上的 search_issues,在 Grodex 里叫 mcp_github_search_issues。这样外部工具永远不会和内置工具撞名;调用时靠前缀反解出 (server, tool),生成对应身份的能力 ID。
适配器:把"远程函数"包装成本地工具
McpToolAdapter 把 MCP 服务器上的一个工具包装成标准的 Tool + ToolRuntime:
metadata():名字、描述、schema 照搬;并发/副作用/策略按最保守的默认:Serial、NonIdempotent、Ask------外部工具的语义不可信任,宁可每次先问;execute():按需 spawn 服务器进程(进程对象 drop 时自动 kill,kill_on_drop),发tools/callJSON-RPC,拿回结果。

契约修订栅栏:防"按旧 schema 规划、按新 schema 执行"
MCP 工具带一个 contract_revision。调用准备时,PreparedMcpCall 把当时的修订号 + 参数哈希成一个 plan_hash 钉住;执行前 verify_revision 检查当前修订是否还一致------不一致就直接拒绝,报 "stale MCP tool call"。这堵住了分布式系统里最阴险的一类 bug:模型看着旧版本文档规划,执行时却是新版实现。
5. 冻结、叠加与代数:能力的一生
这是区域 5 的枢纽:loop 层的 CapabilityManager 怎么让"能力的集合"既有活性、又可控。
代数(Generation):不可变快照 + 环形缓冲
能力不是一份可变的大哈希表,而是一列不可变的代数快照 (ring buffer)。每次 register_tool / unregister_tool:
- 代数号 +1;
- 克隆上一代、插入/移除该项 → 形成新一代;
- 挤掉最旧的代(默认保留若干代,至少 2 代)。
代与代之间完全隔离 :对某一代做的查询,永远基于那一代的内容,不会"回落"到更新的代。代码里有一条明确的 P1-1 防漂移修复:某代被挤出缓冲后,调用若还引用它,必须拒绝,而不是偷偷用最新代顶替------宁可报错,不能"假装"。
冻结:Turn 开始 snapshot_base
每回合 snapshot_base() 冻结"最新代"的 schema + 运行时 + 代数号。之后这一 Turn 内,模型看到的工具清单和执行的运行时都出自这份冻结基线(加 overlay),中途注册新工具不影响本回合------这正是 01 篇 §3 说的"模型看到的世界,在 Step 内不许变"在工具维度上的形态。
叠加:回合内的 promote / demote
回合内允许受控地增删工具,通过 overlay(叠加层):
promote(name, spec, runtime)/demote(name)只改 overlay,不碰冻结的 base;- 每个 Step 算
effective_specs(base, overlay):base + overlay 新增 − overlay 移除; - 按名字排序 输出------注释原话是:HashMap 的迭代顺序是随机的,不排序的话,工具数组每个 Step 都会变,前缀一变,prompt cache 命中率直接归零。这是对 02 篇 §4 缓存纪律的又一次呼应;
- 回合结束
adopt_overlay()把叠加原子采纳进注册表,代数 +1------本回合的变更从下一回合开始生效,永不中途悄悄变。

发布统一:别让"管能力的"和"看能力的"各说各话
注册/注销工具时,还会 bump 一个共享的 CapabilityPublisher(同时推进工具代数与根代数),让 CapabilityManager、ACP 协议、StepContext 这些观察者永远看到同一个推进------避免"执行时用的是新工具,前端还展示旧清单"的错位。
6. 与安全内核的接缝(区域 6 的预告)
能力层不负责"放行",但它是安全内核的对象 和凭证:
- 默认策略 :每个工具带
default_policy(允许/询问),是权限引擎的第一层输入; - 派发凭证 :每个
PreparedCapabilityCall钉住capability_revision、policy_generation、args_hash、operation_id、policy_ceiling。审批如果基于某版参数和策略,之后任何一项变了,旧审批自动失效; - 吊销栅栏 :一个单调的
LiveRevocationFence。安全策略收紧(如 kill-switch)时revoke_all()把代数 epoch +1------已经被冻结的旧调用,只要还没产生副作用,就会被实时拒绝;回合内的 overlay 若还"假装"停在旧 epoch,也会被拒绝(stale-epoch 守卫); - 崩溃分类 :
side_effect_map(工具 → 副作用等级)供区域 7 的恢复器使用:只读/幂等的进行中调用可以安全自动重放;非幂等的必须交给人类裁决(01 篇 §7 的"结果未知"就是这么来的)。
一句话:能力层回答"有什么、按哪版、冻结到什么程度",安全内核回答"准不准、在什么边界内跑"。 两者在派发瞬间接上电。
7. 一张表:三类能力的异同
| 内置工具 | Skill | MCP 工具 | |
|---|---|---|---|
| 本质 | 可执行的代码 | 给模型看的指令 | 远程函数 |
| 来源 | 编译进二进制 | 磁盘 Markdown | 外部服务器进程 |
| 命名 | read_file... |
deploy... |
mcp_{server}_{tool} |
| 并发默认 | 读类并行 / 写类串行 | --- | 串行 |
| 副作用默认 | 读类只读 / 写类非幂等 | --- | 非幂等 |
| 策略默认 | 读类允许 / 写类询问 | 信任则注入 | 询问 |
| 变更时 | 注册新代 | 下一 Turn 采纳 | 契约修订栅栏 |
| 谁执行 | 本地 Rust | 无(纯提示) | 子进程 JSON-RPC |
8. 小结
一句话总结区域 5:能力的集合被当成"版本化的资产"来管,而不是一把钥匙。 三类来源(内置 / Skill / MCP)汇入注册表后,每个 Turn 冻结一份基线,回合内靠叠加层受控增删、回合末原子采纳;每代不可变、代间隔离、挤出即拒绝;每个调用派发时带上修订号、参数哈希、策略版本和幂等键的完整凭证------能力因此既灵活(可热更新、可叠加、可吊销)又可审计(任何时候都能说清"按哪一版、凭什么")。