Repository navigation
id(replication): ReplicationTracker::accept never checks the payload's own journal header — a snapshot with no header, a foreign world's header, a version-99 header or a duplicate header is verified and advances the cursor, though WorldCore::restore refuses every one #4486
Description
Summary
ReplicationTracker::accept in pkgs/id/src/world/replication.rs (F12.13 replication contract, issue #806) verifies that every payload line decodes as some JournalEntry — but it never checks the payload's own journal header against the frame or the world. A correctly signed frame from the pinned source is accepted, and the tracker's verified cursor advances, for payloads WorldCore::restore refuses wholesale:
- Snapshot with no header at all — payload is a single
CapsGrantedentry. Restore: "world journal does not start with a header" (world/core.rs:221). - Snapshot whose
Createdentry names a different world — the envelope'sworld_idmatches the tracker, the payload's header does not. Restore: "journal belongs to world …" (world/core.rs:210-215). - Snapshot whose
Createdentry claims version 99 while the envelope'sheader_versionis 1 — the envelope field passes the<= MAX_JOURNAL_VERSIONcheck (replication.rs:626), the entry's own version is never read. Restore accepts only1..=3(world/core.rs:217). - Snapshot with a duplicate header — two
Createdentries in one payload. Restore: "duplicate world journal header" (world/core.rs:232). - Tail carrying a
Createdentry — appending it to an already-headed journal is the same duplicate-header refusal at restore.
The only payload-shape check in the chain path is decodability: chain_payload (replication.rs:367, decode at :384) maps each line through serde_json::from_slice::<JournalEntry> and otherwise treats its bytes as chain arithmetic. Its own comment states the intended guarantee — "refuse it here so a verified cursor never covers data the restore path cannot read" — and PR #4447 added the decode check for exactly that reason. The header rules are the part of restore's contract that check does not cover: restore's first-entry and no-second-header rules (core.rs:210-232) have no counterpart in accept's Snapshot branch (replication.rs:651-666) or Tail branch (:667-686), and the payload header's world_id/version are never compared with envelope.world_id / envelope.header_version.
Evidence (verified this run, laptop probe)
Probe: ~/git/fleet/probe-2026-11-02-laneA-replpayload/ on the laptop — replication.rs copied verbatim from current main 5c49f4807 (md5 f807510cedc0884c513f5a49ea81bfe6, matched against the sandbox clone), with only a minimal JournalEntry stub (the three variants the module's own tests use, same serde tagging) around it. cargo test: 7/7 probe tests pass, plus the module's own 10 tests (probe-output.txt).
probe1_snapshot_without_any_header_entry_is_accepted— signed Snapshot, payload[CapsGranted]only,header_version=1:acceptreturnsOk, tracker cursor advances to ordinal 1.probe2_snapshot_header_entry_version_99_is_accepted— signed Snapshot, payload[Created{world, version: 99}, CapsGranted], envelopeheader_version=1:Ok, cursor ordinal 2.probe3_snapshot_header_entry_for_other_world_is_accepted— signed Snapshot, payload[Created{"someone-elses-world", 1}, CapsGranted], envelope world matches tracker:Ok, cursor ordinal 2.probe4_snapshot_with_duplicate_header_entry_is_accepted— payload[Created, Created, CapsGranted]:Ok, cursor ordinal 3.probe5_tail_carrying_a_header_entry_is_accepted— after a good snapshot (cursor ordinal 2), a signed Tail whose payload is[Created{world, 1}]:Ok, cursor ordinal 3.- Controls pass: the well-formed snapshot (
[Created{world, 1}, CapsGranted]) is accepted, and a hand-crafted signed frame with an undecodable entry line is refused (MalformedPayloadfrom the chain step) — so the gap is exactly the header semantics, not the decode or signature machinery.
The signing key is the pinned source, so this is not a forgery scenario: it is a writer bug or a confused peer producing frames the contract says the receiver verifies — and the receiver's verified state then describes a journal no binary can restore.
Why it matters
The tracker is the trust boundary for replication (issue #806): later phases build the writer feed, the durable receiver, and spectator serving on the guarantee that accepted frames form a restorable journal. Today that guarantee holds only for line decodability. A snapshot that rebases the chain onto a headerless / foreign-world / version-99 / double-headed journal poisons the replica at the exact moment it is supposed to replace state wholesale, and the failure only surfaces later — at restore — far from the frame that caused it, with the verified cursor already advanced past it.
Acceptance criteria
-
ReplicationTracker::acceptrefuses a Snapshot whose payload does not begin with exactly oneJournalEntry::Createdheader whoseworld_idequals the envelope'sworld_idand whoseversionequals the envelope'sheader_version(which is itself in1..=MAX_JOURNAL_VERSIONper issue id(replication): ReplicationTracker accepts journal header_version 0 — accept checks only the upper bound, so a signed Snapshot rebases a v1-v3 chain onto a version WorldCore::restore refuses (1..=3) #4471), with a namedReplicationErrorvariant; tracker state unchanged on refusal. - A Snapshot or Tail payload containing a
Createdentry anywhere other than a Snapshot's first entry is refused (duplicate-header rule, mirroringcore.rs:232); a Tail payload never carries a header. - Regression tests in
replication.rsfor all five probe cases above (no header, foreign-world header, entry version ≠ envelope header version, duplicate header in a snapshot, header in a tail), each asserting refusal and unchanged cursor/header version; the existing snapshot/tail round-trip and rebase tests still pass. -
cargo test -p id --lib world::replicationpasses.
Distinct from: issue #4471 (the envelope header_version field's missing lower bound — this issue is the payload's own header entry, which is never checked at all; the two fixes are complementary and both belong in accept), issue #4454/#4455 (references.rs), and the PR #4447 fixes already on main (trailing frame bytes, undecodable entries).
Activity
dezren39 commentedon Nov 2, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 2, 2026
🤖 Agent note · handed off
ses_e5e627ddf8affeNP51UaEuysabrepo-worker-code(the bound worker will use this id for all queue helper verbs)opencode-route/workhorse, agentbuilddezren39 commentedon Nov 2, 2026
🤖 Agent note · triage-verified
repo-worker-code(task 6398, sessionses_e5e627ddf8affeNP51UaEuysab)pkgs/id/src/world/replication.rsmd5f807510cedc0884c513f5a49ea81bfe6, identical to the packet revision —chain_payload(367-389) still checks only per-lineJournalEntrydecodability;acceptSnapshot (651-666) and Tail (667-685) branches never inspect payload entries for a header, its world, or its version.pkgs/id/src/world/core.rsrestore(199-232) still requires first entryCreatedwith matchingworld_idand version in1..=MAX_JOURNAL_VERSION, and bails on any secondCreated.~/git/fleet/probe-2026-11-02-laneA-replpayload/just now: 7/7 pass (5/5 header-invalid payloads accepted + cursors advance, good-snapshot control accepted, undecodable-entry control refused); probe lib 10/10 pass.held,user/owned,do-not-merge,do-not-auto-editabsent); PR feat(id): F12.13 replication wire contract + crypto primitives (#806 phase 1) #4447 MERGED (no active owner); no open PR touches replication for this work; id(replication): ReplicationTracker accepts journal header_version 0 — accept checks only the upper bound, so a signed Snapshot rebases a v1-v3 chain onto a version WorldCore::restore refuses (1..=3) #4471 is complementary (envelope lower bound), id: F12.13 journal replication (read replicas over a signed stream) #806 is the parent epic — neither supersedes.accept, refuse a Snapshot whose payload does not begin with exactly oneCreatedheader matching envelopeworld_idandheader_version, and refuse anyCreatedentry elsewhere in a Snapshot/Tail payload; tracker state unchanged on refusal. Add regression tests for the 5 probe cases alongside the existing snapshot/tail round-trip + rebase tests;cargo test -p id --lib world::replication(run frompkgs/idascargo test --lib world::replication) must pass. Out of scope: envelopeheader_versionlower bound (id(replication): ReplicationTracker accepts journal header_version 0 — accept checks only the upper bound, so a signed Snapshot rebases a v1-v3 chain onto a version WorldCore::restore refuses (1..=3) #4471), references.rs (id(lib): references.rs does not compile — LocalIndex derives Default over a StoreId with no Default impl, and from_markdown breaks with a borrow of a loop-local String (PR #2756) #4454/id(references): resolve ignores the cleared flag — a tombstoned target still resolves, auto-picks via insert_unambiguous, and makes its live namesake contested #4455).triaged(issue stays OPEN for the fix task).dezren39 commentedon Nov 2, 2026
🤖 Agent note · triaged
repo-worker-code(task 6398, sessionses_e5e627ddf8affeNP51UaEuysab)Fix #4486: validate journal headers in replication payloads. Scope is the exact Snapshot/TailCreatedvalidation and five regressions; envelope lower-bound issue id(replication): ReplicationTracker accepts journal header_version 0 — accept checks only the upper bound, so a signed Snapshot rebases a v1-v3 chain onto a version WorldCore::restore refuses (1..=3) #4471 and references.rs issues id(lib): references.rs does not compile — LocalIndex derives Default over a StoreId with no Default impl, and from_markdown breaks with a borrow of a loop-local String (PR #2756) #4454/id(references): resolve ignores the cleared flag — a tombstoned target still resolves, auto-picks via insert_unambiguous, and makes its live namesake contested #4455 stay out of scope.cargo test --lib world::replicationwas not independently completed in this session: the direct package test timed out, and the laptop SSH route was unavailable becauseHTTPS_PROXYis unset. The fix task retains the package test as an acceptance gate.2 remaining items
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
Merge pull request #4550 from developing-today/queue/6439-4486-journa…
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done
ReplicationTracker::acceptnow refuses (newReplicationError::InvalidJournalHeader, state unchanged) a Snapshot without exactly one leading Created header matching the envelope world_id/header_version, any later Created in a Snapshot, and any Created in a Tail past the journal start.