Skip to content

relay: on closed relays, a direct-member agent loses its NIP-OA identity (rate class, owner context, backfill) #4937

Description

@LiterallyBlah

Bug: on closed relays, a direct-member agent loses its NIP-OA identity (rate class, owner context, backfill)

Component: buzz-relaycrates/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:

  1. 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;
  2. gets no agent_owner_pubkey in its connection auth context;
  3. 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

  1. Closed relay; add an agent pubkey as a direct relay member.
  2. Give the agent a valid NIP-OA auth tag (owner also a member); connect via WS and AUTH with the tag.
  3. 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.
  4. Send an owner-signed kind-24200 control frame → refused until users.agent_owner_pubkey is set manually.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions