连 bug 都照抄:Google 搬走代码,删了署名

cover

Hello,我是飞飞。

前天晚上我刷到一篇博客,标题叫《I expected better from Google》。换成人话是:我本以为 Google 会做得更好。

第一反应是那种熟悉的疲惫:又来了,大厂拿走别人的开源代码,作者写篇长文控诉,评论区骂两天,然后没有然后。

我差点就划过去了。

往下多看了两眼,才发现这故事拐了个弯。真正让我坐直的是时间线里的一处改动,它只占了几个小时,却把整件事的方向换了一次。

我天天在抄别人的仓库

先把我的底交代了。我天天用 Claude Code,开各种来路不明的第三方仓库是常事。刷到个有意思的开源项目就 clone 下来,让它帮我跑起来。

pip installnpm install、跑 setup,说实话我基本不细看它每一步执行了啥,报错了就一句「你看着修」。这句是我六月写的,当时在讲另一件事。现在回头看,它是我读这条新闻时绕不开的前提:我就是那个天天白用别人代码的人。

所以我想先反问你一句:你今天装进来的那些包,到底是谁写的?你能说出几个名字?

我说不出几个。我真正想知道的是另一件事:如果哪天我把自己的东西开源出去,被人拿走,我手里到底有什么。

顺着这个问题往下看,我发现答案比我以为的薄。

一个 bug 把事捅出来

Minitap 是个小团队,项目叫 mobile-use,去年八月开源的,做的是让 AI agent 去操作真实的安卓和 iOS 应用。写的是 Apache 2.0 许可证。

今年八月十三号,Google 上线了一个项目叫 Artemis,干的是同一件事:把自然语言指令变成可靠的安卓自动化。仓库一个月攒了五千多 star。

Minitap 团队打开那个仓库,看到的是自己的代码。

有个细节,乍看最不像证据。mobile-use 里有个 agent 叫 Hopper,负责从批量数据里抽信息。这名字是他们的工程师 Jean-Pierre 随口起的,因为他喜欢 Minecraft。而 Google 那份 Hopper 的指令文本,和他们的逐字相同。

一个随口起的名字,配上一份逐字相同的指令,就很难是巧合了。

还有一个更硬的。这两个项目在旧版本里共享同一个 bug:一段小工具代码把结果写进文件,下一次运行去读自己写的这个文件,读不出来。Minitap 在两个实现里都复现了这个失败。一个共享的 bug,比一百行相同的代码更能说明问题。

那署名呢?mobile-use 早期的包文件里列着三位作者:Pierre-Louis Favreau、Jean-Pierre Lo、Nicolas Dehandschoewercker。跟这三个名字一起躺在文件里的,还有 mobile-use 自己的版本号,3.6.3。

而这三样东西头顶上顶着的,是一行 Google LLC 的版权声明。

这就像抄作业的时候,把人家名字一起抄了上去。

后来那一版把三个名字全删掉,换成了另一个名字。那批文件里唯一的变化,就是作者列表。

说实话,到这一步,我心里的结论已经写好了:这是一次有意的抹除。

但接下来看到的东西,把它又推翻了一次。

Google 到底补了什么

Minitap 九月十一号在 Artemis 仓库开了个 issue,同一天发了那篇博客。当天晚上十一点十九分,Google 的仓库里出现了一个提交。

这个提交干了几件事:加了一个 NOTICE 文件,给十七个文件补上衍生声明,里面点名了两家上游项目,列出六位原作者的名字。

它还顺手改了一批命名,把 Hopper 改成 entity_extractor,同时删掉一批文件,砍掉好几百行原样照搬的代码。单是一个 adb_tunnel.py,就少了三百二十七行。

整件事里,只有这一次把补救做全了。

那它为什么又没了?

凌晨两点三十二分,它被撤回了。撤回的理由写得很平常:请不要把无关的文档和代码放在同一个提交里。说人话就是,一次提交别混着干好几件事。

二十七分钟后,同一个账号重新提交了一次。这回只加了版权头,README 底部加一行。NOTICE 文件没加回来,六位作者的名字一个都没提。

流程图

补好的那半留下了,补全的那半撤回了。

而被留下的那条提交,它的原始说明里写着一句话:per Apache 2.0 requirements。按 Apache 2.0 的要求。这句话是 Google 自己写的。

我不太确定该怎么读它。一种读法是承认之前不符合要求,另一种读法是一句工程惯例式的描述。能确定的是,被撤回的那个版本,恰好是把事情做全的那一个。

删名字算不算有意

看到这里,最容易被带走的判断是:删名字等于故意抹除,实锤了。

这个判断我得按住一下。

有人在 Hacker News 上提了个站得住的反面解释。他的意思是,包文件里那一栏作者名,说的是「这个项目现在归谁维护」。它跟「这段代码当初是谁写的」,没多大关系。一个项目从别人那儿派生出来,覆盖这一栏是常见做法,不代表有意隐藏什么。

我核了一下,这个说法在 Python 项目里确实说得通。

还有一种可能。Artemis 也许最初是个内部项目,后来才开源。经手的工程师没认真读过许可证,也没人盯着这一块。

这两个解释我都接受。它们解释不了的是另一件事。

真正被许可证管住的,是源码文件里的版权声明、LICENSE 文件,还有上游的 NOTICE。

这三样跟作者栏完全是两回事。打个比方:作者栏像门口挂的门牌,谁在这屋里办公就写谁;版权声明、LICENSE、NOTICE,是刻在地基上的字。门牌换个名字,地基上的字一个字都不会跟着改。摘门牌能解释成换租客,地基被人铲了,就没法解释了。

从一开始,这三样一个都没有。

到今天为止,artemis 的根目录里仍然没有 NOTICE 文件。

还有一层反差,是 Minitap 自己在博客里点出来的。Google 给过 Kubernetes 和 TensorFlow。二〇〇八年 Chrome 发布时,官方博客明确致谢了 WebKit 和 Firefox,原话是「We owe a great debt to many open source projects」。Google 甚至有一份公开文档,讲怎么处理别人家的代码。

Minitap 那句原文我记了很久:That is exactly why I expected better。正因为你一向做得好,我才更失望。

那条线靠什么管住

Apache 2.0 第四条讲得很清楚。你把别人的代码再发一遍出去,不管原样还是改过的,都得留着人家的版权和署名声明,也得写清楚你动了哪里。如果上游带了 NOTICE 文件,那里面的署名内容也得一起保留。

这一条是写在许可证正文里的硬条件。

所以 Minitap 在 NOTICE 里写的那句话,是整个文件里最值得读的一行。他们写:Apache 2.0 只要求保留版权声明,我们另外请求衍生作品在文档里署名 Minitap。这是一个请求,不是法律要求。

他们主动把要求往下让了一格,只把最低那条线当线。而 Google 最初连这条最低线都没守住。

那这条线实际靠谁执行?

还是那个 issue。里面除了 Minitap 的人,还有个叫 acockrell 的。他看不下去,提了一个 PR。

说明里没骂人,就一句:这个 PR 帮你把署名修好。

一条条把 Google 漏掉的那些补回去,替 Google 干 Google 自己该干的活。

提交没通过。

拦他的是 Google 的机器人。弹出来的要求是:先签一份贡献者许可协议,把你这部分版权让给 Google。连一个经手的人都没有。

底下有条评论我看了两遍:Google 喜欢你授予它版权,但显然毫不在意别人的版权。

一个人跑过来,帮 Google 补上本该 Google 补的署名。他得到的回应,是一份要求他先让出署名的协议。

还有一处细节。社区里另一个人用 agent 跑了八十一秒,找出十三个同样源自 mobile-use、却没被加上声明的文件。而 Artemis 仓库里有个 issue 的标题,到现在还在用 Hopper 这个名字。

那次改名没能改干净。抄作业抄上去的名字是擦掉了,可铅笔压出来的印子还在。

我打算放一份 NOTICE

绕一圈,回到开头那个问题:我手里到底有什么。

答案不多。一份许可证文本,一份 NOTICE 文件,加上一条靠别人愿意守才成立的约定。

这件事到今天也还没收场。issue 还开着,七条评论里没有一条来自 Google 官方。那个被撤回的 NOTICE 会不会回来,我不知道。

我没有立场替谁下判决。但这个案子让我改了一个想法。

我那几个仓库,接下来会做一件以前觉得多余的事:在根目录放一份 NOTICE,把「这段代码从哪来、哪些是别人写的」写清楚。许可证只逼我留版权声明,我想多写一点。

这个动作不解决任何制度问题,我清楚。那条线到底该靠什么管,不是我写一份 NOTICE 能回答的。

我只是今天刚看完,有人把所有该做的都做全了,两个小时后又被自己人撤回。

所以我宁可多写一行。

最后想问你一句:你手边天天在用的开源项目,有多少是你连作者名字都没看过的?