diff --git a/src/ca2a_runtime/transport/a2a_sdk.py b/src/ca2a_runtime/transport/a2a_sdk.py index 34a7b7d..030d513 100644 --- a/src/ca2a_runtime/transport/a2a_sdk.py +++ b/src/ca2a_runtime/transport/a2a_sdk.py @@ -263,8 +263,14 @@ def attach_to_sdk_message(message: Any, request: PeerRequest) -> Any: speak HTTP. """ _require_protobuf() - merged = dict(metadata_from_sdk_message(message)) - merged.update(a2a_adapter.attach_ca2a_metadata({}, request)["metadata"]) + # Attach against the message's *current* metadata, not a throwaway ``{}``: the + # dict adapter clears an absent optional key (sealed_payload / caller_offer / + # holder_proof) with ``meta.pop(...)``, and those pops only bite when they run + # against the real metadata. Passing ``{}`` here would make them no-ops and let + # a reused Message ship a prior request's stale optional keys. + merged = a2a_adapter.attach_ca2a_metadata( + {"metadata": dict(metadata_from_sdk_message(message))}, request + )["metadata"] message.metadata.Clear() json_format.ParseDict(merged, message.metadata) diff --git a/tests/unit/test_a2a_sdk_bridge.py b/tests/unit/test_a2a_sdk_bridge.py index cc482b4..42ad19f 100644 --- a/tests/unit/test_a2a_sdk_bridge.py +++ b/tests/unit/test_a2a_sdk_bridge.py @@ -452,6 +452,52 @@ def test_attach_declares_the_extension_on_the_message() -> None: assert list(message.extensions).count(EXTENSION_URI) == 1 +def test_reused_message_clears_a_stale_sealed_payload() -> None: + """A reused SDK message must not carry a prior request's sealed ciphertext. + + ``attach_to_sdk_message`` mutates in place, so an application that reuses one + ``Message`` across tasks would otherwise ship task N-1's ``sealed_payload`` as + task N's: cross-request payload confusion. + """ + message = Message(message_id="m1") + a2a_sdk.attach_to_sdk_message(message, _request(sealed_payload=b"SECRET")) + a2a_sdk.attach_to_sdk_message(message, _request()) + parsed = a2a_sdk.parse_sdk_message(message) + assert parsed is not None + assert parsed.sealed_payload is None + + +def test_reused_message_clears_a_stale_caller_offer() -> None: + """A reused SDK message must not carry a prior request's caller offer.""" + offer = ChannelOffer( + channel_public_key="k" * 43, + report=AttestationReport( + platform="software-only", + measurement="m", + public_key="k" * 43, + nonce="v1.123.abc.def", + ), + ) + message = Message(message_id="m1") + a2a_sdk.attach_to_sdk_message(message, _request(caller_offer=offer)) + a2a_sdk.attach_to_sdk_message(message, _request()) + parsed = a2a_sdk.parse_sdk_message(message) + assert parsed is not None + assert parsed.caller_offer is None + + +def test_reused_message_clears_a_stale_holder_proof() -> None: + """A reused SDK message must not carry a prior request's holder proof.""" + chain, leaf_key = _chain_with_keys() + node = PeerNode(LocalPolicy.of({"read"}), trusted_root_issuers={chain[0].issuer}) + message = Message(message_id="m1") + a2a_sdk.attach_to_sdk_message(message, _request(node=node, chain=chain, leaf_key=leaf_key)) + a2a_sdk.attach_to_sdk_message(message, _request()) + parsed = a2a_sdk.parse_sdk_message(message) + assert parsed is not None + assert parsed.holder_proof is None + + # -------------------------------------------------------------------------- # Not-a-cA2A-message, and failing closed # --------------------------------------------------------------------------