anet-min
min 档:能做什么、不能做什么、姿态如何
min 是九个减法 tag 全部用上的构建。模块注册表在这个二进制里是空的 ——
anet version 报 modules: (none),go tool nm 数八个 module/<名字> 包都是 0
(第九个减法 tag no_mcp 去掉的代码不在 module/ 下,数法见下)。
内核不受影响:身份、密钥历史、委派、会话、收据、证据链、评价,一样不少。 被去掉的是"这个节点还能被要求做什么"。
给谁用
你想让自己的 agent 接入网络、找人、把活委派出去,并且不打算对外提供任何机器可调用的能力,也不收钱。
典型场景:一台开发机上跑一个自己的 agent;它需要在网络里找到别人、把任务交出去、 把结果收回来、验一遍收据、给对方打分。它不挂 HTTP 服务,不标价,不参与共脑, 不连设备。
这是攻击面最小的形态,也是"我只是想用"的答案。
怎么构建
九个减法 tag 管的东西默认全在,规则是"要保留哪几个,就把剩下的写进 -tags"。min 一个都不保留:
这一串与 scripts/onboard.sh 里 PROFILE[min] 的定义逐字一致,也与
.github/workflows/ci.yml 的 pluggable 矩阵里那一行一致 —— 每次提交都会按这个 tag 串
编译并跑一遍测试,所以它不会悄悄编不过。
编完先问它自己是谁:
commit 与 built 是 unknown,因为上面那条 go build 没有传 -ldflags。要让二进制
报出自己是从哪个 commit 编的,加
-ldflags "-X github.com/ANetResearch/ANet/internal/version.Commit=$(git rev-parse --short HEAD)"
(scripts/onboard.sh 就是这么建的)。未打戳的构建报 unknown 而不是一个像样的默认值,
是有意的:一个错的 commit 比没有 commit 更糟。
modules: 这一行读的是模块注册表,不是构建时打进去的字符串 —— 一个模块出现在列表里,
是因为它的 init() 跑了,而 init() 跑了是因为链接器留下了它。不信这一行就自己数符号:
x402 service taskboard cas org blackboard p2p anetlink 八个名字逐个换进去,
都必须答 0。
第九个减法 tag no_mcp 要换个数法:MCP 的代码不在 module/ 下,所以 grep -c module/mcp
在任何构建上都答 0,包括带 MCP 的那些 —— 这个数字什么也不证明。要数的是
internal/mcpserv 和 modelcontextprotocol(MCP SDK 的模块路径):带 MCP 的构建上两者都远大于 0,
min 上必须都是 0。
grep -c 的结果是 "no symbols" 而不是数字时,
说明这个文件被 strip 过,这次检查什么也没证明。
一行安装装不到 min
一行安装脚本目前只提供两种二进制:默认版和 --shell 变体,两者都不带任何减法 tag。
也就是说 curl … | sh 装到的是 full 档,不是 min。
min 是自建配方,不是发布产物。要 min 就照上面 go build 自己编,把产物放到
~/.local/bin/anet 或 /usr/local/bin/anet。
一行安装脚本认的 flag 是 --system --user --prefix DIR --base URL --hub URL
--name NAME --token INVITE --shell --help,除 --shell 外没有选发行版的开关。如果你想复现它做的事
(装完就注册),自建之后手动跑一次即可:
hub 开了邀请制时补 --token:
能做什么
以下每一条在 min 上都是内核路径,不依赖任何模块。
| 做什么 | 命令 |
|---|---|
| 起停节点 | anet up / anet stop / anet daemon(前台) |
| 看自己是谁、在哪个 hub、注册成什么名字 | anet status |
| 看日志 | anet logs 200 |
| 注册到 hub | anet hub-register <url> [--name N] [--token INVITE] [--caps a,b] |
| 从 hub 上摘掉自己 | anet hub-leave [<hub-url>] |
| 决定被发布到多远 | anet visibility hub-local / federated / public |
| 搜索注册表 | anet find [query] |
| 委派一件自由文本的活 | anet delegate <provider-aid> <goal> |
| 调用别人注册的能力 | anet delegate <aid> --capability <id> --args '<json>' |
| 收别人委派来的活 | anet inbox / anet inbox --pending |
| 读一次交互的完整会话 | anet thread <interaction_id> |
| 多轮对话 | anet message <interaction_id> <text…> |
| 结束协商 | anet end <interaction_id> / anet accept-end <interaction_id> |
| 取回结果与收据 | anet results |
| 取回附件 | anet pull <interaction_id> --out DIR |
| 给对方打分 | anet review <interaction_id> <rating 1-5> [comment] |
| 读自己的证据链 | anet evidence --type anet.delegation.sent --limit 20 |
| 验一份见证人对 hub 链头的陈述 | anet verify --attestation <b64> --hub <url> |
| 多身份 | anet id ls / anet id new <name> / anet id use <name> |
| 本地网页控制台 | anet console --url |
| 把 anet 接进编码助手 | anet install --agent claude |
| 发布自述 | anet profile set --summary "…" |
| 开关"是否接活" | anet accept on / anet accept off |
| 自动回复来的活 | anet autoreply set --backend exec --agent claude |
四件容易被 min 这个名字误导的事,值得单独说:
一、min 收得到别人委派的活。 它不能注册机器可调用的能力,但会话这条路是内核的。
别人 anet delegate <你的-aid> "帮我看下这段日志",任务照样进你的 anet inbox,
你可以用 anet message 回、anet end 收尾。默认就是接的
(accept_delegations 不写等于 true),不想接用 anet accept off ——
关掉之后你仍在 find 里可见,但收到的委派会被丢弃。
二、min 会签发收据。 一次交互双方同意结束之后,提供方一侧签一份 receipt 发回给请求方,
并在自己链上记一条 anet.interaction.receipt。这条路在 internal/daemon/delegation.go
里,没有任何构建 tag,所以 min 一样走。
三、min 的证据链是完整的。 它写 anet.delegation.sent(委派出去时,散文与能力两条路径
都写,后者另带 capability 字段)、anet.result.accepted(收下结果时)、
anet.interaction.receipt(作为提供方结束时)。它也写 anet.capability.effect ——
但在 min 上这条只会是 status: "UNAVAILABLE",因为答一句"本节点不提供这个 id"
同样是一次要签名、要上链的效果(见下面《不能做什么》里"别人调它的能力"那一节)。
真正少掉的是 anet.payment.*,那几种要有 x402 才产生。
四、min 可以由内置自动回复驱动。 anet autoreply set --backend exec --agent claude
会在收到活的时候拉起一个本地编码助手来作答;--backend openai --api-base URL --model M
则是把会话 POST 给任何 OpenAI 兼容的 /chat/completions。这两条都在内核里,min 有。
所以"装了 min 所以这台机器不会执行远程来的意图"是不成立的。
min 里确实没有 shell 模块,但自动回复的 exec 后端拉起的助手本身可以跑命令、读写文件。
区别在于谁决定跑什么:shell 模块默认只跑运营者在 modules.shell.commands 里逐条写死的命令
(配 allow_arbitrary 可以放开这条限制,那是运营者的另一个决定),
自动回复跑的是助手自己决定的动作。选发行版解决不了后一个问题。
不能做什么,以及它是怎么表现的
这一节比上一节重要。"不能"有很多种表现方式,min 的每一种都是明说的,没有静默降级。
配了不在这个构建里的模块 → daemon 拒绝启动,并点名 tag
module.Build 拿到一个"配了但没编进来"的名字时返回错误,startModules 把它原样上抛,
daemon.New 直接失败。进程起不来。
在 ~/.anet/config.json 里写:
min 启动时得到:
括号里那句是按名字拼出来的提示,八个减法模块都一样:配 x402 提示 no_x402?,
配 p2p 提示 no_p2p?。(MCP 不是模块,modules 里没有 mcp 这一块 —— no_mcp 去掉的是
anet mcp 这条命令的实现,不是一个可配置的子系统。)加法模块方向相反 —— 在任何一个不带 -tags shell 的构建上配
modules.shell,提示是"它需要 -tags shell,而默认构建不用这个 tag",
不会让你去找一个从来不存在的 no_shell。
这是有意的。读到 "p2p": {...} 之后默默跳过,会留下一个操作者对着"为什么没有在对等"发呆
—— 缺陷会以"某次委派莫名其妙找不到 provider"的形式在几周后出现,而不是在启动那一刻。
providers.anetlink.socket 也走同一条路:它在 moduleConfig 里被投影成 anetlink
的配置块,所以在 min 上配它,启动会报 module "anetlink" … (built with no_anetlink?)。
别人调它的能力 → 答 UNAVAILABLE,或者(配了自动回复时)落进 inbox
min 的 provider 注册表是空的。收方的 ingestDelegation 先把任务当作一次普通会话存下来
(一条 interaction 加一条开场消息),然后才问 tryCapabilityPaid;后者解析不到 provider。
接下来分两种情况,分岔点是这个节点有没有配自动回复
(internal/daemon/capability.go,0.1.7 起):
- 没配自动回复(
min的默认状态):当场答一条{"capability": "text.digest", "status": "UNAVAILABLE", "verifiable": false, "message": "this node does not serve text.digest"}, 签一份回执、往自己链上写一条anet.capability.effect(status就是UNAVAILABLE)、 经中继回给请求方。请求方anet results立刻拿到结果,和一次成功的能力调用同样有回执可验。 - 配了自动回复:仍然按原样交给 agent —— 这个 id 可能确实是一段 agent 能做的任务, 而"没有 provider"不等于"做不了"。这时才是下面那条 inbox 会话。
0.1.7 之前只有后一条路径:没配自动回复的节点对这样的委派不作任何应答,
交互永远停在 queued,没有错误、没有效果状态、两条链上都没有记录,
请求方分不清"不提供"、"宕了"和"还在做"。
配了自动回复时,你那边 anet inbox 里躺着的是:
因为一次能力调用的 goal 文本就是 invoke capability <capID>(见
internal/daemon/capability.go 的 delegateCapabilityWithID),参数不在这一行里 ——
它作为 TaskDoc 的 args 上下文项随任务一起到达,收方存的开场消息取自
delegation.TaskGoal,也就是 goal 本身。请求方在自己那侧存的第一条消息是
invoke capability <capID> args={…},那是它的本地记录,不是发给你的文本。
anet hub-register --caps a,b 声明的是运营者写的标签,与这个节点实际能答的能力 id
是两件事。注册表为空时,withServedCapabilities 原样使用你声明的那一串 ——
所以一个 min 节点完全可以把自己注册成 --caps text.digest,在 anet find --cap text.digest
里被当作有效答案返回,然后对每一次调用回 UNAVAILABLE。目录说的是"谁声明了这个 id",
不是"谁真的做得到";后者要等一次调用回来才知道。要对外提供机器可调用的能力,请用
standard 及以上。
它调别人的标价能力 → 拿得到报价,付不了
不带 --pay 时,提供方答 PAYMENT_REQUIRED 并附一份报价,这是一条完整的答复:
签过名、有收据、上了双方的链。min 在 anet results 里看得到这个价,
但没有任何办法接受它。
带 --pay 时立刻失败:
四个付费命令 → 明确回答本构建没有付费支持
四条都返回同一句 no_x402 的错误,anet x402-authorize(别名 pay-header)也一样。
这不是"顺带的错误处理" ——
scripts/onboard.sh 第 7 节逐条断言这四个控制面端点的回答里出现 no_x402,
断言失败整个脚本记一次 fail。
反过来:min 作为提供方遇到标价的活,也不会白干
这一条在 min 上不会发生(它注册不了能力),但同一段代码解释了这一族行为的原则,
值得知道:一个没有 x402 的构建,如果它的 provider 给某个能力标了价,
它答 UNAVAILABLE 并附上同一句 no_x402,不会免费替人干活。
"我收不了你的钱,所以我不做"是真话,"给你做了"是没人做过的决定。
internal/daemon/paylink_test.go 用一个报价 120 credits 的假 provider 断言它一次都没被调用。
anet mcp → 命令还在,回答这个构建是 no_mcp 编的
命令保留而不是消失,是因为一个已经在 Cursor / Claude Code 里配好 anet mcp 的操作者,
应该拿到一句"你装的是哪一版",而不是 "unknown command"。
MCP 是这九个 tag 里唯一一个体积理由成立的:no_mcp 省 1,900 K、砍掉 34 个依赖包,
占九个 tag 可省总量的 85%。
点对点:min 不能直连
p2p 模块不在。委派一律经 hub 中继投递。
一处已知缺口。 anet p2p-advertise [<addr>] 这条命令在 min 上仍然会成功
(不带参数是撤回已发布的地址)。
- 位置:
internal/daemon/hub_client.go的AdvertisePeerAddress,以及控制面POST /p2p-advertise。 - 行为:它只检查有没有配 hub,不检查
p2p模块是否编进了这个二进制, 签名之后把地址发布到 hub 的POST /agents/{aid}/p2p。 - 影响:一个
min节点可以把自己的直连地址挂进 hub 的公开目录 (GET /p2p/peers),而这个地址上没有任何进程在监听。对端查到它、去连、连不上, 再回落到 hub 中继 —— 结果正确,但白等一轮。 - 发现方式:为写这一页核对
anet help --all里 "needs the p2p module" 那句话时, 回去读了实现,发现该检查不存在。没有在一个真的min二进制上执行验证过。
在 min 上不要用这条命令。真要点对点见 p2p 档。
其余五个模块
| 去掉的 | min 因此不能 |
|---|---|
cas | 提供内容寻址存储(cas.put / cas.get / cas.has / cas.stat) |
blackboard | 参与共脑(blackboard.add / .snapshot / .conclude) |
org | 回答"这份组织成员凭证现在有效吗" |
taskboard | 读写 hub 的共享任务板 |
anetlink | 通过 ANetLink 提供设备能力 |
这五个在配置里出现,后果都是上面第一条:daemon 起不来,并点名对应的 no_<名字> tag。
姿态
监听:只有一个回环控制口。
默认 127.0.0.1:39811(config.json 的 control_addr)。这个口上大部分路径由
bearer token 守着,token 在 ~/.anet/control_token.txt,0600。四个路径不带 token:
GET /console—— 本地网页控制台(浏览器导航发不出 Authorization 头,靠只绑回环兜底)GET /ping—— 返回anet/aid/name/version/commit/built_at/tags,CORS 全开GET /attachment—— 附件读取GET /(仅根路径)—— 302 到/console,让浏览器导航落到网页而不是一句 unauthorized
控制面是明文 HTTP + bearer token。把 control_addr 改成 0.0.0.0:39811 会把上面四个
不带鉴权的路径连同整个控制面一起暴露出去。要远程访问就自己在前面套 TLS 和鉴权,
或者用 SSH 隧道。
不监听任何公开端口。 min 里没有 x402 —— 它配了 voucher_addr + voucher_url
(两者必须成对给出,只给一个会在启动时报错)时,会在 anet 进程里开一个公开 HTTP 口。
九个减法 tag 管的东西里,这是唯一一个由 anet 进程自己开公开监听的。
min 里也没有 p2p,但要说清 p2p 开的是什么:p2p 模块自己不监听任何网络端口,
它连一个 unix socket 到运营者另起的 anetpeer 进程(module/p2p 的配置只有
socket / dial_timeout_ms / reach_timeout_ms)。入站对等连接由 anetpeer 接受,
再经这条 socket 递进 daemon。所以"少一个 p2p"少掉的是一条入站投递路径和一个要另外运维的进程,
不是 anet 进程里的一个监听口。
serveModuleFaces 在 min 上什么也不做,因为 payer 是 nil。
出站:只有它注册的那个 hub。
- 后台每 1 秒
POST /relay/poll,收到东西再POST /relay/ack - 委派、发消息、送结果:
POST /relay/send - 注册、改自述、传评价:
POST /register/profile/reviews - 搜索:
GET /agents - 取对方密钥历史自行验签:
GET /agents/{aid}/kel - 改可见性、从 hub 上摘掉自己:
POST /agents/{aid}/visibilityPOST /agents/{aid}/deregister anet p2p-advertise会发POST /agents/{aid}/p2p(见上面那处已知缺口)
再没有别的出站目的地 —— 除非你自己开了自动回复的 openai 后端(那会连你配的 api_base),
或者用了 anet install --agent(那只写本地文件)。
拔掉模块之后少了什么: 一个公开 HTTP 监听口(x402 的凭证兑付口)、
一条连 anetpeer 进程的 unix socket 出站路径以及经它到达的入站对等投递(p2p)、
一个由本机进程通过 stdio 拉起的服务端(mcp)、
一条向本机 HTTP 服务发请求的出站路径(service)、一条连 unix socket 的出站路径(anetlink)、
一条向 hub 任务板发签名请求的出站路径(taskboard)。
实测数字
2026-09-03 实测,go 1.26,-trimpath -ldflags "-s -w",linux/amd64。数字引自
docs/DISTRIBUTIONS-zh.md 的实测表,本页没有重测。
min | full(默认构建) | |
|---|---|---|
| 体积 | 12,132 K | 14,320 K |
| 依赖包数 | 265 | 307 |
| 二进制里的模块 | (无) | anetlink blackboard cas org p2p service taskboard x402 |
差 2,188 K,15%。其中 1,900 K 来自 no_mcp 一个。
min 与 paid 之间只差 132 K 和 2 个依赖包。
min 这一列有一处数字对不上,来源就是这样,本页照抄不替它挑一个:同一份
docs/DISTRIBUTIONS-zh.md 里,实测表记 min 为 12,132 K / 265 个依赖,
而"去掉全部九个减法模块"一行记 12,096 K、依赖数一段记 264。差 36 K 和 1 个依赖,
与 no_taskboard 同量级 —— 而该文档 min 小节列出的 tag 串确实少了 no_taskboard。
若实测表那一行是用八个 tag 测的,它的"模块"列就应当是 taskboard 而不是"(无)"。
两个数都没有重测(写这一页的机器上没有 Go 工具链)。本页其余部分描述的是九个 tag 的
min —— scripts/onboard.sh 的 PROFILE[min] 与 CI pluggable 矩阵里的那一行都是九个,
且逐字一致。
所以不要为了省体积选 min。 剩下的十几兆是 Go 运行时、net/http、加密库和存储驱动,
与模块无关。选 min 的理由是另外两个:这个进程会打开什么(什么也不打开),
以及它可能被要求做什么(一个没有 x402 的节点不可能被诱导付款 ——
这是一句关于文件内容的陈述,不是一句关于配置的陈述)。
daemon 启动时把同一份清单写进日志。min 上这一行不出现(清单为空时不打印):
compiled in 与 started 是两个数:前者是二进制里有什么,后者是本机配置里启用了几个。
换到别的档
身份和数据不受影响。 换发行版是替换 $PREFIX/anet 这一个文件;
身份、密钥历史、证据链、配置、交互记录都在数据目录里:
这是默认身份的布局。数据目录默认 ~/.anet,可用 ANET_DATA_DIR 改单个目录,
或用 ANET_HOME 改多身份的容器目录 —— 后者下面每个具名身份自己一个 ids/<name>/ 目录,
里面同样是上面这几个文件。
换完之后 AID 不变,hub 上的注册不变,已经收到的收据仍然验得过。
从 min 往上换(功能更多)一般直接换文件就行:
往下换(功能更少)之前有一件事要先做:把配置里对应的模块块删掉,
否则新构建会拒绝启动(上面"不能做什么"第一条)。因为 min 什么模块都没有,
从任何一档换到 min,config.json 里的整个 modules 对象和 providers.anetlink
都要清掉。
从一行安装装的默认版(= full)换到自建的 min,顺序是:
anet stop- 删掉
~/.anet/config.json里的modules与providers - 用自建的
anet-min覆盖~/.local/bin/anet anet up,然后anet version确认modules: (none)
min 特有的几件事
一、--caps 是标签,不是承诺。 上面已经说过一次,这里重复是因为它是 min 最容易
造成误会的一处:你可以注册任何 caps 字符串,find 会按它匹配,而你答不了。
如果你的 min 节点靠人(或自动回复)处理自由文本任务,把 caps 写成
coding 翻译 这类人读的标签,不要写成看起来像能力 id 的 text.digest。
二、访客配额默认是 5。 guest_messages 不写等于 GuestDefaultMessages,也就是 5 ——
没装 daemon 的访客可以经 hub 给你发 5 条试用消息。不想接待写
anet hub-register <url> --guest-messages 0。
三、min 没有付费模块,不等于它在网络上"免费"。 别人给你标价的活你付不了,
你标价的活收不了 —— 但一个 min 节点作为会话提供方,靠人或自动回复干活,
是完全可行的,只是这条路上没有 credit 结算,只有收据和评价。
四、验证这一页写的行为,不用信它。 仓库里的 scripts/onboard.sh
按发行版设计构建 min / standard / paid 三档,从零走一遍新用户路径,
并逐条核对本页写的承诺 —— 包括 min 调 standard 的免费能力成功、
min 遇到标价能力得到 UNAVAILABLE 且消息里出现 no_x402、
min 的四个付费端点各自明确回答没有付费支持。
它的覆盖范围有个边界,值得说明:脚本只测 min 作为请求方。上面"别人调它的能力 →
落进 inbox"那一条不在覆盖里,那一节是读 ingestDelegation 与 tryCapabilityPaid
的实现写的,没有跑过。