Skip to content

fix(intent-bridge): preserve canonicalization error classification - #318

Open
altrudev wants to merge 2 commits into
agentrust-io:mainfrom
altrudev:fix/intent-bridge-transcript-json-equality
Open

fix(intent-bridge): preserve canonicalization error classification#318
altrudev wants to merge 2 commits into
agentrust-io:mainfrom
altrudev:fix/intent-bridge-transcript-json-equality

Conversation

@altrudev

@altrudev altrudev commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #317 / #333.

What

#333 correctly moved the required transcript-call comparison off Python container equality, but it wraps an RFC 8785 canonicalization failure from transcript.before.tool_call back into AuthorizationMismatch.

That classification is inconsistent with this module's own contracts:

  • IntentBridgeError: malformed, untrusted, stale, or cannot be evaluated.
  • AuthorizationMismatch: execution evidence does not match the signed authorization.

A transcript call with no RFC 8785 canonical form cannot be evaluated; it has not been shown to mismatch.

This PR removes that wrap and lets the existing _jcs() boundary raise IntentBridgeError, while preserving the explicit object-type guard so a non-object transcript call remains AuthorizationMismatch.

The comparison is performed directly over canonical bytes with compare_digest(_jcs(a), _jcs(b)). The signed tool_call_digest semantics are unchanged.

Regression coverage

Existing coverage from #333 continues to pin:

  • integer/boolean substitutions as AuthorizationMismatch;
  • identical canonical calls as accepted;
  • non-object transcript calls as AuthorizationMismatch.

This PR adds explicit cases for transcript calls containing values the pinned canonicalizer cannot represent (2**60 and NaN) and requires:

  • IntentBridgeError;
  • specifically, not AuthorizationMismatch;
  • the diagnostic to identify transcript.before.tool_call.

Scope

No schema, wire-format, authorization, scope, digest, or signature semantics change.

This is only an exception-classification correction at the canonicalization boundary, plus the direct canonical-byte comparison already discussed on this PR.

The branch is rebuilt directly on current main after #325 and #333. No CHANGELOG.md hunk, per maintainer request.

AI-assistance disclosure: ChatGPT assisted with repository triage, diff reconstruction, test drafting, and branch cleanup. altrudev reviewed the bounded claim and remains responsible for the contribution.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

🔴 Contributor Check: HIGH

Check Result
Profile HIGH
Credential LOW
Overall HIGH

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:HIGH Contributor check flagged HIGH risk label Sep 10, 2026

@lywinged lywinged left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is an approval waiting on one addition. Reviewed at 63ae280 on a247244. It is posted as a comment rather than an approval because the PR is a draft and the addition below will move the head, and the maintainer-approval check only counts an approval on the commit it was given on; I will approve the head that carries the entry once you mark it ready.

The defect is real and the fix is the right shape. On main, a signed and digested call of {"approved": 1} with a transcript before.tool_call of {"approved": true} passes the transcript check; on this head it is refused. It is the identity relation the #300 review asked for in the tool-schema check, applied here where the transcript is compared: the signature covers RFC 8785 bytes, so comparing the two calls as _jcs bytes puts the transcript under the same relation as the digest it sits beside.

What I measured rather than read:

  • Five lines in and one out in intent_bridge.py, reusing the module's own _jcs wrapper and the compare_digest it already imports.
  • The five new tests pass. With the old comparison restored, four fail and the unchanged-call control passes.
  • The four CI steps with the pinned install, on 3.11 and on 3.12: green, 1407 passed and 1 skipped.
  • The new canonicalisation step cannot leak. A transcript call carrying 2^60 or NaN raises IntentBridgeError from _jcs, because IntegerDomainError and FloatDomainError subclass CanonicalizationError in the pinned rfc8785 0.1.4.

One thing the body does not say, and the entry below should. "The existing AuthorizationMismatch result and diagnostic are preserved" holds for a call that differs. For a transcript call JCS cannot serialise, the exception moves: main raised AuthorizationMismatch from the inequality, this head raises IntentBridgeError from _jcs before the comparison is reached. Same documented base type, and the treatment the authorization, the declaration and the executed call already get, so nothing to change in the code.

The one thing to add: a CHANGELOG.md entry under Unreleased, Fixed. Both code fixes already there carry one, and it is the record a reader of the next release will have. The shape the others use: what was accepted, what is now refused, that the signed digest was never affected, and the exception note above.

@imran-siddique

Copy link
Copy Markdown
Member

Second conflict in an hour, same file, and this one is mine again: merging #322 re-conflicted #318 and #323 on the CHANGELOG.md block. You rebased the first round inside twenty minutes and I am not going to ask you to do that again for the same reason.

Please drop the CHANGELOG.md hunk from #318 and #323 entirely. Nothing else changes. Force-push without it and both become mergeable immediately and stop being able to conflict each other or anything else in the queue.

I will write the changelog entries for all of them in one maintainer commit once they land, using the wording from your PR bodies. You lose nothing: the entries still say what your changes did, and the attribution is in the commits and the PRs.

Why this rather than another rebase. Every open PR in this repository appends to the same block, so each merge conflicts every other one, and the number of rebases grows with the size of the queue rather than with the size of anyone's change. Two of your PRs have now paid for that twice. The durable fix is a fragment directory, one file per change assembled at release, and I am looking at it separately.

intent_bridge.py in #318 and provenance.py in #323 both merge clean against current main. The changelog is the only thing in the way of either.

Qiang-Xu
Qiang-Xu previously approved these changes Sep 11, 2026

@Qiang-Xu Qiang-Xu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good!

@imran-siddique

Copy link
Copy Markdown
Member

You opened this three minutes after filing #317 and it says "Closes #317". I wrote the same fix from scratch today and merged it as #333 without checking whether one was already open. That is my error, not a race: this PR has been sitting here since the 10th, and I had its number in front of me.

On the difference between the two, you are right and I was wrong. Your changelog entry argues that a transcript call the pinned canonicalizer has no form for should raise IntentBridgeError from the canonicalization boundary, matching how authorization, declaration and executed-call canonicalization failures are already classified. I did the opposite and wrapped it back to AuthorizationMismatch, on the grounds that your own issue text asked to keep the existing result class.

The class docstrings settle it against me. IntentBridgeError is "malformed, untrusted, stale, or cannot be evaluated"; AuthorizationMismatch is "Execution evidence does not match the signed authorization". A call with no canonical form cannot be evaluated, and it does not fail to match. My wrap makes transcript.before.tool_call the one place in the module where an unrepresentable value reads as a mismatch, while the executed call six lines above raises IntentBridgeError for the identical input.

So there is still unique content here and I would rather it landed as yours. The whole of it is now that classification: drop the try/except IntentBridgeError wrap and let _jcs raise, keeping the isinstance guard in front so a non-object stays a mismatch. If you would rather not rebase this again, say so and I will land it citing this PR, but the change is yours either way.

Your byte comparison is also the better construction. compare_digest(_jcs(a), _jcs(b)) over the canonical bytes needs no hash at all, where mine hashes the transcript call to reuse the digest already computed for tool_call_digest. That one I would call a wash and not worth a second PR.

@altrudev altrudev closed this Sep 12, 2026
@altrudev
altrudev force-pushed the fix/intent-bridge-transcript-json-equality branch from 8bd9bb6 to 6737005 Compare September 12, 2026 21:21
Signed-off-by: altrudev <266135212+altrudev@users.noreply.github.com>
Signed-off-by: altrudev <266135212+altrudev@users.noreply.github.com>
@altrudev altrudev reopened this Sep 12, 2026
@altrudev altrudev changed the title fix(intent-bridge): compare transcript call by canonical JSON fix(intent-bridge): preserve canonicalization error classification Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Rebuilt cleanly on current main after #325/#333. The PR now contains only the remaining classification correction plus its regression coverage: non-object transcript calls stay AuthorizationMismatch, while RFC 8785-unrepresentable transcript calls propagate IntentBridgeError from _jcs. No CHANGELOG.md hunk, per the maintainer request. Fresh CI and CodeQL are both green on 7cefdbf.

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

Labels

needs-review:HIGH Contributor check flagged HIGH risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants