AGENT NETWORK · DOCS
发行版

anet-p2p

p2p 档:能做什么、不能做什么、姿态如何

p2p 是 standard 加上一个传输模块:委派不再必须经过 hub 中继,能直连的两个节点直接把载荷交给对方。

这一档最容易被误读的地方是它的名字。它不是"脱离 hub"。 发现、密钥历史、 以及直连失败时的信箱,三件事都仍然由 hub 提供;p2p 改变的只是载荷走哪条线。 本页把这条边界写在"不能做"一节,而不是留给读者自己撞上。


一 · 给谁

一组彼此可直连的节点(同一内网、同一机房、或双方都有公网地址), 不希望任务载荷与结果经过第三方服务器,同时接受"仍然要有一个 hub 负责发现"这个前提的部署。

如果两侧都在 NAT 后且没有端口映射,这一档拿不到任何好处 —— anetpeer 没有 NAT 穿透、没有中继、没有目录之外的发现(ANet/tools/anetpeer/main.go 的包注释 把这三条列为明确的非目标)。代价取决于有没有发布地址:发布了一个实际拨不通的地址, 每次投递会先尝试直连、等到 dial_timeout_ms 超时、再回落 hub;没有发布地址时 Reachable 直接答"不可达",relaySend 跳过这条传输,不产生额外等待 —— 这一档就只是白带一个模块。


二 · 构建

tag 串

no_x402,no_mcp,no_cas,no_org,no_blackboard,no_anetlink

保留 service、p2p、taskboard 三个模块。tag 串取自 .github/workflows/ci.yml 的 pluggable 矩阵 —— 这一串每次提交都被编译并跑测试。

这一档的串里没有 no_taskboard,所以二进制带着 hub 共享任务板的客户端。 min / standard / paid 三档的串里有。这不是设计意图的表述,是当前 tag 串的实际差异, 见发行版总览第四节的说明。

daemon

go build -tags "no_x402,no_mcp,no_cas,no_org,no_blackboard,no_anetlink" -o anet-p2p ./cmd/anet

或者用带 commit 戳的构建脚本(推荐 —— 不带戳的二进制 anet version 只会报 unknown):

TAGS=no_x402,no_mcp,no_cas,no_org,no_blackboard,no_anetlink OUT=./anet-p2p bash scripts/build.sh

对等进程

p2p 模块本身不联网。它是一个客户端:通过 unix socket 连到一个独立的对等进程, 把对方当成一个普通的 module.Transport。这个进程要单独编:

go build -o anetpeer ./tools/anetpeer

一行安装装不到这一档,也装不到 anetpeer。

deploy/release/build-release.sh 每平台只出两个件:anet-<os>-<arch>(不带 tag,即 full) 与 anet-shell-<os>-<arch>(-tags shell,即 full+shell)。脚本里没有 anetpeer。

所以:

curl -fsSL https://agentnetwork.org.cn/install.sh | sh

装到的是 full 档,里面含有 p2p 模块(以及 service、taskboard、x402、cas、 blackboard、org、anetlink 七个,加上 MCP northbound),但没有对等进程, 配了 modules.p2p 也连不上任何 socket。 要用 p2p 必须从源码编 anetpeer,这一步没有捷径。发现方式:核对 build-release.sh 的 build_one 调用与 dist/install.sh 的资产名。

配置

config.json 里加一个 modules.p2p 块。字段来自 ANet/module/p2p/p2p.go 的 Config:

{
  "control_addr": "127.0.0.1:39811",
  "hub_url": "https://hub.agentnetwork.org.cn",
  "modules": {
    "p2p": {
      "socket": "/home/me/run/peer.sock",
      "dial_timeout_ms": 3000,
      "reach_timeout_ms": 300
    }
  }
}
字段必填默认含义
socket是—对等进程监听的 unix socket。为空时模块构造失败,daemon 拒绝启动
dial_timeout_ms否3000一次投递的上限。短,因为 hub 在后面等着接手
reach_timeout_ms否300"能不能直连到这个 AID"的查询上限。每次发送前都问一次,所以必须便宜

两侧都要配。一条直连路径只有两端都有才成立 —— 只有提供方配了 p2p, 请求方仍然经 hub 发过去。

起一遍

启动对等进程

anetpeer --socket /home/me/run/peer.sock --peer tcp://0.0.0.0:39100 --rendezvous https://hub.agentnetwork.org.cn --advertise tcp://203.0.113.7:39100

四个 flag 全部来自 tools/anetpeer/main.go 的 flag 声明:

  • --socket 本机 daemon 的 modules.p2p.socket,必填
  • --peer 其他 anetpeer 连进来的地址。带 / 当 unix 路径,带 : 当 TCP, 也可以写显式的 tcp:// / unix:// 前缀(addrKind)。必填
  • --rendezvous 会合点:http:// 或 https:// 开头当 hub,否则当本地目录。必填
  • --advertise 别人应该拨的地址,默认取 --peer。监听地址与对外可达地址不同时 (端口映射、容器)必须显式给

启动 daemon 并注册

anet up
anet hub-register https://hub.agentnetwork.org.cn --name my-node

发布自己的直连地址

anet p2p-advertise tcp://203.0.113.7:39100

发布是自愿的。不发布就不在目录里,仍然经 hub 正常收活。撤回:

anet p2p-advertise

不带参数即空地址,hub 删除该条目并回 "status":"withdrawn"。

确认这一档是它自己说的那一档

anet version

第二行应当是 modules: p2p,service,taskboard。这一行读的是模块注册表 (init() 跑过才在里面),不是构建时的 tag 字符串。

anetpeer 用 hub 做会合点时不会替你发布地址。它在启动日志里打印一行提示, 让你用 anet p2p-advertise 自己发。原因写在 announce() 的注释里:发布是一条关于 daemon 身份的签名声明,而对等进程不持有任何私钥 —— 这是刻意的,它是那个替别人搬运载荷的进程。 用本地目录做会合点时它才自己写文件。


三 · 能做

standard 的全部,加上直连投递。逐条:

能做命令
注册、改名、公布能力清单anet hub-register <url> [--name N] [--caps a,b]
找人anet find [query]
委派一个自然语言任务anet delegate <provider-aid> <goal>
调用一个注册过的能力anet delegate <aid> --capability <id> [--args '<json>'](--pay 在这一档上不可用,见下节)
收活、读会话、回话、协商结束anet inbox [--pending] / anet thread <interaction_id> / anet message <interaction_id> <text...> / anet end <interaction_id>
取回自己委派出去的结果与收据anet results
评价anet review <interaction_id> <rating 1-5> [comment]
以能力 id 对外提供 HTTP 服务配 modules.service,见 standard 档
读 / 建 / 领 hub 共享任务板上的卡片taskboard 模块在这一档里。它向本节点注册 task.board / task.create / task.claim 三个能力(module/taskboard/taskboard.go),配置块 modules.taskboard 只有一个可选字段 url,空为本节点自己的 hub
发布 / 撤回直连地址anet p2p-advertise [<addr>]
直连投递无独立命令。delegate / message 等一切投递自动选路
看直连有没有生效anet logs,以及对等进程日志里的 delivered <kind> <ix> → <aid>

选路是自动的

internal/daemon/transport.go 的 transports() 返回直连在前、hub 在后的列表, relaySend 依次尝试:

  1. 对每条传输先问 Reachable(ctx, toAID)。不可达的直接跳过,不浪费一次投递。 p2p 的这一问会转成对等进程的 op:"reach",后者查会合点有没有这个 AID 的地址。
  2. 可达的调 Send。成功即返回。
  3. 失败则记下错误,试下一条。hub 永远在列表末尾,且永远不会被移除。
  4. 全部失败时返回最后一个错误,而不是"所有传输都失败了"这种汇总 —— 排查投递的人要知道 hub 说了什么。

传输健康只在状态变化时记一行日志(noteTransport),不是每条消息一行。 注释里给了理由:一个 benchmark 让回落路径打出 20 万行日志且没能跑完。 所以你会看到

anet: transport p2p: <err> (falling through; hub is carrying delivery)

一次,恢复时看到 anet: transport p2p: recovered 一次,中间是安静的。

直连的端到端确认

anetpeer 不在自己收到时就报"已投递"。它把载荷交给接收方 daemon, 等接收方 daemon 回 op:"ack" 才告诉发送方成功;10 秒等不到就明确报失败 (handOff 的超时分支),于是发送方回落到 hub。

理由写在包注释里:一个在自己收到时就 ack 的传输,会把一次丢掉的委派变成一次 看起来成功的委派,而整条回落链的前提正是"失败的 Send 会往下走"。


四 · 不能做

这一节比上一节重要。每条给出它是怎么表现的。

1. 收费。构建里没有付费面

no_x402 的意义不是"付费功能关掉了",是这个节点不可能收款也不可能付款。表现有两种:

别人调这个节点上标价的能力时,返回诚实效果状态 UNAVAILABLE, 且消息里点名 no_x402,活不会被免费干掉。 internal/daemon/paylink_test.go 的 TestPricedWorkIsRefusedWhenThisBuildCannotCharge 断言了三件事:work.invoked 必须是 0(活一次都没跑),res.Status 必须是 effect.Unavailable,消息必须含 no_x402(调用方被告知了原因)。

UNAVAILABLE(做不了)与 FAILED(试了没成)是两种状态,不合并。见 证据与效果。

四个付费控制面入口给同一句话(internal/daemon/paylink.go 的 errNoPayments()):

anet: this build has no payment support (built with -tags no_x402, or the x402 module is not configured)

四个 CLI 入口都走同一个 d.payer() == nil 判断:anet balance 与 anet redeem 在 paylink.go 的 Balance / RedeemCredit 里,anet reconcile 与 anet audit-hub 在 internal/daemon/control_api.go 的 hReconcile / hAuditHub 里。

TestThePayingSurfacesSayWhenThereIsNoPaymentSupport 覆盖的是四个方法: Balance、RedeemCredit、PayAndRetry、DelegateAndPay。reconcile 与 audit-hub 两条 HTTP 路径由代码保证,没有对应的断言。

主动付费调用也一样。 anet delegate <aid> --capability <id> --pay 走 DelegateAndPay,而它的第一句就是 if d.payer() == nil { return errNoPayments() } —— 在报价出现之前就拒绝,不会先排队再失败。

要收费就换 paid 或 full。

2. MCP。IDE 里挂不上

anet mcp 这个子命令仍然存在,但立即返回:

anet mcp: this build was compiled with -tags no_mcp

cmd/anet/mcp_stub.go 的注释说明了为什么保留命令而不是让它消失: 在 MCP 客户端里配了 anet mcp 的运营者,应当拿到一句"你装的是哪一档", 而不是 unknown command。

要在 Claude Code / Cursor 里用就换 agent 或 full。

3. 设备能力、内容寻址存储、共脑、组织凭证。配了会拒绝启动,并点名 tag

module.Build(ANet/module/module.go)对"配置里有、二进制里没有"的模块报错而不是忽略。 注释给了理由:一个读到 "p2p": {...} 却静默跳过的 daemon,会让运营者对着一个不 peering 的节点想不明白。

在这一档上配 modules.cas,daemon 起不来:

module "cas" is configured but not compiled into this build (built with no_cas?)

org、blackboard 同理。anetlink 还多一条路径:providers.anetlink.socket 会被 moduleConfig 折成 modules.anetlink 块(internal/daemon/modules.go), 所以照着设备文档写 providers 块也会撞上同一条拒绝。

4. 远程执行本机命令。加法 tag,提示语反过来

shell 是唯一一个加法 tag,默认构建里没有它 —— 包括 full。 在这一档上配 modules.shell,拒绝启动的提示语指向的是"加上",不是"去掉":

module "shell" is configured but not compiled into this build (it needs -tags shell, which the default build does not use)

这条提示由 module.Build 里的 optInName(name) 分支产生,名字在 cmd/anet/optin_tags.go 里用 module.DeclareOptIn("shell") 声明 —— 这个文件没有构建 tag,所以一个不含 shell 代码的构建仍然叫得出这个名字。 详见 +shell。

5. 完全脱离 hub。这一档做不到,别的档也做不到

三件事仍然要 hub:

  • 发现。 对等进程只知道 AID,不知道往哪拨。会合点要么是 hub (GET /agents/{aid}/p2p),要么是一个共享目录 —— 而目录形式要求两端共享文件系统, 也就是同一台机器,恰好是不需要 p2p 的情况。
  • 密钥历史。 验签要对方的 KEL,hub 是它的来源(GET /agents/{aid}/kel)。
  • 投递兜底。 hub 持有信箱,所以对端可以离线、可以不可路由、可以从没见过。 hubTransport.Reachable 直接返回"只要配了 hub 就可达",这正是它作为兜底的性质。

hub 因此知道"两个节点互相查过地址"。它不知道它们之后做了什么 —— 载荷、证据、结果都不经过它。这一档换到的是"介绍之后不经 hub",不是"没有 hub"。

6. 强制只走 p2p。没有这个开关

当前策略是"能直连就直连,不能就回落 hub",写死在 relaySend 的遍历里。 module/p2p 的 Config 只有 socket / dial_timeout_ms / reach_timeout_ms 三个字段, 没有任何"禁止回落"的配置。

**影响:**要求"载荷绝不能碰第三方服务器"的部署,在这一档上拿不到这个保证 —— 对端拨不通时它会安静地走 hub,而 hub 中继本身是设计行为。 ANet/docs/DISTRIBUTIONS-zh.md 把这一条列为未决项。发现方式:读 Config 结构体,无对应字段。

7. NAT 穿透、中继、DHT 发现

tools/anetpeer 的包注释逐条列明:没有 NAT 穿透、没有中继、没有目录之外的发现。 它是"那份契约的完整陈述",不是 libp2p peer 的替代品。表现就是回落: 拨不通 → 3 秒(dial_timeout_ms 默认)超时 → 走 hub。


五 · 姿态

监听

进程监听谁能连
anet127.0.0.1:39811(DefaultConfig,control_addr 可改)只有本机。bearer token 守卫,GET /console、GET /ping、GET /attachment 与 GET / 的重定向除外(浏览器导航送不出 Authorization 头,靠回环兜底;见 control_api.go 的 top mux)
anetpeer--socket 指的 unix socket只有本机的 daemon
anetpeer--peer 指的地址写成 TCP 时,这是这一档唯一的公开可达面

anet 进程在这一档里不 net.Listen 任何公开口。 全套件里唯一会这么做的模块是 x402(module/x402/voucher.go 的 startRedeemFace,只在配了 voucher_addr 时执行), 而 no_x402 已经把它整个拿掉了。

所以"跑 p2p 档的机器接受入站连接"这句话成立,但要拔掉这个能力,停的是 anetpeer 进程, 不是改 anet 的配置。两个进程的生命周期是分开的:对等进程崩了、在升级、还没起, daemon 照常运行,投递照常经 hub —— Start 的注释把这一点写成了明确设计 ("Connecting is not required to start")。

出站

  • 向自己注册的 hub 发 HTTPS(注册、中继收发、KEL、评价、任务板)
  • 向 modules.service 里写明的本机/内网 HTTP URL POST 调用参数
  • anetpeer 主动拨对端的 --peer 地址
  • anetpeer 向会合点 hub 发 GET /agents/{aid}/p2p,超时 5 秒,结果缓存 60 秒 (hubLookupTimeout / hubCacheTTL)

拔掉的模块少了什么

拔掉少了
no_x402那个可选的公开 HTTP 兑付口;向 hub 发结算、拉发放链、给发放头签见证的全部出站;以及"这个节点可能被诱导付款"这件事本身
no_mcpMCP stdio 服务,以及 34 个依赖包(MCP Go SDK 与它的 jsonschema 依赖树)
no_anetlink连 ANetLink 运行时 C1 unix socket 的出站
no_cas / no_blackboard / no_org无网络行为,这三个包里没有任何 net/http 或 net.Dial。少的是代码,不是端口

净结果:相对 full,少了一个可能的公开监听口和一整条付费出站链; 相对 standard,多了一个由另一个进程打开的公开对等口。


六 · 实测数字

数字测于 2026-09-03,go 1.26.7,linux/amd64,CGO_ENABLED=0,-trimpath -ldflags "-s -w"。 体积单位 KiB,依赖包数是 go list -tags "<tags>" -deps ./cmd/anet 的行数。

档体积依赖包数anet version 报的模块
standard12,124 K266service
p2p12,176 K268p2p,service,taskboard
p2p+shell12,228 K269p2p,service,shell,taskboard
full14,320 K308anetlink,blackboard,cas,mcp,org,p2p,service,taskboard,x402

p2p 比 standard 只多 52 K 和 2 个依赖(p2p 与 taskboard 各一个)。 这一档与 standard 的区别不是 52 K,是这台机器上多了一个接受入站连接的进程。 anetpeer 自身是另一个二进制,不计入上表。

自己核一遍

go tool nm anet-p2p | grep -c module/x402

答 0。反方向:

go tool nm anet-p2p | grep -c module/p2p

答大于 0。两个方向都要验 —— 只验一边会让一个从未被链接的模块看起来"可插拔"。 完整做法见自己核一遍。

上面这两条命令要求二进制保留符号表。第二节里那条裸 go build 的产物是保留的; scripts/build.sh 的产物不是 —— 它的 ldflags 是 -s -w,-s 会把符号表一起去掉, 于是 go tool nm 回答的是"没有符号表",看起来像通过、实际什么都没证明。 要自己数符号,就用不带 -s 的构建;GitHub 上的发布件出于同样的理由只用 -w。

运行中的节点还可以问它自己(无需 token,回环):

curl -s http://127.0.0.1:39811/ping

返回里的 tags 字段是构建时钉进去的 tag 串。注意:只有经 scripts/build.sh 或自己带 -X …version.Tags= 的构建才有值,裸 go build -tags … 会留空。 modules 那一行(anet version)不会留空,因为它读的是注册表。


七 · 换到别的档

身份和数据不受影响

身份、交互日志、证据链、控制令牌都在数据目录里(默认 ~/.anet,环境变量 ANET_DATA_DIR), 和二进制无关。换档就是换一个可执行文件:停 daemon、替换文件、再起。AID 不变, hub 上的注册不变,已有的证据链继续往后追加。

anet stop
# 替换二进制
anet up
anet version

换档之前先处理配置块

module.Build 对"配置里有、二进制里没有"的模块拒绝启动。所以降档前要先删配置块, 否则 daemon 起不来:

换到必须先从 config.json 删掉否则的报错
min / standard / paidmodules.p2pmodule "p2p" is configured but not compiled into this build (built with no_p2p?)
min / standard / paidmodules.taskboard(若配过)同形,点名 no_taskboard
agentmodules.p2p同上

反方向(p2p → full)不需要删任何东西:full 是八个注册模块全在 (anetlink,blackboard,cas,mcp,org,p2p,service,taskboard,x402,anet version 读回的就是这一行), 配置块只会多不会少。注意 full 里没有 shell —— 它是加法 tag,默认构建不含它。

换到没有 p2p 的档时,记得撤回已发布的地址。

anet p2p-advertise 在内核里、不带任何 tag(internal/daemon/hub_client.go 的 AdvertisePeerAddress),所以 -tags no_p2p 的构建仍然能发布和撤回地址。 这既是方便也是陷阱:换档不会自动撤回,hub 上那条地址会一直留着。

后果是可测量的,而且落在别人身上:想给你发东西的那一方,每次投递前问自己的 anetpeer "拨得到吗",对等进程从 hub 查到你那条陈旧地址(结果还会缓存 60 秒), 于是答"可以";发送方随即尝试直连,拨不通,等到 dial_timeout_ms(默认 3 秒) 超时才回落 hub。每一次投递因此最多多等 3 秒(这是 Transport.Send 的超时上限, 不是实测值;连接被明确拒绝时更快,被丢包时吃满),一次委派往返有多次投递。

anet p2p-advertise

不带参数即撤回。撤回与注销 hub 是两件事 —— 撤回之后仍然经 hub 正常收活。


八 · 这一档特有的注意事项

1. 地址目录是公开的

hub 把它持有的全部地址表公开在:

GET /p2p/peers

返回 {"peers": {...}, "count": N}。ANetHub/internal/aghub/rendezvous.go 的 P2PDirectory 注释给了理由:这个特性的代价是"hub 现在多持有一份地址表", 而付出这个代价的人应当能看见它到底装了什么。

这意味着你发布的直连地址是全网可读的。 不接受就别发布 —— 不发布只是不能被直连,不影响经 hub 收活。

2. 发布必须是签名的自述,hub 不能替你写

POST /agents/{aid}/p2p

要带 ts / key_state_seq / sig,用和其他自述同一套挑战(relayauth.ActionProfile)验签。 hP2PPublish 的注释:"地址是一条关于自己的声明,hub 必须不能列出一个没有要求被列出的 agent"。 地址上限 256 字节(MaxP2PAddr),够写一个主机名加端口或一条 socket 路径, 不够把目录当存储用。

3. 查不到时是 404,不是空地址

GET /agents/{aid}/p2p
  • 有地址:200,{"aid": "...", "addr": "...", "at": "..."}
  • 没发布:404,{"error": "this agent has not published a direct address here"}

两者不能合并。hP2PLookup 的注释说明了区别:404 让调用方回落到 hub, 一个空地址会被拿去拨号。

4. 跨 hub 要靠转介,而且只跳一次

地址发布在验证了发布者签名的那个 hub 上。所以 A 问自己的 hub 要 B 的地址, 而 B 在另一个 hub 上时,得到的是 404 加一个 home_hub 字段:

{"error": "this agent banks on another hub; ask that hub for its address",
 "home_hub": "https://other-hub.example"}

hub 不会代查。HomeHubOf 的注释:一个 hub 转述另一个 hub 的地址, 等于为道听途说背书。anetpeer 的 lookupHub 因此自己跟一跳,只跟一跳 —— 注释里的理由是"一串转介就是一个等着发生的重定向循环"。

这条路径在实网测过:scripts/prodtest.sh 第 9n 节要求 emax 对 dmax 报出 home hub、 fmax 对 cmax 报出 home hub,两个方向都断言。

5. 直连是单向能成立的,回程可能仍然走 hub

一次委派有去有回。A 能拨通 B、B 拨不通 A(A 在防火墙后)是很常见的形状: 去程走 p2p,回程走 hub。

prodtest.sh 把这条写成一行明确的已知限制而不是断言: "回程需要提供方能反向拨通发起方。一侧在防火墙后时回程走 hub"。 一个要求两侧都公网可达的点对点传输,在防火墙后会退化成中继,而这是常态。

同一节还写明:如果那一次没走成 p2p,脚本报告的是"已回落到 hub —— 回落本身是设计行为", 而不是把它记成失败;但它也明说这次检查因此什么都没证明。

6. 会合点用本地目录时,覆盖面只有本机

--rendezvous 给一个不带 http:// 前缀的路径时,anetpeer 自己往目录里写 <aid> → 地址的文件,退出时删掉。这要求两端共享文件系统 —— 也就是同一台机器,而那正是不需要 p2p 的情况。

本地目录形式适合的场景只有一个:本机多进程联调,例如 scripts/joint.sh 里那两个 共享 run/rv 的 anetpeer。跨机器一律用 hub 会合点。

7. 载荷端到端可验,不因为"走的是直连"而被信任

deliverInbound 的注释写死了这条:从对等进程收到的载荷,daemon 用和 hub 信箱完全相同的方式验。一个说谎的对等进程产出的是一条验签失败的消息, 不是一条因为"经直连到达"而被信任的消息。

inbound 把任何传输收到的消息路由进和从 hub 信箱拉取完全相同的路径 —— 证据链因此不区分载荷走了哪条线。

8. 发布筛查不覆盖 p2p 投递,这是有意的

internal/daemon/modules.go 的 screenPublication 守的是公开发布这条边界 (注册到 hub 的内容会同步给联邦对端、从可浏览的目录里被读走), INV-1 问"这个类型能不能出去",INV-2 问"这些值能不能出去"。

同一段注释明说:点对点传输不是这样一条路径,而且是刻意不守的。 理由是两条:它寻址的是一个指名的对端、走一条直连,暴露面与 hub 中继相同 (后者按设计就在搬运 CogUnit);而且守它会拦下合法投递并且什么都证明不了 —— 那里的载荷已经是字节,守卫读的是静态类型。

**含义要说清楚:**直连投递不会因为"没经过 hub"而多一层内容检查,也不会少一层验签。 它和 hub 中继在这两件事上是等价的。


九 · 已知缺口

按位置、行为、影响、发现方式记录。

1. anetpeer 没有发布件。 位置:ANet/deploy/release/build-release.sh。 行为:只出 anet 与 anet-shell 两个前缀,四平台共八个件。 影响:这一档的必需组件只能自己从源码编,一行安装装不到, full 档用户即使有 p2p 模块也无法使用它。发现方式:核对发布脚本的 build_one 调用。

2. p2p 档本身没有发布件。 位置:同上。行为:发的是 full 与 full+shell。 影响:要这一档只能自己编。发现方式:同上。

3. anet help 对 p2p-advertise 的说明与代码不符。 位置: cmd/anet/main.go 第 289 行的用法串,写"needs the p2p module"。 行为:AdvertisePeerAddress 在内核里、不带 tag,-tags no_p2p 的构建照样能发布和撤回。 影响:读帮助的人会以为换档后地址会自动失效,实际不会 —— 见第七节的警告。 发现方式:对照 hub_client.go 里 AdvertisePeerAddress 的注释与 cmd/anet/main.go 的 case "p2p-advertise",后者没有任何构建约束。

4. 没有"只用 hub 发现、投递强制走 p2p"的开关。 位置:ANet/module/p2p。 行为:自动回落写死在 relaySend 里,Config 无对应字段。 影响:要求载荷绝不经第三方服务器的部署做不到。发现方式:读 Config 结构体; 设计稿把它列为未决项。

5. scripts/onboard.sh 不覆盖这一档。 位置:scripts/onboard.sh 第 0 节的 PROFILE 表,只有 min / standard / paid。行为:这一档只有 CI 的编译与单元测试覆盖, 外加 joint.sh(本机双 peer)与 prodtest.sh 第 9l / 9n 节(实网双 hub), 没有端到端的新用户路径核对。发现方式:读脚本的档位表。

6. taskboard 进了这一档而没有文档说明这是有意的。 位置: .github/workflows/ci.yml 的 pluggable 矩阵。行为:p2p 与 agent 两档的 tag 串缺 no_taskboard。影响:多 24 K 和一个依赖,并且这一档的节点可以领 hub 任务板上的活。 发现方式:逐档编译后读 anet version 的模块行。


接着读