# AI 智能体发布软件包，避免发布令牌泛滥

AI 编程智能体应该能够安装依赖，但不应拥有发布、取消发布、删除或重新标记软件包的权限。这些操作在命令行中看起来相邻，后果却完全不同。把它们视为同一种权限，会让日常开发工作变成发布权限。

我见过团队为了让智能体执行一次无害的安装命令，逐渐把发布凭据散落到各处。令牌随后出现在 shell 环境、配置文件或命令输出中，同一次运行里的任何工具都可能再次使用它。后续故障很少始于戏剧性的入侵。它往往始于智能体对着错误的注册表运行 `npm publish`，在任何人查看 tarball 前就移动了 `latest`，或者因为过于字面地理解测试清理请求而尝试取消发布。

## 读取注册表不等于拥有发布权限

软件包安装和软件包发布使用同一类注册表端点，但它们回答的是不同的信任问题。下载是在问：「这个进程可以获取某个构件吗？」发布是在问：「这个进程可以用这个名称创建一个公开或组织可见的版本吗？」删除或取消发布是在问：「这个进程可以改变一个其他构建可能已经在使用的版本的历史和可用性吗？」

不要因为注册表客户端让这些操作看起来相似，就把这些问题合并到一个凭据中。软件包管理器可能会从同一个 `.npmrc` 文件读取 `npm ci`、`npm publish`、`npm dist-tag add` 和 `npm unpublish` 的配置。文件布局只是客户端的便利设计，不是权限设计。

对智能体来说，至少应将注册表操作拆分成以下几组：

- 读取：查询元数据、下载 tarball、验证完整性和安装依赖。
- 发布：在获准的软件包名称下创建一个新的不可变版本。
- 路由：修改 `latest`、预发布标签或注册表访问设置等可变频道。
- 破坏性操作：取消发布、注册表允许时删除软件包，以及移除发布标签。

发布和路由的区别，比许多发布脚本承认的更重要。带版本号的软件包本身可能没有问题，但把它标记为 `latest` 就会让新的使用者转向它。反过来，如果用户必须主动选择专用标签，那么在专用标签下发布的预发布版本风险可能很低。软件包内容和使用者到达这些内容的路径，是两项不同的控制。

即使注册表限制了删除操作，删除也应单独分类。不同注册表的规则并不一样，有些被称为「删除」的操作只是隐藏构件或将其标记为不可用。这并不意味着它无害。它可能破坏可复现安装、事件调查，以及依赖被移除版本的团队。智能体绝不能因为某个版本几分钟前才发布，就自行推断清理是安全的。

## 过于宽泛的发布令牌会以普通方式出问题

智能体环境中的注册表令牌，本质上是一项采用便捷文件格式保存的授权授予，而不是一条范围明确的指令。如果智能体可以读取环境或配置文件，那么依赖项、问题模板、构建日志或复制的终端文本中的提示注入，就可能找到通往凭据的路径。

常见建议是使用注册表提供的最小范围令牌。这条建议正确，但还不完整。即使令牌仅限发布，它仍可能发布不想要的版本，发布到其账户有权限访问的错误软件包，或发布到被篡改配置选中的注册表端点。范围可以减小影响范围，却不能为每次发布建立意图确认。

我不止一次处理过这样的故障：

1. 团队给智能体一个注册表令牌，让它执行发布检查。
2. 项目配置指向生产注册表，因为开发者机器使用的就是它。
3. 智能体需要验证软件包内容，于是运行了无害的 `npm pack`。
4. 后续指令要求「测试发布」，智能体运行了 `npm publish`，而不是发布到隔离目标。
5. 命令成功了，因为令牌和软件包名称都有效。直到自动化流程或用户看到新版本，团队才发现问题。

这条链路不需要任何攻击者。设计把一个凭据交给了负责规划的系统，却相信它会维护运行环境没有强制执行的边界。

不要让凭据进入智能体进程。让独立的操作网关持有凭据，在检查目标并取得所需的人类审批后，执行特定的 HTTP 请求或命令。Sallyport 遵循这种模式：智能体收到的是已授权操作的结果，而不是注册表令牌本身。

但这种设计仍需要清晰的操作定义。一个批准「对 registry.example 的任何请求」的网关，只是把宽泛令牌问题移到了按钮后面。请求必须展示足够详细的信息，让审批者能够区分 tarball 获取、版本发布、软件包发布和取消发布。

## 在执行前让发布请求可检查

人无法审批「发布软件包」这种模糊指令。审批界面应该显示软件包标识符、确切版本、注册表主机、操作类型，以及可能发生的标签变更。如果智能体无法提供这些字段，它还没有准备好发布请求。

对于兼容 npm 的注册表，应将工作拆分为构件检查阶段和注册表变更阶段。检查阶段不需要发布权限：

```sh
npm ci
npm test
npm pack --json
```

`npm pack --json` 会返回它准备创建的 tarball 的结构化信息。相关部分大致如下：

```json
[
  {
    "id": "@acme/widget@1.4.0",
    "name": "@acme/widget",
    "version": "1.4.0",
    "filename": "acme-widget-1.4.0.tgz",
    "files": [
      {"path": "README.md", "size": 2400},
      {"path": "dist/index.js", "size": 18420},
      {"path": "package.json", "size": 910}
    ]
  }
]
```

检查文件列表，不要只看命令退出码。我会寻找本应保持私有的源代码目录、包含凭据的测试夹具、缺失的编译输出目录，以及指向错误入口文件的软件包元数据。`npm pack` 正是让这些错误低成本暴露的地方。

然后单独收集注册表状态。npm CLI 文档将 `npm view` 描述为查看注册表中软件包元数据的方式。用它提出具体问题，不要接受一大批未经筛选的数据：

```sh
npm view @acme/widget version dist-tags --json
npm view @acme/widget@1.4.0 dist --json
```

第一个命令告诉你现有版本以及标签指向哪里。发布后，第二个命令很有用，因为 `dist` 包含注册表记录的 tarball 地址和完整性数据。发布流程应将这份输出保存在发布记录中，同时移除秘密信息，因为它记录的是注册表实际接受了什么，而不是本地目录原本想发送什么。

根据这些事实生成审批请求。好的请求会写明：将 `@acme/widget@1.4.0` 发布到 `registry.example`，不移动任何标签。糟糕的请求只会写：运行 `npm publish`。前者让审阅者能够发现命名空间错误，后者则要求审阅者从一个可能不可信的命令中重新推断意图。

## 用临时软件包验证注册表路径

临时软件包是测试这条路径最安全的方式，它能验证一个准备好的 tarball 如何变成可获取的注册表版本。它不能证明生产软件包已经准备就绪，只能证明身份验证、注册表选择、发布机制和干净安装在类似真实发布的控制下能够协同工作。

使用你控制的命名空间下的软件包名称，并明确标明它只是临时测试。不要模仿热门软件包名称，也不要使用以后可能意外变成真实产品的名称。放入一个很小且无副作用的模块。它的任务是被发布和安装，而不是展示应用行为。

这个最小化的 `package.json` 能让测试过程清晰可读：

```json
{
  "name": "@acme-release-test/relay-check-2025-04",
  "version": "0.0.1",
  "description": "Temporary registry release-path check",
  "main": "index.js",
  "files": ["index.js", "README.md"],
  "publishConfig": {
    "access": "restricted"
  }
}
```

选择符合实际软件包类型的访问设置。如果真实软件包是公开的，不要盲目复制 `restricted`，也不要仅仅因为这样更像真实场景，就把临时测试公开。测试的目的是验证相同的授权和目标使用者边界。如果你必须测试公开发布行为，请使用明确的临时公开名称，并在开始前确认注册表的保留和取消发布规则。

在全新的目录中运行测试，避免缓存元数据和现有工作区让结果看起来比实际更好：

```sh
mkdir release-path-check
cd release-path-check
npm init -y
npm install @acme-release-test/relay-check-2025-04@0.0.1
node -e "console.log(require('@acme-release-test/relay-check-2025-04'))"
```

成功的 `npm install` 比成功的发布响应更能回答关键问题：干净的使用者项目能否解析并获取指定版本？如果软件包使用 exports、类型声明、命令行二进制文件或 postinstall 脚本，也要在这个全新目录中测试相应的公开入口。注册表保存了软件包，不代表使用者一定能正常使用它。

不要为了让账户看起来整洁就删除测试软件包。按照注册表的正常规则保留审计轨迹，或者在合适时使用弃用标记。发布测试应该让智能体和团队了解真实的发布后记录是什么样的。删除它只会让双方以为发布历史可以随意抹去。

## 软件包内容和注册表接受是两项独立测试

团队经常把 `npm pack` 称为发布测试。其实它是软件包内容测试。接着，团队又把成功的 `npm publish` 称为发布测试。其实它是注册表接受测试。两者互不替代，把其中任何一项当作完整测试，都会造成可预见的空缺。

软件包内容测试会检查 tarball 是否包含预期文件和元数据。它能发现 `.npmignore` 事故、过于宽泛的 `files` 数组、缺失的构建构件，以及源代码和清单之间的版本不一致。其中许多工作都可以在没有网络的情况下完成。

注册表接受测试会检查注册表是否认可凭据和命名空间，是否接受该版本，是否保存 tarball，是否记录完整性信息，以及使用者是否能够获取它。它能发现错误的注册表主机、缺失的组织权限、发布配置错误，以及与本地开发不同的授权路径。

使用者测试会提出第三个问题：干净的项目能否安装确切版本，并按照用户的使用方式调用它？缺失的对等依赖、错误的 `exports` 条目，以及对工作区文件的意外依赖，都会在这里暴露出来。

按这个顺序保留测试。因为「反正总能取消发布」就先发布，是不严谨的做法。取消发布不是回滚按钮。有些用户、镜像、缓存和构建记录可能仍保留软件包，而其他使用者却失去了获取它的能力。错误版本在操作上可能可以补救，但仍会制造本来可以通过本地 tarball 检查避免的工作和混乱。

## 审批应匹配调用的后果

对于执行大量预期读取操作的运行，会话级审批很有用。要求用户审批每一次元数据查询，会让他们养成不看内容就点击的习惯。这就是审批疲劳，也会让真正重要的提示更难得到关注。

但发布和删除应该打断这个流程。它们会以普通依赖下载不会有的方式改变外部状态。新软件包版本需要单独确认，dist-tag 变更需要另一次确认，每项破坏性操作也都需要单独确认。如果智能体提出发布两个软件包，就显示两个请求。批量审批会隐藏审阅者真正需要查看的确切版本。

每次调用的提示应包含足够的信息，让人能够基于正确理由拒绝操作：

- 注册表主机以及软件包作用域或所有者。
- 操作类型：发布、移动标签、弃用、取消发布或删除。
- 涉及的确切版本和请求中的标签变更。
- 发起请求的智能体进程，让审阅者能够拒绝意外的调用方。
- 根据发布计划生成的简短理由，而不是来自工具输出的自由文本。

不要要求人在当下解析授权标头或比较不透明的哈希值。那些属于审计细节。实时决策界面应以清晰的语言展示操作后果，同时由系统保留底层请求供日后审查。

Sallyport 的逐次调用密钥控制很适合发布边界：注册表凭据可以要求每次使用都审批，而智能体仍能在独立的会话决定下执行日常工作。这并不能替代对软件包内容的审查，却能防止发布凭据在一次早先的点击后变成后台权限。

## 把标签视为使用者路由变更

一个本身没有问题的版本，也可能因为 dist-tag 而对用户不安全。在兼容 npm 的注册表中，不带明确版本的安装通常会跟随 `latest` 标签。移动这个标签会改变新安装获取的内容，即使已经发布的 tarball 没有变化。

将发布和标签移动设为两个独立请求。先发布候选版本，再在干净的测试项目中按确切版本获取它。完成检查后，再由人决定是否移动 `latest`。这样会留下一个有用的暂停点：构件已经通过不可变版本可见，但路由决定尚未发生。

命令清楚地展示了这一区别：

```sh
npm publish --tag candidate
npm view @acme/widget dist-tags --json
npm dist-tag add @acme/widget@1.4.0 latest
```

第一个命令创建版本并为它分配候选路由。最后一个命令修改许多用户遵循的路由。发布脚本如果把两者隐藏在同一个辅助函数中，就会移除最有价值的人类判断节点。

不要让智能体靠猜测来修复标签错误。如果 `latest` 指向错误版本，智能体应报告当前标签映射、目标版本和拟议修正方案，再由审阅者确认。这里多一次点击的成本很小，而把错误软件包发送给每个新安装用户的代价要大得多。

## 让审计记录在事故调查后仍然有用

单靠命令历史无法回答是谁授权了注册表变更、哪个智能体运行发起了它，或有人是否在事后编辑过日志。发布操作需要一份能够关联请求、审批、执行结果和注册表返回元数据的记录。

记录软件包名称、版本、注册表主机、操作类型、最终状态，以及发布前构件检查结果的引用。不要记录令牌、授权标头或可能含有凭据的原始配置文件。一份好的审计记录可以让维护者在几个月后回答一个实际问题：我们发布了这个版本、移动了标签，还是只是尝试过？

防篡改证据很重要，因为发布日志往往是在出问题后才变成证据。Sallyport 将会话事件和单独调用保存在不同日志中，这些日志由加密的哈希链审计日志投影生成；`sp audit verify` 可以在离线状态下验证这条链，无需保险库密钥。这比信任仓库中可变的文本文件更可靠。

日志不会让危险操作变得安全。日志能让你在审批提示被误解、注册表目标错误或发布流程做出无人预期的事情时，还原完整路径。日志还应配合能够立即撤销正在运行的智能体会话的机制。当发布开始表现异常时，阻止下一次调用比之后写一份完美的复盘报告更有价值。

## 将临时测试写入发布契约

临时发布测试应该是计划中的发布路径检查，而不是生产发布失败后的临时补救。明确它何时运行：注册表集成发生变化时、凭据处理发生变化时、采用新的软件包管理器配置时，或准备授予新的智能体工作流发布权限之前。

在可能的情况下，让它的权限比生产权限更窄，但不要把它设计得过于人为，以至于错过真正的故障模式。测试相同的注册表类别、相同的请求代理、相同的凭据存储方式和相同的干净安装验证。如果生产发布需要人类审批，测试也必须需要审批，否则测试的不是同一个系统。

第一项有价值的动作，是从智能体可见的环境变量中移除注册表写入凭据，然后让一个临时软件包走过你准备信任的完整受控路径。审批前读取软件包文件列表。发布后检查确切版本。删除应始终作为单独审批的操作，因为一个整洁的测试账户不值得用来换取发布边界上的意外漏洞。
