Note
Written by claude-opus-5-5 via Claude Code on behalf of Matt.
This was generated by AI during triage.
Triage: bug, evidence-affecting, ready for an agent. Reproduced on current main.
The checking script shipped inside every evidence package passes a package that the app fails as unreadable. Rewriting a record's number as 01 instead of 1 gave a full pass from the script and an "Invalid JSON" failure from the app, both for an ordinary Case and for a member's record in a Shared Case. No value changes, so the edit hides nothing, but the two checkers disagree on the same package.
Leading zeros are one case of a wider gap. The script reads each record with a tool that accepts several number spellings the app rejects, and a second record on the same line. Scope is widened to that whole set, at the maintainer's direction during triage. The brief below is the contract for the work.
Agent brief
Agent Brief
Category: bug
Summary: verify.sh must reject, as Invalid JSON, every signed chain line that the package verifier's JSON.parse rejects and jq accepts, so the two checkers never disagree on whether a line is readable.
Current behavior:
The shipped verify.sh decides whether a signed chain line is valid JSON by running it through jq empty, then reads its fields through jq. The package verifier parses the same line with JSON.parse and reports Invalid JSON when that throws. jq is more lenient than JSON.parse, and recomputes each Entry Hash from its own normalized reading, so a line spelled the lenient way keeps a matching hash and passes every script step. The script already closes two lenient cases by hand, a byte-order mark at the start of a line (bom_led) and a NUL byte (nul_line), and has no check for the cases below.
Spellings jq 1.8.2 accepts and JSON.parse rejects (checked during triage):
| Line content | jq reads it as |
|---|---|
01, 00, -01, 01.5 (leading zero) | 1, 0, -1, 1.5 |
+1 (leading plus) | 1 |
1., .5, 1.e5 (bare decimal point) | 1, 0.5, 1E+5 |
nan, NaN, -nan | null |
Infinity, infinity, -Infinity | ±1.7976931348623157e+308 |
{"a":1}{"b":2} (two values on one line) | two values |
Desired behavior:
For every line verify.sh reads as a Manifest Entry, in the Case manifest and in every member chain of a Shared Case package, the script fails that line as Invalid JSON whenever JSON.parse would throw on it for any reason in the table above. It reports the line where it reports a byte-order mark or NUL byte today, with Invalid JSON in the finding. A line that JSON.parse accepts is never newly rejected: strings that contain these spellings as text (for example a URL or title holding 01 or NaN) still pass. The check must not depend on jq's own number parsing, which has differed between jq releases.
Key interfaces:
- The shipped script is generated as a template string,
VERIFY_SCRIPT. The new check sits besidebom_ledandnul_lineand runs wherever they run: the Case manifest's step 4 loop andcheck_chainfor member chains (step 4a). - The package verifier's verdict is the reference: the chain check that calls
JSON.parseand returnsbroken(i, 'Invalid JSON'). It does not change. - The script stays POSIX
shplus the tools it already requires (jq,openssl, the sha256 tool). No new required tool.
Acceptance criteria:
- In the conformance table of the test that runs both shipped verifiers against one real package, a row for each spelling in the table above, applied to a Case manifest line (with the unsigned index rehashed), shows the package verifier and
verify.shboth failing, withverify.shprintingInvalid JSONat step 4. - A Shared Case export test rewrites a member chain line's
indexas01, andverify.shreportsFAIL [step 4a] <chain> (member <code>): entry 1: Invalid JSON, matching the package verifier'schain-broken ... Invalid JSON. - At least one other spelling from the table (
NaNor a second value on the line) is covered on the member chain path too. - A line whose string values contain
01,+1,.5,NaNandInfinityas text still passes both checkers. - Every existing
verify.sh, runbook and Shared Case export test passes unmodified, and a package from an unmodified Case still readsPASS.
Verification path:
pnpm test tests/main/services/verifyRunbookExecution.test.tspnpm test tests/main/services/sharedCaseExport.test.tspnpm preflight, which must be green at the PR head. These tests needjqandopenssl. CI requiresjq, so the rows cannot be skipped there.
Starting points (as of main 4f2b4387, may move):
src/main/services/verifyScript.ts:bom_ledandnul_line,check_chain, and the step 4 manifest loop.src/shared/verify/manifestChain.ts: theJSON.parsethe verdict must match.tests/main/services/verifyRunbookExecution.test.ts: the conformance table, whose NUL-byte and byte-order-mark rows are the pattern to follow.tests/main/services/sharedCaseExport.test.ts: the member-chain byte-order-mark test is the pattern for the Shared Case row.
Evidence-affecting: yes. verifyScript.ts is on the blocking tier of the evidence path list, and the change alters a verification verdict.
Out of scope:
- Number spellings both checkers accept but write back differently, such as
1E+5or-0, wherejqkeeps the literal and canonical JSON does not. If one of them changes a recomputed Entry Hash, record it in the PR's findings and do not fix it here. - JSON files the script reads other than chain lines (
export-entry.json,evidence.json). If one shows the same disagreement, record it in the PR's findings. - Any change to the package verifier, the runbook prose beyond what names the new check, or the
JSON.parsereading rule.