Skip to content

docs/spec: close Go verifier findings on Anchor Format §1 escaping rules, verifier-compatibility README, and Acta crosswalk (#536) - #537

Open
drk-ram wants to merge 3 commits into
agentrust-io:mainfrom
drk-ram:drk-ram-docs-close-go-verifier-findings-3-16-19
Open

drk-ram wants to merge 3 commits into
agentrust-io:mainfrom
drk-ram:drk-ram-docs-close-go-verifier-findings-3-16-19

Conversation

@drk-ram

@drk-ram drk-ram commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Closes the three remaining verified findings from the independent Go verifier report (#448 items 3, 16, and 19; tracked in #536):

  1. Implementation findings from the Go verifier (#444): 26 items #448 Item 19 (spec/registry-anchor-v1.md §1, Canonical claim bytes, src/agentrust_trace/sign.py, and conformance/src/trace_tests/inclusion.py): Replaces the prose summary that deferred byte-level string/number escaping details to Python's json.dumps with the five explicit, language-independent canonical JSON rules, and excludes unpaired UTF-16 surrogates (U+D800 to U+DFFF) in strings and object keys from the anchor-leaf profile:
    • Object keys sorted lexicographically by Unicode code point, recursively (UTF-16 surrogate pairs are not used for key comparison; see §0).
    • Separators "," and ":", no whitespace.
    • String escaping: " (U+0022), \ (U+005C), and the five control characters U+0008, U+0009, U+000A, U+000C, and U+000D use the two-character escapes \", \\, \b, \t, \n, \f, and \r; / (U+002F) is not escaped; all other control characters (U+0000 to U+001F) and all BMP characters in U+007F to U+D7FF and U+E000 to U+FFFF (including U+007F, which is escaped as \u007f) use lowercase four-hex-digit \uXXXX escapes; supplementary-plane characters (U+10000 to U+10FFFF) use lowercase UTF-16 surrogate pairs (a high surrogate in U+D800 to U+DBFF followed by a low surrogate in U+DC00 to U+DFFF, e.g. \ud8xx\udcxx); output is encoded as ASCII bytes.
    • Integer zero is written 0 (never -0).
    • No trailing newline.
    • Unpaired UTF-16 surrogates (U+D800 to U+DFFF) in strings or object keys are placed outside the profile and rejected by both agentrust_trace.sign.anchor_bytes (UnanchorableValue) and trace_tests.inclusion.canonical_claim_bytes (InclusionError).
  2. Implementation findings from the Go verifier (#444): 26 items #448 Item 3 (examples/verifier-compatibility/README.md): Updates the opening note, introductory paragraphs, and failure discussion so they accurately state that Obligations 2 and 3 of verification.md: format-versioning and verifier-compatibility obligations (evidence outlives verifiers) #116 (nonempty accepted_profiles declaration and reporting both profile and accepted_profiles on the verification result) are now normative in spec/trace-v0.2.md §3.3 and documented in docs/verification.md ("Step 4: Check the EAT profile"), while Obligations 1 (emitter version declarations) and 4 (downgrade disclosure) remain deferred.
  3. Implementation findings from the Go verifier (#444): 26 items #448 Item 16 (docs/crosswalks/acta-decision-receipts.md): Updates the two stale v0.1 section 3.3.2 references (lines 13 and 60) to 3.3.3 to match spec/trace-v0.2.md §3.3.3.
  4. Regression tests (tests/test_registry_anchor_canonical_rules.py): Pins all five serialization rules and unpaired-surrogate rejection across both agentrust_trace.sign.anchor_bytes and trace_tests.inclusion.canonical_claim_bytes (including key and value escaping) as well as the documentation fixes in spec/registry-anchor-v1.md, examples/verifier-compatibility/README.md, and docs/crosswalks/acta-decision-receipts.md.

Cold-reader review:

  1. The cold reader noted that writing only \ud8xx\udcxx for supplementary-plane surrogate pairs could be read literally as restricting high surrogates to U+D800–U+D8FF and low surrogates to U+DC00–U+DCFF, excluding code points such as U+10FFFF (\udbff\udfff). Updated spec/registry-anchor-v1.md §1 and tests/test_registry_anchor_canonical_rules.py to specify the full surrogate ranges (a high surrogate in U+D800 to U+DBFF followed by a low surrogate in U+DC00 to U+DFFF, e.g. \ud8xx\udcxx).
  2. The cold reader noted that the What failure is and is not section of examples/verifier-compatibility/README.md still referred to "The draft normative text for verification.md: format-versioning and verifier-compatibility obligations (evidence outlives verifiers) #116, held for a maintainer to carry". Updated that paragraph to reference verification.md: format-versioning and verifier-compatibility obligations (evidence outlives verifiers) #116 and spec/trace-v0.2.md §3.3 consistently with the updated opening section.
  3. The cold reader noted that string-escaping tests in tests/test_registry_anchor_canonical_rules.py exercised string values but not object member names for control/short/hex escapes. Added test_rule_3_escaping_applies_to_object_keys_as_well_as_values.
  4. Both the cold reader and @imran-siddique pointed out that U+007F to U+FFFF included unpaired UTF-16 surrogates (U+D800 to U+DFFF) and that anchor_bytes and canonical_claim_bytes accepted and escaped them (\ud800), whereas UTF-8 peers cannot represent unpaired surrogates. Updated spec/registry-anchor-v1.md §1 to scope BMP escapes to U+007F to U+D7FF and U+E000 to U+FFFF, put unpaired UTF-16 surrogates (U+D800 to U+DFFF) outside the profile, and rejected them in both anchor_bytes and canonical_claim_bytes with test_unpaired_surrogates_in_values_and_keys_are_rejected.

Closes #536
Refs #448

Contributor: Raghuvar Durga (@drk-ram)

Type of change

  • Editorial (typo, link fix, clarification: no normative effect)
  • Non-breaking spec change (new optional field, new platform profile, informative addition)
  • Breaking spec change (requires 14-day comment period and Project Lead sign-off)
  • Schema change
  • Example addition

Spec section

Companion specification spec/registry-anchor-v1.md §0 and §1 (Canonical claim bytes), examples/verifier-compatibility/README.md (referencing spec/trace-v0.2.md §3.3), and docs/crosswalks/acta-decision-receipts.md (referencing spec/trace-v0.2.md §3.3.3).

Tests for a fix

tests/test_registry_anchor_canonical_rules.py::test_unpaired_surrogates_in_values_and_keys_are_rejected fails with DID NOT RAISE UnanchorableValue (for anchor_bytes) and DID NOT RAISE InclusionError (for canonical_claim_bytes) when the unpaired-surrogate rejection in src/agentrust_trace/sign.py and conformance/src/trace_tests/inclusion.py is switched off (python tools/review_bar.py --base origin/main --head HEAD: 7 fail on behaviour, 0 fail on a missing name or file).

Validation commands run and results:

  • pytest tests/test_registry_anchor_canonical_rules.py tests/test_requirement_keywords_are_attributable.py tests/test_safe_integer_range.py tests/test_public_functions_raise_what_they_document.py tests/test_untrusted_input_stays_on_the_documented_error.py -q — passed (348 passed)
  • pytest conformance/tests -q — passed (778 passed, 5 xpassed)
  • python tools/review_bar.py --base origin/main --head HEAD — passed (7 fail on behaviour, 0 fail on a missing name or file)
  • ruff check tests/test_registry_anchor_canonical_rules.py src/agentrust_trace/sign.py conformance/src/trace_tests/inclusion.py — passed
  • python tools/check_dashes.py — passed (no dashes outside examples/, spec/trace-v0.1.md)
  • mypy src/agentrust_trace — passed (Success: no issues found in 15 source files)

Checklist

  • DCO sign-off on all commits (git commit -s)
  • CHANGELOG.md updated (for any normative change)
  • Breaking changes marked with <!-- CHANGED: #NNN: description --> in spec text
  • Backward compatibility statement included (for breaking changes)

…verifier-compatibility README, and Acta crosswalk (agentrust-io#536)

- State the five language-independent canonical JSON rules in
  spec/registry-anchor-v1.md §1 (recursive Unicode code-point key sorting,
  ',' and ':' separators without whitespace, exact string and surrogate-pair
  escaping rules including \u007f for U+007F, integer 0 without minus sign,
  and no trailing newline).
- Update examples/verifier-compatibility/README.md to reflect that
  Obligations 2 and 3 of agentrust-io#116 are now normative in spec/trace-v0.2.md §3.3
  and documented in docs/verification.md ("Step 4: Check the EAT profile"),
  while Obligations 1 and 4 remain deferred.
- Update the two stale v0.1 section 3.3.2 citations in
  docs/crosswalks/acta-decision-receipts.md to 3.3.3.
- Add tests/test_registry_anchor_canonical_rules.py pinning all five rules
  across agentrust_trace.sign.anchor_bytes and
  trace_tests.inclusion.canonical_claim_bytes plus the documentation fixes.

Closes agentrust-io#536
Refs agentrust-io#448

Signed-off-by: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@drk-ram
drk-ram requested review from a team, lywinged and rajnisht7 as code owners October 15, 2026 22:01
…gentrust-io#536)

The root CI workflow installs only the root agentrust-trace package and does
not place conformance/src on PYTHONPATH. Load conformance/src/trace_tests/inclusion.py
via importlib.util.spec_from_file_location so test_registry_anchor_canonical_rules.py
collects and runs in the root CI environment.

Signed-off-by: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@imran-siddique imran-siddique left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Checked 92ef2fd. The new test file passes 32 of 32 here, also on a trial merge with #533, and turning ensure_ascii off in canonical_claim_bytes fails 4 of them.

One normative gap. #536 item 4 said lone surrogates are rejected. They are not: anchor_bytes({"s": "\ud800"}) and canonical_claim_bytes both return {"s":"\ud800"}, and the new bullet folds them into U+007F to U+FFFF. A Go or Rust peer cannot hold that string, so its leaf hash differs. Please put unpaired surrogates outside the profile next to non-integer numbers, and either reject them in both serializers here or open a follow-up issue for the code.

Comment thread spec/registry-anchor-v1.md Outdated
```

Claims MUST be JSON objects. TRACE Trust Records contain only strings, integers, booleans, nulls, arrays, and objects. Claims containing non-integer numbers are outside this profile, because cross-language float serialization is not canonical, and claims containing an integer outside -9007199254740991 to 9007199254740991 are outside it for the same reason: an implementation of the four rules above in a language whose only number type is the IEEE 754 double writes one value for two distinct integers. TRACE v0.2 §3.2.2 already holds a Trust Record to that range, so a schema-valid record cannot reach this; a claim that is not a Trust Record can, and this is where it is excluded.
Claims MUST be JSON objects. TRACE Trust Records contain only strings, integers, booleans, nulls, arrays, and objects. Claims containing non-integer numbers are outside this profile, because cross-language float serialization is not canonical, and claims containing an integer outside -9007199254740991 to 9007199254740991 are outside it for the same reason: an implementation of the five rules above in a language whose only number type is the IEEE 754 double writes one value for two distinct integers. TRACE v0.2 §3.2.2 already holds a Trust Record to that range, so a schema-valid record cannot reach this; a claim that is not a Trust Record can, and this is where it is excluded.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add unpaired surrogates (U+D800 to U+DFFF not part of a pair) to the out-of-profile list here.

@pytest.mark.parametrize("serialize", SERIALIZERS)
def test_rule_4_integer_zero_is_written_without_minus_sign(serialize) -> None:
"""Integer zero (-0) is serialized as 0, never -0."""
assert serialize({"n": -0}) == b'{"n":0}'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

-0 is the int 0 before it reaches the serializer, and json.loads("-0") is 0 too, so this cannot fail. Keep the spec bullet for implementations with a signed zero; drop or relabel the test.

…serializers (agentrust-io#536)

- Exclude U+D800 to U+DFFF from the BMP escape range in
  spec/registry-anchor-v1.md §1 (scoping BMP \uXXXX escapes to U+007F to
  U+D7FF and U+E000 to U+FFFF) and state that unpaired UTF-16 surrogates in
  strings or object keys are outside the anchor-leaf profile.
- Reject unpaired UTF-16 surrogates in agentrust_trace.sign.anchor_bytes
  (raising UnanchorableValue) and trace_tests.inclusion.canonical_claim_bytes
  (raising InclusionError).
- Add test_unpaired_surrogates_in_values_and_keys_are_rejected in
  tests/test_registry_anchor_canonical_rules.py.

Signed-off-by: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@drk-ram

drk-ram commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 563a8c3:

  • Scoped the BMP \uXXXX bullet in spec/registry-anchor-v1.md §1 to U+007F to U+D7FF and U+E000 to U+FFFF, and placed unpaired UTF-16 surrogates (U+D800 to U+DFFF) in string values or object keys outside the profile alongside non-integer numbers and unsafe integers.
  • Updated src/agentrust_trace/sign.py (_reject_unanchorable) and conformance/src/trace_tests/inclusion.py (_outside_profile) to reject unpaired UTF-16 surrogates in both string values and object keys (UnanchorableValue / InclusionError).
  • Added est_unpaired_surrogates_in_values_and_keys_are_rejected in ests/test_registry_anchor_canonical_rules.py (7 fail on behaviour under ools/review_bar.py).

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Independent technical review of 563a8c3, including the surrogate refusal update. No blocking defect found in the six-file change.

Validation in a fresh Python 3.12.14 environment installed from the repository's hashed root and conformance test locks, with both packages installed editable:

  • Reference suite: 4,070 passed, 48 skipped; 97% aggregate coverage.
  • Conformance suite: 778 passed, 5 xpassed.
  • New canonical-rules module: 37 passed.
  • Ruff passes for root src/tests and the changed conformance inclusion module; mypy passes all 15 reference source files. Diff whitespace and dash checks pass. A broader root-config Ruff scan of conformance reports 45 diagnostics, all in 16 files unchanged by this PR; that broader scan is not a clean lint result.

Independent byte controls: I wrote a separate Python encoder directly from the five stated rules, without json.dumps or rfc8785 for expected bytes. Both serializers exactly match its 12,963,355-byte output for a single string containing every one of the 1,112,064 Unicode scalar values, and match additional recursive key-order, escaped-key, safe-integer-boundary, boolean/null, slash, and signature-field cases. The alphabet-output SHA-256 is b48cf55e7ca0ce46a0ea7ee015e657c15aee5c6d8f1de05908cb8a7af82a3f6f.

All 2,048 surrogate code points were independently exercised as a value, object key, and nested value against both serializers: 12,288 expected refusals, with the documented exception types. This confirms the newest fix covers the original gap on both surfaces.

Behavioral weakening checks retained symbols and valid ASCII controls: disabling each serializer's string refusal or key refusal separately produces 5 failing refusal cases and 2 passing positive controls (four mutations); disabling ensure_ascii in the conformance serializer produces 1 failing BMP-escaping case and 3 passing controls. All temporary mutations were restored and the checkout is clean.

Scope: this is source/runtime evidence from a separately written Python oracle, not an independent-language interoperability result or hardware acceptance claim. The integer-zero case is a positive serialization control; Python integer -0 is already 0 before serialization. This COMMENT records technical findings and does not replace maintainer approval or the project's normative-change process.

This branch has not been deployed

No deployments
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.

docs/spec: close Go verifier findings on Anchor Format §1 escaping rules, verifier-compatibility README, and Acta crosswalk (#448 items 3, 16, 19)

3 participants