fix: unify NIP-OA relay membership enforcement across all ingress paths#490
Merged
tlongwell-block merged 2 commits intomainfrom May 6, 2026
Merged
fix: unify NIP-OA relay membership enforcement across all ingress paths#490tlongwell-block merged 2 commits intomainfrom
tlongwell-block merged 2 commits intomainfrom
Conversation
When SPROUT_REQUIRE_RELAY_MEMBERSHIP=true, agents whose owner is a relay member should be allowed to connect via NIP-OA attestation. Previously, this fallback only worked for media and git transport endpoints. WebSocket NIP-42 auth had a separate enforce_ws_relay_membership() with no NIP-OA support, the HTTP bridge passed None for the auth tag, and the audio WebSocket handler also lacked the fallback. This patch removes the duplicate enforcement function and routes all five ingress paths through the single shared enforce_relay_membership() helper that already supports NIP-OA owner-delegation: - WebSocket NIP-42 (handlers/auth.rs): extract auth tag from the signed AUTH event, pass to shared helper - HTTP bridge /events, /query, /count (api/bridge.rs): read x-auth-tag header instead of passing None - Audio WebSocket (audio/handler.rs): extract auth tag from AUTH event, pass to shared helper - MCP client (relay_client.rs): include auth_tag in NIP-42 AUTH events (initial connect, reconnect, mid-session re-auth) - MCP media upload (upload.rs): send x-auth-tag header on PUT requests Additional spec conformance: - extract_auth_tag_json() rejects events with >1 auth tag per NIP-OA spec Security properties: - Fail-closed: DB errors, missing tags, invalid sigs all deny access - WS path: auth tag is integrity-protected by the event Schnorr signature - HTTP path: verify_auth_tag binds attestation to the NIP-98-authenticated agent pubkey (cannot replay another agent's tag) - No new attack surface: the only new accept path is valid NIP-OA sig + owner is relay member, which is the intended behavior
The ACP has its own WebSocket relay client (separate from sprout-mcp's RelayClient). It was not including the NIP-OA auth tag in NIP-42 AUTH events or sending x-auth-tag on HTTP bridge requests, so agents using SPROUT_AUTH_TAG could not connect to membership-enforced relays. Changes: - Parse SPROUT_AUTH_TAG into a nostr::Tag and pass to HarnessRelay::connect - Thread auth_tag through do_connect, send_auth_response, handle_ws_message, try_autonomous_reconnect, wait_for_reconnect, process_handshake_buffer - Add auth_tag_json field to RestClient, send x-auth-tag header on all HTTP bridge POST requests (/query, /events) E2E verified: agent with NIP-OA tag connects, discovers channels, receives tasks, and replies — all through a relay with SPROUT_REQUIRE_RELAY_MEMBERSHIP=true.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When
SPROUT_REQUIRE_RELAY_MEMBERSHIP=true, agents whose owner is a relay member should be allowed to connect via NIP-OA attestation. Previously this fallback only worked for media and git transport endpoints — WebSocket NIP-42 auth, the HTTP bridge, and the audio WebSocket handler all lacked it.This patch removes the duplicate
enforce_ws_relay_membership()function and routes all ingress paths through the single sharedenforce_relay_membership()helper that already supports NIP-OA owner-delegation.What Changed
handlers/auth.rs)/events,/query,/count(bridge.rs)Nonefor auth tagx-auth-tagheaderaudio/handler.rs)Nonefor auth tagrelay_client.rs)auth_tagbut never sent it in NIP-42upload.rs)x-auth-tagheaderSecurity Properties
verify_auth_tagbinds the attestation to the NIP-98-authenticated agent pubkey — cannot replay another agent's tagauthtag are treated as having no valid tag (per NIP-OA spec)Unified Flow
Testing
cargo build --workspace✅cargo clippy --workspace --all-targets -- -D warnings✅cargo fmt --all -- --check✅cargo test --workspace✅ (all 1,251 tests pass)Stats
6 files changed, +116/−68 (net code reduction from deleting the duplicate function)