fix(buzz-relay): record the agent→owner relationship for direct members - #6098
Open
Schnitzel wants to merge 1 commit into
Open
fix(buzz-relay): record the agent→owner relationship for direct members#6098Schnitzel wants to merge 1 commit into
Schnitzel wants to merge 1 commit into
Conversation
An agent registered as a relay member in its own right never gets
`users.agent_owner_pubkey` written, so every owner-gated feature is
silently disabled for it — and nothing anywhere reports why.
`check_relay_membership` tests direct membership first and short-circuits
on `MembershipDecision::Member`. The owner is only resolved on the
`ViaOwner` branch, reached solely by agents that are *not* members and
are admitted by NIP-OA delegation; the other backfill path in auth.rs is
gated on `!require_relay_membership`. So a closed relay throws away a
NIP-OA proof the agent did present, on every single connection, purely
because the agent was also granted membership.
The visible symptom is Buzz Desktop's per-agent ACP activity tab. It is
fed by kind 24200 observer frames, the relay gates those on
`users.agent_owner_pubkey`, and with no mapping it rejects all of them:
restricted: observer frame is not authorized for this agent owner
That rejection is invisible from both sides. The relay only increments
`buzz_events_rejected_total{reason="auth"}` and logs nothing at all,
while buzz-acp does not surface the `OK=false` — so the agent reports
`relay observer enabled`, resolves an owner, looks entirely healthy, and
every frame it sends is dropped. The operator sees an empty tab and
reasonably concludes the UI is broken.
Desktop-managed agents are unaffected, which makes this harder to place:
they are minted with an owner rather than added to `relay_members`, so
the same agent works when created in the app and fails when run by the
harness. Granting an agent membership is what breaks it.
This resolves the owner for a direct member too, adding
`MembershipDecision::MemberWithOwner`. Membership is still decided
exactly as before — the new variant only carries a relationship that was
already proved out of the check so it can be recorded.
The tag verification and owner-is-member lookup are shared with the
delegation path in `resolve_nip_oa_owner`, so the security posture is
unchanged: an owner that is not itself a relay member is still not
recorded, which matters on a closed relay where that would hand
agent-management authority to someone who cannot connect. The extra
lookup runs only for callers that present an auth tag, so ordinary
client connections do no additional work.
Both call sites that consume the owner — `handlers/auth.rs` and
`api/bridge.rs` — already pass it straight to `materialize_nip_oa_owner`,
so they need no change and the fix reaches every transport at once. The
remaining callers discard the value.
Signed-off-by: Michael Schmid <michael.schmid@amazee.com>
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
An agent registered as a relay member in its own right never gets
users.agent_owner_pubkeywritten, so every owner-gated feature is silently disabled for it — and nothing anywhere reports why.check_relay_membershiptests direct membership first and short-circuits onMembershipDecision::Member. The owner is only resolved on theViaOwnerbranch, reached solely by agents that are not members and are admitted by NIP-OA delegation; the other backfill path inhandlers/auth.rsis gated on!require_relay_membership. So a closed relay throws away a NIP-OA proof the agent did present, on every connection, purely because the agent was also granted membership.The visible symptom is Buzz Desktop's per-agent ACP activity tab. It is fed by kind 24200 observer frames, the relay gates those on
users.agent_owner_pubkey, and with no mapping it rejects every one:That rejection is invisible from both sides. The relay only increments
buzz_events_rejected_total{reason="auth"}and logs nothing at all, whilebuzz-acpdoes not surface theOK=false— so the agent reportsrelay observer enabled, resolves an owner, looks entirely healthy, and every frame it sends is dropped. The operator sees an empty tab and reasonably concludes the UI is broken.Desktop-managed agents are unaffected, which makes this hard to place: they are minted with an owner rather than added to
relay_members, so the same agent works when created in the app and fails when run by the harness. Granting an agent membership is what breaks it.This resolves the owner for a direct member too, via a new
MembershipDecision::MemberWithOwner. Membership is still decided exactly as before — the new variant only carries a relationship that was already proved out of the check so it can be recorded.Notes for review:
resolve_nip_oa_owner, so an owner that is not itself a relay member is still not recorded. That matters on a closed relay, where it would otherwise hand agent-management authority to someone who cannot connect. Happy to relax it if you would rather treat the NIP-OA tag as self-proving here, asextract_nip_oa_ownerdoes on open relays.handlers/auth.rsandapi/bridge.rs— already pass it straight tomaterialize_nip_oa_owner, so the fix reaches every transport at once. The remaining callers discard the value.agent_owner_pubkeyis first-write-wins, so this records the relationship where one is missing and never overwrites an existing one.Existing deployments with an already-affected agent still need the row backfilled once; this stops it recurring, and any reconnect after upgrading writes it.
Related issue
None found — I searched issues and PRs for the observer-frame rejection, kind 24200, and
agent_owner_pubkeyand could not find this reported. Related in spirit to #6097, which fixes a different silent gap for harness-run agents (the kind 10100 directory entry), but the two are independent and touch different crates.Testing
cargo test -p buzz-relay --lib relay_members— 6 passed, including three new cases covering that a direct member surfaces a proved owner, that delegation still does, and that admission without proof surfaces none.cargo clippy -p buzz-relay --all-targets -- -D warnings— clean.cargo fmt -p buzz-relay.require_relay_membership=true,allow_nip_oa_auth=true) runningghcr.io/block/buzz:sha-1b3dbca, with an opencode agent underbuzz-acpandBUZZ_ACP_RELAY_OBSERVER=true. Itsusersrow hadagent_owner_pubkey = NULLwhile every desktop-created agent on the same relay had it set, and the activity tab was empty for a demonstrably working agent. Populating that one column — which is what this patch causes to happen automatically — made the tab populate.No UI change in this repo's code; the desktop tab renders as it always has, once the frames are allowed through.