Skip to content

DIVE-3560: fix buzz dms list — query relay-emitted kind:39000, not the never-emitted kind:41001 - #7

Merged
lodar merged 1 commit into
mainfrom
dive-3560-dms-list-clean
Aug 18, 2026
Merged

DIVE-3560: fix buzz dms list — query relay-emitted kind:39000, not the never-emitted kind:41001#7
lodar merged 1 commit into
mainfrom
dive-3560-dms-list-clean

Conversation

@lodar

@lodar lodar commented Aug 18, 2026

Copy link
Copy Markdown

buzz dms list queried {kinds:[41001], "#p":[me]}. KIND_DM_CREATED = 41001 is declared in crates/buzz-core/src/kind.rs and emitted by nothing — the constant appears in its own declaration and one membership list, nowhere else in the tree. handle_dm_open creates the channel and then publishes NIP-29 group discovery, never a 41001. So the verb has returned [] for every account on every relay since it was written, while channels list, channels members and messages get all saw the same conversation. That is why the buzz plugin's inbound DM bridge (5dive-plugins#44) discovers zero DM targets on a live box: a user DMs an agent and gets silence.

The fix

Query the relay-emitted truth: kind:39000 NIP-29 group metadata, which emit_group_discovery_events tags with d = channel uuid, t = channel_type, and — for DMs only — one p tag per participant. Filter to t == "dm". The d tag is exactly what messages get/send --channel already takes.

  • dm_list_filter() extracted so a regression to a kind nothing emits is a red test rather than a silent empty list.
  • parse_query_events(): a relay error object is now an error. The old unwrap_or_default() made a dead relay, a wrong predicate and an empty inbox one observation.
  • dms_from_group_metadata(): keeps t=dm with a non-empty d that lists us (re-checked locally, so a relay ignoring #p cannot widen a consumer's poll set); a valueless ["t"] tag no longer masks a later ["t","dm"]; newest revision per uuid wins; sorted newest first.
  • KIND_DM_CREATED annotated reserved-and-unemitted.

Why t=dm and not a name/membership heuristic

kind:39000 is absent from required_scope_for_kind's match in handlers/ingest.rs, whose default arm is Err("restricted: unknown event kind"). A client cannot publish one — only the relay keypair authors it. So t=dm is relay-attested channel_type, not a channel NAME or a membership SHAPE, both of which any user can manufacture. That distinction is load-bearing: consumers skip the @-mention test on the DM path, so whatever marks a conversation as a DM is what authorises unsolicited input into a session.

Threat note: anyone may legitimately dms open --pubkey <agent>, so stranger DMs are open by design. What a forgery would buy is aiming the DM marker at a public channel and making the agent answer every message there unprompted — that is the attack the ingest allowlist closes.

Grading

358/0 buzz-cli (9 new tests), 249/0 buzz-core, clippy 0 warnings, cargo fmt clean — measured on this head. 9 mutants all killed, each asserted to have actually applied, baseline restored byte-identical after each: accept any channel_type; drop the participant re-check; accept an empty uuid; degrade a relay error to []; keep the oldest revision; query 41001 again; valueless-tag masking; unscoped #p; limit cap removed.

Worth flagging for reviewers: with the filter left inline, re-introducing the original defect (kind back to 41001) kept all 8 tests green — a pure mapper cannot see the query it was handed. Extracting dm_list_filter() and asserting on it is what kills that mutant.

Not verified

test_dms_list_query_shape_returns_the_dm (with a kind:41001 negative control) is #[ignore] per repo convention and needs a running relay. No relay and no docker on the authoring seat, so it is unexecuted — CI or a relay box grades it. The originating row's DONE line — a DM answered in the same DM — also stays unmet until a fixed binary is built, released and installed.

DIVE-3560.

…e never-emitted kind:41001

`dms list` queried {kinds:[41001], "#p":[me]}. KIND_DM_CREATED (41001) is
declared in buzz-core but no code path in this repo ever emits it, so the verb
returned [] for every account on every relay while channels list, channels
members and messages get all saw the same conversation. An empty list rather
than an error is why this read as "no DMs exist" instead of "my predicate
matched nothing" — and it is why the buzz plugin's inbound DM bridge
(5dive-plugins block#44) discovers zero DM targets on a real box.

The relay-emitted truth is kind:39000 NIP-29 group metadata, which
emit_group_discovery_events tags with t=<channel_type> plus one p tag per
participant for DMs. kind:39000 is absent from required_scope_for_kind's
allowlist, so client ingest rejects it as "restricted: unknown event kind" —
only the relay can author one. t=dm is therefore relay-attested channel_type,
not the attacker-controlled name/membership heuristic rejected on this row,
which matters because consumers skip the @-mention test on the DM path.

- dm_list_filter(): extracted so a regression to a kind nothing emits is a red
  test, not a silent empty list.
- parse_query_events(): a relay error object is an error, not an empty inbox.
  The old unwrap_or_default() made a dead relay indistinguishable from no DMs.
- dms_from_group_metadata(): keeps only t=dm with a non-empty d tag that lists
  us; a valueless ["t"] tag no longer masks a later ["t","dm"]; newest revision
  per channel wins; sorted newest first.
- Annotated KIND_DM_CREATED as reserved-and-unemitted.

Grading: 358/0 buzz-cli tests (9 new), 249/0 buzz-core, clippy clean, fmt clean.
9 mutants all killed, each asserted to have applied, baseline restored
byte-identical: accept any channel_type; drop the participant re-check; accept
an empty uuid; degrade a relay error to []; keep the oldest revision; query
41001 again (it survived until dm_list_filter was extracted and tested);
valueless-tag masking; unscoped #p; limit cap removed.

NOT VERIFIED: the e2e arm test_dms_list_query_shape_returns_the_dm (with a
kind:41001 negative control) is #[ignore] per repo convention and needs a
running relay. No relay, no docker and no reachable box from this seat, so it
is unexecuted here.
@lodar
lodar merged commit aa2d47b into main Aug 18, 2026
49 of 54 checks passed
@lodar
lodar deleted the dive-3560-dms-list-clean branch August 24, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant