JCS JSON is the canonical form, over a constrained value space
Status: accepted (2026-08-10) · lands in AILAB-619
Records hash under RFC 8785 JCS, and stay JSONL on disk. SPEC.md additionally constrains the value space so that JCS’s hard cases are unreachable: no floating-point values anywhere, digests as fixed-length lowercase hex, integers as u64 below 2^53, and absent fields omitted rather than null.
The decision turns on one property of ADR-0001: because request_digest is SHA-256 over raw request bytes and payloads live in the Envelope, arbitrary user data never enters the canonical form. The canonicalizer only ever sees Aegis’s own field set. JCS’s genuinely difficult parts — ES6 number formatting, float edge cases, unnormalized Unicode in attacker-chosen keys — all live in user data, and there is none.
Considered options
Deterministic CBOR (RFC 8949 §4.2) is the stronger canonical form in the abstract — fewer footguns, native byte strings, no hex-encoding decision to get wrong. It was rejected on the D4 exit criterion: “a stranger with the README, a laptop, and fifteen minutes can… verify the audit chain themselves.” A binary evidence artifact cannot be cat’d, grep’d, or eyeballed, and the surrounding ecosystem — the shipped JSONL audit file, MCP’s JSON-RPC — is JSON throughout. Adoption cost matters more than encoding elegance when the format is the durable asset.
An explicit length-prefixed field transcript (the Certificate Transparency approach) is arguably the easiest to reimplement correctly — no library in any language, zero number ambiguity. Rejected as bespoke: an RFC is easier to hand a vendor than a hand-rolled encoding, and every added field would need a spec revision naming its byte position.
Consequences
- The value-space constraints are normative, not stylistic. They are what make JCS safe here, so SPEC.md states them as requirements and the record types enforce them by construction — which the digest newtypes from ADR-0001 already do for the hash fields.
- Omit, never null. Already how
wall_msandpeak_memory_bytesserialize (core/src/audit.rs:73,77). A canonical form cannot leave absent-vs-null to the emitter. - The
u64 < 2^53bound is for cross-language verifiers, not for Rust. A JavaScript verifier readingseqas aNumberis the realistic third-party implementation, and silently losing precision above 2^53 would break verification in a way nobody would attribute to the format. - Storage shape is unchanged. JSONL with one record per line, as shipped. JCS defines the hash input, not the file.
- Test vectors must include the canonicalization step, not just the final hash — otherwise an implementer who canonicalizes wrong sees only “hash mismatch” with no way to locate the error.