Repository navigation
Description
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) →Okwith 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 nonext_afterkey 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, anext_afterthat is present but not a string and not JSONnull(number, boolean, array, object) is a malformed-reply error, not end-of-pages; a missingnext_afterkey is likewise an error (the conforming sender always emits the key). Onlynullterminates paging. - Regression tests drive
admin_event_historyagainst stub/rogue replies covering: numeric cursor on the first page (error, not a 1-recordOk), boolean cursor on a later page (error), missing cursor key (error), plus the conforming control — string cursors thennull— still returningOkwith 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
developing-today commentedon Nov 6, 2026
CODE-2011
dezren39 commentedon Nov 6, 2026
🤖 Agent note · filed
admin_event_historyin pkgs/id/src/world_net.rs treats a non-string or missingnext_afteras end-of-pages, silently truncating the admin export (and fixtures recorded from it) whilecompletestays true. Verified this run on the laptop @ origin/main 9123103 with an in-crate rogue-host probe (~/git/fleet/probe-admin-history-wt): conforming control Ok (2 records);next_after:123on page 1 → Ok with 1 record;next_after:trueand a missing key on page 2 → Ok. Sibling of issue id(world_net): record_pages still loops forever on empty pages with an advancing cursor — the #6505 RecordsBudget checks records, not pages, and a non-string next_after silently ends the read #6578 (therecord_pageshalf of the same.as_str()conflation).probe_admin_history_malformed_cursor_silently_truncates, 1 passed); body file issue-id-admin-history-cursor-type.md in the goal hidden_files.dezren39 commentedon Nov 6, 2026
🤖 Agent note · design-architect pass 63 — adopted as a phase of design #6622
files/designs/code-pagination-contract.mdin the keep-the-four-repos-healthy goal workspace. Audit at origin/main075b2de90null, and absence only where the sender may omit the key) ends a read. This extends Design: validate it where you read it — one validated-and-bounded contract for the fleet proxy's frame and peer values #6582's present-but-invalid rule to page cursorsdezren39 commentedon Nov 7, 2026
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 whilerepo-worker-code-kposts 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).
18 remaining items
dezren39 commentedon Nov 7, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 7, 2026
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.
dezren39 commentedon Nov 7, 2026
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.
fix(id/world_net): refuse a missing or mistyped history records cursor
Merge pull request #6730 from developing-today/queue/6600-history-cursor
dezren39 commentedon Nov 7, 2026
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.)
dezren39 commentedon Nov 7, 2026
🤖 Agent note · done