Bug: on closed relays, a direct-member agent loses its NIP-OA identity (rate class, owner context, backfill)
Component: buzz-relay — crates/buzz-relay/src/api/mod.rs (check_relay_membership), consumed by handlers/auth.rs and the HTTP bridge
Severity: agents on closed relays are rate-limited as humans; owner-authorized control planes (drain, observer control) silently refuse
Symptom cluster (one cause, three faces)
On a relay with require_relay_membership = true, an agent that is a relay member in its own right and presents a valid NIP-OA auth tag at NIP-42 AUTH:
- is rate-limited under
human_messages_per_min (default 60/min) instead of agent_standard_messages_per_min — one working turn's observer telemetry (~90 msg/min) exceeds it, producing a permanent rate-limited: quota exceeded storm on the WS path;
- gets no
agent_owner_pubkey in its connection auth context;
- never receives the
users.agent_owner_pubkey backfill, so owner-signed NIP-AO control frames (drain, observer control) are refused with restricted: observer frame is not authorized for this agent owner until the mapping is inserted by hand.
Root cause
check_relay_membership short-circuits to MembershipDecision::Member for any direct member and consults the NIP-OA tag only as a membership fallback for non-members (ViaOwner). The open-relay path has an explicit opportunistic-extraction branch for exactly these consumers (extract_nip_oa_owner, gated on !require_relay_membership) — the closed-relay member arm has no equivalent. So agent-ness downstream (ctx.agent_owner_pubkey.is_some() in connection.rs) is derivable only for agents that are NOT members, which inverts the natural provisioning path of enrolling agents as members.
Fix that worked for us
In the member arm, when allow_nip_oa_auth is on and a tag is presented, extract the (cryptographically self-proving) owner and return ViaOwner(owner) instead of Member. Membership stops being a reason to ignore the tag. All existing consumers either discard the owner or feed it to the intended backfill, so no behavior is lost.
Reproduction
- Closed relay; add an agent pubkey as a direct relay member.
- Give the agent a valid NIP-OA
auth tag (owner also a member); connect via WS and AUTH with the tag.
- Publish >60 events/min (any working agent with observer telemetry does this naturally) → quota storm despite
BUZZ_RATE_LIMIT_AGENT_STANDARD_MESSAGES_PER_MIN being far higher.
- Send an owner-signed kind-24200 control frame → refused until
users.agent_owner_pubkey is set manually.
Bug: on closed relays, a direct-member agent loses its NIP-OA identity (rate class, owner context, backfill)
Component:
buzz-relay—crates/buzz-relay/src/api/mod.rs(check_relay_membership), consumed byhandlers/auth.rsand the HTTP bridgeSeverity: agents on closed relays are rate-limited as humans; owner-authorized control planes (drain, observer control) silently refuse
Symptom cluster (one cause, three faces)
On a relay with
require_relay_membership = true, an agent that is a relay member in its own right and presents a valid NIP-OAauthtag at NIP-42 AUTH:human_messages_per_min(default 60/min) instead ofagent_standard_messages_per_min— one working turn's observer telemetry (~90 msg/min) exceeds it, producing a permanentrate-limited: quota exceededstorm on the WS path;agent_owner_pubkeyin its connection auth context;users.agent_owner_pubkeybackfill, so owner-signed NIP-AO control frames (drain, observer control) are refused withrestricted: observer frame is not authorized for this agent owneruntil the mapping is inserted by hand.Root cause
check_relay_membershipshort-circuits toMembershipDecision::Memberfor any direct member and consults the NIP-OA tag only as a membership fallback for non-members (ViaOwner). The open-relay path has an explicit opportunistic-extraction branch for exactly these consumers (extract_nip_oa_owner, gated on!require_relay_membership) — the closed-relay member arm has no equivalent. So agent-ness downstream (ctx.agent_owner_pubkey.is_some()inconnection.rs) is derivable only for agents that are NOT members, which inverts the natural provisioning path of enrolling agents as members.Fix that worked for us
In the member arm, when
allow_nip_oa_authis on and a tag is presented, extract the (cryptographically self-proving) owner and returnViaOwner(owner)instead ofMember. Membership stops being a reason to ignore the tag. All existing consumers either discard the owner or feed it to the intended backfill, so no behavior is lost.Reproduction
authtag (owner also a member); connect via WS and AUTH with the tag.BUZZ_RATE_LIMIT_AGENT_STANDARD_MESSAGES_PER_MINbeing far higher.users.agent_owner_pubkeyis set manually.