Skip to content

fix(verifier): declarative suite rows read up to 4 MiB and truncation is judged honestly - #10

Merged
nathanclevenger merged 1 commit into
mainfrom
fix-declarative-row-body-cap
Aug 9, 2026
Merged

fix(verifier): declarative suite rows read up to 4 MiB and truncation is judged honestly#10
nathanclevenger merged 1 commit into
mainfrom
fix-declarative-row-body-cap

Conversation

@nathanclevenger

Copy link
Copy Markdown
Contributor

The bug (a real verifier false-fail)

Declarative suite rows — the card-declared suite@1 sub-run (runSuiteRows, src/discovery.ts) and pinned/verifySuite endpoint requirements (src/pinned.ts) — were fetched under the Observer's 256 KiB discovery-surface default body cap. A real API endpoint that legitimately returns hundreds of KB of VALID JSON (apis.vin /listings, ~308 KB) had its body severed mid-token, readCapped's truncation never reached the Evidence, and judgeExpect reported the lie "body is not JSON" although the target answered 200 with a perfectly valid document.

The fix

  1. SUITE_ROW_MAX_BODY_BYTES = 4_194_304 (4 MiB — the exec module-artifact ceiling, EXEC_MAX_MODULE_BYTES): the runSuiteRows child observer and the pinned-mode observer now read row responses up to 4 MiB. Rows fetch sequentially, so peak retained memory stays one body ≤ the cap. Exported from the package root alongside the other published caps.
  2. Evidence.truncated: readCapped now reports whether it cut the read (at the streaming cap, or refusing a declared-oversized Content-Length before reading), the observer records it on the Evidence, and judgeExpect fails a truncated body with the honest body too large: truncated at N bytes by the verifier read cap — never a misleading "not JSON", and never judging partial data that happens to parse. captureInto refuses to bind vars out of a cut body. The field is optional (absent, never false), so stored evidence bundles replay byte-identically.

Not more lenient — proven both directions (test/suite-row-body-cap.test.ts)

  • (a) a ~308 KB and a 1 MiB valid-JSON row response PASS their JSON expectations (card-declared path AND verifySuite path);
  • (b) a genuinely non-JSON body still FAILS body is not JSON;
  • (c) a body beyond even the raised cap still FAILS — with the honest truncation reason, not "not JSON";
  • plus units: observer records truncated on both cut paths; a truncated-but-parseable body is refused; an untruncated severed body still fails "not JSON".

Full suite: 44 files, 1267 passed / 4 skipped. tsc --noEmit and npm run build clean.

🤖 Generated with Claude Code

… is judged honestly

A real verifier bug (the apis.vin/listings false-fail): declarative suite
rows — the card-declared suite@1 sub-run (runSuiteRows) and pinned/
verifySuite endpoint requirements — were fetched under the Observer's
256 KiB discovery-surface default cap. A legitimate ~308 KB valid-JSON API
response was severed mid-token, readCapped's truncation never reached the
Evidence, and judgeExpect reported the lie "body is not JSON" although the
target answered 200 with a perfectly valid document.

Two changes, neither making the grader lenient:

- SUITE_ROW_MAX_BODY_BYTES (4_194_304 — the exec module-artifact ceiling):
  the runSuiteRows child observer and the pinned-mode observer now read row
  responses up to 4 MiB. Rows fetch sequentially, so peak retained memory
  stays one body <= the cap.
- Evidence.truncated: readCapped now reports whether it cut the read (at
  the streaming cap, or refusing a declared-oversized Content-Length), the
  observer records it, and judgeExpect fails a truncated body with the
  honest "body too large: truncated at N bytes by the verifier read cap"
  — never a misleading "not JSON", and never judging partial data that
  happens to parse. captureInto refuses to bind vars out of a cut body.

Both directions proven in test/suite-row-body-cap.test.ts: a ~308 KB and a
1 MiB valid JSON row PASS; a genuinely non-JSON body still FAILS "body is
not JSON"; a body beyond even the raised cap FAILS with the truncation
reason. The field is optional (absent, never false) so stored evidence
bundles replay byte-identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nathanclevenger
nathanclevenger merged commit c5126bc into main Aug 9, 2026
2 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8085c883a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/discovery.ts
// default; under that default a large VALID body was severed mid-token and
// misjudged "body is not JSON". Beyond even this cap, `Evidence.truncated`
// makes judgeExpect fail with the honest truncation reason.
const sub = observer.child({ allowWrites: false, budget, maxBodyBytes: SUITE_ROW_MAX_BODY_BYTES })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound aggregate suite-row evidence

When a card-declared suite uses the allowed 25 requirements and each endpoint returns close to the new 4 MiB cap, every response remains stored in sub.items and is then retained in the parent evidence bundle. Sequential fetching therefore does not keep peak retained data to one response: an untrusted target can produce roughly 100 MiB of body evidence before the bundle is digested and serialized, potentially exhausting the verifier. Add an aggregate byte budget or otherwise account for all retained row bodies.

Useful? React with 👍 / 👎.

Comment thread src/pinned.ts
// card-declared path uses (a legitimate large JSON response must not be
// severed mid-token and misjudged "not JSON"). An explicit caller cap
// still wins.
maxBodyBytes: opts.maxBodyBytes ?? SUITE_ROW_MAX_BODY_BYTES,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the raised cap scoped to pinned endpoint rows

In verifySuite/verifyPinnedSpec runs without an explicit maxBodyBytes, this same observer is passed to observeTarget at line 203 before the endpoint-requirement loop. Consequently all root variants, discovery documents, OpenAPI data, and card-directed probes now receive the 4 MiB cap, even though the change is intended only for endpoint rows; oversized discovery surfaces that were previously bounded at 256 KiB can now be retained and parsed. Use a dedicated observer or per-request cap for the endpoint requirements rather than raising the parent observer's cap.

Useful? React with 👍 / 👎.

Comment thread src/http.ts
offset += chunk.byteLength
}
return new TextDecoder('utf-8').decode(buf)
return { text: new TextDecoder('utf-8').decode(buf), truncated }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not reject responses exactly at the read cap

For a streaming response whose final chunk exactly fills maxBodyBytes, the existing value.byteLength >= remaining branch sets truncated = true even though no byte was discarded. Returning that flag now records Evidence.truncated, so a complete valid JSON body of exactly 4 MiB—or exactly 256 KiB for default observers—fails schema/path expectations as oversized, whereas it passed before this change. Distinguish an exact complete read from an actual over-cap read, using the declared length when available or checking for EOF.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant