Repository navigation
Description
🤖 Design-architect pass 26 (2026-10-31) — design issue. Full design doc: files/designs/code-parsed-is-not-validated.md in the keep-the-four-repos-healthy goal workspace. Audit at origin/main 60f68d2d.
Summary
Eleven filings in two days are one defect: code parses a JSON (or MessagePack) value and then trusts its shape. The parse is guarded — malformed input correctly gets a 400 / clean exit — but the parsed value's shape is never checked before the first dereference. Three failure costumes:
- Crash — raw
TypeError/AttributeErrornaming neither input nor key: sevenbin/*-proxy.mjsproxies exit the process on anullbody (bin proxies: a JSON null body crashes seven chat proxies — parse succeeds, first deref throws, process exits 1 (clef and opencode-route already 400) #2816); sync-models blocks all providers on a wrong-shapedopencode.jsonc(sync-models: parseable-but-wrong-shaped opencode.jsonc crashes raw — non-object provider/models values throw unnamed TypeErrors in run() and in the JSONC tree editor #2832); the t3code seed script dies on a wrong-shapedsettings.json, disabling all seeding including the OpenCode-1.x safety disable (t3code-seed-providers: seed script crashes on parseable-but-wrong-shaped settings.json — one bad shape disables all seeding and the OpenCode-1.x safety disable #2738); sync_research_workflow raw traceback (sync_research_workflow: parseable-but-wrong-shaped manifest/profile JSON crashes with a raw traceback (or a misleading 'missing directory: .agents/skills/r') instead of the tool's clean diagnostic #2800); cold-store-reaper's D1 error extractor crashes on the error body it exists to explain (cold-store-reaper: D1 response handling assumes object shapes throughout — array/string bodies and non-object errors/result entries raise AttributeError instead of ColdStoreError #2618); id web frame handlers throw out ofonmessage/receive()(id(web): collab handleMessage trusts MessagePack frame shape — a null frame throws out of onmessage, and a field-less INIT silently initializes an empty editor at an undefined version #2824, 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). - Fabrication — nothing throws, so defaults manufacture a result: an array body passes clef-proxy's
typeofguard and fabricates an upstream Clef call (clef-proxy: a JSON array body passes the #2816 'oracle' guard and fabricates an upstream Clef call — typeof [] is 'object', so [] and [1,2] run with the default model and empty state/questions instead of a 400 #2938); scalar/array bodies to antigravity-direct fabricate an upstream request for a model named-mediumand return 200 (antigravity-direct: a non-object JSON body is never a 400 — 'null' 500s at body.model, and scalar/array bodies fabricate an empty upstream request for model '-medium' and return 200 #2821); provider-catalog-sync's(data.data ?? [])reads a missingdatafield as an empty catalogue and flags every model as vanishing (provider-catalog-sync: enumerateProxy trusts the /v1/models shape — a missing data field silently flags every model as a live vanishing, and an id-less entry crashes --apply #2827); a field-less collab INIT silently initialises an empty editor (id(web): collab handleMessage trusts MessagePack frame shape — a null frame throws out of onmessage, and a field-less INIT silently initializes an empty editor at an undefined version #2824). - Coerced garbage stored — projector
POST /linkschecks onlysubject/ref, storesString(relation)("a,b","[object Object]"as durable rows), throws uncaught on non-stringnote/createdBy, stores an emptysubject(projector: POST /links validates only subject/ref — a non-string note/createdBy throws out of the handler, relation is String()-coerced and stored as 'a,b' / '[object Object]', and an empty subject is stored #2974).
Why a design, and why now: pass 24 assessed the proxy slice as "already converging on its own oracle pattern." That is superseded by its own evidence — the designated oracle is wrong: #2816 names clef-proxy as already-correct and exempts it from behaviour change, but clef's bare typeof guard is exactly what #2938 proves admits arrays. A family whose reference implementation is incorrect is cementing the wrong guard into seven more files, and it has grown past HTTP bodies into files, responses and frames, where no oracle exists.
The rule (recorded here; full text, alternatives and risks in the design doc):
Parsed is not validated. Immediately after any parse — request body, config/manifest file, upstream response, peer frame — validate the shape the code is about to rely on, at the boundary, before the first dereference and before any side effect. The validated shape is exactly the shape dereferenced: plain object where fields are read, array where elements are iterated, the type of each field read, bound or stored. Coercion (
String(x),?? [],|| {}) is not validation.
Corollaries: (1) the vocabulary is exact — plain object means the bin/opencode-route-proxy.mjs:659 idiom (typeof + non-null + not-array; isinstance(v, dict) in Python), which also checks the messages entries it derefs at :713 — the one correct in-tree embodiment; (2) failure is the surface's own error — JSON 400 before any upstream call/store write (HTTP), the tool's named diagnostic naming file + key with no partial work (CLI), the module's own error type with the real message (response readers), drop-and-log (frames) — never a raw throw, a fabrication, or a process exit; (3) absence is not emptiness — a missing relied-on field is a shape failure, not an empty collection; (4) one shared probe matrix everywhere: null, array, scalar, {}, each relied-on field wrong-typed.
Boundaries: #2576 governs aggregator document contents, #3005 measurement-string grammar, #2835 env-string grammar — none states what a parsed value must look like before it is dereferenced, and none of their scopes names these surfaces. This design composes with them, it does not reopen them.
Phased breakdown (phases adopt the origins — no parallels)
- Phase 1 — HTTP request bodies. clef-proxy: a JSON array body passes the #2816 'oracle' guard and fabricates an upstream Clef call — typeof [] is 'object', so [] and [1,2] run with the default model and empty state/questions instead of a 400 #2938 (fix the oracle first: clef's guard becomes the exact plain-object vocabulary) + bin proxies: a JSON null body crashes seven chat proxies — parse succeeds, first deref throws, process exits 1 (clef and opencode-route already 400) #2816 (seven crashing proxies adopt the corrected guard; its "no change for clef" bullet is superseded by clef-proxy: a JSON array body passes the #2816 'oracle' guard and fabricates an upstream Clef call — typeof [] is 'object', so [] and [1,2] run with the default model and empty state/questions instead of a 400 #2938) + antigravity-direct: a non-object JSON body is never a 400 — 'null' 500s at body.model, and scalar/array bodies fabricate an empty upstream request for model '-medium' and return 200 #2821 (antigravity-direct: 400, no fabricated upstream request) + projector: POST /links validates only subject/ref — a non-string note/createdBy throws out of the handler, relation is String()-coerced and stored as 'a,b' / '[object Object]', and an empty subject is stored #2974 (projector
/links: every stored field type-checked, noString()coercion, JSON error shape). - Phase 2 — parsed files & upstream responses in tools. sync-models: parseable-but-wrong-shaped opencode.jsonc crashes raw — non-object provider/models values throw unnamed TypeErrors in run() and in the JSONC tree editor #2832 (sync-models) + t3code-seed-providers: seed script crashes on parseable-but-wrong-shaped settings.json — one bad shape disables all seeding and the OpenCode-1.x safety disable #2738 (t3code seed) + sync_research_workflow: parseable-but-wrong-shaped manifest/profile JSON crashes with a raw traceback (or a misleading 'missing directory: .agents/skills/r') instead of the tool's clean diagnostic #2800 (sync_research_workflow) + provider-catalog-sync: enumerateProxy trusts the /v1/models shape — a missing data field silently flags every model as a live vanishing, and an id-less entry crashes --apply #2827 (provider-catalog-sync) + cold-store-reaper: D1 response handling assumes object shapes throughout — array/string bodies and non-object errors/result entries raise AttributeError instead of ColdStoreError #2618 (cold-store-reaper D1). Independent of Phase 1.
- Phase 3 — frames + closing audit + contract statement. id(web): collab handleMessage trusts MessagePack frame shape — a null frame throws out of onmessage, and a field-less INIT silently initializes an empty editor at an undefined version #2824 + 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 (id web frames: shape-checked past the type tag, dropped-and-logged, never half-applied) plus a new closing-audit issue (gated on Phases 1–2): inventory every parse-then-deref boundary in
bin/,scripts/,pkgs/tools, pm-stack non-aggregator handlers and id web, disposition each, explicitly disposition cloudflare-export: GraphQL errors assumed to be an array of {message: string} — other shapes throw TypeErrors misclassified as fetch-failed, and a single-object errors is silently ignored #2608 (cloudflare-export GraphQLerrorsshape) against design(aggregator): ingest, fail-closed — one validation contract for every document the PM stack consumes #2576's Phase 3 as-built scope so it is decided once, and write the contract where this repo states such contracts.
Open questions stay in the design doc (per-entry skip vs whole-job refusal per Phase 2 tool; outbound frame shape; a mechanical net after the inventory; Go surfaces out of scope — typed unmarshal fails closed and no origin is a Go surface).
Acceptance
- Phase 1 origins (clef-proxy: a JSON array body passes the #2816 'oracle' guard and fabricates an upstream Clef call — typeof [] is 'object', so [] and [1,2] run with the default model and empty state/questions instead of a 400 #2938, bin proxies: a JSON null body crashes seven chat proxies — parse succeeds, first deref throws, process exits 1 (clef and opencode-route already 400) #2816, antigravity-direct: a non-object JSON body is never a 400 — 'null' 500s at body.model, and scalar/array bodies fabricate an empty upstream request for model '-medium' and return 200 #2821, projector: POST /links validates only subject/ref — a non-string note/createdBy throws out of the handler, relation is String()-coerced and stored as 'a,b' / '[object Object]', and an empty subject is stored #2974) closed under the recorded rule, with the probe matrix as tests per handler
- Phase 2 origins (sync-models: parseable-but-wrong-shaped opencode.jsonc crashes raw — non-object provider/models values throw unnamed TypeErrors in run() and in the JSONC tree editor #2832, t3code-seed-providers: seed script crashes on parseable-but-wrong-shaped settings.json — one bad shape disables all seeding and the OpenCode-1.x safety disable #2738, sync_research_workflow: parseable-but-wrong-shaped manifest/profile JSON crashes with a raw traceback (or a misleading 'missing directory: .agents/skills/r') instead of the tool's clean diagnostic #2800, provider-catalog-sync: enumerateProxy trusts the /v1/models shape — a missing data field silently flags every model as a live vanishing, and an id-less entry crashes --apply #2827, cold-store-reaper: D1 response handling assumes object shapes throughout — array/string bodies and non-object errors/result entries raise AttributeError instead of ColdStoreError #2618) closed under the recorded rule
- Phase 3: id(web): collab handleMessage trusts MessagePack frame shape — a null frame throws out of onmessage, and a field-less INIT silently initializes an empty editor at an undefined version #2824/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 closed; the closing audit issue closed with the full inventory disposition (incl. cloudflare-export: GraphQL errors assumed to be an array of {message: string} — other shapes throw TypeErrors misclassified as fetch-failed, and a single-object errors is silently ignored #2608's ownership decided once) and the contract statement landed
- Register row for this design in
files/potential-projects.mdkept truthful as phases land
Activity
12 remaining items
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 4, 2026
🤖 Agent note · picked up
02cafe76-eafd-4d0b-aaa6-2b7a737d68dcdezren39 commentedon Nov 4, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 4, 2026
🤖 Agent note · handed off
opencode; handed off to the laptop claimant (opencode-claimant), which launches the bound opencode session to drive task 8009 to its terminal state.dezren39 commentedon Nov 4, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 4, 2026
🤖 Agent note · triaged
origin/main(1a8e6e7a22d9). The design remains live; no direct PR or superseding design was found.status/waiting-dependency, and its prior triage confirms the defect is still live but owned by this design (no separate fix task). Phase 3: id(web): collab handleMessage trusts MessagePack frame shape — a null frame throws out of onmessage, and a field-less INIT silently initializes an empty editor at an undefined version #2824 is CLOSED; 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's implementation landed in merged PR fix(id-web): validate world snapshot and event frames #4236 (mergeee5f0d4, ancestor of currentorigin/main), though 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 remains OPEN pending its issue owner closing it.