Skip to content

id(peers): --gossip collection bypasses the announcement freshness/ordering guards — replayed, ancient and far-future announcements are reported as discovered peers #4377

Closed

Description

@dezren39

Problem

id peers --gossip collects peer announcements through a path that never applies the freshness or ordering guards the rest of the system relies on — so a replayed, ancient, far-future, or out-of-order announcement is reported as a discovered peer, and can displace a fresher result for the same node.

Evidence (main 15af52b2d)

  • pkgs/id/src/commands/peers.rs:373 discover_via_gossip collects into a raw map at peers.rs:442:
    let mut peers: HashMap<EndpointId, PeerAnnouncement> = HashMap::new();
    and inserts every postcard-decodable announcement unconditionally at peers.rs:449:
    peers.insert(announcement.node_id, announcement);
    timestamp_secs is never read in the function. This is one of only two PeerAnnouncement ingest sites in pkgs/id/src (grep-verified this run); the other, commands/serve.rs:1633, goes through the guarded table.
  • The guarded path it bypasses: PeerDiscovery::update (pkgs/id/crates/id-core/src/discovery.rs:456-472) refuses announcements stamped more than STALE_THRESHOLD (120 s) in the past or the future, and drops an announcement older than the stored one for the same node. Its doc comment states why the guard lives in the table: "so every caller gets them (the serve.rs receive loop needs no trust logic of its own)" — this caller does not use the table at all.
  • The merge in cmd_peers compounds it: gossip results are inserted into all_peers at peers.rs:165-176 after the RPC results (peers.rs:150), so a stale gossip announcement also overwrites a fresher RPC/ListPeers entry for the same node in the printed output.

Verification (this run, laptop probe)

Probe ~/git/fleet/probe-2026-11-02-laneA-peersgossip/ (laptop; single-file rustc, replicating the exact unconditional-insert collection loop — discover_via_gossip itself needs a live gossip topic, so the merge it performs is what was exercised):

Distinct from #1552 (closed: it fixed PeerDiscovery::update, the serve path — this CLI collection path was not covered) and from #2944 (open: the serve peer table's authentication/bounds — this is the one-shot CLI collector, which has no table and needs only the freshness/ordering checks, not authentication).

Why it matters

id peers is the diagnostic an operator runs to answer "who is out there". A departed peer replayed (or simply slow-gossiped) inside the collection window is printed as discovered with its stale name/blob count/timestamp, and a fresher RPC answer for the same node is silently replaced by the older gossip copy — the exact failure #1552 closed for serve, still live in the CLI.

Acceptance criteria

  • discover_via_gossip (or a shared helper it uses) refuses announcements stamped more than STALE_THRESHOLD from the local wall clock, in either direction — the same bound PeerDiscovery::update applies.
  • Within the collection window, an announcement older than the already-collected one for the same node does not replace it (equal timestamps may keep last-arrival-wins, matching the table's tie rule).
  • A stale gossip announcement cannot overwrite a fresher RPC result when cmd_peers merges the two sources (or the merge is shown to be safe because both sources are now freshness-checked).
  • Unit tests cover: 1970-stamped announcement excluded, u64::MAX-stamped announcement excluded, older-after-newer does not regress, fresh in-order announcements still collected.
  • Existing peers/discovery tests pass.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    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 (new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-11-02 07:35 CST
    • Found via: filed this run
    • Task: Filed issue id(peers): --gossip collection bypasses the announcement freshness/ordering guards — replayed, ancient and far-future announcements are reported as discovered peers #4377 — id peers --gossip collection (peers.rs discover_via_gossip) inserts every decodable announcement into a raw HashMap with no freshness/ordering check, bypassing PeerDiscovery::update's guards; verified with a laptop probe of the exact collection loop (ghost ts=1 collected, older replay overwrites newer, u64::MAX collected; controls pass).
    • Expectation: effort/s — triage confirm on current main, then route the fix (share PeerDiscovery's bound/ordering or route collection through it; tests per the issue's acceptance criteria)
    • Links: probe (laptop) ~/git/fleet/probe-2026-11-02-laneA-peersgossip/; queue packet enqueued this run (repo-health-code-pilot)
  3. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Design architect, pass 39 (2026-11-02): this issue is adopted as Phase 4 of design #4502 (one guarded door — every gossip claim enters through its table) and linked as a sub-issue of #4502. Design doc: files/designs/code-id-gossip-admission.md (goal workspace).

    Recorded decision pinning the shape this issue's acceptance left open ("discover_via_gossip (or a shared helper it uses)"): collect through the table — discover_via_gossip collects into a temporary PeerDiscovery and reads back its non-stale peers; do not re-implement the freshness/ordering checks in peers.rs. The collector's existing hand-rolled MAX_PEERS guard (which copied the capacity rule and missed freshness/ordering) is the proof a second copy diverges. With both merge sources freshness-checked, the cmd_peers merge order stops mattering for correctness. Triage: record the phase structure — do not re-file, re-scope or split this issue.

  4. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  5. 49 remaining items

  6. added and removed
    work/assignedQueue worker lifecycle state
    on Nov 3, 2026
  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor
    • Session / ID: repo-fleet-monitor run 2026-11-02 20:12 CST
    • Found via: monitor reconciliation
    • Task: Claim-evaporated repair: task 6444 is queued unclaimed (attempts 2/2, checkpoint stage handed_off) while this issue still carried claimed labels. Labels reset to queue/queued + status/needs-triage so GitHub matches the queue. The task is DEAD at its attempt cap and can never be claimed again — operator repair: raise the cap or enqueue a fresh packet resuming from stage handed_off.
    • Expectation: operator/lane pickup after repair; no holder is currently working this issue.
  8. 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
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  11. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    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/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 jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions