Self-join (kind:9021) hard-codes role=member, making a self-joined agent permanently unmentionable
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
`handle_join_request` (kind:9021) hard-codes `MemberRole::Member`. An agent that joins a channel itself is therefore recorded as a human member — and because clients use the channel role as the "is this an agent" signal, that agent becomes **invisible to @mention autocomplete**, with no in-app way to repair it.
The same channel reached through a different entry point works fine, which is what makes this hard to spot.
## Three entry paths, three different role decisions
| Entry path | How the role is decided |
|---|---|
| Desktop invite | `role: user.isAgent ? "bot" : "member"` — `desktop/src/features/channels/ui/MembersSidebar.tsx` |
| `kind:9000` / `add-member` | `handle_put_user` reads a `role` tag; falls back to `Member` only for a brand-new member |
| **`kind:9021` / self-join** | **hard-coded `MemberRole::Member`; the joiner cannot express a role** |
`kind:9021` carries no role tag and `buzz channels join` exposes no `--role`, so this is not a client omission — there is no way to ask for anything else.
## Why `Member` is the wrong default here specifically
`MemberRole`'s own documentation says `Bot` is not a lower rung:
```rust
// crates/buzz-core/src/channel.rs
/// The hierarchy for permission checks is: Owner > Admin > Member > Guest.
/// Bot is a **separate designation** — it is not part of the linear hierarchy.
```
So assigning `Member` to an agent is not a conservative privilege choice — it is a category error, and it is the category clients read.
## Impact
1. Agent self-joins an open channel (the only channels where `9021` is accepted).
2. It lands as `member`.
3. Desktop's mention eligibility requires `isAgent === true || member.role === "bot"`, so it never appears in `@` autocomplete.
4. Repair requires `role: bot` on a `kind:9000`, which the relay restricts to owner/admin (`only owners/admins may change an active member's role`) — and the Desktop role menu offers only admin/member/guest, and hides itself entirely for members already marked `bot`. There is no in-app path back.
Observed on a self-hosted relay: an agent self-joined at `T`, was unmentionable for the entire session, and only recovered after an owner-signed `add-member --role bot`.
## Suggested direction
The relay already defines an agent discriminator and relies on it elsewhere:
```rust
// crates/buzz-db/src/usage.rs
/// Agent discriminator: `agent_owner_pubkey IS NOT NULL`.
```
Using that same discriminator in `handle_join_request` fixes the self-join path without touching `handle_put_user`, so explicit admin/member/guest assignment for humans is unaffected. PR to follow.
Alternatives, if maintainers prefer a different shape: accept an optional `role` tag on `kind:9021`, or have clients repair the role after joining. Both seem worse — the first lets a joiner self-assign, the second spreads the decision across clients that already disagree.
## Note on closed relays
`agent_owner_pubkey` is exactly the field that stays NULL on relays with `require_relay_membership = true` (#5581). Where that is unresolved, the discriminator reports every agent as a human and this fix is a no-op — so the two are the same knot, and #5581 is a prerequisite for closed deployments rather than a separate concern.
Contributor guide
Assessment
This issue has not been assessed yet.