Skip to content

id(world_net): admin_event_history treats a non-string or missing next_after as end-of-pages — the admin export, and fixtures recorded from it, silently truncate while complete stays true #6600

Closed

Description

@dezren39

Problem

In pkgs/id/src/world_net.rs, WorldClient::admin_event_history (the admin export behind world_fixture::record) validates a string next_after cursor strictly — it must equal the page's last key (:1740-1748) and advance past the previous cursor — but it never checks the cursor's type:

match frame["next_after"].as_str() {
    Some(next) => { /* equality + advance checks */ }
    None => break,
}

.as_str() yields None for a JSON null (the conforming terminator), for a missing next_after key, and for any non-string value (number, boolean, array, object) alike — and every one of those breaks the loop and returns Ok(history). A malformed cursor is indistinguishable from a complete export.

The conforming host can only ever send a string or null: the sender, event_history_admin_session in src/world_session/mod.rs, sends ServerFrame::Records { next_after: Option<String> }, which serializes the key always, as a string or null. A missing or non-string cursor is therefore a malformed reply by construction — a host bug, a version-skewed host, or a hostile host — never a legitimate end of pages.

Evidence (verified this run, laptop @ origin/main 9123103)

Probe: in-crate test in a laptop worktree (~/git/fleet/probe-admin-history-wt, test probe_admin_history_malformed_cursor_silently_truncates, rogue iroh host speaking the world ALPN, event_count: 0, complete: true):

  • Control, conforming: page 1 {a:1} → next_after:"a", page 2 {b:2} → next_after:null → Ok, 2 records. ✅
  • Defect N: page 1 {a:1} → next_after:123 (number) → Ok with 1 record — the export silently stops after the first page; any later pages are never read, no error.
  • Defect B: page 2 {b:2} → next_after:true (boolean) → Ok, 2 records — the malformed cursor accepted as the terminator.
  • Defect M: page 2 {b:2} with no next_after key at all → Ok, 2 records — a missing cursor accepted the same way.

Sibling strictness in the same function shows the intended posture: current_sequence is extracted with .as_u64().context("history reply has no sequence")?, complete with .as_bool().context(...), and event_count is type- and range-checked before it is trusted. The records cursor is the one field read leniently. Note this is the mirror of the record_pages half of #6578 (same .as_str() conflation, different loop): #6578's fix scope explicitly excluded this path as "already strict" — it is, for string values; the type check is what is missing here.

Why it matters

This export is how a world's history is captured for fixtures: world_fixture::record (src/world_fixture.rs:310) embeds history.records directly into the fixture header and history.complete alongside it. A silently truncated records page stream produces a fixture that reports complete: true while its records map is missing every record after the malformed page — replay and verification then run against a world state that was never the world's, with no error anywhere. Admin exports are also the audit/backup path for a world; "the export succeeded" must mean the export is whole.

Acceptance criteria

  • In admin_event_history's records loop, a next_after that is present but not a string and not JSON null (number, boolean, array, object) is a malformed-reply error, not end-of-pages; a missing next_after key is likewise an error (the conforming sender always emits the key). Only null terminates paging.
  • Regression tests drive admin_event_history against stub/rogue replies covering: numeric cursor on the first page (error, not a 1-record Ok), boolean cursor on a later page (error), missing cursor key (error), plus the conforming control — string cursors then null — still returning Ok with all records.
  • Existing world_net / world_session / world_fixture tests stay green (conforming hosts always send string-or-null, by construction).

Distinct from #6578 (the record_pages loop: empty advancing pages plus the same cursor-type conflation in that loop) and from #6505 (caps/ordering/non-advancing cursors in record_pages): this is the admin-history export loop in the same file, whose string-cursor checks are strict but whose cursor-type check is absent — and whose caller records the result into fixtures as complete.

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)
    on Nov 6, 2026
  2. developing-today commented on Nov 6, 2026

    @developing-today
    Contributor
  3. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · design-architect pass 63 — adopted as a phase of design #6622

  5. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Picked up by repo-worker-code-k — working this now. (Fleet queue fallback mode: the queue backend is down, so this issue's comments carry the claim. The claim stays live while repo-worker-code-k posts progress markers; after 30 minutes of silence any other worker may pick this up.)

    Lane K triage: verifying the admin_event_history non-string/missing next_after truncation claim against current main (world_net.rs).

  6. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 7, 2026
  7. 18 remaining items

  8. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Heartbeat: implementation in progress — fix drafted in laptop worktree ~/git/fleet/6600 (branch queue/6600-history-cursor): parse_admin_history_cursor factored out of admin_event_history (the #6641 pattern), bailing on a missing or non-string next_after; only an explicit JSON null ends the export. Example test + proptest added mirroring the module_hash coverage. Environment note: the laptop's cargo registry copy of cc-1.8.0 was corrupt (missing src/target/*.rs, E0583 on any fresh build); repaired by deleting the extracted copy so cargo re-extracts from the cached .crate. Targeted test run is compiling now.

  10. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Heartbeat: targeted_tests_passed — cargo test --lib world_net on the laptop: 39/39 pass, including the 12 admin_event_history-filtered tests (new example test + proptest for parse_admin_history_cursor, begin-frame parser coverage, rogue-host records tests, budget tests). Next: commit, PR, Codex check, merge if clean.

  11. added a commit that references this issue on Nov 7, 2026
  12. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Released by repo-worker-code-p (fallback mode): issue #6600 fixed and merged via PR #6730 (#6730, merge da971f0) — admin_event_history now parses the records cursor through parse_admin_history_cursor: explicit null terminates, a string continues, a missing key or any non-string type bails as a malformed reply instead of silently truncating the export. Verified: cargo test --lib world_net 39/39 on the laptop (new example test + proptest included), Codex review on PR #6730 completed with no findings. Issue auto-closed by the PR's Fixes line. (Resumed from lane code-k's CONFIRMED triage, released free earlier this evening.)

  13. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/idPackage/app: id (pkgs/ or apps/ in this repo)kind/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)status:donetask/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