AG-UI 仓库 npm 包许可证策略校验:package-licenses 工具链与 policy.json 详解
AG-UI 仓库 npm 包许可证策略校验:package-licenses 工具链与 policy.json 详解
导读:本文聚焦 AG-UI 仓库(Agent-User Interaction Protocol)中用于治理公共 npm 包许可证合规性的
scripts/package-licenses工具链,围绕policy.json策略文件、check.mjs实现、test/check/pack/archive四种运行模式展开讲解。读完本文,你将掌握 AG-UI 是如何以“策略文件驱动”的方式统一校验源码清单与真实打包产物的 SPDX 字段和完整 LICENSE 文本,并理解其在发布前检查、标签修复流程中的落地方式,可直接复用于你自己的多包 Monorepo 仓库。
一、什么是 package-licenses:策略与检查分离的许可证治理
在 AG-UI 这类包含数十个 TypeScript 包(SDK、中间件、框架集成)的 Monorepo 中,许可证合规是发布前的硬性门槛。scripts/package-licenses/ 目录下维护了一套不依赖第三方工具的自研检查脚本,核心设计原则是:
- 策略独立:
policy.json记录每个公共 npm 包及其“预期许可证”,该文件独立于各包package.json中的license字段——也就是说,策略是权威基准,包清单中的字段是待检对象,两者不一致即视为失败。 - 新增包必须登记:引入新的公共包时,必须同步在
policy.json中添加条目;同时,已有第三方包的许可证文本必须原样保留,不得擅自替换。
该目录共包含五个文件:
| 文件 | 职责 |
|---|---|
| policy.json | 公共 npm 包与预期许可证、许可文本路径的映射表 |
| check.mjs | 检查主程序,支持 check / pack / archive 三种模式 |
| check.test.mjs | 基于 Node 内置 node:test 的单元测试 |
| project.json | Nx 工程定义,声明 test / check / pack 三个 target |
| README.md | 使用说明(本文所依据的主体文档) |
二、policy.json:28 个包的许可证策略基准
policy.json 是一个 JSON 数组,当前包含 28 个条目,覆盖三类包:TypeScript SDK 包(sdks/typescript/packages/*)、中间件(middlewares/*)以及各框架集成(integrations/*/typescript)。每个条目包含四个字段:
{
"directory": "integrations/a2a/typescript",
"names": ["@ag-ui/a2a"],
"license": "MIT",
"notice": "integrations/a2a/typescript/LICENSE"
}
| 字段 | 含义 | 说明 |
|---|---|---|
directory |
包所在目录(相对仓库根) | 用于定位该包的 package.json,也是 archive 模式匹配策略条目的依据 |
names |
允许的包名列表 | 数组形式,可覆盖同一目录下包名可能变化的情况;verifyArchive 会校验归档内 manifest.name 必须命中该列表 |
license |
预期 SPDX 许可证标识 | 必须与包 package.json 的 license 字段完全一致 |
notice |
审批通过的 LICENSE 文本文件路径 | 检查时会将该文件内容与打包产物内的 package/LICENSE 做逐字符(归一化后)比对 |
从当前策略可以看出许可证分布:
- 绝大多数包使用 MIT(如
@ag-ui/client、@ag-ui/core、@ag-ui/a2a、@ag-ui/mastra之外的集成等); - Apache-2.0 用于
integrations/community/spring-ai/typescript(@ag-ui/spring-ai)和integrations/mastra/typescript(@ag-ui/mastra); - CLI 包以
create-ag-ui-app为包名,登记于sdks/typescript/packages/cli条目。
值得强调的是,MIT 与 Apache-2.0 在策略中保持互不混同:即使某个包实际写入了合法的 Apache-2.0 标识,只要策略预期是 MIT,检查依然失败(这一点有对应测试用例佐证,见下文第五节)。
三、三个核心命令:test / check / pack
仓库根目录下(Nx 工作区)直接运行即可:
pnpm nx run package-licenses:test
pnpm nx run package-licenses:check
pnpm nx run package-licenses:pack
三个 target 在 project.json 中均通过 nx:run-commands 执行器定义,且全部关闭了 Nx 缓存("cache": false),保证每次运行都真实校验:
| target | 实际执行的命令 | 作用 |
|---|---|---|
test |
node --test scripts/package-licenses/check.test.mjs |
运行 Node 内置测试运行器,验证 verifyArchive 的各类通过/拒绝分支 |
check |
node scripts/package-licenses/check.mjs check |
纯源码级检查:扫描所有公共包,核对 package.json 的包名、许可证字段与策略一致 |
pack |
node scripts/package-licenses/check.mjs pack |
在临时目录真实执行 pnpm pack 产出归档,再逐包校验 SPDX 字段与完整 LICENSE 文本 |
pack 模式的关键语义
- 真实产物校验:
pack目标在临时目录(os.tmpdir()下创建package-licenses-*)中为每个策略条目执行pnpm pack --pack-destination <临时目录>,然后对生成的.tgz归档运行verifyArchive,比对的是打包后的 SPDX 字段与完整的 LICENSE 文件,而非源码文件。 - 不发布:整个过程只打包、不执行任何
npm publish/pnpm publish,可安全地在发布前运行。 - CLI 包的前置构建:由于
create-ag-ui-app需要先编译,README 明确要求先在构建环境中执行CLI_ENV=local pnpm nx build cli,再运行pack;其他纯源码包无需构建即可用该目标检查源码打包形态。 - 发布路径的二次把关:正式发布流程在最终产物发布前,也会对构建出的归档再执行一次检查(“The release paths also check the final built archive immediately before publication”),形成“源码检查 + 打包检查 + 发布前检查”的三层防线。
四、check.mjs 实现剖析:verifyArchive 与 checkSources
check.mjs 是全部检查逻辑的实现,可拆为两个核心函数加一个主入口。
4.1 verifyArchive:归档内容校验
verifyArchive(archive, expected) 负责校验单个 .tgz 归档,流程如下:
- 清单完整性:通过
tar -tzf列出归档内文件,强制要求package/package.json与package/LICENSE恰好各存在一份,多一个或少一个都直接抛错。 - 安全读取:通过
tar -xOf <归档> <文件>将目标文件内容读到标准输出,不落盘、不解压、不执行任何包内代码;同时用maxBuffer: 1024 * 1024对单文件读取做上限约束(文件清单读取上限为 16 MB),从实现层面规避了解压炸弹与恶意脚本风险。 - 包名校验:解析归档内
package.json,manifest.name必须命中策略条目的names数组,否则抛出Unexpected package name。 - SPDX 字段校验:
manifest.license必须与策略中的license完全相等,缺失或不同都会失败。 - 完整 LICENSE 文本校验:将归档内
package/LICENSE与策略notice指向的源码文件内容做比对,比较前先经过normalize()(把\r\n统一为\n并trim())——注意,这只是归一化行尾与首尾空白,不会对许可文本本身做任何宽容处理,文本不完整(如只有MIT三个字母)或与审批文本不同都会被拒绝。
4.2 checkSources:源码级策略强制
checkSources(entries) 负责扫描源码树,做两件事:
- 公共包强制登记:用
git ls-files -z "**/package.json"列出所有被 Git 跟踪的package.json(-z以 NUL 分隔,安全处理含空格路径),逐个解析。只要包非私有(无"private": true)且包名命中@copilotkit/*、@copilotkitnext/*、@ag-ui/*命名空间或等于copilotkit/create-ag-ui-app,就必须存在对应的策略条目,否则抛出Public package ... needs a license policy entry。这从制度上杜绝了“新增公共包却忘记登记许可证”的漏网情况。 - 策略条目反向校验:对
policy.json中每个条目,读取其directory/package.json,确认包名命中names、license字段与策略一致,且notice指向的文件非空(空许可证文本文件也会报错)。
主入口 main([mode, archive, directory]) 根据 mode 分发到 archive、check、pack 三条路径;pack 路径对每个条目在其 packDirectory ?? directory 目录下执行 pnpm pack,并在 finally 中清理临时目录。脚本还通过 process.argv[1] 与 import.meta.url 比对,仅在作为主程序直接执行时运行 main,因此 verifyArchive 可被测试文件干净地导入复用。
五、archive 模式:检查单个已有归档
对已经存在的归档文件,无需走完整 pack 流程,可直接用单归档模式:
node scripts/package-licenses/check.mjs archive /absolute/path/package.tgz <policy-directory>
其中 <policy-directory> 必须是策略条目中登记的 directory(如 sdks/typescript/packages/client),脚本据此定位对应的策略条目与 notice 文本。该模式同样遵守“只读不执行”原则:仅通过 tar 流式读取元数据与文本,不提取、不执行任何包内代码。
archive 模式会拒绝以下任一情况:
- 未知的包名(不在
names列表中); license字段缺失或与策略不符;- 归档内缺少
package/LICENSE; - LICENSE 文本不完整或与审批文本不一致。
六、测试用例:verifyArchive 的行为契约
check.test.mjs 使用 Node 内置 node:test + assert/strict 编写,不依赖任何第三方测试框架。每个测试通过 setup() 在临时目录里手工构造一个“真实 npm 形状”的归档(tar -czf 打出 package/ 目录),随后直接调用导入的 verifyArchive。8 个用例完整覆盖了通过/拒绝矩阵:
| 测试用例 | 预期行为 |
|---|---|
| 预期许可证 + 完整 LICENSE 文本 | 通过 |
| 有 LICENSE 文件但 SPDX 字段缺失 | 拒绝(license must be MIT) |
| 合法但不同的 SPDX 值(如 Apache-2.0 预期 MIT) | 拒绝 |
| 缺少 LICENSE 文件 | 拒绝(LICENSE) |
文本截断或不同(如只写 MIT) |
拒绝(notice) |
| 归档属于其他包 | 拒绝(name) |
| 预期即为 Apache-2.0 | 通过(证明各许可证保持独立、不被替换) |
商用包以 UNLICENSED + 完整 notice |
通过 |
这套测试把“MIT 与 Apache-2.0 互不混淆”“UNLICENSED 商用包可单独放行”“缺字段即失败”等关键策略固化为可回归的行为契约,任何对 verifyArchive 的改动都必须先通过这些用例。
七、发布与标签修复的合规流程
README 对发布环节给出了明确的许可证治理纪律:
- 发布修复闭环:通过正常发布流程发布新版本后,必须核对 registry 上的元数据并下载其归档再次检查,确认无误后才可将该版本标记为“已修复”。
- 标签只指向已验证版本:仅允许把受支持的标签(如
latest)更新到经过验证的版本;操作时需要记录旧标签与新标签各自的指向,以便追溯。 - 旧标签不轻易删除:未经评审不得移除旧标签;同时,历史版本一旦发布即不可变,即使后续用修正后的版本更新了默认标签,历史不可变版本仍可能保留缺失的元数据——即“修正默认标签 ≠ 历史版本自动合规”,需要在发布说明中如实反映。
这一节本质上是把“许可证检查”与“发布事故的标签修复”串成一条治理流程:检查工具负责发现与验证,修复流程负责在符合可追溯、可评审的前提下收敛默认标签指向。
八、适用边界与注意事项
最后,README 给出了一句重要的边界说明:许可证检查不能替代完整的依赖或安全审计("A license check does not replace a full dependency or security audit")。这提醒使用者:
- 本工具只解决“包名 + SPDX 字段 + LICENSE 文本与策略一致”的合规问题,不扫描依赖漏洞、不分析许可证的传染性条款;
check模式仅覆盖git ls-files跟踪的package.json,未纳入 Git 的新增包不会被扫描;pack模式依赖本机pnpm与tar环境,且 CLI 包需先构建(CLI_ENV=local pnpm nx build cli)。
对于需要在自有多包仓库复刻这一方案的团队,最小落地路径是:拷贝 policy.json 的结构维护策略表,用 check.mjs 的 checkSources + verifyArchive 两个函数分别守住“源码登记”与“产物文本”两道闸门,再以 check.test.mjs 的用例集保证检查逻辑本身的稳定性——这正是 AG-UI 仓库在数十个公共包场景下维持许可证一致性的可复用工程实践。