LLM之Agent(六十九)|PI(八)发布流程与供应链安全

pi 团队把"npm 依赖变更"当成需要评审的代码变更来对待:从版本号怎么升、发布怎么触发,到锁文件谁说了算、CI 怎么审计依赖,每一环都有明确、可验证的机制,而不是靠人肉记忆。

学习目标

  • 理解 lockstep 版本管理是什么、为什么所有包共享一个版本号。
  • 走一遍 npm run release:patch/release:minor 背后完整的发布流程。
  • 理解 GitHub Actions OIDC trusted publishing 如何让发布不再需要本地 npm publish/OTP。
  • 知道 .npmrcsave-exactmin-release-age 分别防的是什么风险。
  • 理解 package-lock.jsonnpm-shrinkwrap.json、生命周期脚本白名单三者如何共同构成依赖锁定治理。
  • 知道 CI 里 npm audit 检查了什么、多久跑一次。

Lockstep 版本管理

发布流程原来直接写在 AGENTS.md 的 "Releasing" 一节里,现在这一节已经收缩成一句话,指向一个独立的技能文档 .pi/skills/release.md------把可复用的操作流程从 AGENTS.md 里搬出去、单独维护,是这个仓库最近的一个整理动作(后面会再遇到一次,见"从源码运行 pi"相关章节)。release.md 开门见山:

Lockstep versioning : all packages share one version; every release updates all together. patch = fixes + additions, minor = breaking changes. No major releases.

实际打开各包的 package.json 也能验证这一点:packages/agentpackages/aipackages/chordpackages/coding-agentpackages/tuipackages/telemetrypackages/protocolpackages/clientpackages/serverpackages/session-backends/sqlite-nodeversion 字段目前全部是同一个值(例如都是 0.85.1)。新加入的 chord 包也被纳入了这条 lockstep 规则,并没有因为它"可以脱离 pi 独立使用"而单独走自己的版本号------这与很多 monorepo 采用的"每个包独立语义化版本"策略不同,pi 选择让所有可发布包永远保持版本号一致,简化了"这个功能到底在哪个版本引入"的心智负担。

需要特别注意版本语义的错位用法:minor 版本号在 pi 里被用来表示破坏性变更(breaking changes)patch 才对应"修复 + 新增功能",而且明确不做 major 发布0.x.y 会一直停留在 0 大版本)。这与严格语义化版本(SemVer)里"minor 表示向后兼容的新特性,major 才表示破坏性变更"的惯例不同,属于这个项目自定义的版本规则,阅读或参与贡献时不能想当然套用标准 SemVer 理解。

对应的根目录脚本:

复制代码
"version:patch": "npm version patch --workspaces --no-git-tag-version --no-workspaces-update && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts",
"version:minor": "npm version minor --workspaces --no-git-tag-version --no-workspaces-update && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts",
"version:major": "npm version major --workspaces --no-git-tag-version --no-workspaces-update && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts"

npm version <bump> --workspaces 让每个 workspace 包各自完成一次版本号递增,scripts/sync-versions.js 再把所有包的版本号收敛成同一个值(并同步内部工作区依赖的版本约束),最后用 --package-lock-only --ignore-scripts 重新生成 package-lock.json(只更新锁文件,不做真正的 npm install 副作用,也不跑生命周期脚本)。

发布流程:npm run release:patch / release:minor

真正触发发布的入口:

复制代码
"release:patch": "node scripts/release.mjs patch",
"release:minor": "node scripts/release.mjs minor",
"release:major": "node scripts/release.mjs major"

.pi/skills/release.md 描述了完整的人工 + 自动化流程,按顺序是:

  1. 更新 CHANGELOG :发布前要确认是否已经在 main 最新提交上跑过 /cl 提示词,审计并更新每个包 CHANGELOG.md 里的 [Unreleased] 小节。
  2. 本地冒烟测试 :用 npm run release:local -- --out /tmp/pi-local-release --force(对应脚本 scripts/local-release.mjs)在仓库之外的目录构建一次未发布的产物,分别对 Node 安装版和 Bun 二进制版跑 --help--version--list-models-p "Say exactly: ok" 以及一次真正的交互式会话,验证启动、鉴权、真实一次问答都没问题。之所以要"在仓库之外"跑,是为了确保它不能解析到 workspace 本地文件,模拟的是终端用户拿到已发布包之后的真实体验。
  3. 执行发布脚本 :PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:patch
    PI_ALLOW_LOCKFILE_CHANGE=1 npm_config_min_release_age=0 npm run release:minor
    这里出现的两个环境变量都只应该在发布这条命令里临时设置:PI_ALLOW_LOCKFILE_CHANGE=1 放行发布过程中必然发生的锁文件变更提交(见下一节的 pre-commit 锁文件守卫);npm_config_min_release_age=0 临时关闭 .npmrc 里的 min-release-age 年龄限制,因为发布时可能需要立即解析刚发布不久的内部包版本,正常开发环境下这道限制应保持开启。
    release.mjs 会依次完成:递增所有包版本号、更新 changelog、重新生成发布相关产物、跑一次 npm run check、提交 Release vX.Y.Z、打 tag vX.Y.Z、在各 changelog 里补一个新的空 [Unreleased] 小节、提交 Add [Unreleased] section for next cycle,最后 push main 分支和标签。标签一旦 push 出去就不能重跑发布脚本
  4. CI 校验并公告 npm 发布结果 :push vX.Y.Z 标签会触发 .github/workflows/build-binaries.yml
  5. 发布或公告失败时 :检查失败的 job;发布脚本本身是幂等的(会跳过 npm 上已经存在的包版本),公告 job 会在更新标记前重新核实包的可用性;修复问题后重跑失败的 job/workflow 即可,不要为同一个版本重跑 release:patch/release:minor

GitHub Actions OIDC Trusted Publishing

build-binaries.yml 里的 publish-npm job 是发布链路的关键一步:

复制代码
publish-npm:
  runs-on: ubuntu-latest
  needs: stage-github-release
  environment: npm-publish
  permissions:
    contents: read
    id-token: write
  steps:
    ...
    - name: Upgrade npm for trusted publishing
      run: |
        npm install -g npm@11.16.0 --ignore-scripts
        npm --version
    - name: Publish npm packages
      run: node scripts/publish.mjs

几个关键点:

  • permissions.id-token: write 是 GitHub Actions OIDC(OpenID Connect)的开关:CI job 会向 GitHub 的 OIDC 提供方申请一个短期身份令牌,npm 注册表验证这个令牌来确认"这次发布确实来自你声明的这个仓库、这个工作流",而不需要在 CI 里配置任何长期存活的 npm token。
  • environment: npm-publish 把发布这一步绑定到一个受保护的 GitHub Environment,可以配合仓库设置对这个 Environment 加审批、限制可触发的分支/标签等治理措施。
  • 升级到 npm@11.16.0 是因为 trusted publishing 依赖较新版本 npm CLI 的支持。
  • 整个过程"不需要本地 npm publishnpm whoami、OTP(一次性密码)或 WebAuthn 流程"------.pi/skills/release.md 原话如此,也就是说没有任何人能在自己电脑上手动 npm publish 出一个官方版本,发布权限完全收敛在这一条 CI 流水线里。
  • 发布成功后,announce-pi-dev-release job(依赖 publish-npm 成功)会重新核实每一个公开 workspace 包在 npm 上确实能解析到这次发布的精确版本号、且对应 tarball 可下载,然后把"已验证的发布标记"写入 Cloudflare R2。pi.dev/api/latest-version 只读取这个已验证标记,规则明确规定它"绝不能在这个 job 成功之前,就从 npm 侧提前宣布一个发布"------避免用户端看到"最新版本号"已经更新、但实际 npm 包还没完全就绪或校验失败的竞态。

.npmrcsave-exactmin-release-age

仓库根目录 .npmrc 只有两行:

复制代码
save-exact=true
min-release-age=2
  • save-exact=true :让 npm install <pkg> 默认写入精确版本号(1.2.3)而不是范围版本号(^1.2.3)到 package.json。这与前面《构建测试与开发流程》提到的 check:pinned-deps 检查是配套的------.npmrc 保证"新增依赖时默认就是精确版本",check:pinned-deps.mjs 再兜底检查"是否有人手改成了范围版本"。
  • min-release-age=2 :要求 npm 在解析依赖版本时,只考虑发布时间超过 2 天的版本。这是防御供应链投毒的一个具体机制:如果某个依赖的维护者账号被盗,攻击者发布一个恶意版本后,通常会在短时间内被社区或自动化工具发现并从注册表下架;min-release-age 相当于给"发现窗口"留出缓冲时间,避免在依赖发布的当天就把这个版本引入到锁文件里。发布流程里之所以要临时把它设为 0npm_config_min_release_age=0),是因为发布时经常需要立刻解析刚刚发布的、来自本项目自己的内部包版本,此时这条年龄限制反而会挡住正常发布。

README 的 "Supply-chain hardening" 一节把这条规则概括为:

Direct external dependencies are pinned to exact versions. Internal workspace packages remain version-ranged.

也就是说精确锁版本只针对外部 依赖,@earendil-works/pi-* 这类内部工作区依赖仍然使用类似 ^0.84.1 的范围版本------因为内部包版本始终 lockstep 递增,用范围版本反而方便同一次发布内互相引用。

依赖锁文件治理:谁是唯一真相源

README 明确定义:

package-lock.json is the dependency ground truth.

围绕这句话,实际落地了三层机制:

  1. 提交守卫.husky/pre-commit 钩子会先执行 node scripts/check-lockfile-commit.mjs。这个脚本会对比 HEAD:package-lock.json 和暂存区里的 package-lock.json,如果检测到锁文件被改动且没有设置 PI_ALLOW_LOCKFILE_CHANGE=1(或 true/yes),就会阻止这次提交,逼着开发者显式确认"我是真的想改锁文件",而不是不小心在某次 npm install 后顺手把锁文件变更也提交进去。
  2. coding-agent 的 shrinkwrap :发布出去的 CLI 包 @earendil-works/pi-coding-agent 会额外附带 packages/coding-agent/npm-shrinkwrap.jsonfiles 字段里显式包含这个文件),由 scripts/generate-coding-agent-shrinkwrap.mjs 从根 package-lock.json 生成,目的是让通过 npm 直接安装这个 CLI 包的最终用户,其传递依赖 版本也被精确锁定,不会因为上游某个间接依赖发新版本而在安装时产生漂移。npm run check 里的 check:shrinkwrap(带 --check 参数)负责校验这个 shrinkwrap 文件与根锁文件是否保持同步。
  3. 生命周期脚本白名单generate-coding-agent-shrinkwrap.mjs 内部维护了一个显式的 allowedInstallScriptPackages 白名单,例如:const allowedInstallScriptPackages = new Map( \["@google/genai@2.21.0", "preinstall is a no-op in the published package",
    "esbuild@0.28.2", "postinstall selects and verifies the platform-specific esbuild binary",
    "protobufjs@7.6.6", "postinstall only warns about protobufjs version scheme mismatches",
    ]);
    这张表会随着依赖版本升级和新依赖加入持续变化------比如 esbuild 这一条就是新加入 monorepo 的 chord 包引入 esbuild 作为依赖之后,才第一次出现在这张白名单里的,具体版本号和条目数量不必死记,重点是理解这个机制本身。README 对这一机制的描述是:"Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed." 也就是说:只要新增/更新的某个依赖带有生命周期脚本(hasInstallScript),而它没有出现在这张白名单里,check:shrinkwrap 就会失败------逼着人工去读这个脚本到底做了什么、判断是否安全,然后显式加一条带理由的白名单记录,而不是默默放行任何带安装脚本的新依赖。

CI 里的 npm audit

.github/workflows/npm-audit.yml 是一个独立的、定时触发的工作流:

复制代码
on:
  schedule:
    - cron: '37 7 * * *'
  workflow_dispatch:

jobs:
  audit:
    steps:
      - run: npm ci --ignore-scripts --no-audit --no-fund
      - name: Audit production vulnerabilities
        run: npm audit --omit=dev --audit-level=moderate
      - name: Verify registry signatures
        run: npm audit signatures --omit=dev

三个细节值得注意:

  • 每天定时跑一次(UTC 07:37),而不是只在 PR 或 push 时跑------这样即使代码完全没变,只要 npm 注册表新披露了某个已安装依赖的漏洞,也能在最多一天内被发现。
  • 先用 npm ci --ignore-scripts --no-audit --no-fund 装依赖(--no-audit 是为了避免 npm ci 自带的审计和这里手动跑的 npm audit 重复),再用两条独立命令分别做:npm audit --omit=dev --audit-level=moderate 检查生产依赖(跳过 devDependencies)里中等及以上严重程度的已知漏洞;npm audit signatures --omit=dev 校验这些包的注册表签名,确认包内容没有被篡改。
  • README 里把这两条命令合称为"a scheduled GitHub workflow runs npm audit --omit=dev plus npm audit signatures --omit=dev",与 CI 文件内容完全对应。

从 Release 源码包构建 standalone 二进制

README 的另一段面向"想自己打包 pi 二进制"的用户或发行版维护者:

复制代码
VERSION="<release-version>"
tar -xzf "pi-${VERSION}-source.tar.gz"
cd "pi-${VERSION}"
./scripts/build-binaries.sh --offline-model-data --platform linux-x64 --out "$PWD/out"

GitHub Release 附带的版本化源码归档由该 release 的 SHA256SUMS 文件校验完整性;--offline-model-data 让构建使用归档里自带的模型数据快照而不是联网刷新(对应上一篇提到的 build:offline);这个脚本本身仍然会执行"装依赖、构建整个 monorepo、编译 Bun 可执行文件、拷贝运行期资源"的完整流程。脚本的参数集最近被精简过:如果包维护者希望自己单独管理依赖,现在只需要传 --skip-install 跳过 npm ci 这一步(旧版本里同时存在的 --skip-deps 参数已经不再出现在脚本里);如果只想重新打包已经构建好的产物、跳过整个包构建阶段,可以传 --skip-build。本地安装、文档中演示的 npm 安装步骤,以及 pi update --self 自更新命令,在支持的地方也都统一使用 --ignore-scripts

动手练习

  1. 只读地跑一次 node scripts/check-pinned-deps.mjs(在仓库根目录),观察它如何遍历所有 package.json 并报告不符合精确版本号规则的依赖(正常情况下应该没有输出,因为仓库自身是通过这项检查的)。
  2. 阅读 scripts/generate-coding-agent-shrinkwrap.mjsallowedInstallScriptPackages 这个 Map,思考:如果你要新增一个带 postinstall 脚本的依赖,你需要做哪几件事才能让 npm run check 通过?
  3. 对照 .github/workflows/build-binaries.ymlpublish-npm job 的 permissions 字段,解释一下为什么它只需要 id-token: write 而不需要在仓库 secrets 里配置任何 NPM_TOKEN

小结

pi 的发布与供应链安全体系可以概括为"把每一个可能被人为疏忽绕过的环节都变成自动化的强制检查":lockstep 版本号消除了"这个包到底该发哪个版本"的判断成本;发布权限完全收敛到 GitHub Actions 的 OIDC trusted publishing,本地无法手动发布;.npmrc 的两条配置分别管住了"依赖版本精度"和"依赖发布新鲜度"两个风险维度;package-lock.json 作为唯一真相源,配合 pre-commit 守卫、coding-agent 专属 shrinkwrap、生命周期脚本白名单三层机制,防止依赖状态被静默篡改;每日定时的 npm audit 则是持续运行的最后一道监控网。这些机制单独看都不复杂,但组合在一起,构成了一套相当扎实的供应链防护。

相关推荐
罗西的思考1 小时前
[Agent Memory / 强化学习] MemPO源码学习笔记 — (2)— 训练
人工智能·深度学习
Forerror20261 小时前
大模型网关解决什么问题?MAI Gateway(魔芋企业级AI网关)给出企业级答案
人工智能·ai工具·ai安全·maigateway·企业ai网关·企业级大模型治理网关·大模型财务治理
Gu0Qiang1 小时前
从 0 到 1 打造 AI 提示流编排器:条件分支路由、Kahn 图剪枝与 HTTP 节点实战(开源系列 06)
人工智能·github
驭风少年君1 小时前
Codex 从入门到进阶
人工智能·aigc·codex
JAI科研1 小时前
YOLO 完全指南(七):YOLO识别工程化 (上)
人工智能·深度学习·神经网络·yolo·目标检测·计算机视觉·transformer
Forerror20261 小时前
深入理解大模型网关解决什么问题:MAIGateway(魔芋企业级AI网关)架构与核心价值解读
人工智能·ai网关·maigateway·大模型财务管控·企业级大模型治理网关
AI深栈1 小时前
第 10 章 · Embedding、VectorStore 与 RAG
java·人工智能
jsl_jsl_jsl1 小时前
《一个 Agent 平台怎么接入多家大模型:Provider 槽位制设计》
人工智能
ι:2 小时前
Codex 自主调用 Visio 绘图完整教程
人工智能·visio·codex