fix(relay): carry bans to agents and close ban gaps on write and HTTP paths - #8010
Merged
Merged
Conversation
… paths An agent's access derives from its owner's, but the ban and timeout checks only looked at the authoring pubkey, and commands, ephemeral events, observer frames, moderation reads, and invite mint all bypassed them. Removing a member also left their sockets open. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…l revoke Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The ban check now folds in the agent owner's status via users, so the fault-injection schema must carry that table or the ban check errors before the allowlist and membership steps under test. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ke gaps Root and audio admission now bind the proven identity before the final ban/membership read, so a ban whose disconnect lands mid-AUTH is either seen by the read or cancels the bound socket. Agents whose NIP-OA owner link cannot be recorded are refused on both sockets, since revoking the owner finds agents through that link. A failed owned-agent lookup during revoke now propagates and keeps retrying in the background instead of being logged and dropped. NIP-43 self-leave revokes live access like admin removal. Media and Git keep 503 for a failed restriction lookup instead of reporting it as a 403. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Each test drives the production path: a ban committed while AUTH is paused after its policy reads, root and audio refusal when the agent owner link cannot be recorded, an audio-only agent closed by its owner's revoke, the owned-agent lookup retry, NIP-43 self-leave closing root, audio and agent sockets, and media/Git keeping 503 when only the restriction lookup fails. The observer-expiry barrier test moves to the Postgres lane because the observer write gate now reads restriction state before its barrier. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Root and audio sockets record the admitted agent owner, so revoking the owner closes them clusterwide with no database read; the detached retry task is gone. The final admission fence reads membership from the writer, a WebSocket NIP-43 leave now reaches ingest, a direct ban revokes before its audit insert, and media/Git policy-lookup failures return the NIP-FI authorization-unavailable body outside Off mode. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Each regression drives the production path and fails with its fix removed: WebSocket self-leave, removal during AUTH behind a stale replica, revocation of a recorded-owner agent with no agent lookup, Enforce/Off 503 bytes for media and Git, and a ban whose audit insert fails. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
had a problem deploying
to
codex-review
October 1, 2026 15:27 — with
GitHub Actions
Error
This branch was successfully deployed
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.
Stack: #8005 → this PR
An agent's access comes from its owner, but the community ban and timeout checks only looked at the pubkey that signed the event. Several write paths returned before those checks ran, admission could miss a ban that landed mid-AUTH, and removing or losing a member left live sockets open. This PR closes those gaps.
Behavior
restriction_statefolds in the restriction row of the agent's owner (users.agent_owner_pubkey) at read time. Nothing is copied onto the agent, so unbanning or un-timing-out the owner restores the agent immediately. Every hot-path caller (AUTH, ingest, audio, HTTP) picks this up.handlers::auth::final_admission_denial), then refuse if a disconnect already cancelled the socket. A ban or removal whose disconnect ran before the bind is visible to those fresh reads; one that runs after it finds and closes the bound socket. Both reads come from the writer, never the bounded-stale replica, so a removal that committed before the bind cannot be undone by a lagging replica. Both fail closed, and the check covers a delegated agent's owner. Each admitted socket records the agent's owner, from the NIP-OA tag or the stored link.users.agent_owner_pubkeyfor a NIP-OA agent before admitting it, and refuse admission (error: internal error recording agent owner) if that write fails, because revoking the owner finds the agent's sockets through that link.AppState::revoke_live_accesssends one clusterwide disconnect that closes, on every pod, each root and audio socket in the community whose principal or recorded owner is the target. A secondaryusers.agent_owner_pubkeysweep covers the one socket the owner match misses: an agent admitted with no owner link that was linked later (for example by an HTTP NIP-OA request). If that sweep's lookup fails, the error is returned; there is no retry task. The kind-9040 ban, the report-action ban (left non-terminal so recovery re-runs it), relay-admin removal (9031), and NIP-43 self-leave all use it and report an incomplete revoke instead of claiming success. Each revokes right after its change commits; the ban no longer waits on its audit insert, whose error is still returned.handle_eventused to fan it out and answerOK truewithout removing membership. It is now routed to ingest; no other ephemeral kind changes.enforce_write_restrictionruns ahead of command routing (DM open, DM add member, DM hide, workflow definitions and triggers, approvals), product feedback, reports, and moderation or relay-admin commands. Ephemeral events and agent observer frames return before ingest inhandle_event, so they go through the same gate there.enforce_relay_membershipruns a ban check after the membership decision, including on open relays: 403blocked: you are banned from this communityfor a ban, 503 for a failed lookup. Media upload, media GET/HEAD and Git transport keep that 503 instead of reporting it as "not a relay member": in NIP-FI Enforce mode they send the canonicalAuthorizationUnavailableresponse (text/plain; charset=utf-8,authorization unavailable\n), and in Off mode each keeps its legacy unavailable response. Media storage failures are not reclassified. Git transport checks membership and then runs its own ban check once per request. The NIP-OA owner is resolved fromx-auth-tagwhen present. HTTP checks bans only: a timeout blocks writes, HTTP writes already reach the ingest gate, and reading stays allowed under a timeout.Unchanged by design: a lost Redis fan-out can leave another pod's socket open until it reconnects, and there is no recurring ban recheck on open sockets.
Path table
POST /events,/query,/count(bridge.rs)enforce_relay_membershipcallerPOST /gifs/share(gifs.rs), media upload and fetch (media.rs), workflows (workflows.rs)git/transport.rs), git settings (git/settings.rs)GET /moderation/reports,/moderation/audit,/moderation/restrictedPOST /api/invites(invite mint)POST /events, both via ingest)/api/join-policy*/,/info,/.well-known/nostr.json,/health,/_liveness,/_readiness/hooks/{id}/operator/*,/api/admin/v1,/internal/git/policy/_mesh/demo/echomesh_demo_echo)Known limits