Repository navigation
Description
Summary
Chat presence documents a receipt-side membership-scoping guarantee that no production code path implements: PresenceTable::update_scoped — the only entry point that filters a heartbeat's channels to channels the sender is a member of — has no caller outside its own module. The one production receipt path, the gossip receive task in id serve, stores verified heartbeats through the explicitly unscoped PresenceTable::update, so any channels a heartbeat names are kept as claimed.
Evidence (verified this run, main @ 97e3e8e)
pkgs/id/src/chat_presence.rs:24-31— module doc, "Membership scoping": "On receipt, [PresenceTable::update_scoped] filters a heartbeat's channels to those the sender is a member of (the caller's membership predicate), so a node cannot advertise presence in — or probe — a channel it does not belong to."pkgs/id/src/chat_presence.rs:222-228—update()is documented as "with no membership scoping (the transport layer's view; scoping is the receiver's job — seeupdate_scoped)".pkgs/id/src/commands/serve.rs:1615-1628— the only production receipt path:decode_frame→heartbeat.verify()→recv_presence.update(heartbeat). Signature verification stops forgery of another node's heartbeat, but a node signs its own key freely, so verification does not stop a node claiming channels it does not belong to — that is exactly what scoping was for.- Caller search (run twice, independently: sandbox clone @ 97e3e8e and laptop clone):
grep -rn "update_scoped" pkgs/id/src/returns only references insidechat_presence.rsitself (the definition, its doc comments, and its tests). No production caller exists. - The sender side shows the wiring is half-built:
serve.rs:1591-1599broadcasts this node's heartbeat with no channels, with the comment "Channel membership lives in the chat layer above serve, so this heartbeat names no channels; receivers scope and read it through PresenceTable." The chat layer that would name channels — and the receiver-side membership predicate that would scope them — do not exist yet.
Why it matters
The module doc states the scoping as a present-tense property of the system, and it is the only thing standing between "presence is scoped to authorized channel members" and "any node that learns or guesses a channel's group hash can advertise presence in it and keep a live entry in every peer's table." Today the exposure is latent — the built-in sender names no channels, and no production reader renders rosters yet (roster_at / roster_for_viewer_at / live_nodes_at also have no production callers). But the moment the chat layer starts naming channels on send and reading rosters, it will inherit an unscoped table while believing the documented guarantee holds. This is the same failure shape as #4227 (a protective evaluator that exists, is tested, and is never wired in).
Acceptance criteria
- The production receipt path in
id servescopes incoming heartbeats to the sender's membership — i.e. it callsupdate_scopedwith a membership predicate backed by the group fold (or an equivalent mechanism) — or the module documentation is corrected to state plainly that receipt scoping is not wired, andupdate()is not described as leaving scoping "to the receiver" when no receiver does it. - A test demonstrates the end-to-end property at the serve/receipt level (not only
update_atunit tests): a correctly self-signed heartbeat naming a channel the sender does not belong to does not leave that channel in the receiver's table. - Existing
chat_presencetests still pass; the signature-verification behaviour inserveis unchanged. - Distinct from id(chat-presence): heartbeat typing list is never deduplicated or bounded — one channel yields a 3.3 MB signed frame, defeating MAX_HEARTBEAT_CHANNELS #3991 (typing list dedup/bounding) and id(chat-presence): PresenceTable has no node-count bound — every distinct self-signed key is stored for a full TTL, so minted keys grow serve's memory without limit #4004 (table node-count bound) — this issue is only about the missing membership scoping on receipt.
Activity
dezren39 commentedon Nov 2, 2026
🤖 Agent note · filed
PresenceTable::update_scoped— the only receipt path that filters a heartbeat's channels to the sender's memberships, per the module doc at chat_presence.rs:24-31 — has no caller outside chat_presence.rs (caller grep run independently on the sandbox clone @97e3e8e0a and the laptop clone; only the definition, docs, and tests reference it). The production receipt path in serve (serve.rs:1615-1628) verifies the signature, then stores the heartbeat via the explicitly unscopedupdate(). Enqueued as a repo-task/v1 triage packet on queue repo-health-code-pilot.dezren39 commentedon Nov 2, 2026
🤖 Agent note · done
Verification evidence:
grep -rn "update_scoped" pkgs/id/src/returns only references inside chat_presence.rs itself (module doc :26, update() doc :226, definition :234, cross-ref :243) — no production caller exists.58 remaining items
dezren39 commentedon Nov 2, 2026
🤖 Agent note · handed off
queue/queued+status/needs-triage. Open PR: PR fix(id): scope received presence to group membership #4541 (verify and shepherd it on resume — do not re-implement).dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up
merge origin/main into queue/6231-4363-presence-scoping
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
PresenceTable::update_scoped; unreadable/unfoldable group log refuses the heartbeat. Regression testreceived_presence_keeps_only_channels_the_sender_belongs_to+ serve suite 30/30 and chat_presence 13/13 green locally; CI green (production line limit pass).