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-core、buzz-acp、buzz-relay、buzz-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 = 20000与EPHEMERAL_KIND_MAX = 29999,并有配套谓词is_ephemeral(kind)(crates/buzz-core/src/kind.rs)。
核心术语
| 术语 | 含义 |
|---|---|
| Agent | 拥有自己 Nostr 密钥对、代表 Owner 执行会话的 AI 进程 |
| Owner | Agent 所归属的人类(或系统),其 pubkey 是 Agent 的 provision 主体 |
| Observer Frame | 一个携带单条遥测或控制信息的 kind 24200 事件 |
| Session | 由共享的sessionId关联的一次有界 Agent 执行 |
在 crates/buzz-acp/src/observer.rs 中,这些概念被直接落地:ObserverEvent结构体携带seq、timestamp、kind、agent_index、channel_id、session_id、turn_id、started_at与payload字段,与 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 源码中,agent与frame两个标签名及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) | agent | owner | agent |
| Control(owner → agent) | owner | agent(目标) | agent(目标) |
Buzz 中继在agent_observer_route()中通过event.pubkey、recipient与agent三者关系精确判定方向(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": { ... } }字段约束与语义:
- REQUIRED:
seq、timestamp、kind、payload; - OPTIONAL(可为 null):
agentIndex、channelId、sessionId、turnId—— 例如会话建立之前的sessionId可能未知,客户端必须优雅处理null; seq按会话单调递增,用于丢帧检测;timestamp是带亚秒精度的 RFC 3339 字符串,如"2026-04-29T12:00:41.500Z";agentIndex在多 Agent 场景中标识具体 Agent;sessionId/turnId用于跨会话、跨轮次关联帧;payload与kind相关(可为{});未知的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):
- 先通过
spawn_blocking(verify_event)做 NIP-01 签名验证; - 校验
created_at是否落在 ±5 分钟新鲜度窗口内(详见下文「中继行为」); - 解析路由(方向判定);
- 授权快路径:若该连接通过 NIP-OA 认证且其已核实的 owner 与帧的目标 owner 一致,直接跳过数据库查询;否则查
state.db.is_agent_owner(community, agent, owner),结果缓存在observer_owner_cache(crates/buzz-relay/src/state.rs 附近); - 不匹配则拒绝:
"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 事件后必须依次执行:
- 按 NIP-01 校验事件签名;
- 按上述规则验证授权;
- 通过内存 pub/sub 向匹配订阅者扇出;
- 不调用常规的事件摄入或持久化路径。
此外中继应强制执行每个 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>}收到事件后客户端必须:
- 校验事件签名;
- 使用自己的私钥与
event.pubkey解密content; - 解析解密载荷,并按
kind(telemetry)或type(control)分发; - 忽略未知的
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 侧应对 |
|---|---|---|
| 元数据泄露 | 路由标签(p、agent、frame、created_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_KINDS、is_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_controls、OBSERVER_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_authorized、global_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),仅供参考