anet-full
full 档:能做什么、不能做什么、姿态如何
full 是不加任何减法 tag 的默认构建。九个减法模块一个不拔,加法 tag shell 不加。
它同时是今天唯一有发布件的档:一行安装拿到的 anet-<os>-<arch> 就是 full。
所以对绝大多数用户来说,"我装了 anet" 等于 "我装了 full"。
九个名字。其中八个是 module.Module;mcp 不是 —— 它是 anet mcp 这条子命令加上
internal/mcpserv 包,由 //go:build !no_mcp 控制,不经过模块注册表,
而是由带同一个 tag 的 cmd/anet/module_mcp_declare.go 用 module.DeclareCompiled("mcp")
把名字补进这一行。要在文件里数它,用 go tool nm ./anet | grep -c internal/mcpserv。
一 · 给谁
三类人:
- 开发与联调。 你在改 anet 本身,或者在写一个要跟 anet 对接的东西,需要所有接缝都在手边。
仓库里的本机多进程联调脚本
scripts/joint.sh用的就是裸go build ./cmd/anet, 即full;scripts/prodtest.sh打的是实网上跑的发布件,那也是full。 - 演示。 一台机器上要能展示委派、收费、共脑、设备控制、任务板,不想为每个环节换二进制。
- 确实用得上被裁掉的那几个模块的部署。 共脑(
blackboard)、组织成员凭证(org)、 内容寻址存储(cas)、设备能力(anetlink)—— 这四个在其余五档里全部不存在,要它们就只能是full,或者自己拼一个 tag 串。
不是给谁: 如果你的场景是"我只想接入并委派",那是
min;"把已有 HTTP 服务挂上去",那是
standard;"靠能力收 credit",那是
paid。选 full 不会让这些场景更好用,只会让这台机器
多带七八个用不上的子系统 —— 而其中两个(x402、p2p)是套件里仅有的两个
会让节点接受外部连接的模块。
二 · 怎么编出来
tag 串:空的
full 没有 tag 串。这不是"文档省略了",而是它的定义:
CI 里对应的也不是 pluggable 矩阵的某一行 —— 矩阵里全是 no_* 组合,没有空行。
full 由 .github/workflows/ci.yml 的 test 与 race 两个 job 覆盖:
裸的 go vet ./...、go test ./...、go test -race ./... 编的就是 full。
与发布件一致的编法
deploy/release/build-release.sh 用的就是这一串。注意 -w 而不是 -s -w:
-s 会连符号表一起剥掉,go tool nm 之后答的是 "no symbols"。
一个剥过的二进制上 grep -c module/shell 返回 0,看起来像通过了检查,实际什么也没证明。
可插拔的判据是"能被第三方在自己手上的文件上数出来",所以发布件保留符号表,
代价是 1.4 MB(14,320 K → 15,760 K)。发布脚本自己也先检查符号表在不在,
不在就直接失败,理由写在脚本里:"the check below would pass vacuously"。
一行安装对应什么
装到的就是 full。装完顺手注册:
hub 开了邀请制时补 --token:
脚本认的全部 flag 是 --system --user --prefix DIR --base URL --hub URL
--name NAME --token INVITE --shell。其中只有 --shell 会换二进制
(换成 anet-shell-<平台>,即 full+shell),没有任何 flag 能选到别的档。
编完先核一遍
这一行读的是 module.Compiled() —— 模块注册表,init() 跑过才在里面 ——
不是构建时打进去的 tag 字符串。version.Tags 那个字段确实存在,但 version 子命令
故意不打印它:go build -tags shell 不带 -X ldflag 编出来的二进制,
模块在里面而 Tags 是空的,一个会对能执行宿主机命令的二进制说"默认构建"的字段,
比没有这个字段更糟。
八个 module/ 之外还要单独数两样 —— mcp 的代码不在 module/ 下,
而 shell 是这一档不该有的那个:
第二条是 full 最需要自己确认的一条。full 的名字容易读成"全部都有",
而 shell 恰恰不在里面 —— 这是加法 tag 的全部意义。
三 · 编进来 ≠ 起来了
这一节放在"能做什么"前面,因为 full 上最容易误判的就是这件事。
daemon 启动时把两个数写进日志:
compiled in 是二进制里有什么,started 是本机配置里实际起来了几个。
一个刚装完、config.json 里没有 modules 块的 full 节点,这一行是
八个名字,started: 1。
为什么是 1 而不是 0:八个模块的工厂里有七个在配置块为空时返回 (nil, nil)
—— 注释写作 "compiled in, not configured" —— 唯独 module/x402 的
New 无论有没有配置都返回一个实例。
full 开箱即带付费能力,不需要写 modules.x402。
位置:module/x402/x402.go 的 New。行为:len(raw) == 0 时不返回 (nil, nil),
而是返回一个零配置的 Module;startModules 因此把它设为这个节点的 payer。
影响:一个只跑了 anet up + anet hub-register <hub-url> 的 full 节点,anet balance 读得到余额,
anet delegate --pay 这条路径是通的 —— 不需要任何配置。
(能不能真的买下一次调用,还要看这个 hub 的账本上它有没有余额;
这里说的是"付款能力在不在",不是"这一笔一定成交"。)
发现方式:为写这一页对照八个模块的工厂函数,只有这一个没有空配置短路;
New 与 startModules 的代码路径 + 一个空配置 full daemon 的 (started: 1) 日志佐证。
没有在缺 modules.x402 的情况下实际跑完一次付款。
这不是缺陷,是有意的:没有账本就没有价格,而账本来自 hub 而不是配置文件。
但它意味着"我没配 x402 所以这台机器不会付款"在 full 上是不成立的。
真要这个性质,用 min 或任何带 no_x402 的档 ——
那是一句关于文件内容的陈述,不是一句关于配置的陈述。
反过来,另外七个模块必须配了才起。没配 modules.cas,cas.put 就没有 provider;
没配 providers.anetlink.socket,设备能力就一个都注册不上。
full 给你的是"随时可以配",不是"已经在跑"。
四 · 能做什么
内核部分
full 包含 min 页"能做什么"表里的每一条,一条不少:
起停、身份、注册、find、delegate、inbox、thread、message、end、
results、pull、review、evidence、verify、visibility、多身份、控制台、
自动回复。那些都在内核里,不带任何 tag。
下面只列 full 相对 min 多出来的部分。
service —— 把本机 HTTP 服务挂成能力
别人用 anet delegate <你的-aid> --capability text.digest --args '{"text":"anet"}' 调用。
price 省略或 0 表示免费。
x402 —— 收费与付费
| 做什么 | 命令 |
|---|---|
| 读 hub 账本上的余额与明细 | anet balance |
| 调用标价能力并付款 | anet delegate <aid> --capability <id> --args '<json>' --pay |
| 手工签一份付款授权 | anet x402-authorize |
| 把 credit 交回 hub 换外部凭据 | anet redeem <amount> --ref <reference> |
| 拿自己的记录对 hub 的账 | anet reconcile |
| 验 hub 的发放链 | anet audit-hub |
| 验一份见证人对链头的陈述 | anet verify --attestation <b64> --hub <url> |
配置块两个可选字段,详见 paid 页:
mcp —— 给编码助手用的 stdio 服务
9 个工具:agents_find task_delegate task_results task_inbox task_message
task_end evidence_read credit_balance node_status。
p2p —— 直连投递
需要一个独立进程 anetpeer,p2p 模块只是它的客户端:
然后把自己的直连地址发布到 hub 的目录:
对端用 GET /agents/{aid}/p2p 查,目录本身经 GET /p2p/peers 公开。
taskboard —— hub 的共享任务板
三个能力:task.board(读七列看板)、task.create、task.claim。
两个写操作由节点自己签名(post 里的 sign("task.create") / sign("task.claim")),
hub 按它持有的密钥历史验;task.board 是只读的,不签名 ——
模块里给的理由是签名只会说明谁在看。
配置块的内容可以是空对象 {}(用本节点自己的 hub),但这个块必须存在:
modules.taskboard 整个缺失时工厂返回 (nil, nil),模块不起。也可指定别的 board:
blackboard —— 共脑
三个能力:blackboard.add(args 里 unit 是 base64 的 CogUnit)、
blackboard.snapshot、blackboard.conclude(两者取 task_id)。
贡献由作者自己签名,本节点只验不签。
cas —— 内容寻址存储
四个能力:cas.put(body,base64)、cas.get / cas.has / cas.stat(cid)。
dir 没有默认值,必须写。单块上限默认 32 MiB。
org —— 组织成员凭证
两个能力:org.verify(credential,base64)、org.info。
anetlink —— 设备能力
配置走 providers(不是 modules),由 moduleConfig 投影成 anetlink 的配置块:
对面是 anetlinkd --c1-socket /run/anetlinkd/c1.sock。
daemon 不知道"设备"这个概念,它只看到一个 C1 provider —— 这是 C1 契约的内容。
五 · 不能做什么,以及它是怎么表现的
full 是六档里"不能"最少的一档,但"不能"的每一条都值得知道它长什么样。
一、不能在宿主机上执行命令
module/shell 不在这个二进制里。配了 modules.shell 会让 daemon 拒绝启动:
注意括号里那句话与减法模块不一样。配一个 no_* 拔掉的模块,提示是
"built with no_service?" 这种形式;配 shell,提示明确说要 -tags shell,
而不会让你去找一个从来不存在的 no_shell。
这条分支在 module/module.go 的 Build 里,靠 optInNames 这张表 ——
表填在 cmd/anet/optin_tags.go,那个文件没有构建 tag,所以一个不含 shell 代码的
二进制仍然知道 "shell" 这个名字该配哪个 flag。
要这个能力就装 --shell 变体,或自己 go build -tags shell。
即便如此,它在没有 modules.shell 配置块时不注册任何能力,配了但调用方名单为空时拒绝所有人。
二、不能强制"只走 p2p"
p2p 起来之后的策略是能直连就直连,不能就回落到 hub 中继,没有开关可以关掉回落。
"注册到 hub 只为发现、投递一律不经 hub"这个模式今天做不到。
设计稿把它列为未决项,代码里没有对应的配置字段。
三、hub 仍然是必须的
即便配了 p2p,发现仍然经 hub:anet find 查的是 hub 的注册表,
对端的直连地址查的是 hub 的 GET /agents/{aid}/p2p,别人的密钥历史查的是
GET /agents/{aid}/kel。纯 p2p 无发现是做不到的。
taskboard 更直接 —— 没有 hub 它根本起不来:
这个错误是致命的,daemon 起不来。理由写在模块里:板子是 hub 的, 一个没有 hub 的节点起来了也只会注册三个对每次调用都答错误的能力。
四、几个"配了就必须配对"的地方,都在启动时拦
| 配置 | 写错的表现 |
|---|---|
modules.service 的 capabilities 为空 | service: no capabilities declared |
| 同一个能力 id 写两遍 | service: capability "x" declared twice |
只写 voucher_addr 不写 voucher_url(或反过来) | x402: voucher_addr and voucher_url must be set together |
modules.p2p 缺 socket | socket is required |
modules.cas 的 dir 打不开 | cas: open <dir>: … |
modules.org 的 genesis 不是 base64 / CBOR 坏了 | org: genesis not base64 / org: genesis malformed |
providers.anetlink.socket 连不上 | register provider on <sock>: … |
七条都让进程起不来,不是记一条日志继续跑。这是 startModules 的规定:
一个编进来了、也配了、却起不来的模块是运营错误,带着缺失的能力启动是那种
"几周后以某次委派莫名其妙找不到 provider 的形式出现"的失败。
五、没有 hub 时,付费面的说法与 min 不同
full 上永远不会出现 no_x402 那句话 —— 模块在。没有 hub 时得到的是另一句:
两句话说的是两件事:min 说的是"这个二进制里没有付费代码",
full 说的是"代码在,但还没告诉它在哪家 hub 记账"。
五种诚实效果状态的分法在这里 ——
"不知道"和"知道没问题"不合并,"没有能力"和"没有配置"也不合并。
六、二进制自己说不出它是哪一档
anet version 只打印版本、commit、构建时间和模块列表,不解析任何 flag。
没有 anet version --profile。你拿到一个二进制,能看出它带了哪八个模块,
要自己对着总览页的矩阵才知道那叫 full。
六 · 姿态
full 是套件里默认姿态最开的一档,也是唯一一个两个"会接受外部连接"的模块都在里面的档。
但"在里面"和"打开了"是两件事,值得逐条说清楚。
默认监听:只有一个回环控制口
刚装完、没配任何模块块的 full 节点,监听面与 min 完全一样:
127.0.0.1:39811(config.json的control_addr),bearer token 在~/.anet/control_token.txt(0600)- 四个不经 bearer 的路径:
GET /console(本地网页控制台)、GET /ping(CORS 全开, 返回anet/aid/name/version/commit/built_at/tags)、GET /attachment,以及GET /(302 重定向到/console)
控制面是明文 HTTP + bearer token。改成 0.0.0.0:39811 会把上面四个不经 bearer 的路径
连同整个控制面一起暴露。要远程访问自己套 TLS 和鉴权,或者走 SSH 隧道。
两个可能打开的公开口,都要显式配置
x402 的凭证兑付口。 startRedeemFace 第一件事就是 if addr == "" { return nil } ——
不配 voucher_addr 就不 net.Listen。配了会在日志里写:
注意即使 x402 模块总是启动(上面第三节),这个口仍然要显式配置才开。
p2p 的入站对等连接。 这个口根本不在 daemon 进程里 —— 它在 anetpeer 里。
不跑 anetpeer,modules.p2p 就连不上 socket;跑了 anetpeer --peer 0.0.0.0:34001,
监听的是 anetpeer 而不是 anet。p2p 模块之所以是进程外的,是依赖数的算术:今天 anet4 内核的 go.mod 有 5 个直接依赖,
go.sum 110 行(55 个模块版本);module/p2p/p2p.go 的注释记着对照的另一头 ——
anet3 的 libp2p 那一套要在一千多个包的树里带进 32 个模块。
而 build tag 拿掉的是代码,从来不是 go.mod 里的依赖:每次构建仍然解析它,
CI 仍然下载它,供应链审计仍然要覆盖它。
出站:比 min 多五条
min 的出站只有它注册的那个 hub(relay 轮询、注册、自述、评价、find、KEL)。
full 在此之上,按你配了哪些模块,可能多出:
| 模块 | 出站目的地 |
|---|---|
service | 你在配置里写的那些本机(或内网)HTTP 服务 |
x402 | hub 的 /x402/*(结算、发放链、见证)与 /agents/{aid}/balance、/agents/{aid}/ledger、/agents/{aid}/kel |
taskboard | hub 的 /tasks/* |
p2p | anetpeer 的 unix socket;对等节点由 anetpeer 去连 |
anetlink | anetlinkd 的 unix socket |
mcp、cas、blackboard、org 四个没有出站行为。
拔掉模块少的是什么
full 一个也没拔,所以这一节反过来读 —— 从 full 往下裁,每一步少的是:
no_x402:一个可能的公开 HTTP 监听口,一条向 hub 结算的出站路径, 以及"这个节点可能被诱导付款"这件事本身no_p2p:一条入站对等连接路径no_mcp:一个由本机进程 stdio 拉起的服务端(以及 1,900 K 和 34 个依赖包)no_service:一条向本机 HTTP 服务发请求的出站路径no_anetlink/no_taskboard:各一条出站路径(unix socket / hub/tasks/)no_cas/no_blackboard/no_org:不打开任何东西,少的是功能而非风险
七 · 实测数字
2026-09-03 实测,go 1.26,linux/amd64。
full | min | |
|---|---|---|
体积(-trimpath -ldflags "-s -w") | 14,320 K | 12,132 K |
体积(发布件,-trimpath -ldflags "-w") | 15,760 K | — |
| 依赖包数 | 307 | 265 |
| 二进制里的模块 | anetlink blackboard cas org p2p service taskboard x402 | (无) |
internal/mcpserv 符号 | > 0 | 0 |
module/shell 符号 | 0 | 0 |
full 与 full+shell 的差:28 K,1 个依赖包(-s -w 下 14,320 K → 14,348 K,
依赖 307 → 308)。加法 tag 值得单独一个 tag 的理由与体积无关,是它能做什么。
九个减法模块全拔只省 15%。 其中 1,900 K(占可省总量的 85%)来自 no_mcp 一个;
其余八个里最大的 no_x402 是 124 K,另外七个都在百 K 以内,最小的 no_service 只有 16 K。
剩下的十几兆是 Go 运行时、net/http、加密库和存储驱动,与模块无关。
所以不要为了省体积在 full 和别的档之间选。 选择的理由是另外两个:
这个进程会打开什么,以及它可能被要求做什么。
八 · 升级与切换
身份和数据不受影响
换发行版是替换 $PREFIX/anet 这一个文件。身份、密钥历史、证据链、配置、交互记录
全在数据目录里:
默认 ~/.anet,用 ANET_DATA_DIR 改单个目录,ANET_HOME 改多身份的容器目录。
换完 AID 不变,hub 上的注册不变,已经收到的收据仍然验得过。
从别的档换到 full
功能只增不减,配置不用动:
从 full 换到别的档
先删配置块,再换二进制。 顺序反了新构建会拒绝启动(第五节第一条)。
换到 min 就把整个 modules 对象和 providers 一起清掉。
full ↔ full+shell
两者是同一 commit、同一档、只差一个加法 tag 的孪生版:
反向(从 full+shell 退回 full)要先删 modules.shell 配置块。
九 · full 特有的注意事项
一、x402 无条件启动
第三节已经说过一次,这里重复是因为它是 full 上最容易造成误会的一处:
不写 modules.x402 不等于这个节点没有付费能力。要"这台机器不可能付款"这个性质,
只能靠编译期拔掉它。
二、发布件与设计稿不一致
DISTRIBUTIONS-zh.md 的发行建议一节明确写:先只发 standard 与 agent 两档的孪生版,
不发 full+shell —— 理由是 full 已经带 p2p 的入站连接,再叠上命令执行,
是本套件里姿态最开的组合,应当由部署方自己构建。
而 deploy/release/build-release.sh 每平台实际发的两个件正是 full 与 full+shell。
位置:ANet/deploy/release/build-release.sh 的 build_one "$plat" "shell" "anet-shell"。
影响:设计稿建议不发的组合是现在的发布件之一;同时 min / standard / paid /
agent / p2p 五档没有任何发布件。发现方式:核对发布脚本与设计稿的发行建议一节。
要 full+shell,先读 shell 页把配置约定看完,
不要只因为 --shell 是一个 flag 就顺手加上。
三、onboard.sh 不覆盖 full
scripts/onboard.sh 第 0 节的 PROFILE 表只有三档:min、standard、paid。
full 的行为承诺由 CI 的 test / race job(裸的 go test ./...)与
scripts/joint.sh / scripts/scenario.sh / scripts/prodtest.sh 覆盖,
但没有一遍"从零到能用的新用户路径"是在 full 上逐条核对过的。
四、org 配了之后,org id 成为禁止发布的字符串
module/org 实现了 module.Confidential,ForbiddenTokens 返回这个 org 的 id。
daemon 在两处公开发布上过 screenPublication:向 hub 注册(RegisterWithHub)与
发布自述(PublishProfile)—— 这两样会被联邦同步、会被公开目录读到。
内容里出现 org id 就拒绝发布:
这是 INV-2。"哪个 org"本身就是成员关系的泄露,不管有没有发凭证。
所以在 full 上配了 org 之后,给自己写 anet profile set --summary 时
不要把 org id 写进去 —— 会被拦下来。
范围仅此两处。评价上传(UploadReview → POST /reviews)今天没有经过
screenPublication,尽管评价同样会被 hub 公开。位置:internal/daemon/hub_client.go,
UploadReview 里没有这个调用点。发现方式:核对 screenPublication 的全部调用点。
五、taskboard 要求 hub 那侧也挂了任务板
hub 的 /tasks/ 挂载点在 ANetHub/cmd/anet-hub/wire_taskboard.go,
同样由 //go:build !no_taskboard 控制。你这边 full 带着任务板客户端,
不等于对面那个 hub 一定有任务板。
对面没有挂载点时,请求落到 hub 的静态资源兜底路由(internal/aghub/server.go 的
GET /):task.board 的 GET 拿到 404,模块报 taskboard: board answered 404 Not Found;
task.create / task.claim 的 POST 连方法都不匹配,拿到 405,模块报
taskboard: <hub> answered 405 Method Not Allowed。
"把 hub 自己的措辞原样带回来"是另一条分支:只有当 hub 有任务板、并在 JSON 里
回了 error 字段时,post 才用那句话作错误(taskboard: <hub 的原话>),
不翻译成一个自己编的状态码。
这一段读的是 module/taskboard/taskboard.go 与 hub 的路由表,没有起一个
-tags no_taskboard 的 hub 实测过。
六、--caps 是标签,不是承诺
anet hub-register <hub-url> --caps a,b 声明的是运营者写的字符串,与这个节点实际注册了哪些能力 id
是两件事。在 full 上这更容易搞混,因为你可能真的注册了 cas.put、task.board
这些能力 —— 但只在你写了对应配置块之后。没写就是没有,而 --caps 照样让你在
find 里被按那个字符串找到。