Skip to content

Design: one guarded door — every gossip claim enters through its table (pkgs/id presence + peers) #4502

Open
4 of 6 issues completed

Description

@dezren39

Design doc: files/designs/code-id-gossip-admission.md in the keep-the-four-repos-healthy goal workspace. Audit at origin/main 381fecb60 — every defect below re-verified present at the cited sites this pass (2026-11-02); chat_presence.rs md5 at audit HEAD matches the md5 in the origins' laptop probes.

Summary

pkgs/id's gossip plane carries two kinds of self-signed claim — presence heartbeats (chat_presence.rs, #2809) and peer announcements (crates/id-core/src/discovery.rs) — and each kind is supposed to be admitted through a table whose guards canonicalise and bound it. The discovery module states the principle outright (on PeerDiscovery::update): the freshness and ordering checks live in the table "so every caller gets them (the serve.rs receive loop needs no trust logic of its own)". Presence never got the same treatment, and even discovery has one caller that bypasses its table. Five filings are one defect with five addresses:

The contract

A gossip claim is stored only in the form its table's guards produce — canonical, scoped, and bounded — and every ingest path, constructor, receiver, and one-shot collector alike, enters through those guards. There is no second door.

Recorded decisions (full reasoning, alternatives and risks in the design doc):

  1. Lists are sets, capped by distinct count, canonicalised once at admission — one canonicalisation step shared by PresenceHeartbeat::new and PresenceTable::update_at: deduplicate first-seen, apply the membership filter (receipt path), then cap at MAX_HEARTBEAT_CHANNELS. typing is deduplicated after filtering to the stored channels, making it a true subset: typing.len() ≤ channels.len() ≤ 64 for every stored heartbeat, however it arrived. A stored heartbeat never contains duplicates in either list. (Pins the shared-helper shape neither origin fixed — two private copies of the rule are how the paths diverged.)
  2. The presence table is bounded exactly as the discovery table is — MAX_PRESENCE_NODES = 1_000, named beside MAX_HEARTBEAT_CHANNELS / PRESENCE_TTL_SECS, aligned with MAX_PEERS. At capacity a new node is admitted only by evicting an entry already past its TTL (oldest such, ties by key bytes — mirroring PeerDiscovery::update); if every entry is fresh, the new node is refused; an existing node always refreshes (subject to the ordering guard). This pins 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's open eviction choice: not refuse-all-new (dead entries would lock out legitimate nodes for a full TTL), not evict-soonest-expiring (a flood could churn live entries out early); stale-only eviction means fresh minted keys displace nothing still live.
  3. Membership scoping is wired, not documented away — id(chat-presence): receipt never scopes heartbeats to sender membership — update_scoped has no production caller, serve stores channels unscoped #4363 offers two endings (wire update_scoped, or correct the doc); this design records wire it: the doc states the intended id(chat): presence heartbeat protocol over gossip (ephemeral, TTL-based) #2809 property, the read side already implements its half, and presence becomes load-bearing the moment the chat layer names channels — wiring receipt after that leaves a window where rosters are read from an unscoped table everyone believes is scoped. The predicate is backed by the receiver's own group fold for the channel (GroupView member set — serve already folds group logs in its group-warnings path); a channel the receiver holds no fold for scopes to empty (membership the receiver cannot see cannot be confirmed). run_gossip_loop gaining the membership source/predicate as a parameter is part of the phase. Sender side unchanged — serve's own heartbeat still names no channels, which is precisely why the wiring lands now.
  4. No side doors: a collector uses the table, not a raw map — discover_via_gossip collects through a temporary PeerDiscovery and reads back its non-stale peers. id(peers): --gossip collection bypasses the announcement freshness/ordering guards — replayed, ancient and far-future announcements are reported as discovered peers #4377 leaves the shape open ("or a shared helper it uses"); this design pins through the table — re-implementing freshness/ordering in peers.rs creates the second copy whose divergence is this design's subject (the collector's hand-rolled MAX_PEERS guard at peers.rs:460–462 copied the capacity rule and missed the other two). With both sources freshness-checked, the cmd_peers merge order stops mattering for correctness.
  5. The unscoped door is dispositioned, not left ambient — PresenceTable::update remains only if the closing audit names a production caller that legitimately needs the transport view; otherwise it is removed or made crate-private in the audit phase. A public unscoped admission path beside the scoped one is how id(chat-presence): receipt never scopes heartbeats to sender membership — update_scoped has no production caller, serve stores channels unscoped #4363 happened.

Phased breakdown

Phases adopt the five filed origins as sub-issues (no parallels); Phase 5 is a new closing-audit issue, gated on Phases 1–4.

Open questions (in the design doc, not filed)

Whether decode_frame itself should refuse oversize frames or the gossip layer's bound suffices once stored form is bounded (Phase 5 dispositions the layering); the sender-side mirror rule when the chat layer starts naming channels (that layer's own design question); a hypothetical third gossip claim kind enters through this contract, no phase filed for it.

Related

Design doc (full problem/evidence/alternatives/risks): files/designs/code-id-gossip-admission.md (goal workspace). Origins: #4404, #3991, #4004, #4363, #4377. Siblings, not duplicates: #2898 / #4000 (reconstruction is admission — the journal restore gate; this is the gossip receipt gate), #2944 (discovery table authentication/bounds — its landed MAX_PEERS rule is decision 2's embodiment), #1552 (closed — the serve announcement guards #4377's collector bypasses).

Filed by the design architect (pass 39, 2026-11-02): design issue for the pkgs/id gossip-admission contract — the pass-38 standing candidate, originated on its recorded trigger (#4404, the fourth presence/receipt filing). The five origin issues are adopted as its phases — triage of each origin should record the phase structure and the decisions above rather than re-filing or re-scoping. Phase 5's closing audit is filed separately and gated on Phases 1–4.

Acceptance criteria

  • The design records, on this issue or a linked doc: the single guarded entry point every gossip claim passes through, the table it enters, and the invariants the guard enforces.
  • Migration path for existing gossip claim sites in pkgs/id presence (and any sibling callers) is enumerated.
  • A linked implementation issue (or native sub-issues per protocol section 11) exists before this design is treated as done — a design with no implementation link is not finished work.

Activity

  1. added
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Design architect, pass 39 (2026-11-02): this issue is the design of record for the pkgs/id gossip-admission contract. The five origin issues are adopted as its phases and linked as sub-issues: Phase 1 = #4404 + #3991 (canonical lists at admission), Phase 2 = #4004 (bounded table), Phase 3 = #4363 (scoped receipt in serve), Phase 4 = #4377 (peers collector through its table), Phase 5 = #4503 (closing gossip-ingest inventory + contract statement, gated on Phases 1–4, status/blocked).

    The five recorded decisions are in the issue body and the design doc (files/designs/code-id-gossip-admission.md, goal workspace). This design is the pass-38 standing candidate originated on its recorded trigger: #4404 was the fourth presence/receipt filing. Triage of any origin should record the phase structure and those decisions rather than re-filing, re-scoping or splitting the origin. Register entry added to files/potential-projects.md (status: proposed). No user/review: every origin carries explicit acceptance with verified probes, and the open rulings (shared canonicalisation, the MAX_PRESENCE_NODES eviction policy, wiring scoping over doc correction, collector-through-the-table) are the architect's recorded decisions in the doc.

  3. added
    priority/normalOrdinary priority (the default)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Nov 2, 2026
  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Intake review

    Related: Filed by the design-architect as an epic (kind/design, app/id, source/design-architect); sibling design code issue #3456 is referenced in the follow-up chain (see code issue #4457, Phase 3b follow-up of #3456).

    Notes: No priority or effort at filing; this pass sets priority/normal + effort/l (design + fleet-wide claim-path change), adds acceptance criteria and meta/enriched. Status stays needs-triage for the design review.

    Risks: Guarded-door changes touch every gossip claim path; an underspecified guard risks rejecting legitimate claims at runtime. The design should pin failure behavior (reject vs queue vs drop) per claim type.

    Suggested approach: Design review first (Muse review lane), then file the implementation issue(s) natively linked before any code work.

    — fleet monitor intake enrichment, 2026-11-02.

  5. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  6. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    Program tending — sub-issue progress (2026-11-03, via #1352)

    Closed since this thread's last activity (2026-11-03T00:13Z):

    Progress: 3 of 6 sub-issues closed (#3991, #4363, #4377). Remaining open: #4404, #4004, #4503. Recorded for the parent rollup; this is a progress note, not a completion proposal.

  7. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    Triage at current main (3e363c9), queue task 8410

    Phase status, re-verified against GitHub and main:

    Verdict: still live, but in progress. No new fix to route, because routing one now would duplicate draft PR #4571 and open PR #4160. The ordering is recorded here: Phase 1 and Phase 2 land first (via their existing PRs), then Phase 5 closes the epic. This issue stays open as the design of record. No labels added.

    Found via: coverage pass 2026-11-03

  9. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
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/lLarge: multi-day or open-ended; expect staged check-insepicMeta-issue grouping multiple items (org Project Type=Epic)kind/designA design proposal (design-architect): multi-step, phased, decision-bearingmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/design-architectFiled by the repo design architect jobtask/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