Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions src/agentrust_trace/intent_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,20 +240,17 @@ def verify_bridge(
if not isinstance(transcript, dict) or set(transcript) != {"before", "after"}:
raise AuthorizationMismatch("a full before/after transcript is required")
before = transcript.get("before")
if not isinstance(before, dict) or not isinstance(before.get("tool_call"), dict):
before_call = before.get("tool_call") if isinstance(before, dict) else None
if not isinstance(before_call, dict):
raise AuthorizationMismatch("transcript.before.tool_call does not match execution")
# Host-language equality is not this bridge's identity relation. Python holds
# True == 1 and False == 0, nested objects included, so comparing the two call
# objects with != accepts a transcript whose call has different JCS bytes from
# the one the authorization digested (#317). Every other comparison in this
# function is over canonical bytes; so is this one.
try:
before_digest = digest_jcs(before["tool_call"])
except IntentBridgeError:
raise AuthorizationMismatch(
"transcript.before.tool_call does not match execution"
) from None
if not compare_digest(before_digest, tool_call_digest):
# True == 1 and False == 0, nested objects included, so compare the exact RFC
# 8785 bytes instead. If either object has no canonical form, _jcs raises
# IntentBridgeError: that input cannot be evaluated, which is not a mismatch.
if not compare_digest(
_jcs(before_call, "transcript.before.tool_call"),
_jcs(tool_call, "tool_call"),
):
raise AuthorizationMismatch("transcript.before.tool_call does not match execution")
if not isinstance(transcript.get("after"), dict):
raise AuthorizationMismatch("transcript.after must contain the execution result")
Expand Down
16 changes: 16 additions & 0 deletions tests/test_intent_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,19 @@ def test_transcript_call_that_is_not_an_object_stays_an_authorization_mismatch(
transcript={"before": {"tool_call": bad}, "after": {"status": "accepted"}},
now=150,
)

@pytest.mark.parametrize("bad", [{"approved": 2**60}, {"approved": float("nan")}])
def test_uncanonicalizable_transcript_call_is_intentbridgeerror_not_mismatch(
bad: dict,
) -> None:
"""An unrepresentable transcript call cannot be evaluated; it is not a mismatch."""
bridge, key, declaration, intent, args, tool_call, _ = _fixture()
transcript_call = {"name": "send_invoice", "arguments": bad}
with pytest.raises(IntentBridgeError, match="transcript.before.tool_call") as excinfo:
verify_bridge(
bridge, {**key_to_jwk(key), "kid": "key-7"}, declaration=declaration,
pic_intent_digest=intent, pic_args_digest=args, tool_call=tool_call,
transcript={"before": {"tool_call": transcript_call}, "after": {"status": "accepted"}},
now=150,
)
assert not isinstance(excinfo.value, AuthorizationMismatch)
Loading