发行版总览
六档减法构建、一个加法开关,以及为什么这么分
同一份代码可以编出多种 anet。裁剪发生在编译期:一个用 -tags no_x402 编出来的二进制里
没有付费模块的任何一行代码,go tool nm 数得出来。机制与判据见
可插拔;这一页回答的是另一个问题 —— 凭什么这么分档。
结论先说:不是按体积分的。下面第一节给出实测数字,说明按体积分档站不住; 第二节给出真正的三个维度。
一 · 先看数据
下列数字重测于 2026-09-03,go 1.26.7,linux/amd64,CGO_ENABLED=0,
-trimpath -ldflags "-s -w"。体积单位为 KiB,依赖包数是
go list -tags "<tags>" -deps ./cmd/anet 的行数(含 main 包本身)。
| 构建 | 体积 | 依赖包数 |
|---|---|---|
默认(九个减法 tag 一个都不用,不含 shell) | 14,320 K | 308 |
| 九个减法 tag 全用上 | 12,100 K | 265 |
| 差 | 2,220 K(15%) | 43 |
单独去掉每一个模块 —— 每行都是从默认构建出发,只加一个 no_<name>:
| tag | 省下 | 占可省总量 | 依赖减少 |
|---|---|---|---|
no_mcp | 1,896 K | 85% | 34 |
no_x402 | 116 K | 5% | 1 |
no_cas | 52 K | 2% | 1 |
no_blackboard | 36 K | 2% | 1 |
no_org | 28 K | 1% | 1 |
no_p2p | 28 K | 1% | 1 |
no_taskboard | 24 K | 1% | 1 |
no_anetlink | 12 K | <1% | 2 |
no_service | 12 K | <1% | 1 |
加法 tag 反过来算,是加进去多少:
| tag | 加上 | 依赖增加 |
|---|---|---|
shell | 36 K | 1 |
除 MCP 外,没有一个模块对体积有实质影响。 剩下的 12 MB 是 Go 运行时、net/http、
加密库和纯 Go 的存储层,与模块无关 —— 把九个减法 tag 全用上,二进制仍有 12,100 K。
MCP 是唯一的例外,而它贵在依赖不在自己:no_mcp 减掉的 34 个包是 MCP Go SDK 及其
jsonschema 依赖树,internal/mcpserv 自己的代码只是其中很小一部分。
所以按体积分发行版是站不住的。分档要成立,理由必须在别处。
上表用 -s -w 编译,是为了让各档之间可比。GitHub 上发布的二进制不是这么编的 ——
deploy/release/build-release.sh 只用 -w,故意保留符号表,因此发布件比上表大约 1.4 MB
(14,320 K → 15,760 K)。理由是可核对性:go tool nm anet | grep -c module/shell 必须能在
用户手上那个文件上答出 0,而 -s 会让这条命令回答"没有符号表" —— 看起来像通过,实际什么都没证明。
二 · 真正的三个分档维度
1. 攻击面 —— 这个构建会打开什么、接受什么
这是最实在的一条。九个减法 tag 覆盖的子系统里只有 x402 会让 anet 进程自己开一个公开监听口,
只有 p2p 会让这台机器接受入站对等连接(由另一个进程开)。其余模块的网络行为全是出站,
或者根本没有网络行为。详见下一节的表。
2. 可被要求做什么 —— 不在二进制里的能力不可能被诱导使用
-tags no_x402 的意义不是"付费功能关掉了",是这个节点不可能付款。
付费面在这种构建上不是拒绝执行,是不存在:internal/daemon/paylink.go 里
errNoPayments() 是所有付费入口在无模块构建上的统一回答 ——
/balance、/redeem、/reconcile、/audit-hub 四个控制面入口在 min 档上都答这一句,
scripts/onboard.sh 第 7 节逐个核对。
同一条性质的另一面:一个 no_x402 的节点被要求做标价的能力时,答
UNAVAILABLE 并说明原因,而不是免费替人干活。诚实效果状态的五个取值里,
UNAVAILABLE(做不了)和 FAILED(试了没成)是两种状态,不合并 ——
见 证据与效果。
3. 运维复杂度 —— 要配几个块、有几种失败模式
min 除了 hub 地址什么都不用配,失败模式只有一种:连不上 hub。
paid 要理解余额托管在 hub、跨 hub 结算的信任边界、凭证兑付是否需要开公开口。
p2p 要额外跑一个 anetpeer 进程、决定会合点用 hub 还是共享目录、
理解直连失败时会回落到 hub 中继。要配的东西和要理解的失败模式按这个顺序递增,
而体积不按这个顺序递增。
三 · 攻击面:每个模块带来什么
"本进程打开的监听口"一列不含回环控制面 —— 默认 127.0.0.1:39811
(internal/daemon/config.go 的 DefaultConfig),每一档都有,不属于任何模块。
| 模块 | 本进程打开的监听口 | 出站行为 |
|---|---|---|
x402 | 配了 voucher_addr 才开一个公开 HTTP 口;不配就不开 | 向 hub 发结算、拉发放链;开了 witness_hub 会定期给 hub 的发放头签见证 |
p2p | 无。daemon 只连一个 unix socket | 经 anetpeer 主动连对等节点 |
| MCP northbound | 无。anet mcp 是 stdio 子进程,由本机 MCP 客户端拉起 | 只连本机 daemon 的回环控制口 |
service | 无 | 向配置里写明的 HTTP URL POST 调用参数,读回复 |
taskboard | 无 | 向本节点的 hub 发签名请求(读板、领活、交活) |
anetlink | 无 | 连 ANetLink 运行时的 C1 unix socket |
cas / blackboard / org | 无 | 无。这三个包里没有任何 net/http 或 net.Dial 调用 |
shell | 无 | 无网络。在宿主机上执行命令 |
三点需要说清楚:
一、x402 是唯一在 anet 进程里 net.Listen 的模块。 位置是
module/x402/voucher.go 的 startRedeemFace,只在 voucher_addr 非空时执行。
配置里 voucher_addr(监听地址)与 voucher_url(对外可达地址)必须成对设置 ——
分开是因为监听地址常写成 0.0.0.0 或容器端口,把这种地址签进节点卡片等于对外
广告一个谁也到不了的位置。不开这个口的付费节点照样能收费,只是买家走 hub 中继而不是直连兑付。
二、p2p 的入站连接不在 anet 里。 daemon 侧只是一个客户端:它通过 unix socket
连到 anetpeer 进程,把对方当成一个普通的 module.Transport。真正监听的是
anetpeer,它开两个口 —— 一个 unix socket 给本机 daemon,一个给其他 anetpeer
(--peer 参数,可以是 unix 路径也可以是 TCP 地址)。所以"跑 p2p 档的机器接受入站连接"
这句话成立,但要拔掉这个能力,停的是 anetpeer 进程,不是改 anet 的配置。
三、shell 不开端口,但它是唯一把网络请求变成本机命令执行的模块。 请求经
hub 中继或 p2p 到达常规的委派入口,然后落到一条运营者列过名的命令上。它不提权:
命令以 daemon 自己的用户身份运行,一个以 root 启动的 daemon 能跑 root 命令,
这是安装时的决定,写在 unit 文件里,不是这个模块能授予的。
它是本套件里唯一一个用加法 tag 的模块 —— 默认构建里没有它。
两种 tag 写错的代价不对称,这是加法 / 减法两个方向存在的全部理由。
减法 tag 漏写,结果是某人想去掉的模块没去掉;加法 tag 漏写,结果是命令执行能力
进了所有没要过它的构建。CI 因此把 shell 单列一个 job(optin),判据方向相反:
默认构建的 module/shell 符号数必须等于 0,-tags shell 构建必须大于 0。
四 · 六档 + +shell 的组合矩阵
减法 tag 的写法是"列出要去掉的",所以一档的 tag 串越长,这档越小。
下表前五档的 tag 串逐字取自 .github/workflows/ci.yml 的 pluggable 矩阵,每次提交
都被编译并跑测试;full 是不加任何 tag 的默认构建,由 test job 的 go test ./...
覆盖,并在每个 pluggable job 里作为符号对照组编出来。都是有人在维护的组合,
不是文档里凑出来的。
| 档 | -tags | service | x402 | mcp | p2p | taskboard | cas | blackboard | org | anetlink | shell |
|---|---|---|---|---|---|---|---|---|---|---|---|
min | no_x402,no_service,no_mcp,no_taskboard,no_cas,no_org,no_blackboard,no_p2p,no_anetlink | ||||||||||
standard | no_x402,no_mcp,no_taskboard,no_cas,no_org,no_blackboard,no_p2p,no_anetlink | ✓ | |||||||||
paid | no_mcp,no_taskboard,no_cas,no_org,no_blackboard,no_p2p,no_anetlink | ✓ | ✓ | ||||||||
agent | no_x402,no_cas,no_org,no_blackboard,no_p2p,no_anetlink | ✓ | ✓ | ✓ | |||||||
p2p | no_x402,no_mcp,no_cas,no_org,no_blackboard,no_anetlink | ✓ | ✓ | ✓ | |||||||
full | (无) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
任意一档 +shell | 上面那一串再加 shell(full 就是 -tags shell) | ✓ |
shell 一列在全部六档默认形态里都是空的,full 也不例外。这就是加法 tag 的全部意义:
不写 -tags shell 就没有。
公开可达面按档汇总:
| 档 | 本机之外能连上的东西 |
|---|---|
min / standard / agent | 无。只有回环控制面 |
paid | 配了 voucher_addr 才有一个公开 HTTP 口 |
p2p | anetpeer 的对等口(anet 自己仍然不开口) |
full | 上面两项都可能有 |
agent 与 p2p 两档的 tag 串里没有 no_taskboard,min / standard / paid 三档有。
也就是说 agent 和 p2p 的二进制里带着 hub 共享任务板的客户端,另外三档不带。
这不是设计意图的表述,是 ci.yml 的 pluggable 矩阵里六个串之间的实际差异 ——
ANet/docs/DISTRIBUTIONS-zh.md 给出的六档 tag 串里一个 no_taskboard 都没有,
ci.yml 与 scripts/onboard.sh 只给前三档补上了(onboard.sh 也只有这三档)。
影响是 agent / p2p 多带 24 K 和一个依赖,并且这两档的节点可以领 hub 任务板上的活。
要不要统一,未决。
五 · 实测总表
每一行都真编译过。体积与依赖包数是那次构建的实测值;"anet version 报的模块"一列是
module.Compiled() 的输出 —— 它读的是模块注册表(init() 跑过才在里面),
不是构建时刻的 tag 字符串,所以它不会说谎。
MCP northbound 也出现在这一列。 anet mcp 是一个 CLI 子命令
(cmd/anet/mcp.go),不是挂在 module.Host 上的模块,所以它不进注册表 ——
但 cmd/anet/module_mcp_declare.go 带着同一个 //go:build !no_mcp,里面一句
module.DeclareCompiled("mcp") 把这个名字补进 module.Compiled()。
两处受同一个 tag 管辖,所以这个名字不可能比链接器留下的东西活得更久。
此前它被漏报,一个带着九个可用 MCP 工具的构建在这一行里只说 service。
表里的 mcp 是推出来的,不是量出来的。 这批二进制打于 642f970,在漏报修好之前 ——
在那些文件上跑 anet version,任何一行都不会出现 mcp(agent 那行当时读回的是
service,taskboard)。表里的 mcp 按 //go:build !no_mcp 的编译条件补上,
对应主干构建的预期输出,没有在打了补丁的二进制上重新量过。体积与依赖包数不受影响。
| 档 | 体积 | 依赖包数 | anet version 报的模块 |
|---|---|---|---|
min | 12,100 K | 265 | (none) |
standard | 12,124 K | 266 | service |
paid | 12,240 K | 267 | service,x402 |
p2p | 12,176 K | 268 | p2p,service,taskboard |
agent | 14,048 K | 301 | mcp,service,taskboard |
full | 14,320 K | 308 | anetlink,blackboard,cas,mcp,org,p2p,service,taskboard,x402 |
min+shell | 12,156 K | 266 | shell |
standard+shell | 12,176 K | 267 | service,shell |
paid+shell | 12,292 K | 268 | service,shell,x402 |
p2p+shell | 12,228 K | 269 | p2p,service,shell,taskboard |
agent+shell | 14,080 K | 302 | mcp,service,shell,taskboard |
full+shell | 14,356 K | 309 | 上面九个再加 shell |
三条读法:
min到paid只差 140 K 和 2 个依赖。再一次说明分档的理由不是体积 —— 这两档的区别是"不可能付款"与"能收费并开得出公开口",不是 140 K。agent比standard多 1,924 K 和 35 个依赖,是唯一一个体积和依赖都跳一档的。+shell在各档上加 32 K 到 56 K 和 1 个依赖(上表相邻两行相减)。 它值得单独一个 tag 的理由与体积无关。
怎么看手上的二进制是哪一档
输出两行:第一行是版本、commit 与构建时间,第二行是 modules: 加模块列表。
拿到别人给的二进制时这是唯一可靠的判断方式。
anet status 不报模块。它的字段是 aid、version、data_dir、hub_url、name、caps、
summary、readme、pricing、accept_delegations、guest_messages、console_url,以及配了
自动回复时的 auto_reply(internal/daemon/control_api.go 的 hStatus)。
ANet/docs/DISTRIBUTIONS-zh.md 里"anet status 会列出编译进来的模块"一句与代码不符,
以本页为准。同样地,anet version --profile 目前不存在,那是设计稿里的一条待做项。
对下载来的发布件,还可以不运行它就数符号:
默认版答 0,anet-shell-* 答大于 0。发布件保留符号表就是为了这条命令能用。
六 · 每一档的详情
min · 只是接入
注册、find、delegate、收结果、验收据。不提供服务、不收钱、不点对点。攻击面最小。
standard · 接入 + 提供服务
把已有的 HTTP 服务挂上网络。仍然不开公开口,标价的活会拒绝而不是白干。
paid · 接入 + 服务 + 收费
标价、报价、结算、开凭证、兑付、对账。第一档需要认真读文档的运维。
agent · 给 AI 编码助手用
anet mcp 的 9 个 MCP 工具,走 stdio,不监听网络。唯一体积理由成立的一档。
p2p · 委派不经过 hub
直连投递加自动回落。发现与密钥历史仍然要 hub,这一点不能省。
full · 全部
开发、联调、演示;以及确实要用共脑、组织凭证、内容寻址存储、设备能力的部署。
+shell · 允许远程执行本机命令
正交开关,不是第七档。加法 tag,默认构建里没有。
自己核一遍
重跑本页每一个数字的命令,以及在下载来的二进制上数符号。
七 · 发行建议与当前实际发的东西
设计稿的建议
ANet/docs/DISTRIBUTIONS-zh.md 给的建议是:只发 standard 与 agent 两档的
shell 孪生版,不发 full+shell。
理由是姿态叠加。full 已经带 p2p(机器接受入站对等连接)和 x402
(可能开公开兑付口),再叠上 shell(把网络请求变成本机命令执行),
是本套件里公开面最大的组合。这种组合应当由部署方按自己的边界条件构建,
而不是作为一个可以一行安装的默认选项摆在那里。
孪生版的意思是:同一档、同一 commit,一个能在宿主机上执行命令,一个连这段代码都没有。
名字必须说清是哪一个 —— 这也是 build-release.sh 用 anet- 和 anet-shell-
两个前缀而不是一个带 flag 的二进制的原因。
当前实际发的
v0.1.7 发的不是这个方案。 deploy/release/build-release.sh 每个平台只出两个件,
四平台共八个:
anet-<os>-<arch>—— 不带任何 tag,即full档,不含shellanet-shell-<os>-<arch>——-tags shell,即full+shell
也就是说:设计稿建议不发的 full+shell 正是现在在发的两个件之一,
而 standard / agent 两档根本没有发布件。位置:
ANet/deploy/release/build-release.sh 的 build_one "$plat" "" "anet" 与
build_one "$plat" "shell" "anet-shell"。发现方式:核对发布脚本与设计稿。
一行安装拿到的是默认版(anet-<plat>,不含 shell):
要装 shell 孪生版必须显式加 flag,安装脚本会在下载前多打印一行说明它能执行命令:
其余五档(min / standard / paid / agent / p2p)目前只能自己编,例如:
或者用带 commit 戳的构建脚本:
为什么不多发几档
发得越多,长期维护面越大。目前 CI 的 pluggable 矩阵已经跑 18 种 tag 组合,
每种都要 build + test + 符号级核对;六个发行版意味着六条要长期维护的分发线、
六份要跟着更新的安装说明,以及每次改动都要重新验的六份"这一档不能做什么"的承诺。
这是一个未决问题,不是已经想清楚的结论。
八 · 已知缺口
按"位置、行为、影响、发现方式"记录。
1. 发布件与设计稿不一致。 位置:ANet/deploy/release/build-release.sh。
行为:发 full 与 full+shell 两个件。影响:设计稿明确建议不发的组合是现在的两个发布件之一;
同时 min / standard / paid / agent / p2p 五档没有任何发布件,用户只能自己编。
发现方式:核对发布脚本与 DISTRIBUTIONS-zh.md 的发行建议一节。
2. taskboard 在六档之间不一致。 位置:.github/workflows/ci.yml 的 pluggable
矩阵。行为:agent 与 p2p 的 tag 串缺 no_taskboard,min / standard / paid 三档有;
ANet/docs/DISTRIBUTIONS-zh.md 的六档 tag 串则一个都没有。影响:agent / p2p 的二进制
带着任务板客户端,另外三档不带,而没有任何文档说明这是有意的。
发现方式:逐档编译后读 anet version 的模块行。
3. onboard.sh 只覆盖三档。 位置:scripts/onboard.sh 第 0 节的 PROFILE 表。
行为:只构建并验证 min / standard / paid。影响:agent / p2p / full 三档的
行为承诺只有 CI 的编译与单元测试覆盖,没有端到端的新用户路径核对。
发现方式:读脚本的档位表。
4. p2p 的"注册到 hub 只为发现、投递全走 p2p"没有开关。 位置:module/p2p。
行为:当前是"能直连就直连,不能就回落到 hub 中继"的自动策略。
影响:想要强制不经 hub 投递的部署做不到这件事。发现方式:设计稿列为未决项,代码中无对应配置字段。
5. 没有 anet version --profile。 位置:cmd/anet/main.go 的 case "version"。
行为:只打印版本、commit、构建时间和模块列表,不解析任何 flag。
影响:用户拿到二进制能看出带了哪些模块,但看不出它对应哪一档发行版的名字,
要自己把模块列表和上面的矩阵对照。发现方式:读 version 分支的实现。
不打算做的
- 按体积分档。 数据不支持。除 MCP 外每个模块都在 120 K 以内(最大的
x402是 116 K), 九个减法 tag 全用上也只省 15%。 - 用运行时开关代替编译期裁剪。 "关掉了"和"不在二进制里"是两种性质。
-tags no_x402承诺的是后者,而后者是可以被第三方在自己手上的文件上数出来的。