Skip to content

id(world): journal restore never validates Committed/Checkpoint event authors — inputs and chat from never-issued participants restore, replay, and project #2894

Open

Description

@dezren39

Problem

WorldCore::restore (pkgs/id/src/world/core.rs) validates a journal entry's author for some entry kinds but not for the events that carry the world's content:

  • JournalEntry::Issued refuses a parent that was never issued (core.rs:184-188), and JournalEntry::Revoked refuses an unknown or already-revoked participant (core.rs:225-230).
  • JournalEntry::Committed (core.rs:320-355) checks only the sequence (event.sequence == core.sequence + 1). It never checks event.participant_id.
  • JournalEntry::Checkpoint (core.rs:360-430) validates its events' sequences (consecutive, last equals the checkpoint sequence) but never their participant_ids either.

Restore's own contract is that a journal "is internally inconsistent (out-of-order sequences, unknown participants, malformed digests). A bad journal is refused, never partially applied" (core.rs:130-133).

Live code can only produce a Committed event from participant 0 (the host: system_chat, core.rs:578, journaled as Committed at actor.rs:706-709; ProgramInstalled, actor.rs:1283) or from a participant issued in the same journal (chat/prepare_input authorize a capability first). A Committed event naming any other participant ID — one never issued, or issued only later in the journal — is a shape no live path produces, yet restore accepts it:

  1. Ghost inputs are replayed into the program. A Committed Input from a never-issued participant is pushed onto replay (core.rs:342), and open_world (world_store.rs:380) feeds replay through the re-instantiated program — the program executes an input no participant ever sent, and the use-count accounting that live input passes (ensure_uses_left, then spend) is skipped entirely for the ghost (commit_prepared's spend walks the capability chain; an unknown ID spends nothing).
  2. Ghost chat is retained and projected to real participants. A Committed Chat from a never-issued participant is stored in the retained events and appears in snapshots / events_after projections for legitimate participants, attributed to a participant ID that is not in the snapshot's participant list.

Evidence (verified this run, main 690e4722; laptop probe at 8b89d14, core.rs md5-identical)

Probe on the laptop (~/git/fleet/probe-2026-10-30-laneB-restorepart/, a detached worktree of origin/main; integration test pkgs/id/tests/probe_restorepart.rs against the real id crate, token digests built exactly as mint builds them):

  • CONTROL: Created + Issued(1, GUEST) + Committed(seq 1, participant 1, Input) restores Ok, replay length 1.
  • CONTROL: Created + Committed(seq 1, participant 0, Chat) (host line) restores Ok.
  • CASE A: Created + Issued(1) + Committed(seq 1, participant 999, Input) — participant 999 was never issued — restores Ok; the ghost input is in replay (participant 999), and core.participant(999) is None.
  • CASE B: same journal with Chat from participant 999 restores Ok; a snapshot taken with participant 1's legitimate token lists participants [1] but its events contain the chat line attributed to participant 999.
  • CASE C: a Checkpoint whose retained events name participant 999 restores Ok.

Why it matters

Restore is the trust boundary for persisted world state (journals are copied by fork, moved between hosts, and read after crashes/torn writes — world_store.rs open_world). Every neighbouring arm of restore treats an unknown participant as a fatal inconsistency; the two arms carrying actual user content (inputs that drive the program, chat that other participants read) are the ones that skip the check. An input replayed from a nonexistent participant changes program state and published records with no accountable author and no use-limit accounting.

Acceptance criteria

  • Restore refuses a Committed event whose participant_id is neither 0 (host) nor a participant issued earlier in the same journal, with an error naming the participant and sequence, in the style of the neighbouring restore errors.
  • Restore refuses a Checkpoint whose retained events name a participant that is neither 0 nor issued before the checkpoint.
  • Legitimate journals still restore: host (participant 0) chat and ProgramInstalled events, issued participants' chat/inputs, and existing checkpoint/fork fixtures.
  • Regression tests beside the existing restore tests (cf. restore_refuses_a_delegate_outliving_its_parent): ghost input refused, ghost chat refused, ghost checkpoint event refused, host + issued controls pass.
  • Triage pins the rule for an event naming a participant issued later in the journal (the live order is always issue-then-commit, so "issued earlier" is the natural reading) and for an event by a participant revoked earlier in the journal (live chat/input is refused after revocation via authorize, but the event was legitimately committed before the revoke in a real journal — confirm restored historical events by since-revoked participants stay valid).

Dedup checked this run: open+closed search for world journal restore / unknown participant — #2890 (restore never validates Issued scopes; same function, different entry arm and different invariant, filed by Lane A this hour — cross-linked, not a duplicate), #2093/#1598 (world-doctor journal-check disagreements, different surface), #1403 (replay omits chat, opposite direction). No filing covers event-author validation in restore.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    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
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-10-30 19:50 CDT (Fri 2026-10-30)
    • Found via: filed this run
    • Task: Triage the restore event-author validation gap: WorldCore::restore Committed/Checkpoint arms never check event.participant_id against participants issued earlier in the journal (host = 0), so ghost inputs replay into the program and ghost chat projects to real participants.
    • Expectation: effort/s — triage pins the author rule (0 or issued-earlier; revoked-earlier handling), then the fix adds the two checks plus regression tests beside the existing restore tests.
    • Links: evidence in the issue body; probe ~/git/fleet/probe-2026-10-30-laneB-restorepart/ (laptop, detached worktree, core.rs md5-matched to main 690e472); related id(world): journal restore never validates Issued scopes — unnamed bits and JOIN-less capabilities restore clean, and a CHAT-/INPUT-only token can act without ever joining #2890 (Issued scopes, same function, different arm).
  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Agent note (design-architect, 2026-10-30): this issue is Phase 1 of design #2898 ("reconstruction is admission — validate authority-bearing inputs where they are rebuilt", design doc files/designs/code-id-reconstruction-validation.md in the keep-the-four-repos-healthy goal workspace), together with #2890.

    Recorded decisions for triage — do not re-file, re-scope, or split this issue; record the phase structure instead:

    • The author check lands in WorldCore::restore for both the Committed arm (before commit_prepared) and the Checkpoint arm's events[]: a non-system event's author must be issued earlier in the journal and hold the scope its kind requires (INPUT for Input, CHAT for Chat), evaluated against the restored capability record.
    • The sequence-0 / participant-0 system shape is not borrowable by Committed/checkpoint events.
    • Refusals are restore errors naming the entry (the function's existing "refused, never partially applied" contract).
    • If triage finds a legitimate historical scope/kind mismatch in the restore corpus, the issued-author half stands and the mismatch is filed as evidence — the check is not silently dropped.
  4. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Correction (design-architect, same pass): my earlier note's author rule was incomplete. The recorded rule per this issue's acceptance and design #2898 as amended: a Committed/Checkpoint event's author must be participant 0 (host) or a participant issued earlier in the same journal and not revoked before that entry; events by a participant revoked later in the journal stay valid. Host (0) chat and ProgramInstalled are legitimate controls, not exceptions to engineer around.

  5. 194 remaining items

  6. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  7. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 5, 2026
  9. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  10. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 5, 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)queue/parkedQueue task is parked (waiting on CI, time, or an event)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/parkedDeliberately paused with a recorded resume conditiontask/small-fixQueue packet kind: bounded fix with reproduction and targeted testswork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions