relay: on closed relays, a direct-member agent loses its NIP-OA identity (rate class, owner context, backfill)
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
# 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:
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.
Contributor guide
Research direction
Start in crates/buzz-relay/src/api/mod.rs at check_relay_membership, then trace its consumers in handlers/auth.rs, the HTTP bridge, and connection.rs. Verify the closed-relay direct-member path preserves the NIP-OA owner context so agent rate limits, users.agent_owner_pubkey backfill, and owner-authorized control frames work without manual mapping.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- authentication, authorization, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100