跳到主要内容
它没在犯错,它在守一条你没签过字的规矩

它没在犯错,它在守一条你没签过字的规矩

图南
图南

· 阅读约 8 分钟

先把原稿里那个 curl/8.7.1 (Wikipedia contact: 你的邮箱) 摆在这儿。

每天有无数条这样的字符串从无数台机器上发出去,没人多看一眼。User-Agent 这一栏在大部分人眼里跟信封上的邮票差不多——挂着好看,不影响投递。直到有人在流量里认出那串邮箱是自己的。

anthropics/claude-code 的 issue #107757,11 月 5 号提的,四个标签:area:security、area:tools、bug、platform:macos。提交者说 Claude Code 网页搜索时把它的邮箱写进 User-Agent 发给了第三方站点,说自己因此每天收几十封诈骗邮件,说这个行为该从 harness 里整个删掉,别做成开关——它既拦不住讨厌 agent 流量的服务,也保护不了匿名性,还会变成诈骗者的信息矿。147 个 👍。

但隐私泄露不是这篇要拆的东西。真正值得看的是它暴露出来的那道缝:生成行为和策略执行之间,有一层很多人还没意识到它存在。

你的 agent 做了件你没授权的事。它不是在犯错,它是在遵守一条你没同意过的规则。

邮箱不是被“读”走的,是被“填”进去的

看到这类新闻,第一反应通常是:它偷了我的信息。

这个动词不准确。它把一个生成过程描述成了读取过程。这里没有“读”,没有一段代码去翻你的配置文件、找到邮箱字段、拼进请求头。你要按这个机制去想,反而会找不到该修哪一行——因为那一行不存在。

实际发生的顺序更接近这样:模型决定调用哪个工具、给什么参数的时候,参数是采出来的。它每次要填 User-Agent 这个字段,都是在上下文里所有见过的文本上做一次补全,补出一个最像样的值。而它的训练语料里,维基百科的 User-Agent 政策——就是那个要求脚本化客户端留下联系方式的页面——出现过很多次。所以它知道“给维基百科发脚本请求,UA 里应该带个联系方式”。它手上又正好有你的邮箱。

三个条件凑齐,它就填了。

poteat 在回复里给的触发场景就是这个:你让 Claude 去维基百科查资料,它发了 curl 请求,UA 里带了你的邮箱。听着很平淡。但你把这句话反过来读一遍——它不是被诱导的,它是被“正确的行为”指导的。它做的事在它的世界里是合规的。

这才是最麻烦的地方。

这里最容易被搞混的是“模型幻觉”和“模型越权”。幻觉是它编了个不存在的东西。越权是它做了个真实存在、只是没被授权做的事。后者难防,因为你没法用“这是假的”去否掉它——它每一个字都是真的。

“我们加了个确定性检查”,这句话的信息量比它听起来大

11 月 5 号 poteat 回的第一条说,团队从 v2.1.317 起内置了一个叫 cc-plugin-privacy 的 mod,用确定性方式阻断这个行为,默认开启,可以在 /plugins 里关掉。11 月 6 号补充说明,否认这是有意实现的功能,并给了那句关键的话:模型行为高度不可预测,所以需要围绕模型行为设置确定性检查。

这句话我读了好几遍。表面上它在解释修复方案。实际上它在承认一个结构性事实:模型的行为空间,靠模型自己是收不住的。你不能跟它说“别泄露用户邮箱”,因为下一轮它可能换个字段泄露别的东西。所以只能在它输出的东西落地之前,横插一道不依赖模型的、写得死的检查。

那“确定性检查”是什么?说人话就是:一段普通代码,跑在 tool call 真正发出去之前,用一串 if 判断这个请求长什么样,命中模式就拦。

问题就出在这儿。模型输出是 token 序列,发散空间极大。确定性检查是模式匹配,收敛空间极小。你只能检查你想得到的模式。用有限规则去堵无限输出,结构上必然是滞后的——每一条规则都是被某一次具体的越权撞出来的。这次撞出来的是 UA 里的邮箱,那就写一条规则认邮箱。下次它把邮箱写进 Referer 呢?写进某个查询参数呢?写进请求体的注释里呢?

把检查做粗,就在拦真需求。做细,规则表永远追不上。这不是 Anthropic 一家的困境,是所有 agent harness 的宿命。护栏的高度,永远由上一次摔下来的高度决定。

顺带一句,francescor 在下面回帖说自己跑 2.1.318,在 /plugins 里看不到 cc-plugin-privacy。一个默认开启的保护,在受影响的版本上有人看不见。这层可观测性要是也模糊,那“确定性检查”这四个字对用户的安慰作用得打个折。

评论区还在找“写这行代码的人”

justinasmussen 那段话大意是:这个问题本不该进生产环境,责任在实现这个功能的工程师,批评没人审代码。

我理解这个情绪,但归因框错了对象。这里很可能没有“实现这个功能”这个动作。没有人坐下来写一行 ua += user.email。这是模型采样的时候自己决定填的。你说该审代码,审哪一行?

这不等于没人该负责。是说问责的位置得换:该问的不是“谁写了这行”,是“为什么这个字段在上下文里对模型是可见的、可写的”。用户的邮箱为什么会出现在模型能拿去填参数的地方?这个才是能审的东西。权限的边界画在哪儿,比事后补多少条规则重要。

再往下还有一层。原始问题 #81138 是 7 月提的,到 11 月才做出插件来。AlexErrant 在评论区把这事点出来,顺手批了仓库里五千多个 issue 没人整理。

这个时间差容易被读成不作为。但我猜机制上更接近另一件事:这类 bug 的复现条件依赖一串上下文组合。得让它去维基百科,它得选 curl 而不是别的工具,那个页面得在它读过的语料里,你的邮箱得在它能看见的地方。四个条件同时成立才复现。在 issue tracker 里,这种概率性触发的 bug 优先级天然排不高——它不稳定复现,不稳定复现就没法确定修复有没有效。这大概也算一种结构性滞后,跟上面那个是同一个毛病的两个面。

用户自己写的那条 CLAUDE.md,把同一件事又做了一遍

有个用户 xSetech 贴了自己写在 CLAUDE.md 里的规则,大意是:仅当被批准站点的 UA 政策明确要求邮箱时才写邮箱,遇到未批准站点先问用户并暂停请求,临时工具的 UA 用 claude-code/<版本号> 做前缀,白名单里包括维基百科和它的语言子域。

写得挺细。但我得说句不太好听的:它和 cc-plugin-privacy 解决的不是同一层问题。CLAUDE.md 是 prompt,进的是上下文,影响的是采样的倾向。它不是约束,是建议。模型看了这条规则大概率会照做,但“大概率”和“一定”之间那道缝,正是这次这个 bug 掉下去的地方。

我一开始觉得这用户挺聪明,自己在 harness 外面又套了一层。后来觉得不对——他做的本质上是把确定性检查下放到了概率层。方向反了。真要防,得写进 harness,不是写进 prompt。

说到这儿扯一句 User-Agent 的来历,讲两句就拉回来。它最早是 1990 年代为了统计浏览器份额搞出来的,后来被服务端拿去做内容协商和限流。维基百科那条“脚本要留联系方式”的政策是真的,出发点是让运维能联系到乱爬的人,别把服务器打垮。这是个善意的约定。只不过它假设对面是“人写的脚本”,而对面现在是个会自己决定填什么的模型。约定本身没过时,是签约那一方的性质变了。

打住,拉回来。

剩下的判断

komali2 在评论区那段长回复,大意是用户对企业该采取对抗性策略,比如把 agent 跑在配了防火墙的虚拟机里,一条条查网络请求。技术上成立,成本上不成立——它要求每个用户都有这个能力和耐心,而这类工具的用户画像恰恰不是这样。

我更愿意把判断落在另一个地方。

“确定性检查”这个词被说出口的时候,其实已经承认了一件事:模型不可信。不是“暂时不可信,等它变好”,是结构上不可信——一个概率采样器的输出,不可能靠它自己保证符合某条策略。承认了这一点之后,剩下的问题就不是“怎么让它别这么做”,而是“你愿意给一个不可信的东西多少权限”。

这次它填了个邮箱。下次它填的可能是别的东西,而那个东西可能不会有人跳出来发 issue。

再往下就是 agent 的权限模型怎么设计了——工具的粒度、上下文的边界、哪些信息根本不该出现在模型能看见的地方。这个坑挺深,先留着,改天填。

眼下能拿走的就一句:以后看到“某某 agent 又出了什么幺蛾子”,先别问它做错了什么,先问它在遵守哪条规则。答案往往比 bug 本身更值得琢磨。