Pi Coding Agent 03:扩展体系——Skill、Extension 与 Package 的职责边界

在许多开源 Agent 项目的迭代过程中,扩展系统经常会陷入一种令人啼笑皆非的架构混乱:

  • 明明一份几十行的 Markdown 检查清单就能规范好的代码审查步骤,开发者偏偏要写几百行高权限的 TypeScript 代码把它封装成一个黑盒工具;
  • 明明必须 100% 强制拦截的“严禁删除生产数据库”安全红线,却仅仅写在 System Prompt 里祈祷模型道德自律;
  • 当团队需要共享一套能力时,大家只能在聊天工具里机械地互相复制粘贴目录文件。

Pi 深刻洞察了这一痛点,在扩展架构中建立了边界分明的“三层分离机制”:

  • Skill(技能):改变模型知道什么(按需加载的领域操作知识与流程);
  • Extension(扩展):改变运行时能做什么(同进程内执行原生 TypeScript 代码,拦截命令、注入工具);
  • Package(包):改变资源如何交付(将 Skill、Extension、Prompt 与 Theme 进行版本化封装与分发)。

一、先用决策表理清需求边界

在着手编写扩展前,切勿盲目选择“能力最强”的方式,而应根据需求的真实本质进行精确匹配:

实际业务诉求 首选扩展机制 架构选择背后的核心考量
团队规范、代码发布检查清单、排障手册 Skill 纯文本操作指引,按需进入模型上下文,不增加多余的常驻工具与权限面
由用户显式触发的一段可复用 Prompt Prompt Template 相当于快捷宏展开,不需要模型在全局自动匹配
新增自定义工具、扩展命令行斜杠命令、注入状态栏 Extension 必须由程序逻辑在当前运行进程内注册功能或监听生命周期
无条件强制拦截特定高危命令(如 rm -rf) Extension + 外部隔离 拦截必须由程序代码执行,但必须配合底层沙箱防御绕过
将打磨好的规范、工具或界面主题跨团队分发 Package 提供统一的 Git / npm 安装依赖管理与版本更新通道

二、Skill:渐进披露(Progressive Disclosure)的精妙设计

Skill 是一个高度自包含的目录,它的核心入口是 SKILL.md,并可以携带专属脚本与参考文档:

1
2
3
4
5
6
release-review/
├── SKILL.md # 核心说明与元数据
├── scripts/
│ └── check-version.sh # 供模型在必要时执行的辅助脚本
└── references/
└── rollback-policy.md # 复杂的回滚补充参考指引

SKILL.md 的核心灵魂是它的 YAML Frontmatter:

1
2
3
4
5
6
7
8
9
10
---
name: release-review
description: 在正式发布前检查语义化版本号、全量测试覆盖、变更范围以及紧急回滚条件。
---

# Release Review 指南

1. 读取当前项目的 package.json 与 CHANGELOG.md;
2. 运行与本次变更相关的单元测试与静态代码检查;
3. 输出检查证据;任何必需项失败时立即终止发布流程。

为什么说渐进披露(Progressive Disclosure)拯救了上下文窗口?

如果把团队所有的规范文档(可能长达上万行)一股脑全量塞进系统提示词,不仅费用昂贵,还会严重稀释模型的注意力。
Pi 采用了精密的四步渐进式披露机制:

[!WARNING]
警惕:Skill 绝不是安全边界!
Skill 无论写得多么言之凿凿(例如“发布前必须经过双人确认”),它在本质上仅仅是模型的上下文阅读材料。模型依然可能因为幻觉或 Prompt 注入而跳过这些步骤。涉及物理权限的强规则,必须交由 Extension 或外部沙箱强制执行。


三、Extension:掌控运行时的同进程原生代码

与纯文本的 Skill 截然不同,Extension 是运行在 Pi 主 Node.js 进程内部的原生 TypeScript 模块。

当前官方推荐的 Extension 写法是一个标准的工厂函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function auditExtension(pi: ExtensionAPI) {
// 1. 监听所有工具调用,在执行前实施安全准入拦截
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash") return;

const command = String(event.input.command ?? "");
const dangerous = /\b(rm\s+-rf|sudo|drop\s+database)\b/i.test(command);
if (!dangerous) return;

// 重点:如果在无头(Headless)或 CI 模式下,无法弹出 UI,必须 Fail-Closed!
if (!ctx.hasUI) {
return { block: true, reason: "无头模式下禁止执行高危命令,强制阻断!" };
}

// 交互模式下唤起 TUI 人工确认对话框
const allowed = await ctx.ui.confirm("高危命令拦截", `确认允许运行: ${command} 吗?`);
if (!allowed) {
return { block: true, reason: "用户主动拒绝了该命令的执行" };
}
});

// 2. 注册自定义斜杠命令
pi.registerCommand("audit-status", {
description: "查看当前安全审计扩展的运行状态",
handler: async (_args, ctx) => {
ctx.ui.notify("安全审计规则已生效", "info");
},
});
}

[!IMPORTANT]
关键设计:无头模式下的 Fail-Closed(失败默认阻断)
在上面的代码中,请注意 if (!ctx.hasUI) return { block: true }。当 Pi 在自动化脚本、CI/CD 或无头 JSON 模式下运行时,根本没有交互界面。此时必须严格拒绝高危操作!如果因为没有弹窗而默认放行,整个安全防御就会在最脆弱的自动化环节被彻底击穿!


四、加载顺序与 Project Trust(项目信任机制)

在进入一个未知的 Git 代码仓库时,Pi 是如何加载这些扩展资源的?

很多人误以为“只要不信任项目,Pi 就什么文件都不读”。实际的加载流程要微妙得多:

这个时序揭示了一个关键安全事实:AGENTS.md 等纯上下文文件默认会在信任决策前被读取(以便让模型理解当前工程背景)。这意味着恶意仓库完全可以在 AGENTS.md 里埋伏间接提示词注入。因此,“项目未信任”防住了恶意代码在本地立刻运行,但绝没有完全防住模型被恶意文本诱导。


五、Package:将能力打包分发

Package 是承载 Skill、Extension、Prompt 与 Theme 的标准分发格式。

在 package.json 中,通过显式声明 pi 清单即可完成自描述:

1
2
3
4
5
6
7
8
9
{
"name": "@acme/pi-audit-pack",
"version": "1.0.0",
"type": "module",
"pi": {
"extensions": ["./extensions/audit.ts"],
"skills": ["./skills/release-review"]
}
}

安装命令非常灵活:

1
2
3
4
5
6
7
8
# 全局安装(对本机所有项目生效)
pi install npm:@acme/pi-audit-pack

# 项目级局部安装(写入当前目录的 .pi/settings.json,可随 Git 共享)
pi install npm:@acme/pi-audit-pack -l

# 单次临时运行(仅在本次进程中挂载,不持久化)
pi -e npm:@acme/pi-audit-pack

在引入任何第三方 Package 时,请时刻谨记:一个包的安全风险,取决于其中包含的最高风险组件。 只要包内包含一个 Extension,它在被激活时就拥有了与你本地开发环境完全等同的全部系统权限。