AI 编程 Agent 会自动获取依赖、拉取容器镜像、访问未审计的注册表,作者建议对 AI 工具的供应链引入人工复核机制。
我们花了两年时间争论 AI 写的代码好不好看。这场争论产出了大量情绪、相当多的证据,却几乎完全跳过了运营层面的问题。
Coding agent 不只是写代码。它还会获取代码。
它解析依赖、拉取容器镜像、获取文档、阅读文档并根据阅读内容行动。它执行的构建步骤会访问你从未审计过的注册表。每一项都是一个信任决策,以机器速度执行,恰恰发生在人类开发者本会停顿半秒钟、觉得那个包名看起来有点不对劲的地方。
那半秒钟是承重的。我们把它移除了。
这篇文章是我在博客上提出的论点后半部分。如果你想要论据和历史,可以点上面的 canonical piece 链接。以下是我实际会放进仓库的内容。
把七月份的事件放在一起看,而不是分开看。单独看每一个都是正常的供应链故事。但合在一起,它们描述了一些新的东西。
一个被植入后门的、广泛使用的 LLM proxy 库的构建版本被发布到了 PyPI。它存在了大约三个小时。在那个时间窗口内,它被下载了数万次。
三小时是关键细节。
在 agent 时代之前,三小时窗口几乎是一次险情。人类开发者按照人类的时间表安装依赖——在工作时间、分批、经过一定程度的考量之后。自动化系统则持续安装。三小时窗口对抗的是 CI runners 和 coding agents 的庞大群体,这不是险情,是一次完整的收割。
另外:一个流行 coding agent 的 CI action 被投毒了。一个广泛嵌入组件中的 shell 注入漏洞影响到了大量开源部署。一个主要的模型托管平台披露了一起入侵事件,攻击者的活动在Containment之前记录了数千条行为。
不同的攻击,同一个模式。攻击者不再试图攻陷你的开发者。他们试图攻陷你的自动化所信任的东西,因为自动化不会犹豫,也不会八卦。
微软的 AI Red Team 今年把供应链妥协和过度代理(excessive agency)列为命名的 agentic failure modes。两者都是旧有发现,但有了新的覆盖范围。
这些都与模型质量无关。
Agents 无犹豫地解析依赖。被坑过的开发者在输入包名时会眯起眼睛打量一下。Agent 根据对生态系统的模糊记忆生成包名并安装。Typosquatting 对一个没有"看起来不对劲"概念的系统有高得多的命中率。
Agents 扩大自己的爆炸半径。全部价值主张就是 agent 无需询问就能执行下一步。这也是一个坏依赖变成凭证读取、变成环境转储、变成 push 的机制。Excessive agency 不是某个特定实现中的 bug。它是特性,运行在没有人给它划定范围的上下文中。
Agents 持续工作且无人监督。恶意构件发布到移除之间的时间窗口,过去是很少有人在看的窗口。现在这是一个大量自动化正在活跃寻找和拉取的窗口。
这些都不是反对使用 agents 的论点。它说的是那些使人速开发能够幸存的控制措施不能转移,因为它们每一个都假设在获取时刻有一个人在场。
本能是在模型层解决,用更好的 prompts 和更严格的指令。我不认为这有用,原因和"告诉实习生小心点"不是访问控制策略一样。
Agent 能够用推理绕过的约束不是约束。
把边界设在 agent 没有权限的地方。六个控制措施,大致按杠杆效应排序。
单一最高杠杆的控制措施,而且很无聊,是充分理解的技术。Agent 可以安装你注册表里的任何东西。把东西放入你的注册表是一个独立的过程,有不同的规则和不同的审批者。
重要的部分不是添加私有索引。是移除 fallback。配置错误的代理在包缺失时静默回退到公共索引,给你控制的幻觉而不是真正的控制。
# pip.conf: note the absence of extra-index-url.
# extra-index-url is the line that quietly reintroduces the public index.
[global]
index-url = https://artifacts.internal.example/simple/
no-index = false
require-hashes = true
# .npmrc
registry=https://artifacts.internal.example/npm/
# Deny the implicit fallback path
@internal:registry=https://artifacts.internal.example/npm/
audit=false
fund=false
然后从 agent 实际执行上下文内部验证,而不是从你的笔记本:
# Run this as the agent's service account, in the agent's container.
# If either of these resolves, your boundary is decorative.
pip download requests --no-deps -d /tmp/probe 2>&1 | grep -i 'pypi.org' && echo "FALLBACK ACTIVE"
npm view left-pad --registry=https://registry.npmjs.org 2>&1 | head -2
已有的依赖、策略范围内的任何版本:没问题,让 agent 处理。从未出现在你树中的包:那是需要审批的,要附上审批人姓名。
这在 CI 中无需新工具即可强制执行。Diff lockfile,提取新增的包名,对任何之前不存在的东西报错。
#!/usr/bin/env bash
# ci/check-new-deps.sh: fails the build when a lockfile introduces a package
# that has never appeared in this repo's dependency tree before.
set -euo pipefail
BASE="${1:-origin/main}"
ALLOWLIST="ci/known-packages.txt"
# Packages present in the base revision
git show "$BASE:package-lock.json" \
| jq -r '.packages | keys[]' | sed 's|^node_modules/||' | sort -u > /tmp/before.txt
jq -r '.packages | keys[]' package-lock.json \
| sed 's|^node_modules/||' | sort -u > /tmp/after.txt
NEW=$(comm -13 /tmp/before.txt /tmp/after.txt | grep -vxFf "$ALLOWLIST" || true)
if [ -n "$NEW" ]; then
echo "New dependencies introduced. Human approval required:"
echo "$NEW" | sed 's/^/ - /'
echo
echo "If intended, add to $ALLOWLIST in a separate commit with a reviewer."
exit 1
fi
echo "No new packages. Version movement only."
separate-commit 要求是刻意设计的。它迫使审批成为一个可审查的 artifact,而不是埋在一堆 400 文件的 agent 生成 diff 中的一行,而那恰恰是没人会看的地方。
当有人在六个月后问某个二进制文件来自哪里,应该能够回答,而不是变成考古项目。这也不是巧合地是监管框架会要求你提供的大部分内容。
# .github/workflows/build.yml (excerpt)
permissions:
contents: read
id-token: write # required for keyless signing
attestations: write
jobs:
build:
runs-on: ubuntu-latest
steps:
# Pin actions by commit SHA, never by tag.
# A tag is a mutable pointer, which is precisely the class of
# thing that got poisoned in July.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build
run: make dist
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: 'dist/*'
按 SHA pinning 是这件事的低投入、高回报的一半。基于 tag 的 pinning 能防止诚信错误导致的不可复现,但对重定向的 tag 毫无防御能力。
然后设置一个有效期,让它真正过期。
过度权限从来不是在授予时刻的失败。一直是过期的失败,而 agents 完整继承了整个问题。
# Least-privilege default at the workflow root.
# Every job that needs more declares it, visibly, at the job level.
permissions: {}
jobs:
agent-task:
permissions:
contents: read # not write. the agent proposes; it does not merge.
pull-requests: write
environment: agent-sandbox # branch protections + required reviewers apply here
contents: read 这一行是关键。能开 pull request 但不能 push 到受保护分支的 agent 有有限的爆炸半径。对 main 有 write 权限的 agent 有的是你最坏依赖的爆炸半径。
你需要能够在事后区分哪些变更是 agent 辅助的。不是为了追责。是为了解读你自己的质量趋势,如果两种群体在你的历史中无法区分,你就做不到。
# In the agent's commit path
git -c trailer.ifexists=addIfDifferent commit \
-m "Refactor retry handling in shipment poller" \
--trailer "Assisted-By: <agent-id>/<model-version>" \
--trailer "Agent-Run-Id: ${RUN_ID}"
这样问题就可以用一条命令回答,而不是四分之一的猜测:
# Change failure rate for agent-assisted work, isolated from human-authored work
git log --since="90 days ago" --grep="^Assisted-By:" --format="%H" > /tmp/agent-commits.txt
# join against your incident-to-commit mapping
如果你从这篇文章里只带走一样东西,带走这个。它只增加一个 trailer,却是拥有对 AI 代码质量的观点和拥有对 AI 代码质量的测量之间的区别。
不是设计它。测试它,最近,让没有构建这个 pipeline 的人来测。
我不是因为 AI 工作才得出这些的。我是通过运行别人基础设施的十年才得出这些的。
在我靠写生产代码谋生之前,我做系统管理。那个年代培养出的运维纪律归结为三个问题,反复按顺序问。
什么在运行。它从哪里来。谁能改变它。
包管理器侵蚀了第二个问题。我们接受了这一点,大多是因为生产力交换明显值得,而且我们建立了部分答案:lockfiles、checksums、signed artifacts、漏洞扫描。不完美,但是真实的回应。
Agents 正在侵蚀第三个。我们还没有建立起回应。
大约在 2010 年,我写了很多关于 SSL 和邮件服务器配置的文章,反复出现的发现从来不是协议坏了。是系统文档记录的行为和实际行为之间的差距。有人在负载均衡器上终止了 TLS,以为后面的流量是内部的,然后网络在那个假设下发生了变化。
同样的失败在这里以更高的速度和更大的扇出可用。信任边界存在于图表上。运行的系统几周前就已经和图表不一致了。没人重新检查,因为图表看起来还是对的。
我自己历史中的诚实承认:当我管理系统时,我信任架构图因为是我画的。我没有足够频繁地回去验证运行中的系统是否还和我一致。这个习惯比我那些年犯的任何具体技术错误都让我付出更多。
当前这个时刻是那个失败有了更大的表面积。团队在 rollout 时、在评审会议上批准一次 agent 的范围,然后再也不检查生产中的范围是否还和批准的范围一致。六个月的小的权宜变更之后,它就不一致了。
这六个控制措施都不新奇。私有注册表、审批门控、有过期时间的限定凭证、溯源记录、归属、可测试的回滚。每一个都是我们已有的技术,大部分没有费心去做,因为人类开发的节奏使得这个差距是可以生存的。
节奏变了。差距没有自行弥合。
如果你的 agents 今天可以把依赖添加到你的代码库,有用的练习不是再读一份威胁报告。是找出谁批准那个添加、那个人是否知道他们批准了那个,以及答案是否只在文档中强制执行。
从 agent 的容器内部运行第一节的 probe。我有兴趣知道有多少人发现了一个他们不知道存在的 fallback。
完整的论据和这个论点的历史版本在我的网站,上面链接为 canonical。我还写了关于 AI 瓶颈实际上在 SDLC 中移动到了哪里,这是这篇文章的另一半。