这个设计与 第 15 章「不做权限系统」是同一个决定的两面。扩展系统的能力之所以能这么大,正是因为 pi 放弃了在进程内划边界;反过来,正因为扩展能力这么大,进程内的边界也确实没意义了。
文档在「扩展位置」一节开头就把这件事挑明了。
> **Security:** Extensions run with your full system permissions and can execute
arbitrary code. Only install from sources you trust.
| 类别 | 能做什么 |
|---|---|
| 事件(25 类) | 观察生命周期:会话、agent、turn、消息、工具、模型、用户 bash、输入 |
| 工具 | pi.registerTool() 注册新工具,或覆盖同名内建工具(Gondolin 就靠这个) |
| 命令与快捷键 | registerCommand / registerShortcut / registerFlag |
| 渲染 | registerMessageRenderer / registerEntryRenderer / registerMarkdownTransformer |
事件类型的权威列表是一个 TypeScript 联合类型,共 25 个成员。extensions/types.ts:1034(计数由 tools/stats.sh 从该联合类型程序化统计。)
典型是 context 事件——它就是 第 05 章那道 transformContext 闸门的全部内容:coding-agent 把它原样转交给扩展。返回 { messages } 就能改写这次请求的上下文。
还有 tool_call 事件可以 block 一次工具执行并附 terminate——对应 第 04 章的 beforeToolCall。
注意这里的边界:扩展能改「喂进去什么」和「要不要执行」,但改不了循环的形状(第 03 章 §3)。这不是疏漏,是 CONTRIBUTING.md 写明的克制。
ctx.newSession() / ctx.fork(entryId) / ctx.navigateTree(targetId) / ctx.switchSession(path) ——第 07 章那棵会话树的操作对扩展是开放的。文档专门有一节叫「Session replacement lifecycle and footguns」,本身说明这组 API 容易用错。
换来的是扩展能做的事没有上限:覆盖内建工具、把执行路由进 micro-VM、接管渲染、改上下文、注册新的交互模式。第 15 章里 Gondolin 那套隔离方案本身就是一个扩展——这在一个有沙箱的插件系统里是做不到的。
代价直白:装一个扩展等于给它 shell 权限。文档没有回避,把它放在最显眼的位置。
第 02 章提到,「不属于核心的做成扩展」这条规则要可执行,取决于扩展系统够不够强。从 25 类事件 + 工具覆盖 + 渲染接管来看,它确实够强——被拒的 PR 有真实的去处。
但也有明确做不到的:改循环控制流、加新的 LLM provider(那是 pi-ai 的事,走的是另一条路 docs/custom-provider.md)、以及任何需要修改 packages/agent 语义的事情。
25 个事件里同时有 agent_end 和 agent_settled。这个区分直接来自 第 03 章的机制:agent_end 意味着循环不再发事件,而「settled」还要等所有被 await 的订阅者跑完。把这两个时刻分成两个事件,是因为它们真的不同——想在「一切都结束了」之后做事的扩展,要用后者。
context 事件每 turn 触发一次。在这里做网络请求或重计算,成本乘以 turn 数(第 05 章 §4)。content ?? []),说明这在实践中真的发生过。fork / switchSession / navigateTree 会替换当前会话对象,持有旧引用的代码会静默失效。场景你装了一个第三方 pi 扩展。它能读你的 ~/.ssh 私钥吗?
B。extensions.md:111 这与「不做权限系统」是同一个决定的两面:进程内不存在边界,所以也没有给扩展划边界。
场景Gondolin 扩展能把内建工具路由进 micro-VM 执行。它靠的是扩展系统的哪个能力?
A。覆盖内建工具是这个扩展系统与多数插件系统的关键差异——隔离方案本身能作为扩展存在,前提就是扩展能替换 read/bash 这些名字。
ac4ac9e(2026-08-07 快照)。