Skip to content

id(web): world WebSocket receive() trusts frame shape past the type tag — a parseable-but-wrong-shaped snapshot/event throws out of the handler and is dropped mid-processing #2802

Open

Description

@dezren39

id(web): world WebSocket receive() trusts frame shape past the type tag — a parseable-but-wrong-shaped snapshot/event throws out of the handler and the frame is dropped mid-processing

Repo: developing-today/code · Main: c90fcbc7 · File: pkgs/id/web/src/world.ts — receive() at lines 138–225, dispatched from the socket message handler at line 316–319.

Evidence (verified this run, real module)

receive() guards only JSON.parse (lines 140–145). Every case after the frame.type switch then dereferences nested fields as if the shape were guaranteed:

  • snapshot (line 148–150): cursor = frame.snapshot.current_sequence; then for (const participant of frame.snapshot.participants). The only guard is if (!frame.snapshot) return; — a snapshot object without participants throws.
  • event (line 164–166): if (!event) return; then event.kind.kind — an event object without kind throws.
  • Secondary: cursor is assigned frame.snapshot.current_sequence and Math.max(cursor, event.sequence) with no finiteness/shape check, so a missing/non-numeric sequence poisons cursor with undefined/NaN for the rest of the session (it feeds the rejoin after: cursor).

Laptop probe (worktree of c90fcbc7, vitest + happy-dom, reusing the world-records.test.ts FakeSocket harness and the real template markup; probe retained at ~/git/fleet/probe-2026-10-30-laneA-worldframe/, world.ts md5 e24c51db39ebed2505a4bdad1df7f8c4, matching main):

  • Deliver {"type":"snapshot","snapshot":{"world_id":"lobby","current_sequence":9}} → TypeError: frame.snapshot.participants is not iterable at src/world.ts:150, thrown out of receive() (world.ts:318 dispatch). The handler aborts before the participants are rendered, before setStatus("Connected"), and before requestRecords() — zero records requests were sent.
  • Deliver {"type":"event","event":{"sequence":5,"participant_id":1}} → TypeError: Cannot read properties of undefined (reading 'kind') at src/world.ts:166.
  • Control: the well-formed snapshot from world-records.test.ts connects and starts the records fetch exactly as the existing tests expect (probe control test passed).

In a real browser the listener exception surfaces as an uncaught error and that frame's processing simply stops halfway — the user is left on a world page that never shows "Connected" and never loads records, with no error status explaining why (the parse-failure path at line 143 does set an "invalid world frame" status; the shape-failure path sets nothing).

Why it matters

World frames originate from the world host / Roc world app, not from this client — the same untrusted-shape boundary the codebase already defends elsewhere (shell-contract.ts validates shell frames field-by-field; #2738/#2800 cover the same parseable-but-wrong-shaped class in other tools). One buggy or experimental world app that emits a snapshot before its participant list exists (or an event kind variant this client predates) wedges every connected client's world page silently, instead of producing one clear "invalid frame" status. The cursor poisoning variant can additionally corrupt the rejoin position for the rest of the session.

Acceptance criteria

  • A snapshot frame whose participants is missing or not an array does not throw out of receive(); the frame is rejected (or the field defaulted) with a visible error status consistent with the existing invalid-frame path, and a subsequent well-formed snapshot still connects and starts the records fetch.
  • An event frame whose kind is missing or not an object does not throw; same rejection/status behaviour, and subsequent well-formed events still process.
  • cursor never becomes undefined/NaN from a frame: a snapshot/event with a missing or non-numeric sequence leaves the previous cursor intact.
  • Regression tests in the style of world-records.test.ts (FakeSocket harness) pin all three cases plus the well-formed control; existing web tests still pass (bunx vitest run in pkgs/id/web).
  • Well-formed frame handling is byte-for-byte unchanged (snapshot/event/view/records/invite/error paths).

Dedup notes: searched open + closed issues this run for world frame / snapshot / participants / receive — no hit. Distinct from #2747/#2748 (aggregator path-segment decoding) and from the wrong-shape filings in other tools (#2738 t3code seed settings, #2800 sync_research_workflow); this is the id world client's own frame boundary. The validation shape (per-case guards vs a frame-shape check at the top of receive()) is triage's pin — not self-solved.

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 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator-code-a
    • Session / ID: cron job new-issue-creator-code-a, run 2026-10-30 14:05 CDT
    • Found via: filed this run
    • Task: Filed this issue: world WebSocket receive() (pkgs/id/web/src/world.ts:138-225) guards only JSON.parse, then dereferences nested frame shape — a snapshot without participants throws TypeError at world.ts:150 and aborts before "Connected"/records fetch; an event without kind throws at world.ts:166; a missing sequence poisons the rejoin cursor. Verified with the real module on the laptop (vitest FakeSocket harness from world-records.test.ts; control snapshot passed).
    • Expectation: effort/s — done when wrong-shaped snapshot/event frames are rejected with a visible status instead of throwing out of the handler, the cursor can never be poisoned by a missing sequence, and FakeSocket regression tests pin all three cases. Validation shape is triage's pin.
    • Links: probe (laptop) ~/git/fleet/probe-2026-10-30-laneA-worldframe/ · evidence body in goal hidden_files/issue-code-worldframe-shape-body.md
  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Design-architect pass 26 (2026-10-31) — agent note: this issue is adopted as part of Phase 3 of design #3068 ("parsed is not validated"), alongside the new closing-audit issue #3069. Triage: do NOT re-file, re-scope or split it — record the phase structure instead. Unlike the audit issue, this frame work is NOT gated on #3068's Phases 1–2. The design records the rule this phase lands under: frames are shape-checked past the type tag at the boundary; a wrong-shaped frame is dropped and logged, never thrown out of the handler, never half-applied, never silently initialised from. Details on #3068 and in files/designs/code-parsed-is-not-validated.md (goal workspace).

  4. added
    work/assignedQueue worker lifecycle state
    and removed on Nov 1, 2026
  5. 13 remaining items

  6. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  8. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
  9. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as small_fix at priority 50 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

  10. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 4, 2026
  11. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: picked_up
    task: 7964 (repo-health-code-pilot; worker repo-worker-code; OpenChamber scheduled lane c8b9355e-319a-4640-8096-7ad19480d151)
    state: claimed; first checkpoint records origin=openchamber-scheduled-lane.
    verified: #2802 is OPEN; PR #4236 is MERGED by dezren39, and its merge commit is present on current origin/main. This is already implemented, so no worktree, branch, source, or PR was changed.
    next: record the already-done outcome with the merged-PR evidence.

  12. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: done
    task: 7964; outcome: already_done.
    verified: PR #4236 is MERGED at ee5f0d4, and that merge is an ancestor of current origin/main. The merged patch changed pkgs/id/web/src/world.ts and world-records.test.ts; the prior task 5423 recorded the targeted FakeSocket tests. No new code, branch, worktree, or PR was created.
    disposition: queue task complete with evidence. #2802 remains OPEN pending its issue owner closing it; queue/status/work labels were cleared.

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)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/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