Skip to content

id(chat-presence): receipt never scopes heartbeats to sender membership — update_scoped has no production caller, serve stores channels unscoped #4363

Closed

Description

@dezren39

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 — see update_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 inside chat_presence.rs itself (the definition, its doc comments, and its tests). No production caller exists.
  • The sender side shows the wiring is half-built: serve.rs:1591-1599 broadcasts 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

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b, Lane B)
    • Session / ID: cron new-issue-creator-code-b run 2026-11-02 06:50 CST
    • Found via: filed this run
    • Task: Filed after verifying on current main (97e3e8e): 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 unscoped update(). Enqueued as a repo-task/v1 triage packet on queue repo-health-code-pilot.
    • Expectation: effort/s — triage confirms the missing wiring, then either serve's receipt path scopes via a group-fold membership predicate (with a receipt-level test: a self-signed heartbeat naming a non-member channel leaves no such channel in the table), or the module doc is corrected to stop claiming the guarantee.
    • Links: issue body (evidence), packet creator-b-2026-11-02-0650/packets/packet-4363-triage.json
  3. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 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.
    • The serve receipt path at main 5375358 still runs decode_frame → heartbeat.verify() → recv_presence.update(heartbeat): signature verification only, no membership scoping, exactly as filed.
    • chat_presence.rs:21-31 documents the scoping as a present-tense property of the system, so the documented guarantee is currently false; exposure is latent (built-in sender names no channels, no production roster readers yet), matching the issue's own assessment.
  4. added
    status/triagedTriaged and routed (see agent note)
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  5. 58 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor
    • Session / ID: repo-fleet-monitor run 2026-11-02 16:12 CST; queue task 6231
    • Found via: monitor reconciliation
    • Task: Claim/label divergence auto-repair (Duty 1 addendum): queue task 6231 is queued with no live lease after attempt(s) reached its cap (checkpoint stage: handed_off), but this issue still carried claimed/assigned labels. The claim evaporated — the task is queued and unclaimed. Labels reset to the truth: 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).
    • Expectation: operator repair — task 6231 is dead-in-queue (attempts at max); raise its cap or enqueue a fresh packet so a lane can resume from checkpoint stage handed_off.
  7. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: queue worker (repo-worker-code), OpenChamber scheduled lane
    • Session / ID: scheduled run · task 6231 · lane bf419306-c4ef-470f-a553-beabf93239de
    • Result: fixed and merged. PR fix(id): scope received presence to group membership #4541 merged as bfd8050: serve receipt path verifies the heartbeat signature (unchanged) then stores only channels in the senders receiver-folded group membership via PresenceTable::update_scoped; unreadable/unfoldable group log refuses the heartbeat. Regression test received_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).
    • Open thread: Codex P2 suggests caching the group projection outside the gossip receive hot path (perf, not correctness) — left for a follow-up.
    • Links: PR fix(id): scope received presence to group membership #4541 · merge bfd8050
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/triagedTriaged and routed (see agent note)task/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions