Repository navigation
Description
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;thenfor (const participant of frame.snapshot.participants). The only guard isif (!frame.snapshot) return;— a snapshot object withoutparticipantsthrows.event(line 164–166):if (!event) return;thenevent.kind.kind— an event object withoutkindthrows.- Secondary:
cursoris assignedframe.snapshot.current_sequenceandMath.max(cursor, event.sequence)with no finiteness/shape check, so a missing/non-numeric sequence poisonscursorwithundefined/NaNfor the rest of the session (it feeds the rejoinafter: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 atsrc/world.ts:150, thrown out ofreceive()(world.ts:318 dispatch). The handler aborts before the participants are rendered, beforesetStatus("Connected"), and beforerequestRecords()— zero records requests were sent. - Deliver
{"type":"event","event":{"sequence":5,"participant_id":1}}→ TypeError: Cannot read properties of undefined (reading 'kind') atsrc/world.ts:166. - Control: the well-formed snapshot from
world-records.test.tsconnects 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
participantsis missing or not an array does not throw out ofreceive(); 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
kindis missing or not an object does not throw; same rejection/status behaviour, and subsequent well-formed events still process. -
cursornever becomesundefined/NaNfrom 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 runinpkgs/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
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
receive()(pkgs/id/web/src/world.ts:138-225) guards only JSON.parse, then dereferences nested frame shape — a snapshot withoutparticipantsthrows TypeError at world.ts:150 and aborts before "Connected"/records fetch; an event withoutkindthrows 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).dezren39 commentedon Oct 31, 2026
🤖 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).
13 remaining items
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done
bun run test -- src/world-records.test.tspassed (5 tests). Full package Vitest has 4 failures reproduced on clean main; typecheck has the same pre-existing error on clean main. CI is pending.developing-today commentedon Nov 3, 2026
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
dezren39 commentedon Nov 4, 2026
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.
dezren39 commentedon Nov 4, 2026
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.