A2A TCK canonicalisation vectors: our results

Run 27 September 2026 by Council of AI (CSOAI Ltd). Code tested: councilof-ai master 720124db6bfd (2026-09-27 03:39Z).

Short answer

  • Our agent-card census verifier passes 57 of 57 vectors on the 0.x rule path and 57 of 57 on the 1.x path.
  • Beyond the corpus it diverged silently from RFC 8785 on some numbers: 1,972 of 200,230 fuzz cases. Its canonicaliser is now rfc8785.dumps.
  • No census verdict changes. In the latest run, 0 of 35 signed cards have a different signing input under strict RFC 8785.

The vectors

The corpus is the A2A TCK's proposed RFC 8785 (JCS) conformance set, a2aproject/a2a-tck #228, which was open, not merged when we ran it. Head: astrogilda/a2a-tck@97b007237ee4c3b802ed563829e3937a5529244e. MANIFEST.json sha256 d38f4121bcae85f875be5de4802fa9ca4e773aec5e3a9ae1c0134825a5779a98; corpus digest 29b3f2c5a9c2e6b07dc7e925b13c81563e2efd407a62ce05df513fca04e9361c. The runner was the PR's run_python.py from the PR, unmodified, and it wrote every record linked below.

Results by implementation

Target card-signing-input is the exact payload a JWS verifier checks. Target rfc8785 is plain canonicalisation. An outcome other than pass is one of: refused (the code rejected input it should accept, which fails closed), diverged (it produced different bytes, silently) or accepted (it accepted input it should reject).

Pass counts per implementation and target, with the kinds of failure
Implementationcard-signing-inputrfc8785Record
Census verifier, 0.x rule pathEvery published A2A agent-card census verdict comes from this code.57/57not passed: none53/53not passed: noneprobe.json
Census verifier, 1.x rule pathThe same verifier with A2A 8.4.3 defaults removed before signing.57/57not passed: none53/53not passed: noneprobe1x.json
Our card signerSigns CSOAI's own agent card.44/57not passed: 13 refused40/53not passed: 13 refusedsigner.json
Independent verifier of our cardChecks our own card; hand-written on purpose.46/57not passed: 11 refused42/53not passed: 11 refusedindep.json
TypeScript test canonicaliserA vitest over our own card.48/57not passed: 1 diverged, 5 accepted, 3 errored44/53not passed: 1 diverged, 5 accepted, 3 erroredts.json
Arena JCS (for information)Not on the agent-card path.55/57not passed: 2 diverged51/53not passed: 2 divergedarena.json
Sorted-keys JSON, UTF-8 (for information)A different format, not on the agent-card path.46/57not passed: 9 diverged, 2 accepted46/53not passed: 7 divergedsorted_utf8.json
Sorted-keys JSON, ASCII / GSPC Rule A (for information)A different format that defines published card ids; not on the agent-card path.32/57not passed: 18 diverged, 7 accepted32/53not passed: 16 diverged, 5 acceptedsorted_ascii.json
Reference: rfc8785 0.1.4The oracle check, through the same runner.57/57not passed: none53/53not passed: nonereference.json
Census verifier with rfc8785.dumps, 0.x pathThe fix landed with this page.57/57not passed: none53/53not passed: noneprobe_patched.json
Census verifier with rfc8785.dumps, 1.x pathThe fix landed with this page.57/57not passed: none53/53not passed: noneprobe_patched_1x.json

Per-vector outcomes for every implementation: table.json. The whole record, with each file's sha256: summary.json.

Failure classes, stated plainly

  • Census verifier, beyond the corpus. A differential fuzz against rfc8785 found silent divergences in three number ranges the corpus does not reach: a double with 1e-6 ≤ |x| < 1e-4 was cut to six decimals (1.5e-06 gave 0.000002, not 0.0000015); an integral double from 2^53 to 1e21 printed its exact binary value; an integer beyond 2^53 − 1 printed every digit, where RFC 8785 has no form for it. Record: fuzz_beyond_corpus.txt.
  • Our card signer and independent verifier of our card. They refuse any float by design, and the signer also refuses keys outside the Basic Multilingual Plane. Refusal fails closed and never diverges silently. Our own card carries no floats. The signer passes 44 of 57; the verifier 46 of 57.
  • TypeScript test canonicaliser. It diverges on integer-like keys and accepts lone surrogates (1 diverged, 5 accepted, 3 errored). The three errored vectors are NaN and Infinity inputs that JSON.parse rejects before canonicalisation, which also fails closed. It affects one test over our own card.
  • Sorted-keys JSON (GSPC Rule A) scores 32 of 57. That describes a different format, not a defect: its bytes define published card ids and it is not on the agent-card path, so it is not being moved to JCS.

Does any census verdict change?

No. A JWS verdict depends only on the protected header, the payload bytes and the key, so identical payload bytes mean an identical verdict. We recomputed every signed card's signing input under both rule paths, with our canonicaliser and with rfc8785.dumps, and compared the bytes.

Signing inputs that change under strict RFC 8785, per census run
Census runCards readSigned cardsSigning inputs that changeFloats in defect ranges
2026-09-25 (v0.1.1)4223300 of 220
2026-09-27 (latest)4203500 of 220

Counted from the census raw card bodies on the measuring host; the per-card file names hosts and is not published. A JWS verdict depends only on the protected header, the payload bytes and the key, so an unchanged signing input is an unchanged verdict.

What we changed

The census verifier (scripts/census/a2a-card-probe.py) now canonicalises with rfc8785.dumps, one of the two oracles that produced the vectors. An input with no canonical form raises an error, which the verifier reports as UNCHECKABLE with a reason, never as FAILED. The patched verifier passes 57 of 57 on both rule paths. New unit tests pin the three number classes above; each fails on the old code.

Not changed here, and left for a decision: the TypeScript test canonicaliser, and number formatting in our card signer and its independent verifier. Both fail closed today.

Questions and corrections

Email nicholas@csoai.org. Dated corrections go to our corrections ledger.