fix(relay): materialize NIP-OA owner for agents that are direct relay members - #6078
fix(relay): materialize NIP-OA owner for agents that are direct relay members#6078rclod wants to merge 1 commit into
Conversation
themiguelamador
left a comment
There was a problem hiding this comment.
I found one remaining functional gap and one test gap:
- The fix was limited to WebSocket AUTH.
POST /eventshad the same closed-relay direct-member branch and still discarded a validx-auth-tag, so an agent using HTTP ingestion could remain withoutagent_owner_pubkey. - Both new regression tests called the pre-existing
extract_nip_oa_ownercrypto helper directly. They pass on the unpatched base and therefore did not exercise the membership-path behavior that regressed.
I fixed this in commit 03ba80e3e on Complear:review/pr-6078-fix. The shared enforce_relay_membership result now reports a verified owner after any successful admission (open relay, direct member, or owner delegation). WebSocket AUTH and POST /events consume that shared result, and the regression tests now exercise the direct-member decision logic itself.
Verification:
cargo test -p buzz-relay api::relay_members::tests --quiet— 5 passedcargo test -p buzz-relay handlers::auth::tests --quiet— 3 passedcargo clippy -p buzz-relay --all-targets -- -D warningscargo fmt --all -- --checkgit diff --check
The full relay unit run reached 859 passes, 40 ignored, and 10 unrelated environment/global-state failures: the local Postgres instance lacks the buzz role, the mesh sidecar test timed out, and a tracing-global-state assertion fired. The changed-path tests and full strict compile/lint gate are green.
1ecb6ec to
a24e243
Compare
|
Both points were right, and the second more so than stated — thank you.
Test gap. Worse than you described: my Replaced with tests over the behaviour that actually regressed. I split the I could not run Credited you as co-author on the commit. Happy to squash or reword if you'd |
themiguelamador
left a comment
There was a problem hiding this comment.
Re-reviewed a24e243. The substantive fix now covers both WebSocket AUTH and POST /events through the shared admission result, including direct-member and open-relay attestations. One merge gate still fails: cargo fmt --check --all reformats the new MembershipDecision match arm in crates/buzz-relay/src/api/mod.rs. I applied that formatter-only correction and pushed signed commit cc5275ef1 to Complear:review/pr-6078-latest-fix. Verification on the fix: formatting passes; all 6 relay_members tests pass; strict cargo clippy -p buzz-relay --all-targets -- -D warnings passes. The broader relay unit run reached 860 passing, with 10 unrelated environment-dependent failures (missing local buzz Postgres role, mesh demo timeout, and the global telemetry subscriber assertion). Please cherry-pick cc5275ef1; after that this is approvable.
…ission The agent→owner backfill ran only when `require_relay_membership` was false. On a closed relay it never ran, and `enforce_relay_membership` did not fill the gap: it reported an owner only when it had USED the delegation to admit the agent (`MembershipDecision::ViaOwner`). An agent that is also a direct relay member took the `::Member` branch, which returned `Ok(None)` and discarded a valid, cryptographically self-proving attestation. So on a closed relay, any agent enrolled directly in `relay_members` had `users.agent_owner_pubkey` left NULL however valid its attestation. That column is not cosmetic — `is_agent` in the admission path derives from it (so such agents are limited at `human_messages_per_min` rather than `agent_standard_messages_per_min`), owner-managed agent lists have no relationship to display, and `channel_add_policy = "owner_only"` has no owner to compare against, refusing `add-member` for every actor including the real owner. Fixed in the shared helper rather than per call site. The attestation is now verified once, before the admission branches, and `MembershipDecision::OpenRelay` and `::Member` carry it; `enforce_relay_membership` reports it from every admitted variant. Both WebSocket AUTH and `POST /events` consume that shared result, so the open-relay-only workaround each carried is deleted rather than duplicated — the HTTP path had the same gap and would otherwise have kept it. Membership answers "may this caller connect"; the attestation answers "who owns this agent". Deriving the second from the first was the bug. The security boundary is unchanged: the tag's own verification in `extract_nip_oa_owner`. The decision→owner mapping is split into `owner_from_decision` so the behaviour that regressed is testable without a database, with three tests over it — notably that a direct member reports its owner, which is what previously returned None. Co-authored-by: themiguelamador <themiguelamador@users.noreply.github.com> Signed-off-by: rclod <3385524+rclod@users.noreply.github.com>
a24e243 to
f0fbbfc
Compare
|
Reworked commit is verified green — full CI on a fork run: 21 passed, 2 skipped Two notes for whoever picks this up:
|
Fixes #6072.
Problem
On a closed relay (
require_relay_membership = true), an agent enrolleddirectly in
relay_membersnever getsusers.agent_owner_pubkeypopulated,however valid its NIP-OA attestation.
Two correct-in-isolation pieces combine badly.
handlers/auth.rsruns thebackfill only on open relays, on the stated grounds that closed relays are
covered by
enforce_relay_membership. But that helper reports an owner onlywhen it used the delegation to admit the agent:
An agent that is also a direct member takes
::Member, getsOk(None), andits attestation is discarded. On a closed relay it therefore hits neither path.
Why it matters
agent_owner_pubkeydrives at least three behaviours, so the NULL is notcosmetic:
is_agentinconnection.rshuman_messages_per_min(60) instead ofagent_standard_messages_per_min(120)channel_add_policy = "owner_only"add-memberrefused for every actor, including the real owner, withpolicy:owner_only — agent has no owner setThe third is the one that looks like a bug in the client: the agent cannot be
added to any channel by anyone, and no error explains why.
Change
Run the extraction whenever an
authtag is present, independent of howmembership was satisfied. The security boundary is unchanged — it remains the
tag's own verification in
extract_nip_oa_owner, whose doc comment alreadymakes this argument:
Membership and the owner relationship are separate questions; the bug was
treating the second as a by-product of the first.
Also corrects two doc comments that described the old open-relay-only behaviour.
Tests
Two regression tests in
handlers/auth.rs:None— extraction isunconditional now, so its own verification is the only thing between a forged
tag and an owner mapping
Verification
Found on a self-hosted closed relay:
"NIP-42 auth successful"logged, nomaterialization attempted, no warning, and
agent_owner_pubkeyNULL for anagent sending a valid tag.
Full CI green on a fork run (21 passed, Mobile/Web skipped by path filters),
including Rust Lint and Unit Tests. Noting for transparency: I have no Rust
toolchain on the machine I authored this on, so
just ciwas run via that CIrather than locally.