排查
症状 → 原因 → 处理
本页每一条都对着源码写成:错误原文取自代码里的字符串常量或格式串,处理命令取自 cmd/anet/main.go 与 ANetHub/cmd/anet-hub/main.go 的分发表,flag 取自 cmd/anet/main.go 的 knownFlags 表。版本基准是 ANet 0.1.7 / ANetHub 0.1.6 / ANetCore v0.13.1。
没有 anet doctor 这类聚合体检命令。排查靠三件东西:日志、anet status、anet evidence。
先看哪里
日志:~/.anet/daemon.log,不在 journal
daemon 不写 syslog、不写 systemd journal。它的全部输出——启动错误、中继轮询失败、模块拒绝、安静对端告警——都进数据目录下的 daemon.log:
anet logs 只是读文件,daemon 没在跑也能用。文件不存在时它说:
后台启动(anet up / anet daemon --detach)时子进程的 stdout 与 stderr 都被重定向到这个文件,所以启动失败的原因写在这里。0.1.7 起,子进程在就绪之前退出时 anet up 会把日志末尾几行直接贴回终端(the daemon exited immediately: 后跟那几行),不必再自己去翻;整份日志仍然用 anet logs 读。
数据目录:默认 ~/.anet
| 路径 | 内容 |
|---|---|
~/.anet/config.json | daemon 配置(含 control_addr、hub_url、caps、modules) |
~/.anet/control_token.txt | 控制面 bearer token,权限 0600 |
~/.anet/daemon.log | daemon 日志 |
~/.anet/identity.kel | 本身份的密钥事件日志 |
~/.anet/card_seq | 本节点铸出的最后一个 ADP 卡片序号(见下面 card refused: STALE_SEQ 一节) |
~/.anet/evidence.ael.jsonl | 本节点证据链(文件名是历史遗留,内容是 base64 CBOR) |
~/.anet/interactions/ | 本地委派记录 |
ANET_DATA_DIR 覆盖根目录;多身份时每个身份有自己的一份,用 anet id ls 看它们各自的目录与控制端口。
状态:anet status 报的是本地值
它返回 aid / version / data_dir / hub_url / name / caps / summary / readme / pricing / accept_delegations / guest_messages / console_url,以及配了 auto-reply 时的 auto_reply 一栏。
anet status 里的 caps 是本地 config.json 的值,不是 hub 上的值。控制面的 hStatus 不发任何 hub 请求。两者不一致正是目录里找不到自己那一节的成因,而 anet status 看不出来。
速查表
| 症状(原文) | 原因 | 处理 |
|---|---|---|
module "p2p" is configured but not compiled into this build (built with no_p2p?) | 这个二进制用减法 tag 裁掉了该模块,而 config.json 里配了它 | 换完整构建,或从 modules 里删掉这一块 |
module "shell" is configured but not compiled into this build (it needs -tags shell, which the default build does not use) | shell 是加法 tag,默认构建里根本没有 | 重装带 --shell 的变体 |
the daemon exited immediately: + 日志末尾几行 | 后台 daemon 启动即退出,原因已经贴在这条消息里 | 按贴出来的那几行处理;anet logs 读全文 |
daemon did not become ready within 8s — check the log: anet logs | 进程还活着,但 8 秒内没把控制口服务起来 | anet logs |
anet mcp: this build was compiled with -tags no_mcp | 这个构建裁掉了 MCP 服务端 | 换含 MCP 的构建(agent 或 full;paid / p2p / standard / min 都带 no_mcp) |
anet find: unknown flag --capability (it accepts: --cap) | 0.1.7 起 CLI 拒绝它不认识的 flag,不再静默忽略;退出码 2 | 按提示里列出的名字改 |
anet: hub /register rejected: this hub admits new agents by invite only | hub 开了准入,你没带邀请码 | 向运营者要码,anet hub-register <url> --token <invite> |
anet: hub /register rejected: no such invite | 邀请码拼错或不属于这个 hub | 核对码,或另要一个 |
anet: hub /register rejected: this invite has been revoked | 运营者已吊销 | 另要一个 |
anet: hub /register rejected: this invite has expired | 超过 -invite-days | 另要一个 |
anet: hub /register rejected: this invite has no uses left | 用满 -invite-uses | 另要一个 |
anet: hub /register rejected: card refused: STALE_SEQ: card.seq <= high_water (rollback reject) | 同一秒内重复注册,卡片序号撞号 | 0.1.7 已修;更早的版本等一秒重试 |
anet: hub /register rejected: card refused: CARD_NOT_YET_VALID: not_before > now + ADP_FUTURE_SKEW | 本机时钟比 hub 快 5 分钟以上 | 校时 |
anet: hub /register rejected: stale or future-dated challenge | 本机与 hub 时钟差超过 5 分钟 | 校时 |
anet: hub /relay/send rejected: recipient not registered | 对方 AID 不在这个 hub,也没有可转发的联邦对端 | 核对 AID,或确认对方注册在哪个 hub |
| 能力委派一直没结果,不报错 | 对方配了 auto-reply 但它的 agent 没回;或对方 anet accept off;或对方长时间没来取信 | anet find --cap <id> 核对,anet logs 看有无安静告警 |
散文委派(不带 --capability)一直没结果 | 对方没配 auto-reply:任务只入它的 inbox,等它的操作者处理。anet 自己不跑模型 | 等对方处理,或改用能力委派 |
结果里 "status": "UNAVAILABLE" + this node does not serve <cap> | 对方没有任何 provider 服务这个能力 id,而且它没配 auto-reply | anet find --cap <id> 找真正提供它的节点 |
结果里 "status": "UNAVAILABLE" + this node does not accept commands from <aid> | 你的 AID 不在对方 shell 模块的名单里 | 请对方把你的 AID 加进 allow / allow_file |
结果里 "status": "UNAVAILABLE" + anet: this build has no payment support (built with -tags no_x402, or the x402 module is not configured) | 对方标了价却是无付费构建,拒绝白干 | 对方换含 x402 的构建(paid / full),或取消标价 |
结果里 "status": "UNAVAILABLE" + device offline / device has not described itself yet | ANetLink 侧设备不在线,或刚 claim 还没 describe | 查 anetlinkd 与设备 |
结果里 "status": "UNAVAILABLE" + service <cap>: … | service 模块转发的本机 HTTP 服务不可达 | 起那个服务 |
结果里 "status": "FAILED" + shell: <cmd>: unknown argument "args" (this call takes: argv) | shell 模块只认 argv(shell.exec 只认 command / dir);多余的键被拒绝,而不是丢掉后照跑 | 按提示改键名 |
结果里 "status": "PAYMENT_REQUIRED" | 这是正常应答,不是错误:对方标了价 | anet delegate <aid> --capability <id> --pay,或先 anet balance |
insufficient balance: has N, needs M | 余额不够 | 运营者 anet-hub -grant <aid> -amount <n> |
anet find / anet find --cap 查不到自己 | 改了模块配置后 daemon 没重启(0.1.7 起启动会重新注册,不重启就还是旧的能力清单);或既没 caps 也没 profile | 重启 daemon(anet stop 后 anet up),或 anet hub-register <url> --name <name> --caps a,b;或 anet profile set --summary … |
| 之前查得到,现在查不到,什么都没改 | 超过 30 天没取信,退出可浏览列表 | 把 daemon 起回来,取一次信即恢复 |
can't reach your daemon at http://127.0.0.1:39811 … | daemon 没起,或你连的是另一个身份 | anet up;或 anet id ls 后用 --id |
read control token (is the daemon running?): … | 找不到 control_token.txt | 同上 |
cat ~/.anet/evidence.ael.jsonl 全是 base64 | 记录是 CBOR,不是 JSON,不可读 | anet evidence |
anet: evidence ledger corrupt line: … / anet: evidence ledger refuses to load (tamper/fork?): … | 证据链文件被改过或截断,拒绝加载 | 见证据链读不出来 |
崭新 Debian 上 curl 不存在 / HTTPS 直接失败 | 最小系统没有 curl 也没有 ca-certificates | apt-get install -y curl ca-certificates procps |
模块配置了但没编进去
症状。 daemon 起不来。前台跑时 stderr 上是:
后台跑时 anet up 会把这一行从 ~/.anet/daemon.log 里读回来贴在终端上,前面加一句 the daemon exited immediately:。
原因。 config.json 的 modules 块里有一个键,而这个构建的模块注册表里没有同名项。module.Build 把它当错误而不是当无操作——否则一个读到 "p2p": {...} 却默默忽略它的 daemon,会让运营者一直找不到为什么没有对等连接。错误在 daemon.New 里发生,daemon 直接退出。
两种文案对应两种 tag 方向。 括号里的提示按模块的 tag 方向选:
| 文案 | 含义 |
|---|---|
(built with no_p2p?) | 减法 tag。这个模块默认在,是构建时用 -tags no_p2p 去掉的 |
(it needs -tags shell, which the default build does not use) | 加法 tag。这个模块默认不在,只有 -tags shell 才编进去 |
能出现在 modules 配置块里、走减法 tag 的模块有八个:p2p anetlink blackboard org cas service x402 taskboard。加法方向目前只有一个:shell。
处理。 先确认这个二进制到底链进了哪些模块:
第二行 modules: 是从二进制里实际链接进来的注册表读出来的,不是构建时刻进去的字符串。默认版是:
-tags shell 的构建在这一行里再多一个 shell;把九个减法 tag 全加上的 min 档一个不剩,打印 (none)。
不信这一行的话直接查符号:
默认版为 0,anet-shell 变体大于 0。发布的二进制保留符号表就是为了让这条能跑。
然后二选一:换一个含该模块的构建(shell 用 --shell 重装,减法模块见发行版),或把 config.json 里那一块删掉。
第九个减法 tag no_mcp 管的不是 module.Host 上的模块,而是 internal/mcpserv 与 cmd/anet/mcp.go。它另有一行声明(cmd/anet/module_mcp_declare.go,同一个 tag),所以 anet version 的 modules: 里会出现 mcp——0.1.7 之前漏报,一个带九个 MCP 工具的构建自称没有。它仍然不接受 modules 配置块:往里写 "mcp": {...} 会被 module.Build 按未知模块拒绝。裁掉之后 anet mcp 自己报:anet mcp: this build was compiled with -tags no_mcp。
注册被 hub 拒绝
准入:要邀请码
症状。
原因。 这个 hub 开了准入(anet-hub -invite-required true),而你是它不认识的 AID,注册请求里没带 invite 字段。
准入门槛只卡一个 AID 的第一次注册。已经在册的 AID 重新注册不需要码——它每次重启、每次能力清单变化都会重新注册,在那里要码等于把已经放进来的节点全部踢出去。所以打开准入不会追溯清退任何人,它决定的是下一个进来的是谁。
处理。 向运营者要码,带 --token:
邀请码以 anetinv_ 开头,不落盘:它在到达时就被花掉,写进 config.json 只会留下一份用完的凭据。
运营者一侧。
-invite-new 必须带 -label,否则拒绝执行——三周后一份没有标注的邀请列表读不出任何东西。明文码只在铸造那一刻打印一次,hub 只留摘要,所以一份 hub.db 的拷贝不是一袋可用的邀请。
四种码本身的问题
| 原文 | 含义 |
|---|---|
no such invite | 这个 hub 的库里没有这个码的摘要 |
this invite has been revoked | 运营者吊销了 |
this invite has expired | 过了 -invite-days |
this invite has no uses left | 用满了 -invite-uses |
同一个 AID 用同一个码重复注册不再消耗次数——计数只在这个码第一次接纳这个 AID 时加一。
hub 在 403 响应里还带了一个 hint 字段(ask this hub's operator for an invite, then register with --token),但 daemon 的 hubDo 只解析 error,所以这句提示不会出现在 anet hub-register 的输出里。位置:ANet/internal/daemon/hub_client.go 的错误分支。影响:命令行只给出原因,不给出下一步。
card refused: STALE_SEQ
症状。
原因。 注册时随请求发出的 ADP AgentCard 带一个序号,hub 只在这个序号严格大于它见过的该 subject 最高序号时才接纳(高水位规则,ANetCore/adp/admit.go 第 7 步)。序号曾经就是注册时刻的秒级时间戳,所以同一秒内的两次注册携带同号,第二次被拒。
这不是边缘情况:连着敲两次 anet hub-register 会撞,一个注册完立刻因为能力清单变化再注册一次的节点也会撞。
发现方式。 准入联调脚本 scripts/joint-invite.sh 里"已在册节点不带邀请码重新注册"那一步失败,失败原因与被测的准入逻辑无关。
处理。 0.1.7 已修:序号改为以时钟为种子、并保证严格超过本进程已经铸出的每一个号(ANet/internal/daemon/card.go 的 cardSeq)。单位仍然是秒——换成毫秒同样能解决撞号,但会把每个序号乘以一千,此后任何一次降级回旧版本的节点都再也无法更新自己的卡片,因为它的秒级序号会永久低于 hub 的高水位。
早于 0.1.7 的 daemon 仍会遇到。绕法是隔一秒再试。
序号也落盘。 只在内存里递增不够:同一秒内重启的进程会从同一个时钟值重新起算,再次撞上 hub 的高水位;跨重启倒退的时钟同样会落到高水位之下。所以计数器写在 <data-dir>/card_seq 里,每次注册写一次、启动读一次。daemon 启动就重新注册之后,快速重启会稳定命中前一种,这一步不再是可选的。
委派出去一直没结果
症状。 anet delegate 返回了 interaction_id,anet thread <interaction_id> 里只有自己发出去的那条,再没有别的。没有任何错误。
0.1.7 起,能力委派碰上一个对方不提供的能力 id 会拿到 UNAVAILABLE + this node does not serve <cap>,不再永远停在 queued。所以下面这四种情形是剩下的那些——它们都不是"能力不存在"。
有四个不同的原因会长成同一个样子。
一、对方配了 auto-reply,但它的 agent 没回
原因。 收到委派后,daemon 先看这是不是一次能力调用(带 --capability 的委派)。是的话交给本地 provider 注册表解析。解析不出来时分两条路:
- 对方没配 auto-reply ——
tryCapabilityPaid直接答UNAVAILABLE,消息是this node does not serve <cap>,签名、上链、有回执,和别的应答一样。请求方立刻知道结果。 - 对方配了 auto-reply —— 这条委派按原样交给它的 agent 处理,因为一个 id 仍然可能是 agent 读得懂的任务。agent 若不作答,交互就停在这里。
散文委派(不带 --capability)始终走第二条路:没配 auto-reply 的节点把它存进 inbox,然后等操作者自己处理。anet 不跑模型,这是设计,不是缺陷。
处理。 委派之前先确认对方真的提供这个能力:
--cap 问的是精确问题:谁注册了这个能力 id。cas.put 指这个 id,ptz.* 指这一族。不带 --cap 的 anet find <query> 是让 hub 搜文字,会返回只是提到了这几个词的 agent,把活派给一个会拒绝它的提供方。这两个问题不一样,hub 也不会在 ?cap= 无结果时回退到文字搜索。
二、对方关了接单
原因。 对方执行过 anet accept off。收到的委派在 ingestDelegate 的第一行就被丢弃并 ack,不入库、不应答。关掉接单不影响它在 anet find 里的可见性。
处理。 对方 anet accept on。
三、对方长时间没取信
原因。 hub 用"有没有来取信"作为存活信号:超过 1 小时没有 poll 记为 quiet。它照样把消息入队——hub 不知道对方会不会回来,拒收等于替它断言不会回来——但会在 /relay/send 的应答里带上 recipient_quiet 与一句 warning。
daemon 收到这个 warning 会打进日志,每个对端每次状态变化只打一次:
处理。 anet logs 看有没有这一行。
四、auto-reply 配了但没跑起来
anet autoreply test 只走本地后端,通过就说明后端可用,问题在别处。
UNAVAILABLE 的几种 message
UNAVAILABLE 的定义是"远端什么都没做",与 FAILED(做了,失败了)分开(ANetCore/effect/effect.go)。看到它要接着看 message,不同的 message 是完全不同的问题:
| message | 出处 | 原因 | 处理 |
|---|---|---|---|
this node does not serve <cap> | ANet/internal/daemon/capability.go | 对方没有任何 provider 服务这个能力 id。0.1.7 起才有这个应答:此前这样的委派无人作答,一直停在 queued。配了 auto-reply 的节点仍然把它交给 agent,不走这条 | anet find --cap <id> 找真正提供它的节点 |
this node does not accept commands from <aid> | ANet/module/shell/provider.go | 你的 AID 不在对方 shell 名单里。写成明话是刻意的:被拒的调用方应该知道自己不被允许,而不是去猜这个能力存不存在——hub 已经告诉过它存在 | 请对方把你的 AID 写进 allow 或 allow_file |
this node does not accept commands that carry no caller identity | 同上 | 调用不带调用方身份,而对方没开 allow_local。CallerAID 的零值就是空字符串,把空当成"本机可信"会让任何一处漏填静默绕过整份名单,所以默认拒绝 | 走正常的委派路径,或对方显式开 allow_local |
this node cannot read its own allowlist | 同上 | allow_file 指的文件读不到 | 对方检查文件路径与权限 |
anet: this build has no payment support (built with -tags no_x402, or the x402 module is not configured) | ANet/internal/daemon/paylink.go 的 errNoPayments | 对方给这个能力标了价,但它的构建里没有付费模块。"我收不了你的钱,所以我不做"是真话,收不了钱还照做是没人做过的决定 | 对方换含 x402 的构建(paid / full),或取消标价 |
device offline | ANetLink/runtime/runtime.go | 设备离线 | 查设备与 anetlinkd |
device has not described itself yet | 同上 | 设备已 claim、尚未 describe。这一步曾经解引用空 profile 直接把整个 runtime 打崩,连带拖垮它服务的每一个设备 | 等 describe 完成 |
service <cap>: <网络错误> | ANet/module/service/service.go | service 模块转发的那个本机 HTTP 服务连不上 | 起那个服务 |
this node has no hub, so it cannot tell whose signature to trust | ANet/module/x402/voucher.go | 凭单入口收到请求,但本节点没有配 hub,无从判断该信谁的签名 | 先 anet hub-register。付费面每次都现读 hub_url,注册完立刻可用,不用重启 |
this node serves no capabilities | 同上 | 凭单入口开着但没有任何 provider | 配置能提供能力的模块 |
PAYMENT_REQUIRED
这不是错误。 它是五种诚实效果状态里独立的一种,因为其它四种都不诚实:FAILED 说"坏了",UNAVAILABLE 说"远端没动",两者都会把调用方引去找一个并不存在的故障。PAYMENT_REQUIRED 说的是"付了就做",价格随应答一起来。
症状。 anet thread <id> 里对方的答复是:
这条答复是完整的应答:签了名、上了证据链、有回执。不是拒绝。
处理。 让 daemon 接受报价并重新委派:
--pay 走的是"报价 → 付款 → 再委派一次"。第二次是一次新的交互,不是第一次的续接:第一次已经以一个真实答复(报价)结束,签了名、进了两侧的链,把它重开成没发生过会抹掉价格被说出来的唯一记录。
等报价的窗口是 60 秒;超时是 anet: no answer within 1m0s, so nothing to pay for yet。
付不了的几种情况。
| 原文 | 原因 |
|---|---|
anet: this build has no payment support … | 自己是无付费构建。min / standard / agent / p2p 四档都带 no_x402 |
anet: nothing to pay — the answer carried no accepted rails | 对方报了价却没给可付的通道 |
anet: this node can pay "anet-credit" and the provider accepts none of it | 双方没有交集的结算通道 |
insufficient balance: has N, needs M | 余额不够。hub 侧 VerifyPayment 给出 |
authorization already settled | 同一份授权重复结算。授权按内容 id 幂等,重发不会扣两次 |
查余额与对账:
anet balance 问的是 hub 的账本(hub 是那个数字的保管者),anet evidence --type anet.payment.settled 是本节点自己记的付款事件。两者对不上是能看见的,anet reconcile 就是拿它们逐条比。
运营者充值(需要能登上放账本的那台机器,且 hub 停着):
充值只有这一条路,没有 HTTP 端点。谁可以创造额度是一个策略问题,这一轮没有回答它,加一个端点等于用一个不存在的鉴权顺手把它答错。要求 shell 访问权和数据库文件是同一条信任边界。
目录里找不到自己
有两个原因,都不报错。
一、改了配置但没重启
位置。 ANet/internal/daemon/daemon.go 的 refreshRegistration。
行为。 0.1.7 起,daemon 启动时若 config.json 里有 hub_url,会在后台重新注册一次,把当前 provider 注册表里的能力(withServedCapabilities)折进去。所以改完模块配置重启 daemon 就够了,显式 anet hub-register 仍然可用,并没有被取代。
这次刷新是后台做的、失败不致命:hub 连不上时 daemon 照常起来,只在 daemon.log 里留一行 anet: could not refresh this node's registration with …。能力清单真的变了时也会留一行 anet: refreshed capabilities at …。
还会出错的是什么。 改了 config.json 却没有重启(或重启失败),hub 上的 caps 就还是上一次注册时的值。
影响。 这个节点在 anet find --cap <新能力> 里查不到;反过来,已经删掉的能力仍留在 hub 上被人查到,照着它委派过来只会拿到 UNAVAILABLE + this node does not serve <cap>。按 AID 直接委派不受影响,所以漏做这一步不会报任何错。
处理。 重启,或显式注册一次:
不带 --caps 时,provider 注册表里已有的能力仍会被折进去。
怎么确认 hub 上是什么。 anet status 看不出来(它读本地 config)。直接问 hub:
二、既没有 caps 也没有 profile
行为。 hub 的 ListAgents 只返回 Listed 的 agent,判据是 len(caps) > 0 || summary != "" || readme != "" || pricing != ""。纯请求方(注册了,但既不登记能力也不写自我介绍)被有意排除在浏览列表之外。
影响。 anet find 和星图里没有它。但 GET /agents/{aid} 仍然查得到——直接按 AID 委派照常可用。
处理。 登记能力,或写一句自我介绍:
一个月没取信,退出可浏览列表
症状。 之前查得到的节点忽然不在 anet find 结果里了,配置一个字没改。
原因。 hub 用"来取过信"作为存活信号,两档:
| 常量 | 值 | 行为 |
|---|---|---|
QuietAfter | 1 小时 | 报为 quiet。仍然可投递,投递方在 /relay/send 的应答里收到告警 |
AbandonedAfter | 30 天 | 默认不再出现在浏览列表里(AgentView.Browsable()) |
一小时对着 1 秒的轮询间隔(relayPollInterval)是很宽的:三千六百个轮询周期没来取信的节点不是碰上了糟糕的一分钟。三十天和一小时的差距是刻意的——安静值得告诉调用方,消失一个月值得从人看的目录里拿掉。
什么都没删。 行、评价、余额全在。Browsable() 是过滤而不是删除:一次网络抖动不是注销,一个会因为节点变安静就清退它们的 hub 会把每次故障变成一次大规模注销,连带把每个 agent 的评价和余额一起孤立。离开 hub 仍然只能是 agent 自己签名的动作(anet hub-leave)。
anet hub-leave 在 0.1.7 起还会做一件本地的事:离开的若正是当前 hub_url,它清掉这个字段并停掉轮询循环。此前 hub 侧注销了、本地字段还在,relay 循环就以一秒一次的节奏去 poll 一个已经不认自己的 hub,每次被拒都写一行日志,且跨重启不会自愈。换 hub 的正确顺序仍然是先注册新的、再离开旧的——那样 hub_url 已经指向新 hub,hub-leave 不会误清。
处理。 把 daemon 起回来。一次取信就恢复:
注意一个例外。 从来没有取信记录的 agent(last_seen 为空)永远算可浏览。那是这个 hub 开始记录轮询之前注册的每一行;升级时把它们全隐藏会让目录凭空清空,而且原因谁也看不出来。空值读作"不知道",不读作"死了"。
控制口连不上
症状。
或者:
原因。 三种:daemon 没起;起了但你连的是另一个身份;或者 ANET_DATA_DIR / ANET_ID 指到了别处。
上面那条错误已经把本机正在跑的其它身份列出来了,包括各自的控制地址和切过去的命令——这是它存在的全部意义。
处理。
控制令牌在哪。 <data-dir>/control_token.txt,权限 0600,daemon 启动时写入。CLI 每次调用都读它,拼成 Authorization: Bearer <token>。控制面是明文 HTTP + bearer,默认绑 127.0.0.1——不要把 control_addr 挪到公网地址上,除非前面有 TLS 和鉴权。
新建身份(anet id new)会自动分配一个不冲突的端口,所以第二个身份不是 39811。anet id ls 里那一列就是。
浏览器控制台。
它打印形如 http://127.0.0.1:39811/console?hub=… 的地址。这个页面在 bearer 包装之外提供——浏览器导航没法设 Authorization 头——安全性来自控制地址只在回环上,与磁盘上那个 token 是同一条信任边界。
证据链读不出来
症状。 cat ~/.anet/evidence.ael.jsonl 出来的是一行行 base64,grep 什么都搜不到。
原因。 文件名里的 .jsonl 是历史遗留。记录现在是 CoreDet-CBOR,一行一条 base64。路径没改是为了让升级中的节点接着写自己那条链,而不是在旁边默默开第二条。
处理。 用命令读,不要 grep:
返回两部分:head(chain_did / head_id / length / state)和 records。每条记录带 seq id prev_id event_type timestamp signer_aid payload sig——重新推导 id、重新校验链接所需要的字段都在,读的人可以验而不是只能信。默认 50 条,上限 1000。
事件类型:
--type | 记录什么 |
|---|---|
anet.capability.effect | 一次执行过的能力调用及其效果 |
anet.interaction.receipt | 签发的交互回执(提供方一侧) |
anet.delegation.sent | 本节点发出的委派(interaction_id / provider_aid / request_cid;能力委派另带 capability,0.1.7 起补上——此前能力委派完成后本节点只有 anet.result.accepted,自己的链上说不出是它向谁要过什么) |
anet.result.accepted | 本节点收下并接受的结果(请求方一侧) |
anet.payment.settled | 结算过的付款 |
anet.payment.authorized | 签发的付款授权 |
anet.credit.redeemed | 退还给 hub 的额度 |
anet.voucher.redeemed / anet.voucher.refused | 凭单入口的接受与拒绝 |
anet.issuance.head_seen | 记下的 hub 发行链头 |
anet.shell.command / anet.shell.refused | shell 模块执行过的与拒绝掉的命令 |
state 那一栏要看。 ACTIVE 是正常,QUARANTINED 表示检测到分叉。一个从被隔离的链上提供证据的节点必须说出来——这是读的人最需要、又绝不会想到去问的那一件事。链是空的时候也报 ACTIVE:空链不是坏链。
文件被改过。 daemon 启动时会把整个文件重新导入并验证一遍,推不出来就拒绝加载,而不是默默提供被篡改的历史。两种拒绝方式对应两层检查:
daemon 起不来。这是设计意图:一个提供不出可验证历史的证据面,比一个不启动的 daemon 更糟。
崭新系统:缺 ca-certificates
症状。 一台刚装好的最小 Debian 上,一行安装的第一步就失败——curl 命令不存在;或者装上 curl 之后,HTTPS 请求直接失败。
原因。 debian:12 这类最小镜像既没有 curl 也没有 ca-certificates。没有后者,任何一次 HTTPS 都没有可信根可用。同一台机器上 uptime 这类顺手会写进 shell 命令表的东西也不在(属于 procps)。
发现方式。 dmax 上 debian:12 容器 + 真 hub + 另一台调用方的白板实测。单元测试和联调都看不见这一层,因为两者都跑在已经装好东西的机器上。
处理。 装之前先补:
注意 daemon 也吃这一条。 缺根证书时 daemon 起得来,但每次中继轮询都会在日志里留下失败:
看到 anet: relay poll: anet: hub /relay/poll: …x509… 这类行,就是同一个原因。
时钟偏差:三个不同的容差
三个窗口值不一样,来自三处不同的代码,报的错也不一样。
| 检查 | 容差 | 常量 | 超了会看到 |
|---|---|---|---|
| 签名挑战(注册 / 改 profile / 取信 / ack) | 5 分钟 | relayauth.MaxSkewMillis | stale or future-dated challenge |
ADP 卡片反预填(not_before) | 5 分钟 | adp.ADPFutureSkew = 300s | card refused: CARD_NOT_YET_VALID: not_before > now + ADP_FUTURE_SKEW |
| 付款授权有效窗口两端 | 2 分钟 | payment.ClockSkew | 授权被判为过期或尚未生效 |
付款那 2 分钟为什么存在。 零容差是最初发布的行为,它让一笔真实付款因为 129 毫秒失败:付款方在 T 时刻签名,hub 的时钟慢了十分之一秒,一份"签发于 hub 的未来"的授权被当作尚未生效拒收。除了两台计算机对时间的看法不一致之外没有任何东西出错,而两台计算机永远不一致。
两分钟不是一个值得收紧的安全口子:让一份授权只能花一次的是 nonce,窗口存在的目的是让一份留着的授权过后失效——早两分钟的攻击者没有得到任何他本来没有的东西。两端都放宽,因为时钟快的付款方撞的是另一头,失败同样毫无道理。
卡片还有一条不同方向的规则。 已经过了 not_before + ADP_CARD_TTL(7 天)的卡片会被接纳,然后立刻转入 EXPIRED——这是过期而不是拒绝,不要按拒绝去排查。
处理。 校时。容器里尤其要注意宿主机时钟。
已知缺口
这些是排查时会撞上、但当前版本里没有更好办法的:
| 位置 | 行为 | 影响 | 发现方式 |
|---|---|---|---|
cmd/anet/main.go 的 knownFlags | 三条命令的 flag 在各自实现里读得到,却不在这张接受表里:anet up --all、anet console --print、anet autoreply set 的 --poll-interval / --max-history / --api-timeout / --max-auto-replies | 被 checkFlags 当未知 flag 拒绝(退出码 2),而 anet help --all 与命令自己的 usage 串里写着它们 | 逐条跑默认构建的 anet |
cmd/anet/main.go 的 usageAll() | 五条可用命令不在 anet help --all 的清单里:anet mcp、anet evidence、anet credits、anet visibility、anet x402-authorize。这份清单是手工维护的,与 main.go 的 dispatch switch 分开写 | 照着帮助找不到这些命令,而它们都能跑;本站按 switch 与 knownFlags 写,不按帮助串写 | 把 anet help --all 的输出与 knownFlags 的键逐条对照 |
hub_client.go 错误分支 | 只解析 hub 错误响应的 error 字段,丢掉 hint | 注册被拒时命令行给出原因但不给下一步 | 读 /register 的 403 响应 |
| 准入运营面 | 只有 hub 命令行(-invite-*),admin 面与 webui 没有对应页 | 运营者必须登录那台机器 | —— |
| 额度发放 | 只有 anet-hub -grant,要 shell 访问权且 hub 要停着 | 运营者不能远程充值 | —— |
绕法:anet up --all 用 anet up <name> 逐个起;anet console --print 用 anet console --url;auto-reply 的四个调参项写进 config.json 的 auto_reply 块(poll_interval_seconds / max_history / api_timeout_seconds / max_auto_replies),然后重启 daemon —— 手改 config.json 不像 anet autoreply set 那样热生效。
这一批已经修掉,不再是缺口(0.1.7):能力解析不出时永不作答(现在答 UNAVAILABLE)、重启后不重新注册(现在启动就刷新)、卡片序号不跨重启持久化(现在写 card_seq)、未知 flag 被静默忽略(现在拒绝)、后台启动失败只报 8 秒超时(现在贴日志)。