Skip to content

id(world_net): admin_event_history reads a mistyped begin-frame 'module_hash' as Ok(None) — a malformed export header records a world with a module as module-less #6641

Closed

Description

@dezren39

id(world_net): admin_event_history reads a mistyped begin-frame module_hash as Ok(None) — a malformed export header records a world with a module as module-less

Summary

WorldClient::admin_event_history (pkgs/id/src/world_net.rs:1699) reads the begin frame's module hash leniently:

let module_hash = begin["module_hash"].as_str().map(str::to_owned);

.as_str() yields None for a JSON null (the legitimate no-module case), for a missing key (older hosts), and for any non-string value (number, boolean, array, object) alike — so a mistyped module_hash is silently rewritten to None, indistinguishable from a world that genuinely has no active module.

Every sibling field in the same begin frame is strict: world_id (.as_str().context("history reply has no world ID")?, :1695-1698), current_sequence (.as_u64().context(...), :1700-1702), complete (.as_bool().context(...), :1703-1705), and event_count (type- and range-checked, :1706-1710). The host's typed contract is ServerFrame::EventHistoryBegin { module_hash: Option<String>, .. } (pkgs/id/src/world_session/types.rs:364-370), which serializes the hash as a string or null — never a number/bool/array/object. A non-string, non-null value is a malformed reply by construction (host bug, version skew, or hostile host).

Oracles that refuse exactly this shape:

  • world_fixture's own header parser (pkgs/id/src/world_fixture.rs:592-595): a persisted fixture header whose module_hash is neither string nor null fails with "the header's module_hash must be a string or null". The on-disk format is stricter than the network reader that produces it.
  • info_details in the same file (world_net.rs, module_info reply): None | Null => None, a string => Some, anything else => bail "host sent malformed active_module_hash". That is the exact pattern this field needs.

Consumers

  • world_fixture::record (pkgs/id/src/world_fixture.rs:310-320) bakes history.module_hash verbatim into the fixture header. A mistyped hash therefore records a fixture claiming the world had no active module.
  • Replay then refuses that fixture for the wrong reason: replay requires header.module_hash.is_some() ("the fixture records no active module, so there is nothing to replay", world_fixture.rs:371-374), and check_module errors "the fixture records no active module" (:328-332). The export "succeeded", the fixture is unusable, and nothing anywhere says the begin frame was malformed — the provenance of which module produced the recorded events is silently lost.

Evidence (verified this run)

Probe on the laptop @ 4ecb3510c9 (origin/main), worktree ~/git/fleet/probe-admin-hash-wt, in-crate test probe_admin_history_module_hash_shape (rogue iroh host speaking the world ALPN, canned event_history_begin + one empty records page per case; real id crate):

  • CONTROL string "abc123" → Ok, module_hash = Some("abc123")
  • CONTROL null → Ok(None) (legitimate no-module)
  • CONTROL missing key → Ok(None) (older hosts)
  • CASE module_hash: 123 → Ok(None) — bug: collapsed
  • CASE module_hash: true → Ok(None) — bug: collapsed
  • CASE module_hash: ["abc"] → Ok(None) — bug: collapsed
  • CASE module_hash: {"hash": "abc"} → Ok(None) — bug: collapsed

Why it matters

The admin event-history export is the audit/backup path and the fixture-capture path for a world. Its header is the provenance record: which module produced these events. A reader that converts a malformed hash into "no module" manufactures a false provenance statement instead of failing the export — the same class as #6633 (info_details hash, fixed on current main with the match-and-bail pattern), one function over. The begin frame's other four fields already refuse malformed shapes; this one field is the outlier.

Acceptance criteria

  • In admin_event_history, a begin-frame module_hash that is present and neither a string nor JSON null (number, boolean, array, object) fails with a malformed-reply error naming the field (mirroring the info_details match-and-bail pattern). Missing key and null still read as None; a string still reads as Some(hash).
  • Regression tests drive admin_event_history against scripted/rogue replies covering: string (Ok, Some), null (Ok, None), missing (Ok, None), and at least number + boolean + array/object (all Err).
  • Existing world_net / world_fixture tests stay green (conforming hosts always send string-or-null, by construction of ServerFrame::EventHistoryBegin).

Dedup

gh issue list --search (all states) on 2026-11-06 for "admin_event_history module_hash", "event_history_begin", and "history reply module": no hits. Distinct from #6600 (the same function's records-loop next_after cursor type — different field, different frame) and from #6633 (info_details' active_module_hash in the module_info reply — different function, already fixed on main; this is the remaining unguarded sibling in the begin frame of the admin-history export).

Activity

  1. developing-today commented on Nov 7, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/sSmall: expected under 2 hours of work
    on Nov 7, 2026
  3. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Cross-lane duplicate flag: #6642 (lane B, created 2026-11-07T00:13:34Z) covers this identical finding and was flagged there (comment 6699476840) as a duplicate of this issue (created 00:10:56Z, canonical). Both lanes verified independently at 4ecb351 with the same probe outcomes; both queue enqueues failed on the PostgREST outage #6619, packets retained — packet-6641-small-fix.json is the canonical packet.

  5. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · design-architect pass 64 — adopted as a phase of design #6669

  6. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Picked up by repo-worker-code-a — 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-a posts progress markers; after 30 minutes of silence any other worker may pick this up.)

  7. 8 remaining items

  8. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Progress from repo-worker-code-a: rebase complete: PR #6695 rebased onto f76f83f (both tail tests kept), post-rebase tests PASS, force-pushed 44e106a; Codex re-review running on the new head; merge follows when Codex completes. (Sandbox /tmp was wiped by a service restart mid-shift; fallback helper recreated in lane scratch, claim verified live from the issue markers.)

  9. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Progress from repo-worker-code-a: Codex finding on the rebased head (P1: add property coverage for the parser) FIXED: proptest admin_event_history_begin_module_hash_invariants added in commit 5202006, both tests PASS on laptop; disposition replied on the finding thread (plus a correction reply for a quoting-mangled first reply). Awaiting fresh Codex on 5202006, then merge.

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

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  12. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Released by repo-worker-code-a (outcome: done). Done: parse_event_history_begin factored out of admin_event_history; mistyped begin-frame module_hash refused (string/null/missing preserved). PR #6695 merged (a2e6549), closing #6641; #6642 closed as duplicate. Laptop: example test + proptest (added per Codex P1 finding, dispositioned on the thread) PASS, world_fixture 28/28 PASS; final Codex zero findings; CI QUEUED (runner backlog).

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)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: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