AGENT NETWORK · DOCS
参考

效果状态与证据字段

五种状态、双轴信任、Evidence 与 EffectRecord 的字段

这一页是字段级参考。概念与设计理由见 证据与信任, 这里只列字段名、类型、CBOR/JSON 键位、取值判据,以及它们在哪一段源码里被写入或被读出。

权威定义分布在三处:

内容源码
Status 五个常量、Evidence、Effect、Verifiable()ANetCore/effect/effect.go
EffectRecord 及其五个子结构、验收谓词ANetCore/tsir/predicate.go
证据链记录 EventRecord、preimage、GENESIS_PREVANetCore/ael/ael.go

五种状态

const (
	OK              Status = "OK"
	Unverified      Status = "UNVERIFIED"
	Failed          Status = "FAILED"
	Unavailable     Status = "UNAVAILABLE"
	PaymentRequired Status = "PAYMENT_REQUIRED"
)

Effect envelope 有四个字段(CBOR key 1–4):Status、Record *tsir.EffectRecord、 Message string、Evidence *Evidence。判定可验证性的只有一个方法:

func (e Effect) Verifiable() bool {
	return e.Status == OK && e.Record != nil
}

即 OK 但没有 Record 也不可验证。effect_test.go 的用例把这条钉死了: {"ok without record", Effect{Status: OK}, false}。 Evidence 是可加的,有没有它都不影响 Verifiable()(TestEvidenceOptional)。

判据表

状态判据Verifiable()
OK执行了,并且带回了可核对的效果信号(metric 或内容寻址的 artifact)有 Record 时为 true
UNVERIFIED执行了,但当前信任层级下不存在可验证的效果信号。不是失败false
FAILED执行失败false
UNAVAILABLE调用时刻目标不可达,或本节点无法作答false
PAYMENT_REQUIRED提供方愿意做,但要先付款;价格随状态一起返回false

每一种的真实例子与处理方式

OK — Modbus 适配器写 coil 之后 ReadCoils 读回,值与写入一致,才 Metric(metric, got).Requested(...).Protocol("modbus").NativeAck(true).Observed(...).Trust(V2, A1).Verified() (ANetLink/adapters/modbus/modbus.go)。 读回不一致时它走的是 Fail("readback mismatch: wrote %v, device reports %v"),而不是 OK。

调用方:这是唯一能拿去评估 TSIR 验收谓词的状态。谓词只读 Record, 所以先看 verifiable 字段(daemon 把 Verifiable() 原样写进交付物),再读 metrics。

UNVERIFIED — 两条产生路径都在构造上强制:

  • ANetLink/sdk/effect.go 的 EffectBuilder 起手就是 Unverified; Build() 里 if e.Status == effect.OK && e.Record == nil { e.Status = effect.Unverified }。
  • ANetLink/runtime/runtime.go 在返回路径上:设备 Profile.MaxTrust < profile.VerifiableFrom(即 < V2) 时把 OK 降级为 Unverified,并在 Message 为空时写入 downgraded: device trust level does not admit verification。

调用方:不得折叠进 OK,也不得折叠进 FAILED。effect.go 的包注释写明合并会污染上游信任核算。 处理方式是要么找一条独立的读取路径把它升到 OK,要么把"未验证"如实往上传。

FAILED — shell 模块(加法 tag shell)的 run 是一个带完整证据的失败例子: 非零退出码返回 Failed 且 Message 为 exit N;超时被杀返回 Failed 且 Message 为 killed after <timeout>。两种情况都仍然带 Record (exit_code / duration_ms / output_bytes)与 Evidence(observed_state 是命令输出)。

调用方:失败不等于没有信息。Message 与 Record.Metrics 通常够判断是否值得重试。

UNAVAILABLE — 四个已核实的产生点:

  • ANetLink/runtime/runtime.go:设备离线,Message 为 device offline; 设备已注册但还没描述自己,Message 为 device has not described itself yet。
  • ANetLink/adapters/modbus/modbus.go:建连失败(a.client(dev) 出错)。
  • ANet/module/shell/provider.go:调用被拒绝时也用 UNAVAILABLE —— 调用未携带 身份且 allow_local 关闭,或调用方不在白名单。这两种都会同时写一条 anet.shell.refused 到证据链。
  • ANet/internal/daemon/capability.go:本节点没有任何 provider 提供这个能力,且没有配置 auto-reply 时,答 UNAVAILABLE,Message 为 this node does not serve <capability>。 配了 auto-reply 的节点仍然把它交给 agent 处理(TestCapabilityUnresolvableFallsThrough)。 同一文件里还有一条:能力标了价而这个构建没有付款模块(-tags no_x402)时,不做而是答 UNAVAILABLE。

第四条是 2026-09 的改动。此前委派一个对方不提供的能力会永远停在 queued:没有错误、 没有效果状态、两条链上都没有记录,委派方分不清"不提供"、"宕机"和"还在做"。

调用方:与 FAILED 分开处理。ANet/module/service/service_test.go 的 TestUnreachableIsNotFailed 专门守着这个区分:连不上是 UNAVAILABLE, 对面返回 HTTP 4xx/5xx 是 FAILED。前者应当换提供方或稍后再试,后者不应当无脑重试。

shell 模块用 UNAVAILABLE 表达"这个节点不接受你的命令",语义上是拒绝而不是不可达。 Message 会明说(this node does not accept commands from <aid>), 但只看状态字的调用方会把授权失败读成临时故障。

PAYMENT_REQUIRED — 由 ANet/internal/daemon/paylink.go 的 answerPaymentRequired 产生: 能力标了价、调用没带付款时,daemon 不执行,而是把 capabilityResult.Status 置为 PAYMENT_REQUIRED,Message 写成 <capability> costs N credits on <network>, 并把 x402 PaymentRequired body 放进 payment_required 字段。 带了付款但结算失败(payer.Settle 报错或 st.Failed != "")时也是这个状态, Message 是结算失败的原因(internal/daemon/capability.go)。

调用方:读 payment_required.accepts,用 anet delegate <aid> --capability <id> --pay 重新委派。 报价是一次完整应答 —— 它被签名、生成回执、上双方证据链、按普通交付投递, 所以第二次委派是新的一次交互,不是把第一次续上。

一个例外要知道:daemon 调用 provider 时如果 p.Invoke 返回的是 Go error 而不是 effect, internal/daemon/capability.go 把状态写成字面量 "FAILED" 而不经过 effect.Status (res.Status, res.Message = "FAILED", err.Error())。取值相同,但这条路径不经过五常量。


Evidence:一次执行的出处

ANetCore/effect/effect.go。八个字段,CBOR 键 1–8,全部 omitempty:

Go 字段JSON / 线上名类型CBOR含义
Requestedrequestedstring1请求的是什么。适配器惯例是 能力=值,例如 light.onoff=on;shell 与 service 模块写的是能力 id 本身
Protocolprotocolstring2承载这次调用的协议:zigbee / modbus / opcua / onvif / shell / http / taskboard … 无固定枚举
NativeAcknative_ackbool3协议本身确认了这条命令。与"效果被验证"是两件事
ObservedStateobserved_statestring4读回实际报告的状态。Evidence 里唯一能装任意内容的字符串通道 —— Requested / Protocol / Quirk 也是字符串,但语义是固定的
LatencyMSlatency_msint645毫秒。字段注释是 invoke → effect;EffectBuilder 的实现是 NewEffect() 到 Build() 之间的墙钟时间
VerifyTrustverify_trustuint86效果验证档 V0–V4
AuthTrustauth_trustuint87身份认证档 A0–A4
Quirkquirkstring8本次读数应用过的厂商偏差修正,取值是逗号连接的 quirk id 列表(Applied.IDs() 用 strings.Join(ids, ","))

Quirk 的用途在 ANetLink/quirk/registry.go 的 ApplyEffect:修正改了 metric 就打标签。 源码注释:被修正过的值不是设备放到线上的那个值,分不清两者的消费方就无法审计这次修正。 修正只改读数,不改信任(runtime.go 注释:一台谎报单位的设备不会因此更可信)。

渲染成 JSON 的规则

所有对外的面共用一个函数 provider.Provenance()(ANet/provider/provenance.go), 理由写在注释里:因人而异的证据比没有证据更糟。规则是:

  • verify_trust 与 auth_trust 总是出现,即使是 0。
  • protocol / requested / observed_state / quirk 非空才出现。
  • native_ack 只在为 true 时出现。
  • latency_ms 只在 > 0 时出现。

返回 map 而不是 struct 是刻意的:encoding/json 对 map 会排序键, 交付物的字节稳定,回执的 result_cid 才稳定。

一份真实的 evidence(取自生产节点 anet.capability.effect 记录):

{
  "auth_trust": 0,
  "protocol": "taskboard",
  "requested": "task.board",
  "verify_trust": 2,
  "observed_state": "{\"columns\":[{\"key\":\"draft\",\"name\":\"Draft\",\"cards\":[]}]}"
}

跨进程时字段名不变

同一组字段名在四处线协上出现,snake_case 一致:

面位置
C1 HTTP/CBOR(ANetLink 北向)ANetLink/northbound/c1serv/c1serv.go 的 evidenceWire,CBOR 键与 effect.Evidence 对齐
ANetLink 的 MCP 工具ANetLink/northbound/mcpserv/mcpserv.go 的 evidenceOut
daemon 的 anetlink provider 解码ANet/provider/anetlink/anetlink.go
daemon 对外的一切结果与证据链ANet/provider/provenance.go

verify_trust 与 auth_trust

定义在 ANetLink/profile/trust.go。两条轴正交: 一台 Modbus PLC 身份弱(A1)但效果验证可以很强;一个云 API 可能把设备认证得很好,却只能 ack 命令(V1)。

verify_trust(V0–V4)

档含义
V0无法验证 —— 命令进了黑盒
V1只有传输层 ack。云 API 的上限
V2设备自报的状态读回,经本地链路
V3独立的状态确认(另一条读取路径,或另一个传感器)
V4物理闭环 —— 效果是被测量的,不是被汇报的

profile.VerifiableFrom = V2:低于 V2 的效果不允许声称 OK,由 runtime 在返回路径上执行降级。

历史别名仍然有效:type Trust = VerifyTrust,T0 = V0 … T4 = V4。 早期文档里的 T0–T4 阶梯指的就是 verify 轴。

auth_trust(A0–A4)

档含义
A0未知 —— 没有身份
A1网关背书 —— 桥为它担保(例如可信 RS485 段上的一个 Modbus 单元),设备侧无凭据
A2协议认证 —— 设备协议认证了会话(OPC UA 安全通道、签名的 MQTT 客户端)
A3设备密钥认证 —— 每设备一把密钥证明了持有(Matter operational credential)
A4硬件根信任 —— 证明链到安全元件 / TEE

只能被压低

func Clamp(declared, ceiling VerifyTrust) VerifyTrust
func ClampAuth(declared, ceiling AuthTrust) AuthTrust

任何下游都不能超过传输的上限,任何东西(包括 quirk)都不能把它抬上去。

service 模块是这条规则在配置面上的形态。挂一个本机 HTTP 服务上网时, 服务可以在应答的 evidence 里自述,applyDeclared(ANet/module/service/service.go) 会接受 observed_state / protocol / quirk / native_ack 四项 —— 但不读 verify_trust 与 auth_trust。verify 轴被钉在 V1,auth 轴留在 A0。 TestTrustIsNotTheOperatorsToDeclare 用一个在应答里写 "verify_trust":4 的服务验证这一点: 结果仍然是 verify_trust: 1。


EffectRecord 与 metrics

ANetCore/tsir/predicate.go。五个字段,CBOR 键 1–5,全部 omitempty:

Go 字段CBOR类型说明
Artifacts1[]ArtifactPath / SizeBytes / ContentCID / MediaType
Tests2[]TestResultID / Status(1 pass / 2 fail / 3 absent)
Metrics3map[string]float64量测值
Resources4[]ResourceRefKind / ID / MergeClass
Effects5[]EffectRefVerb(1 create / 2 modify / 3 delete / 4 call / 5 get)+ Resource

Metrics 是验收谓词唯一能做数值比较的域:ThresholdClause 的形状是 {Metric string, Op int, Value float64},Op 取 CmpLT / CmpLE / CmpEQ / CmpGE / CmpGT。

metrics 只装 float64。

位置:tsir.EffectRecord.Metrics map[string]float64。 行为:CID、字节块、字符串、列表、嵌套对象都放不进去 —— 编译期就放不进去。 影响:把这些东西塞进 metrics 的尝试只有两种下场,要么编译失败,要么被静默丢弃。 它们的通道是 Evidence.ObservedState(唯一装得下任意内容的字符串字段), 内容寻址的产物走 EffectRecord.Artifacts。 发现方式:ANet/module/service/service.go 的 numbersIn 只把应答的顶层数字提为 metric, 嵌套对象留在 observed_state,注释说明理由是展平会发明字段名。

两条已核实的具体做法:

  • ANetLink 的 EffectBuilder.Snapshot(data, mediaType) 把 JPEG 字节做成 tsir.Artifact{Path: "snapshot", SizeBytes: …, ContentCID: anetcid.SumRaw(data), MediaType: …}。 空帧直接不附:len(data) == 0 时原样返回,注释是"空帧不是证据"。 snapshot_bytes 这个数值 metric 不是 Snapshot() 加的,是调用它的适配器自己加的 (adapters/onvif/media.go、adapters/hikvision/hikvision.go、adapters/dahua/dahua.go 都是 Snapshot(data, mt).Metric("snapshot_bytes", float64(len(data))))。
  • EffectBuilder.StreamURI(uri) 把 RTSP 地址做成 Artifact{Path: uri, MediaType: MediaTypeRTSP}。 注释说明 URI 是句柄不是内容,所以不做内容寻址;没有实际探过流的适配器必须把效果留在 UNVERIFIED。

Build() 的兜底规则:len(metrics) > 0 || len(artifacts) > 0 才构造 Record; Status == OK 而 Record == nil 时降级为 UNVERIFIED。


证据链记录

ANetCore/ael/ael.go 的 EventRecord。键 1–10 构成 preimage, 记录 id 是 preimage 的 CID,签名也签在同一份 preimage 上(sign-binds-id)。

CBORGo 字段JSON(读取面)说明
1ChainDIDchain_did(只在 head 里)这是谁的链。daemon 写的是 did:anet: 前缀形式
2Seqseq序号,从 0 起
3PrevIDprev_id前一条的 id;创世条目用 GenesisPrev()
4EventTypeevent_type见下一节
5VersionMajor—当前只有 2(ael.VersionMajor2)
6Payloadpayload内嵌的 CoreDet-CBOR 数据项
7Timestamptimestampunix 毫秒
8SignerAIDsigner_aid签名者。daemon 写的是不带 did:anet: 前缀的裸 AID
9KeyStateSeq—签名时的密钥状态序号
10CriticalExtensions—可选
11IDid= ComputeID(),不在 preimage
12Sigsig分离签名,读取面渲染为 base64,不在 preimage
13VersionMinor—不在 preimage
14CorrID—P4 追踪关联,不在 preimage

chain_did / seq / signer_aid / key_state_seq 都折进 id,所以一条记录无法跨链重放。

创世哨兵是冻结常量:

GENESIS_PREV = bafyreifi23d3grw75bm2ox7ssj3fq3mrtwxyb646ojzabakucfpf527clm
             = CID(CoreDet-CBOR("anet:ael:genesis"))

由 ael_test.go 的 TestGenesisPrev 守着。

读取面的两个结构

ANet/internal/daemon/ledger.go:

{
  "head": {
    "chain_did": "did:anet:bafyrei…",
    "head_id": "bafyrei…",
    "length": 732,
    "state": "ACTIVE"
  },
  "records": [
    {
      "seq": 729,
      "id": "bafyreie2wqfmxfcljfj5xpmhiiznevurcvctlgix4yd6ml6lsdfvooe7u4",
      "prev_id": "bafyreicukqnf2ylje7tpm4eq4lge3ccv5haoy3rlesza3pkoj5ilvuhf4q",
      "event_type": "anet.result.accepted",
      "timestamp": 1787732183095,
      "signer_aid": "bafyreigr6jrgrrnu7zlzhk2my7cgjooddncozici2674eckzeuyucaqk5u",
      "payload": {
        "interaction_id": "ix_bc40b9a4bfc8c1a9773adf110dc72dd4",
        "receipt_bytes": 434,
        "receipt_verified": true,
        "result_cid": "bafyreigofjldxstz7kbntoujpedmouph75u24lps6gug6cbsoaryd7zcxu"
      },
      "sig": "Cw028U7XqcJAU9SMtAk/uFaUpHjnNAxZB6kUumYKApIZxnJdXmb7FeCiLLxOMut+THFMWA2uM1t5t5Zpls9cAQ=="
    }
  ]
}

head.length 是链长(下一个 seq),head.state 是 ACTIVE 或 QUARANTINED。 链为空时 state 也报 ACTIVE —— 源码注释:空链不是坏链。

payload 里的字节值渲染为 base64,这是显示选择,plainValue 里有注释说明。 CBOR 的 map 键可以不是字符串,plainMap 会把 map[any]any 重新键为字符串。

存储

路径 <数据目录>/evidence.ael.jsonl(Layout.EvidenceLedgerPath())。

文件名与内容不符。

位置:~/.anet/evidence.ael.jsonl。 行为:后缀是 .jsonl,内容是每行一条 base64 的 CoreDet-CBOR。 grep anet.capability.effect 对它返回 0 行,哪怕链里确实有这类事件。 影响:按后缀去排查会得出"链是空的"这个错误结论。 发现方式:早期版本确实写 JSON;事件 id 由 CBOR preimage 推导,而 CBOR 区分字节串与文本串、 JSON 不区分,装了字节的 payload 经 JSON 往返后 id 推不出来, daemon 会在接受过一次结果之后的第一次重启时对着自己写的链报 tamper/fork? 而拒绝启动。 路径保留旧名是为了让升级的节点接着写同一条链,而不是在旁边悄悄开第二条。 读老节点留下的 JSON 行仍然兼容(decodeRecord 见行首 { 走 JSON 分支)。


event_type 清单

事件类型是开放注册的。以下是已在源码中定义的全部取值。

daemon 自己的链

event_type常量位置谁写payload 字段
anet.capability.effectinternal/daemon/ledger.go提供方interaction_id capability caller_aid status verifiable metrics result_cid evidence
anet.interaction.receiptinternal/daemon/ledger.go提供方interaction_id requester_aid result_cid
anet.delegation.sentinternal/daemon/ledger.go委派方interaction_id provider_aid request_cid;能力委派路径另加 capability
anet.result.acceptedinternal/daemon/ledger.go委派方interaction_id result_cid receipt_bytes receipt_verified

一次完成的交互在两条链上都留痕:提供方记 anet.interaction.receipt, 委派方记 anet.result.accepted。receipt_verified 是布尔 —— 一条只记"已接受"的链事后分不清"审核过的完成"与"没人审核得了的完成"。

anet.delegation.sent 有两个写入点,payload 不同:散文委派在 internal/daemon/relay.go,只写三个字段;能力委派在 internal/daemon/capability.go, 另写 capability,因为"调的是哪个能力"正是这条路径要回答的问题。

anet.capability.effect 也有第二个写入点:module/x402/voucher.go 的 voucher 兑付门。 那条路径的 payload 只有 interaction_id capability status via(via 固定为 voucher),没有 caller_aid / verifiable / metrics / result_cid / evidence —— 按字段解析这类记录的读者要能容忍这两种形状。

付款(x402 模块,-tags no_x402 可去掉)

event_type常量位置谁写payload 字段
anet.payment.authorizedmodule/x402/pay.go付款方interaction_id authorization_id pay_to amount network
anet.payment.settled写入方常量在 internal/daemon/paylink.go;module/x402/pay.go 的同名常量只被 reconcile.go 用来读见下见下
anet.credit.redeemedmodule/x402/pay.go赎回方amount reference auth_id verified;hub 返回了回执就另加 receipt,verified 只在该回执验签通过且金额相符时为真
anet.voucher.redeemedmodule/x402/voucher.go提供方voucher_id auth_id payer capability amount network
anet.voucher.refusedmodule/x402/voucher.go提供方reason capability,视情况另加 payer 或 voucher_id
anet.issuance.head_seenmodule/x402/audit.go审计方chain_did seq head_id observed_at

anet.payment.settled 有两种 payload 形状,取决于写它的是哪一侧:

  • 提供方(internal/daemon/capability.go,结算成功后立即写): interaction_id capability transaction amount network。
  • 委派方(internal/daemon/delegation.go 的 recordSettlement,从交付物里读回): interaction_id transaction amount network verified; verified 为真时另加 payee auth_id receipt。 注释说明理由:"提供方说它收到了钱"和"hub 签了字说 credit 动了"是两件不同的事实。

anet.voucher.redeemed 另有一层用途:它是花掉集合的持久化。 启动时回读这些记录重建 spent set(上限 maxSpentReplay 条), 否则进程重启会忘掉所有已用过的 voucher,一次付款可以换两次工作。

shell(加法 tag,默认不在构建里)

event_type常量位置payload 字段
anet.shell.commandmodule/shell/provider.gocaller capability command exit_code killed duration_ms output_bytes
anet.shell.refusedmodule/shell/provider.gocaller capability reason

anet.shell.command 在答复调用方之前写,且无论命令怎么退出都写。 命令的输出不入链(可能很大、可能带任意内容),入链的是"跑了什么、谁要求的、怎么结束的"。

hub 自己的链(不在 daemon 链上)

ANetHub/internal/aghub/issuance.go,payload 统一是 aid / amount / reason:

event_type含义
anet.credit.issuedcredit 被创建:注册赠额或运营发放
anet.credit.retiredcredit 因赎回被销毁
anet.credit.opening链建立之前就已存在的存量。单列出来,读者才能看出总量里有多少是无背书的

读取端点 GET /x402/issuance 与 GET /x402/issuance/head。 每个条目带一个 record 字段,是签过名的 ael.EventRecord(base64 CoreDet-CBOR)—— 核对链的读者必须用它,而不是用旁边那些解析好的 JSON 字段,因为后者是 hub 渲染的。

ANetCore 内置的族

ael.go 定义了三个:genesis、dispute、converge.fork_detected。 分叉锚点由 Ledger.quarantine 在检出等价物(同一 (chain_did, seq) 上两条不同 id)时写入, payload 是一个四键 CBOR map:1 chain_did、2 seq、3 既有记录 id、4 冲突记录 id。 源码注释把它称作 P4.2 的 ForkRecord,但仓库里没有这个名字的 Go 类型 —— 只有这四个键。 genesis 与 dispute 在 ANetCore / ANet / ANetHub 的非测试代码里只有常量,没有写入点: daemon 的链没有单独的创世条目,seq 0 就是第一条真实事件,只是它的 prev_id 是 GENESIS_PREV。


读取与核对

anet evidence
anet evidence --type anet.capability.effect
anet evidence --type anet.payment.settled --since 700 --limit 200
  • --type 精确匹配 event_type,不做前缀或通配。
  • --since 是序号,保留 seq >= since 的条目。
  • --limit 默认 50,上限 1000(defaultEvidenceLimit / maxEvidenceLimit)。 超限时保留的是尾部,即最新的那些;结果按序号升序,最新的在最后。

对应控制面端点是 POST /evidence,请求体三个可选字段 event_type / since / limit;MCP 工具名是 evidence_read。

读取只从已验证的内存账本出,不重读文件。理由写在 ledger.go: 第二条直接解析文件的读取路径会是同一批字节的未验证视图,两者不一致时会变成"谁的副本才对"的争论。

审计 hub 的发行链用:

anet audit-hub

它拉 hub 的发行链、验签、验链接,并与本节点此前记过的 anet.issuance.head_seen 比对。 它不能阻止 hub 发行 credit —— hub 就是发行方 —— 但 hub 无法在不与本节点已有记录矛盾的前提下追溯改写。

查结构体定义:

go doc github.com/ANetResearch/ANetCore/effect Evidence
go doc github.com/ANetResearch/ANetCore/tsir EffectRecord
go doc github.com/ANetResearch/ANetCore/ael EventRecord

anet evidence 是可用的子命令,但没有出现在 anet help --all 的输出里(0.1.7 核实)。 anet visibility、anet x402-authorize、anet mcp 同样如此:usageAll() 是一份手工维护的 清单,与 main.go 的实际分发表有漂移。要看某个命令接受哪些 flag,以 knownFlags 表为准 —— 0.1.7 起 CLI 会拒绝不在表里的 flag 并把接受的列出来,不再静默忽略。


已知缺口

以下四条都是对着 0.1.7 源码核实过的现状,不是设计意图。

1. EffectRecord.Artifacts 过不了 C1 北向。 位置:ANetLink/northbound/c1serv/c1serv.go 的 invokeResp、 ANetLink/northbound/mcpserv/mcpserv.go 的 invokeOut、 ANet/provider/anetlink/anetlink.go 的解码结构、 ANet/internal/daemon/capability.go 的 capabilityResult —— 四处都只有 Metrics map[string]float64,没有 artifacts 字段。 行为:适配器用 Snapshot() 生成的 ContentCID 在跨出 ANetLink 运行时的那一刻被丢弃。 影响:远端调用方收到 snapshot_bytes 这个数字和 observed_state 里的 "N bytes of image/jpeg",拿不到内容寻址的 CID,也就无法据此核对帧内容。 发现方式:在四个仓里检索 Artifacts,ANet/ 与 ANetLink/northbound/ 下没有非测试的命中。

2. ANetLink 的 MCP 面丢 quirk。 位置:ANetLink/northbound/mcpserv/mcpserv.go 的 evidenceOut 结构与其赋值处。 行为:八个 Evidence 字段里只映射了七个,Quirk 不在其中。 影响:通过 ANetLink MCP 工具读到的效果,看不出读数是否被厂商偏差修正过 —— 而这正是 quirk 层被设计出来要防止的那一件事。 发现方式:C1 那条线协有 TestEveryEvidenceFieldCrossesTheWire 逐字段比对 (c1serv.go 的注释明说这是唯一的守护),MCP 这条线协没有等价测试。

3. service 模块的注释与实现描述不一致。 位置:ANet/module/service/service.go 的 applyDeclared。 行为:函数注释说信任取"声称值与 HTTP 应答所能确立的值中较小的那个", 实现是根本不读声称的 verify_trust / auth_trust,verify 轴固定 V1、auth 轴固定 A0。 影响:结果是安全的(配置抬不高信任),但读注释的人会以为存在一条"声称 V1 则接受 V1"的路径,实际没有。 发现方式:TestTrustIsNotTheOperatorsToDeclare 断言的是固定值 1 与 0,不是取较小值。

4. ANetLink MCP 的状态枚举少一档。 位置:ANetLink/northbound/mcpserv/mcpserv.go 的 invokeOut.Status 的 jsonschema 描述, 写的是 OK | UNVERIFIED | FAILED | UNAVAILABLE。 行为:少列 PAYMENT_REQUIRED。 影响:实践中 ANetLink 设备不标价,这一档在该面上不会出现,所以是文案不全而不是行为缺陷; 但把它当成完整状态集来实现消费方会漏一个分支。 发现方式:与 ANetCore/effect/effect.go 的五常量比对。