news 2026/9/12 5:10:35

Buzz Agent Observability(NIP-AO)协议解读:基于 Nostr Kind 24200 的加密 Agent 遥测与控制通道

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Buzz Agent Observability(NIP-AO)协议解读:基于 Nostr Kind 24200 的加密 Agent 遥测与控制通道

Buzz Agent Observability(NIP-AO)协议解读:基于 Nostr Kind 24200 的加密 Agent 遥测与控制通道

【免费下载链接】buzzA hive mind communication platform项目地址: https://gitcode.com/GitHub_Trending/buzz14/buzz

本文以 docs/nips/NIP-AO.md 为主体,结合 Buzz 开源仓库中buzz-corebuzz-acpbuzz-relaybuzz-sdk及桌面/移动客户端的真实实现,系统讲解 NIP-AO 定义的 Kind 24200(Agent Observer Frame)事件协议——它如何为 AI Agent 与 Owner 之间提供一条短暂、加密、实时、不经中继持久化的遥测与控制通道。读完本文,你将掌握该事件类型的完整线格式、NIP-44 v2 加密与解密流程、中继侧授权与限流机制、客户端订阅与缓冲策略,以及基于仓库源码的端到端运行原理。

背景:为什么 Agent 需要一条独立的可观测性通道

AI Agent harness 会执行长时间会话(long-running sessions):调用工具、向模型发送协议帧、产生中间推理过程。Owner(Agent 所服务的人类或系统)需要对这些活动进行实时可见性,用于调试、审计和控制。NIP-AO 的动机在于:

  • 遥测数据必须是实时的,而非事后归档;
  • 遥测数据不应被中继持久化,也不应被第三方看到;
  • 该通道应严格限定在 agent↔owner 关系之内,不携带任何持久状态。

基于此,NIP-AO 定义了Kind 24200(Agent Observer Frame):一个专用的、加密的、短暂(ephemeral)事件通道,仅用于 Agent 与其 Owner 之间的内部会话遥测与控制。

在 Buzz 的 kind 注册表中,该类型被正式登记为:

/// Ephemeral: owner-scoped encrypted agent observer telemetry and control frame. pub const KIND_AGENT_OBSERVER_FRAME: u32 = 24200;

(见 crates/buzz-core/src/kind.rs)。其归属于 NIP-01 定义的 ephemeral 区间 20000–29999,该区间在 Buzz 源码中有显式的边界常量EPHEMERAL_KIND_MIN = 20000EPHEMERAL_KIND_MAX = 29999,并有配套谓词is_ephemeral(kind)(crates/buzz-core/src/kind.rs)。

核心术语

术语含义
Agent拥有自己 Nostr 密钥对、代表 Owner 执行会话的 AI 进程
OwnerAgent 所归属的人类(或系统),其 pubkey 是 Agent 的 provision 主体
Observer Frame一个携带单条遥测或控制信息的 kind 24200 事件
Session由共享的sessionId关联的一次有界 Agent 执行

在 crates/buzz-acp/src/observer.rs 中,这些概念被直接落地:ObserverEvent结构体携带seqtimestampkindagent_indexchannel_idsession_idturn_idstarted_atpayload字段,与 NIP-AO 定义的解密载荷一一对应;ObserverHandle通过进程内broadcast通道向 harness 主循环分发本地事件,同时维护一个有界回放缓冲。

事件结构:一条 Kind 24200 事件的线格式

NIP-AO 规定的 wire event(加密后)结构如下:

{ "kind": 24200, "pubkey": "<sender_pubkey>", "created_at": <unix_timestamp>, "content": "<NIP-44 v2 ciphertext>", "tags": [ ["p", "<recipient_pubkey>"], ["agent", "<agent_pubkey>"], ["frame", "telemetry" | "control"] ] }

硬性约束包括:

  • 事件必须恰好有一个p标签、恰好一个agent标签、恰好一个frame标签;
  • frame取值必须"telemetry""control"
  • 中继对无法识别的frame应当静默丢弃(向发布者返回 OK,以保持前向兼容);客户端必须忽略无法识别的frame值;
  • 当会话运行在 NIP-29 群组上下文中时,可以包含一个h标签。

在 Buzz 源码中,agentframe两个标签名及frame的合法取值被定义为常量(crates/buzz-core/src/observer.rs):

pub const OBSERVER_AGENT_TAG: &str = "agent"; pub const OBSERVER_FRAME_TAG: &str = "frame"; pub const OBSERVER_FRAME_TELEMETRY: &str = "telemetry"; pub const OBSERVER_FRAME_CONTROL: &str = "control";

方向语义

方向签名方(pubkey)p 标签agent 标签
Telemetry(agent → owner)agentowneragent
Control(owner → agent)owneragent(目标)agent(目标)

Buzz 中继在agent_observer_route()中通过event.pubkeyrecipientagent三者关系精确判定方向(crates/buzz-relay/src/handlers/event.rs):

  • event.pubkey == agent && recipient != agent→ Telemetry,期望frame == "telemetry"
  • recipient == agent && event.pubkey != agent→ Control,期望frame == "control"
  • 其余形态一律拒绝:"invalid: observer frame must be agent-to-owner telemetry or owner-to-agent control"

加密模型:NIP-44 v2 端到端加密

所有content字段必须使用 NIP-44 v2 加密:在 secp256k1 ECDH 共享密钥之上使用 XChaCha20-Poly1305。

  • Telemetry:以(agent_privkey, owner_pubkey)加密;
  • Control:以(owner_privkey, agent_pubkey)加密;
  • 解密后的明文在加解密完成后立即从内存中清零(zeroize);
  • 解密后的载荷不得超过 65,535 字节。

在 crates/buzz-core/src/observer.rs 中,加密与解密被实现为两个可复用函数:

  • encrypt_observer_payload::<T: Serialize>(sender_keys, recipient, payload):先序列化为 JSON,校验明文 ≤ 65,535 字节(OBSERVER_MAX_PLAINTEXT_LEN),随后以 NIP-44 v2 加密,加密后使用zeroize立即擦除明文;
  • decrypt_observer_payload::<T: DeserializeOwned>(recipient_keys, event):先校验密文长度落在 NIP-44 v2 的合理包络NIP44_MIN_CONTENT_LEN = 132~NIP44_MAX_CONTENT_LEN = 87_472之间,再解密、反序列化并清零明文。

仓库中的单元测试验证了完整往返路径:observer_payload_round_trips_with_nip44构造一个turn_started载荷,加密后断言content_looks_like_nip44成立,再解密比对 JSON 完全一致;observer_payload_rejects_short_ciphertext则验证短密文(如明文 "not encrypted")会被以InvalidCiphertextLength拒绝(crates/buzz-core/src/observer.rs)。

解密载荷:Telemetry 帧的 ObserverEvent 结构

frame=telemetry时,content解密后是一个ObserverEventJSON 对象:

{ "seq": <monotonic_integer>, "timestamp": "<rfc3339_string>", "kind": "<frame_kind>", "agentIndex": <integer> | null, "channelId": "<channel_uuid>" | null, "sessionId": "<session_id>" | null, "turnId": "<turn_id>" | null, "payload": { ... } }

字段约束与语义:

  • REQUIREDseqtimestampkindpayload
  • OPTIONAL(可为 null)agentIndexchannelIdsessionIdturnId—— 例如会话建立之前的sessionId可能未知,客户端必须优雅处理null
  • seq按会话单调递增,用于丢帧检测
  • timestamp是带亚秒精度的 RFC 3339 字符串,如"2026-04-29T12:00:41.500Z"
  • agentIndex在多 Agent 场景中标识具体 Agent;sessionId/turnId用于跨会话、跨轮次关联帧;
  • payloadkind相关(可为{});未知的kind必须被忽略。

该结构在 crates/buzz-acp/src/observer.rs 中被忠实实现为 Rust 结构体,字段名采用 camelCase(#[serde(rename_all = "camelCase")]),并额外支持started_at(当前轮次开始时间,序列化时为空则跳过)。ObserverHandle::emit()通过原子自增生成seq,用chrono::Utc::now().to_rfc3339()生成时间戳,同时写入有界回放缓冲并广播给订阅者。

帧类型(Frame Kinds)

kind描述
acp_read入站 ACP 协议帧(model → harness)
acp_write出站 ACP 协议帧(harness → model)
turn_started新的 Agent 轮次开始
session_resolved会话完成或终止

在 ACP harness 源码中,这两个 I/O 帧类型有明确的埋点位置(crates/buzz-acp/src/acp.rs):

  • write_ndjson()在向 agent 标准输入写入每条 NDJSON 消息后调用self.observe("acp_write", value.clone())
  • 读取模型返回消息的两处路径均调用self.observe("acp_read", msg.clone())

也就是说,acp_write/acp_read本质上是对 ACP JSON-RPC 流量进出的逐条镜像,Owner 端据此可以在不解密第三方内容的前提下(内容仅 Owner 可解密)实时观测 Agent 与模型之间到底交换了什么。

解密载荷:Control 帧的 cancel_turn 指令

frame=control时,content解密后为:

{ "type": "cancel_turn", "channelId": "<channel_uuid>" }
  • 目前唯一定义的 control 类型是cancel_turn
  • 实现必须忽略无法识别的type值。

桌面客户端将cancel_turn控制指令的实现集中在 desktop/src/shared/api/agentControl.ts 中:它构建{ type: "cancel_turn", channelId }载荷,并通过sendAgentObserverControl(位于 desktop/src/shared/api/observerRelay.ts 附近)发布为 kind 24200 控制帧;取消结果以异步control_resultobserver 帧的形式返回,客户端侧由此跟踪 Agent 何时从 busy 回到 idle(见 desktop/src/features/agents/lib/cancelTurnOutcome.ts 及其测试)。

短暂性契约(Ephemerality Contract)

NIP-AO 对中继提出如下硬性要求与建议:

  • 中继不得将 kind 24200 事件持久化到任何耐久存储;
  • 中继不得将 kind 24200 事件纳入搜索索引;
  • 中继不得将 kind 24200 事件写入审计日志;
  • 中继仅通过内存中的 pub/sub 扇出 kind 24200 事件,绝不经过数据库写入路径;
  • 客户端since=<now>订阅;不支持历史回放;
  • 客户端在有界内存环形缓冲中暂存收到的帧。

Buzz 中继正是这样实现的:在 crates/buzz-relay/src/handlers/event.rs 中,KIND_AGENT_OBSERVER_FRAME被从普通事件摄入路径提前分流到专门的handle_agent_observer_event();该函数不调用ingest_event()不写数据库,而是经由state.pubsub.publish_event(&conn.tenant, EventTopic::Global, &event)走全局内存 pub/sub 通道,再通过fan_out_event_to_local_subscribers()直接向本地 WS 订阅者扇出(crates/buzz-relay/src/handlers/event.rs)。这正是 NIP-AO「in-memory pub/sub,never via database write path」的源码级印证。

订阅侧,Buzz 中继为「kind +#p全约束」的全局订阅维护专门的global_p_kind_index索引,测试用例test_global_p_kind_index_fan_out_targets_matching_p验证了以{kinds:[24200], "#p":[owner]}订阅的连接只收到p标签匹配的帧(crates/buzz-relay/src/subscription.rs)。

授权模型:双向 agent–owner 关系验证

NIP-AO 要求中继在发布路径上做数据库级别的 agent–owner 关系确认,仅凭#p标签匹配是不够的

Telemetry(agent → owner)

  • event.pubkey必须等于 agent pubkey;
  • p标签必须等于 owner pubkey;
  • 中继必须通过认证的 ownership 查找验证is_agent_owner(agent, owner)

Control(owner → agent)

  • event.pubkey必须等于 owner pubkey;
  • p标签必须等于 agent pubkey;
  • 中继必须依据agent标签解析出的 agent 验证is_agent_owner(agent, owner)

未经授权的发布或订阅尝试必须以AUTH required拒绝。

Buzz 中继的handle_agent_observer_event()完整落实了这套流程(crates/buzz-relay/src/handlers/event.rs):

  1. 先通过spawn_blocking(verify_event)做 NIP-01 签名验证;
  2. 校验created_at是否落在 ±5 分钟新鲜度窗口内(详见下文「中继行为」);
  3. 解析路由(方向判定);
  4. 授权快路径:若该连接通过 NIP-OA 认证且其已核实的 owner 与帧的目标 owner 一致,直接跳过数据库查询;否则查state.db.is_agent_owner(community, agent, owner),结果缓存在observer_owner_cache(crates/buzz-relay/src/state.rs 附近);
  5. 不匹配则拒绝:"restricted: observer frame is not authorized for this agent owner"

订阅侧同理:p_gated_filters_authorized要求 kind 24200 的 REQ 过滤器必须带#p且其值必须等于认证读者的 pubkey,测试agent_observer_subscription_requires_matching_p_tag覆盖了缺#p#p为他人、#p为自身三种情形(crates/buzz-relay/src/handlers/req.rs)。Kind 24200 也因此被列入P_GATED_KINDS(crates/buzz-core/src/kind.rs),但因其属于 ephemeral 类型而从不落库,存储层的搜索防御对其不适用。

中继行为:处理流程、限流与新鲜度窗口

中继收到 kind 24200 事件后必须依次执行:

  1. 按 NIP-01 校验事件签名;
  2. 按上述规则验证授权;
  3. 通过内存 pub/sub 向匹配订阅者扇出;
  4. 调用常规的事件摄入或持久化路径。

此外中继强制执行每个 agent pubkey 每秒 100 条事件的限流,并且建议拒绝created_at超出 ±5 分钟新鲜度窗口的事件,以防止捕获事件的重放。

Buzz 中继的实现(crates/buzz-relay/src/handlers/event.rs):

  • 新鲜度窗口(event_ts - now).unsigned_abs() > 300即拒绝,错误信息为"invalid: observer frame timestamp outside ±5 minute freshness window"
  • 限流:滑动窗口计数器observer_rate_limiter(community_id, agent_key)维度统计(同 agent 在不同租户各享独立预算),窗口内超过 100 条即返回"rate-limited: observer frame rate exceeded (100/sec per agent)"
  • 一个值得注意的细节:限流只针对 Telemetry 帧。Control 帧(owner → agent)绕过限流器——它们稀少且绝不能被 Agent 突发遥测挤兑饿死。

客户端行为:订阅、解密与有界缓冲

客户端按如下过滤器订阅:

{"kinds": [24200], "#p": ["<own_pubkey>"], "since": <now>}

收到事件后客户端必须

  1. 校验事件签名;
  2. 使用自己的私钥与event.pubkey解密content
  3. 解析解密载荷,并按kind(telemetry)或type(control)分发;
  4. 忽略未知的kind/type值。

同时

  • 在解密前核对agent标签是否指向已知/受信任的 agent pubkey;
  • 将有界环形缓冲作为接收缓冲(建议上限:800 条事件);
  • 不得请求历史 kind 24200 事件(不携带过去的since、不使用until、不使用ids查询)。

仓库中的客户端实现证据

  • ACP harness 端(Agent 侧)HarnessRelay内置专用订阅OBSERVER_CONTROL_SUB_ID = "agent-observer-control",通过subscribe_observer_controls()订阅发给自己的加密控制帧,并提供独立的observer_control_rx接收通道(crates/buzz-acp/src/relay.rs);
  • 桌面端(Owner 侧):desktop/src/features/agents/useAgentObserverIngestion.ts 在 AppShell 中挂载一次,负责接收 kind 24200 帧、解密并维护受信任 agent 集合;desktop/src/features/agents/agentWorkingSignal.ts 基于turn_started/session_resolved等帧推算 Agent 是否正在工作;
  • 移动端:mobile/lib/features/channels/agent_activity/observer_models.dart 定义了 kind 24200 解密后的 Observer 帧模型,mobile/lib/shared/relay/nostr_models.dart 中有对应的事件类型引用;
  • 端到端回归:desktop/tests/e2e/agent-control-regressions.spec.ts 覆盖了控制帧(cancel_turn)的端到端场景。

安全考量

NIP-AO 明确列出六类安全考量,Buzz 实现均与之对应:

威胁说明Buzz 侧应对
元数据泄露路由标签(pagentframecreated_at)为明文,中继运营者可观察到 agent X 正在以何种速率向 owner Y 流式传输文档建议:追求极致元数据隐私的实现在 NIP-59 gift wrap 中包装事件
无前向保密NIP-44 不提供前向保密;agent 私钥泄露即可解密任何捕获的密文依赖 NIP-44 v2 本身,密钥保管是防线
重放攻击捕获的已签名事件可在无新鲜度检查时被重放中继强制执行 ±5 分钟created_at新鲜度窗口(crates/buzz-relay/src/handlers/event.rs)
恶意中继短暂性契约是中继策略而非密码学保证NIP-44 加密确保即使事件被存储,中继运营者在没有密钥的前提下也无法读取内容
尽力投递重连或队列溢出期间控制帧可能被丢弃控制指令应视为 advisory 且具备幂等语义;Agent不得依赖控制帧的保证投递
操作持久化载体遥测可能短暂存在于进程内存、崩溃转储与应用日志实现最小化解密载荷的日志记录,必须不得在 INFO 及以上级别记录

与其他 NIP 的关系

  • NIP-01:Kind 24200 位于 ephemeral 区间(20000–29999),适用标准事件结构与签名规则;
  • NIP-42:推荐用于中继侧认证门控;
  • NIP-44:所有content字段的强制加密算法;
  • NIP-29:当 agent 会话限定在 NIP-29 群组时,可包含h标签;
  • NIP-XX(PR #2226):NIP-XX 定义 agent 的输出平面,本 NIP 定义可观测性平面(内部 agent 活动),两者互补且不重叠。

在 Buzz 的 NIP 族谱中,NIP-AO 还与NIP-AM(Agent Turn Metric,kind 44200,每轮次结束持久化到 Owner 的用量记录)形成「实时短帧 + 耐久用量」的配套关系,二者同为P_GATED_KINDS成员(crates/buzz-core/src/kind.rs)。

完整示例

示例 1:Telemetry 事件(acp_write帧)

线格式(加密后):

{ "id": "a1b2c3d4...", "kind": 24200, "pubkey": "agent_pubkey_hex", "created_at": 1777464041, "content": "<NIP-44 v2 ciphertext>", "tags": [ ["p", "owner_pubkey_hex"], ["agent", "agent_pubkey_hex"], ["frame", "telemetry"] ], "sig": "..." }

解密后的载荷:

{ "seq": 42, "timestamp": "2026-04-29T12:00:41.500Z", "kind": "acp_write", "agentIndex": 0, "channelId": "52a85618-0f8f-4542-94ec-599e6e1c6f2e", "sessionId": "a1b2c3d4", "turnId": "e5f6g7h8", "payload": { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "shell", "arguments": { "command": "ls -la" } } } }

这条载荷对应的正是 crates/buzz-acp/src/acp.rs 中write_ndjson()对出站 ACP 消息的observe("acp_write", ...)埋点——Owner 可以实时看到 Agent 向模型发出的tools/call请求及其参数。

示例 2:Control 事件(cancel_turn帧)

线格式(加密后):

{ "id": "e5f6a7b8...", "kind": 24200, "pubkey": "owner_pubkey_hex", "created_at": 1777464042, "content": "<NIP-44 v2 ciphertext>", "tags": [ ["p", "agent_pubkey_hex"], ["agent", "agent_pubkey_hex"], ["frame", "control"] ], "sig": "..." }

解密后的载荷:

{ "type": "cancel_turn", "channelId": "52a85618-0f8f-4542-94ec-599e6e1c6f2e" }

参考实现与延伸阅读

NIP-AO 文档末尾引用了参考实现 block/sprout PR #421。在 Buzz 仓库内,可以沿以下路径继续深入:

  • Kind 注册与范围断言:crates/buzz-core/src/kind.rs(24200 常量、P_GATED_KINDSis_ephemeral
  • 加密/解密原语:crates/buzz-core/src/observer.rs(encrypt_observer_payload/decrypt_observer_payload、65,535 字节上限、zeroize)
  • Harness 侧 observer 总线:crates/buzz-acp/src/observer.rs(ObserverEvent、有界回放缓冲、seq生成)
  • Harness 侧中继客户端:crates/buzz-acp/src/relay.rs(subscribe_observer_controlsOBSERVER_CONTROL_SUB_ID
  • 中继侧处理:crates/buzz-relay/src/handlers/event.rs(签名验证、±5 分钟窗口、授权快路径、100/sec 限流、内存 pub/sub 扇出)
  • 中继侧订阅门控:crates/buzz-relay/src/handlers/req.rs 与 crates/buzz-relay/src/subscription.rs(p_gated_filters_authorizedglobal_p_kind_index
  • SDK 构造器:crates/buzz-sdk/src/builders.rs(build_agent_observer_frame,拒绝非 NIP-44 v2 密文内容)
  • Owner 端消费:desktop/src/features/agents/useAgentObserverIngestion.ts、desktop/src/shared/api/agentControl.ts、mobile/lib/features/channels/agent_activity/observer_models.dart

注:NIP-AO 当前状态为draft/optional,文中以 MUST/SHOULD/MAY 标注的要求分别对应 RFC 2119 语义;Buzz 仓库当前实现已覆盖其中大部分 MUST 与 SHOULD 级行为。

【免费下载链接】buzzA hive mind communication platform项目地址: https://gitcode.com/GitHub_Trending/buzz14/buzz

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 5:10:17

Redis 内存碎片率排查:activedefrag 参数调优实操

Redis 内存碎片率排查&#xff1a;activedefrag 参数调优实操在长周期稳定运行的大模型语义缓存&#xff08;Semantic Cache&#xff09;与高并发 Redis 集群中&#xff0c;运维与基础架构团队经常遇到一个极其诡异的**“内存账本黑洞”**&#xff1a; 在 Redis 控制台执行 INF…

作者头像 李华
网站建设 2026/9/12 5:08:31

Qt混合开发:QWidget无缝嵌入QML的工业级解决方案

1. 项目背景与核心价值 在Qt混合开发中&#xff0c;如何将传统的QWidget控件无缝嵌入到QML界面一直是个痛点。WindowContainer的出现彻底改变了这一局面&#xff0c;它像一座桥梁连接了Qt两大UI体系。我在最近的车载HMI项目中就遇到了这样的需求&#xff1a;需要在QML构建的炫酷…

作者头像 李华
网站建设 2026/9/12 5:08:15

YOLOv8+SpringBoot野外AI监测系统工程化实践

1. 项目本质与真实定位&#xff1a;这不是一个“堆砌版本号”的玩具系统&#xff0c;而是一套面向野外监测场景的工程化AI视觉落地框架你看到标题里一连串YOLOv8/YOLOv10/YOLOv11/YOLOv12&#xff0c;第一反应可能是“这又是个蹭热点的PPT项目”——我完全理解。干了十多年AI落…

作者头像 李华
网站建设 2026/9/12 5:07:43

MongoDB 与 Elasticsearch 混合查询:高性能存储与检索解决方案

MongoDB 与 Elasticsearch 混合查询&#xff1a;高性能存储与检索解决方案 MongoDB 与 Elasticsearch 的混合架构是一种常见的数据解决方案&#xff0c;它利用 MongoDB 作为主要数据存储系统&#xff0c;而 Elasticsearch 则专注于提供高效的全文检索和分析能力。这种组合能够充…

作者头像 李华
网站建设 2026/9/12 5:05:23

ARM Cortex-M边缘语音唤醒系统源码深度解析

1. 项目概述&#xff1a;这不是一次简单的代码阅读&#xff0c;而是一次嵌入式AI工程的“X光扫描” 我第一次打开 ML-KWS-for-MCU 这个仓库时&#xff0c;没急着编译&#xff0c;也没急着跑demo&#xff0c;而是先关掉IDE&#xff0c;打开终端&#xff0c;敲下 find . -name…

作者头像 李华