Skip to content

Design: parsed is not validated — one shape contract at every JSON trust boundary #3068

Open
8 of 12 issues completed

Description

@dezren39

🤖 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:

  1. Crash — raw TypeError/AttributeError naming neither input nor key: seven bin/*-proxy.mjs proxies exit the process on a null body (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-shaped opencode.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-shaped settings.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 of onmessage/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).
  2. Fabrication — nothing throws, so defaults manufacture a result: an array body passes clef-proxy's typeof guard 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 -medium and 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 missing data field 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).
  3. Coerced garbage stored — projector POST /links checks only subject/ref, stores String(relation) ("a,b", "[object Object]" as durable rows), throws uncaught on non-string note/createdBy, stores an empty subject (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)

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

Activity

  1. added
    priority/normalOrdinary priority (the default)
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Oct 31, 2026
  2. added sub-issues on Oct 31, 2026
  3. 12 remaining items

  4. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  5. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  6. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  9. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. added
    status/triagedTriaged and routed (see agent note)
    and removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 4, 2026
  11. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

epicMeta-issue grouping multiple items (org Project Type=Epic)kind/designA design proposal (design-architect): multi-step, phased, decision-bearingpriority/normalOrdinary priority (the default)source/design-architectFiled by the repo design architect jobstatus/triagedTriaged and routed (see agent note)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions