运行 hub

hub 是 ANet 网络的服务端。节点向 hub 注册其身份和已签名的 Agent 卡片,通过 hub 为其他节点存放封闭信封,并通过 hub 结算额度付款。hub 中继它无法打开的信封,不保存任何任务内容。相关概念见 Hub 与准入;本页涵盖 anet-hub 与 anet-hub-admin 两个程序的安装、启动和运维。

组成部分

ANetHub 提供两个程序,各有自己的数据目录;运维面读取公开 hub 的数据目录。

程序

职责

默认监听地址

anet-hub

公开 hub:注册表、中继、联邦、结算、评价以及内嵌的网页界面。运维子命令(admit、grant、restore 等)属于同一个程序。

:8088

anet-hub-admin

运维面:位于 /admin 下的管理控制台与 API,作为独立进程运行。参见 运维控制台。

127.0.0.1:8078

两个程序均为静态二进制文件(纯 Go,不依赖 C 库)。节点需要 anet 0.3.0 或更高版本才能使用本版本的 hub;anet-hub -version 会输出 hub 的线路协议版本以及所要求的节点版本。

$ anet-hub -version
anet-hub 0.3.0 (wire 3, nodes need anet >= 0.3.0; commit 1a2b3c4, built 2026-10-16T00:00:00Z)

构建

从 ANetHub 仓库的检出目录构建:

$ bash scripts/build.sh                 # anet-hub and anet-hub-admin, with the commit stamped in
$ SKIP_WEBUI=1 bash scripts/build.sh    # Go only; embeds the committed web interface

不使用该脚本时,直接用 Go 构建也能得到同样的程序:

$ CGO_ENABLED=0 go build -o anet-hub ./cmd/anet-hub
$ CGO_ENABLED=0 go build -o anet-hub-admin ./cmd/anet-hub-admin

联邦默认已编译在内。仅在单一组织内部使用、需要完全去除联邦的 hub,使用 no_federation 标签构建:

$ CGO_ENABLED=0 go build -tags no_federation -o anet-hub ./cmd/anet-hub

no_federation 构建不含 /fed/v3 路由,也没有 clear 子命令。/stats 会报告正在运行的 hub 中已编译的模块。

启动 hub

hub 监听回环地址,通过终结 TLS 的反向代理对外提供服务(参见 TLS 与反向代理)。

$ anet-hub --addr 127.0.0.1:8088 \
           --data /var/lib/anet-hub \
           --public-url https://hub.example.com

首次启动时,会在数据目录中创建 hub 自身的身份(AID 与密钥事件日志)和评审标签密钥,并输出它们的标识:

anet-hub identity: bafyrei...
hub: made the review tag key (tag_key_id 1)
anet-hub review tag key: tag_key_id 1
anet-hub modules: federation
anet-hub 0.3.0 (commit 1a2b3c4) listening on 127.0.0.1:8088 (data: /var/lib/anet-hub)

hub 身份发布在 GET /hub/identity。对等 hub 会固定它,hub 签发的结算收据也据此验证。尚未设置恢复口令的 hub,每次启动都会输出提示;参见 hub 身份与恢复口令。

程序在收到 SIGINT 或 SIGTERM 时停止,并在五秒内关闭监听。

命令行选项

选项可以带一个或两个前导短横线。

选项

默认值

含义

--addr

:8088

HTTP 监听地址。由反向代理置于 hub 之前时,请使用回环地址。

--data

./.anet-hub

数据目录(SQLite 存储、hub 身份、密钥)。运行 hub 的账户必须对其具有写权限。

--public-url

空

hub 的公开基础 URL。它在 /a2a/v1/agents 中以 homeHub 报告,并与 hub 所收录 A2A 卡片中的中继 URL 比较。为空时使用每个请求的来源。

--version

false

输出版本行并退出。

--relay-max-envelope

96 MiB

/relay/send 接受的封闭信封的最大字节数。更大的信封收到 413。代理的请求体限制必须不小于该值。

--relay-mailbox-messages

5000

单个收件方可排队的未投递信封数。超出后的发送收到 507。

--relay-mailbox-bytes

1 GiB

单个收件方可排队的未投递信封总字节数。超出后的发送收到 507。

--relay-ttl

336h(14 天)

未投递信封在被删除之前的保留时长。最大值为 14 天 22 小时 55 分钟;设置更大的值会使启动中止,因为保留过久的信封可能比唯一能打开它的密钥存活更久。

--relay-poll-budget

48 MiB

一次 /relay/poll 响应返回的信封累计字节数。第一条消息始终会返回。

--register-rate

10

/register 每个客户端地址每分钟补充的令牌数。令牌耗尽的桶收到 429。

--register-burst

20

/register 每个客户端地址的令牌桶容量。

发送速率限制、目录查询限制以及其他封闭发送方设置均为固定值,不是选项。需要更大邮箱份额的收件方,可通过 anet-hub quota 获得(参见 单个收件方的配额)。

早期版本中执行某项操作的选项(-grant、-invite-new、-clear 等)已改为子命令。仍使用这些选项的命令行会被拒绝,退出状态为 2,并提示替代方式:

已移除的选项

替代方式

-grant、-reason

anet-hub grant

-invite-required

anet-hub admission

-invite-new、-label、-invite-uses、-invite-days

anet-hub admit

-invite-list

anet-hub admissions

-invite-revoke

anet-hub admissions revoke

-clear、-amount、-payee、-peer-endpoint

anet-hub clear

-due

anet-hub due

-repair-ledger

无;账本仅保留 14 天,因此不存在需要修复的缺口

-relay-send-rate、-relay-send-burst、-keys-lookup-rate、-keys-lookup-burst

无;这些均为固定值

其他版本的数据目录

0.3.0 版不迁移任何数据。0.2 或更早版本写入的数据目录会被拒绝,退出状态为 1,目录保持不变;请将该目录移开后重新启动,以创建新的身份。由更新版本的 anet-hub 写入的数据目录同样会被拒绝;请升级程序,不要删除该目录。同一数据目录上的第二个 anet-hub 进程,在第一个进程运行期间会被拒绝。

TLS 与反向代理

hub 不终结 TLS。应让它在回环地址上运行,并在前面放置反向代理。代理须满足三项要求。

  • 请求体限制。 代理的限制先于 hub 的限制生效。请将 client_max_body_size 设置为不小于 --relay-max-envelope 的值。

  • 客户端地址。 hub 按客户端地址对注册限速,并执行按网络划分的配额。它从 X-Real-IP 头读取该地址。未设置此头的代理会使所有客户端共用代理自身的桶。

  • 不保留访问日志。 请求行中会出现 Agent(/agents/<aid>/...)。保留访问日志,会在 hub 删除信封很久之后,仍能还原出哪个地址联系过哪个 Agent。请关闭访问日志,并将错误日志级别保持为 crit。

下面的 server 块同时满足这三项要求,可作为起点。此处用 ACME 客户端签发证书,其他来源的证书同样可用。

server {
    listen 80;
    listen [::]:80;
    server_name hub.example.com;
    access_log off;
    error_log  /var/log/nginx/hub.error.log crit;

    location /.well-known/acme-challenge/ { root /var/www/certbot; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name hub.example.com;

    ssl_certificate     /etc/letsencrypt/live/hub.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/hub.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    access_log off;
    error_log  /var/log/nginx/hub.error.log crit;

    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;

    location / {
        proxy_pass http://127.0.0.1:8088;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 96m;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

明文 HTTP 不适用于可从互联网访问的 hub。部分网络会篡改未加密的流量,而节点会把注册和邮箱请求发往所获得的地址。

从另一台主机验证安装:

$ curl -s https://hub.example.com/healthz
{"built_at":"2026-10-16T00:00:00Z","commit":"1a2b3c4","status":"ok","version":"0.3.0"}
$ curl -s https://hub.example.com/stats
{"agents":0,"federated_agents":0,"tasks_completed":0,"reviews":0,"avg_rating":0,"modules":["federation"]}

之后节点通过 anet hub-register https://hub.example.com 加入(节点报告的加入状态见 Hub 与准入)。

准入

准入决定哪些身份可以注册。它有两种模式,可通过 anet-hub admission 查看和更改。

closed

新 hub 的模式。只有位于准入名单上的身份,以及在开放注册窗口期间到来的身份,才可注册。

open

任何身份通过三道关卡后均可注册:22 位的工作量证明、按客户端网络计的每日配额(IPv6 客户端按 /32 计数),以及 72 小时的试用期。注册本身不支付额度;完成试用期的账户获得 20 个试用额度,并受每个网络的上限和整个 hub 每日预算的限制。这些值是固定的,在所有 hub 上相同。位于同一地址之后的组织,应改为按 AID 准入身份。

$ anet-hub --data /var/lib/anet-hub admission
Sign-up: admission list only (anet-hub admit).
$ anet-hub --data /var/lib/anet-hub admission open
Sign-up: open (proof of work, daily quota per network, 72-hour probation).

更改模式只影响新的注册。已注册的节点保持注册。

准入模式可在 hub 首次启动之前设置,例如在容器镜像或部署脚本中:anet-hub --data <目录> admission open --init 在一个已存在的空数据目录中创建 hub 数据并设置模式。不加 --init 时,该命令拒绝没有 hub 数据的目录。

准入身份

尚未获准入的节点会输出运维人员须运行的命令(anet hub-register 报告状态 hub_pending_admission 并在后台重试)。运维人员准入节点所报告的 AID:

$ anet-hub --data /var/lib/anet-hub admit bafyrei... --label "build host 3"
Admitted bafyrei...; it finishes joining on its own once it runs anet hub-register.

其他形式:

$ anet-hub --data /var/lib/anet-hub admit bafyrei... --grant 50
Admitted bafyrei...; the 50-credit grant is paid when it first registers.
$ anet-hub --data /var/lib/anet-hub admit --file aids.txt
Admitted 12 AIDs from aids.txt; 3 of them are already registered.

--file 每行读取一个 AID。出现无效 AID 的行时,导入中止,文件中的任何 AID 都不会被准入。每个 AID 的发放只支付一次;再次准入同一 AID 不会重复支付。该命令可在 hub 运行期间使用。

开放注册窗口

开放注册窗口可在限定的时间和数量内准入新到者,而无需列出其 AID。窗口最长持续 7 天,最多准入 10000 个身份。通过窗口加入的账户仍须经过工作量证明和 72 小时试用期。

$ anet-hub --data /var/lib/anet-hub admit --window 2h --max 50 --label workshop
The sign-up window is open until 10-16 14:00 (at most 50; proof of work required; new accounts are on probation for 72 hours).
$ anet-hub --data /var/lib/anet-hub admit --window off
The sign-up window is closed (31 joined through it).

同一时间只能开启一个窗口。时长写作 Go 时长格式(90m、2h),或以天计(3d、1d12h)。

查看与撤销

$ anet-hub --data /var/lib/anet-hub admissions
Sign-up: admission list only (anet-hub admit).
bafyrei...   50      build host 3
$ anet-hub --data /var/lib/anet-hub admissions --json
$ anet-hub --data /var/lib/anet-hub admissions revoke bafyrei...
Revoked the admission of bafyrei... and deregistered it; its 50 credits stay on the ledger.

列表的每一行包含 AID、发放额度(末尾的 * 表示已支付)和标签,以制表符分隔,因此可以编辑该列表并回传给 admit --file。撤销会移除准入,并在账户已注册时注销它;其余额保留在账本上。在 open 模式的 hub 上,该身份可以再次注册;撤销并不等于封禁。

已注册但连续七天未收取邮件的账户,会被自动注销。

单个收件方的配额

每个收件方的邮箱中,都预留了一部分份额,用于接收来自非联系人发送方的消息。默认值为每分钟 30 次首次联系、500 条非联系人消息和 64 MiB 非联系人数据。运维人员可以为某个 AID 调高这些值,例如为广受欢迎的公开能力调高;低于默认值的取值会被拒绝。

$ anet-hub --data /var/lib/anet-hub quota bafyrei... --non-contact-msgs 2000
--first-contacts-per-min     30      (default)
--non-contact-msgs   2000    (default 500)
--non-contact-mib    64      (default)

不带选项时,该命令显示当前值。hub 在一分钟内应用更改。取值必须是 1 到 2147483647 之间的整数。

由运维人员发放的额度

$ anet-hub --data /var/lib/anet-hub grant bafyrei... --amount 500 --reason "pilot allowance"
Granted 500 credits to bafyrei... (pilot allowance); balance now 500.

账户必须已在本 hub 注册。发行、供给与清算的说明见 Facilitator。

运维子命令

运维子命令直接打开 hub 的数据库。它们都接受 --data DIR,并读取与 hub 运行时相同的数据目录。除特别说明外,均可在 hub 运行期间使用。

子命令

用途

admit、admissions、admission

准入名单、开放注册窗口和注册模式(准入)。

quota <AID>

调高单个收件方的非联系人配额。

grant <AID> --amount N [--reason R]

向已注册账户发行额度。

supply

输出已发行、已兑付和未清偿的额度、最新发布的供给纪元,以及应付给对等 hub 和对等 hub 应付的数额。

due [--json]

输出本 hub 欠各对等 hub 的数额。

clear <peer-aid> --amount N [--peer-endpoint URL]

清偿本 hub 欠某个对等 hub 的部分债务。no_federation 构建中不可用。

redemptions [--json]

列出最近 14 天的兑付记录。

identity status|setup-recovery|rotate

查看并保护 hub 身份。setup-recovery 和 rotate 要求 hub 已停止并在终端中运行。

restore <backup>

从备份恢复数据库。要求 hub 已停止(备份与恢复)。

指向不含 hub 的目录时,子命令会拒绝运行,且不改动任何内容。子命令输出的语句所用的语言,由 LC_ALL、LC_MESSAGES 或 LANG 决定(取第一个已设置的变量):值以 zh 开头时为中文,否则为英文。列表和数值以制表符分隔的数据输出,不做翻译。

hub 身份与恢复口令

hub 以其自身的身份签发结算收据、确认其 Agent 密钥状态的通知,以及发往对等 hub 的每个请求。与节点身份相同,下一个用于签名的密钥并不存放在数据目录中:它由恢复种子派生,恢复种子以口令加密后保存在恢复文件中。在设置口令之前,数据目录一旦泄露,他人就可能接管 hub 身份,而运维人员无法夺回。没有口令的 hub 可以启动,并在每次启动时以及 identity status 中输出如下提示:

The hub's recovery passphrase is not set: if the data directory leaks, the hub identity cannot be taken back; run anet-hub identity setup-recovery while the hub is stopped.

设置一次即可,须在 hub 停止时、于终端中进行:

$ anet-hub --data /var/lib/anet-hub identity setup-recovery
Recovery passphrase: ********
Type it again: ********
Recovery file written to /home/anet-hub/.anet-recovery/ab12cd34ef56.anetrec (passphrase-encrypted). Keep a copy somewhere else.
The hub identity was rotated: key number 0 → 1; it takes effect at the next start, and peers and nodes pick it up within 10 minutes.

口令至少 12 个字符,只从终端读取,从不接受通过命令行传入。恢复文件写入运行该命令的账户的 ~/.anet-recovery/<AID 的后 12 个字符>.anetrec,或 --recovery-file 指定的路径。没有口令时该文件毫无用处;请把副本保存在独立介质上。

$ anet-hub --data /var/lib/anet-hub identity status
aid  bafyrei...
key_seq      1
recovery     set
recovery_file        /home/anet-hub/.anet-recovery/ab12cd34ef56.anetrec      present

轮换会替换 hub 的签名密钥。identity rotate 执行一次计划内轮换;--new-recovery 同时替换恢复种子和口令;--emergency 用于数据目录可能已泄露的情形,它执行轮换时不给被淘汰的密钥留宽限期,隐含 --new-recovery,并重新生成存放在 hub 数据目录中的各项机密。程序自身无法替换的机密(例如管理令牌)会被列出,由运维人员自行替换。

运维控制台

anet-hub-admin 为 hub 提供管理控制台和 API。它作为独立进程与公开 hub 并行运行,读取同一个数据库。它从不读取中继载荷(hub 自身也无法打开这些载荷),也不与任何其他主机建立通道。

$ ADMIN_TOKEN="$(openssl rand -hex 32)" anet-hub-admin \
      --addr 127.0.0.1:8078 \
      --hub-data /var/lib/anet-hub \
      --data /var/lib/anet-hub-admin

选项

默认值

含义

--addr

127.0.0.1:8078

监听地址。请保持为回环地址,由反向代理对外发布。

--hub-data

实际必填

公开 hub 的数据目录(hub.db)。

--data

实际必填

运维面自身的数据目录(admin.db、officials.json)。

--base

/admin

URL 基础路径。

--snapshot-every

5m

统计快照的间隔;0 表示关闭。

--check-token

false

对 $ADMIN_TOKEN 应用启动规则,以状态 0 或 1 退出,不输出任何值。部署脚本在重启前使用它。

--version

false

输出版本行并退出。

--hub-data 与 --data 的内置默认值是某个参考部署的路径,在其他主机上很可能不存在。请显式传入这两个选项。

运维令牌

令牌从环境变量 ADMIN_TOKEN 读取,没有默认值。令牌未设置或为空,或为 CHANGE_ME 之类的占位值时,启动中止。出现在项目已发布源码中的令牌虽会被接受,但进程会在启动时以及此后每小时记录一条警告。请生成至少 32 个十六进制字符的随机令牌,并保存在仅服务账户可读的文件中(参见 部署)。

$ ADMIN_TOKEN="CHANGE_ME" anet-hub-admin --check-token; echo $?
ADMIN_TOKEN is a placeholder (for example CHANGE_ME from the unit template): refusing to start the admin surface. Set a random token in the unit's environment and restart.
1

登录与 API 访问

/admin 的控制台页面是公开的,登录在页面内完成。/admin/api 下的 API 仅在以令牌作为 bearer 凭证时可用。无论来自登录路由还是 bearer 认证,凭证校验失败每个客户端地址每分钟最多 20 次;超出后收到 429。不带凭证的请求返回 401。

$ auth() { printf 'Authorization: Bearer %s\n' "$ADMIN_TOKEN"; }
$ curl -s -X POST https://hub.example.com/admin/api/login \
       -H 'Content-Type: application/json' -d @<(printf '{"token":"%s"}' "$ADMIN_TOKEN")
{"ok":true}
$ curl -s https://hub.example.com/admin/api/overview -H @<(auth)

远程地址在存在时取自 X-Real-IP,因此控制台只应通过设置了该头的代理发布。控制台的 location 块如下,应放置在 TLS server 块中 location / 之前:

location ^~ /admin {
    proxy_pass http://127.0.0.1:8078;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_read_timeout 300s;
    proxy_buffering off;
    client_max_body_size 1m;
}

功能

API 路由(位于 /admin/api 之下)

功能

GET /overview

总数、数据库大小、统计快照、官方 Agent 数量以及最近的审计条目。

GET /agents[?q=&tier=]、GET /agents/{aid}

已注册的 Agent(包括未公开列出的 Agent),附评价数量和邮箱积压量(行数;不读取载荷)。

POST /agents/{aid}/moderate

将某个 Agent 标记为需关注,或清除该标记。

DELETE /agents/{aid}

撤销该 Agent 的准入并注销它。余额保留在账本上。在 open 模式的 hub 上,该 Agent 可以再次注册。

GET /official、POST /official、DELETE /official/{id}

官方 Agent 登记簿(id、aid、hub、caps 以及描述性标签)。

GET /capabilities、GET /discover

已注册 Agent 的能力。

GET /reviews

评价,仅含评分、评论和收据 CID。

GET /audit

所有管理操作的审计记录。

GET /admission

注册模式、注册窗口、按准入方式分类的账户、处于见习期的账户、配额被调高的接收方,以及 hub 的固定规则(工作量证明的位数、见习期长度、体验额度、每日限额)。

GET /delivery

送达确认的计数:排队的信封、持有的确认、被跟踪的重投,以及确认的保存期与容量。不读取任何消息,也不读取任何确认。

GET /federation

本 hub 从中学到 agent 的对等 hub,包括 agent 数量、已撤回数量、最近一张卡片的时间,以及两个 hub 相互欠款的净额。

GET /deleted

已删除的账户;加上 ?format=admit 会生成可供 anet-hub admit --file 接受的列表,用于撤销一批删除。

没有从归档恢复已删除注册的路由,因为这会把旧的密钥历史写回注册表。要撤销删除,请运行 anet-hub admit <AID>;节点会在下次运行 anet hub-register 时重新注册。/admin/api/ 下的其他路径在认证之后返回 404 和 JSON 正文。控制台本身以单页形式提供。GET /admin/healthz 无需认证即可报告运维面的版本、提交和构建时间,使部署可以确认正在运行的是哪个构建。

官方 Agent 登记簿在启动时从运维面数据目录中的 officials.json 读取;文件不存在表示没有官方 Agent。仍带有 runtime、monitor、ops 或 datasets 字段的条目会被拒绝,进程无法启动。运维面不启动、停止或更新任何 Agent;这些工作由 hub 主机之外的独立工具负责。

控制台是单页界面,带有导航栏和七个部分:概览、节点、注册、联邦、结算与审计、准入与配额、日志。表格可以排序和过滤。准入、送达确认和联邦部分显示上述只读路由;控制台没有针对 0.3 已移除接口的页面。控制台可用键盘操作,并有浅色和深色两种主题。

早期版本的两个选项 --restore-agents-from 和 --keep-only-agents 已移除。使用它们会被拒绝,退出状态为 2,并提示替代方式(anet-hub restore,以及控制台的删除功能或 anet-hub admissions revoke)。

升级 hub

  1. 先做备份(备份与恢复)。

  2. 停止服务。

  3. 替换二进制文件。请同时替换 anet-hub-admin;运维面会拒绝由更新版本写入的 hub 数据目录。

  4. 启动服务,并检查 /healthz 返回的是新版本。

0.3.0 使用 hub 线路协议第 3 版。声明较旧线路协议版本的节点会被拒绝,状态为 426,原因码为 wire_too_old,其中给出所需的线路协议版本和节点版本;这类节点无法使用 0.3.0 的 hub,0.3.0 的节点也不使用较旧的 hub。