From 45522ae0db705b96b35957b6f6171c8b04935196 Mon Sep 17 00:00:00 2001 From: Connor Moss Date: Tue, 3 Nov 2026 10:56:25 -0500 Subject: [PATCH 1/2] fix(compat): parse the v0.3 REST extended agent card as JSON --- src/a2a/compat/v0_3/rest_transport.py | 5 +-- tests/compat/v0_3/test_rest_transport.py | 57 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/a2a/compat/v0_3/rest_transport.py b/src/a2a/compat/v0_3/rest_transport.py index 362946736..9e938812e 100644 --- a/src/a2a/compat/v0_3/rest_transport.py +++ b/src/a2a/compat/v0_3/rest_transport.py @@ -331,11 +331,8 @@ async def get_extended_agent_card( response_data = await self._execute_request( 'GET', '/v1/card', context=context ) - resp_proto = ParseDict( - response_data, a2a_v0_3_pb2.AgentCard(), ignore_unknown_fields=True - ) card = conversions.to_core_agent_card( - proto_utils.FromProto.agent_card(resp_proto) + types_v03.AgentCard.model_validate(response_data) ) self.agent_card = card return card diff --git a/tests/compat/v0_3/test_rest_transport.py b/tests/compat/v0_3/test_rest_transport.py index e627b444f..4ec3a5b5b 100644 --- a/tests/compat/v0_3/test_rest_transport.py +++ b/tests/compat/v0_3/test_rest_transport.py @@ -6,6 +6,7 @@ import pytest from a2a.client.errors import A2AClientError +from a2a.compat.v0_3 import types as types_v03 from a2a.compat.v0_3.rest_transport import CompatRestTransport from a2a.types.a2a_pb2 import ( AgentCapabilities, @@ -178,7 +179,13 @@ async def test_compat_rest_transport_get_extended_agent_card(transport): async def mock_send_request(*args, **kwargs): return { 'name': 'ExtendedAgent', + 'description': 'Extended', + 'version': '1.0.0', + 'url': 'http://example.com', 'capabilities': {}, + 'defaultInputModes': ['text/plain'], + 'defaultOutputModes': ['text/plain'], + 'skills': [], 'supportsAuthenticatedExtendedCard': True, } @@ -191,6 +198,56 @@ async def mock_send_request(*args, **kwargs): assert response.capabilities.extended_agent_card is True +@pytest.mark.asyncio +async def test_compat_rest_transport_get_extended_agent_card_keeps_security( + transport, +): + """v0.3 servers send the card as JSON, not as the v0.3 proto.""" + card = types_v03.AgentCard( + name='ExtendedAgent', + description='Extended', + version='1.0.0', + url='http://example.com', + capabilities=types_v03.AgentCapabilities(), + default_input_modes=['text/plain'], + default_output_modes=['text/plain'], + skills=[ + types_v03.AgentSkill( + id='skill-1', + name='Skill', + description='Skill', + tags=['tag'], + security=[{'bearer': []}], + ) + ], + security_schemes={ + 'bearer': types_v03.SecurityScheme( + root=types_v03.HTTPAuthSecurityScheme(scheme='bearer') + ) + }, + security=[{'bearer': []}], + supports_authenticated_extended_card=True, + ) + + async def mock_send_request(*args, **kwargs): + # Same serialization as REST03Handler.on_get_extended_agent_card. + return card.model_dump(mode='json', exclude_none=True) + + transport._send_request = mock_send_request + + response = await transport.get_extended_agent_card( + GetExtendedAgentCardRequest() + ) + + scheme = response.security_schemes['bearer'] + assert scheme.WhichOneof('scheme') == 'http_auth_security_scheme' + assert scheme.http_auth_security_scheme.scheme == 'bearer' + assert list(response.security_requirements[0].schemes) == ['bearer'] + assert list(response.skills[0].security_requirements[0].schemes) == [ + 'bearer' + ] + + @pytest.mark.asyncio async def test_compat_rest_transport_get_extended_agent_card_not_supported( transport, From bbcecd652b16a4c099052389598d01dd091650c0 Mon Sep 17 00:00:00 2001 From: Connor Moss Date: Wed, 4 Nov 2026 09:50:27 -0500 Subject: [PATCH 2/2] fix(compat): read the v0.3 REST extended card with parse_agent_card Use parse_agent_card and warn on missing required fields instead of strict validation, so a card missing required fields still parses, as before. Revert the existing test payload to the minimal card. --- src/a2a/compat/v0_3/rest_transport.py | 10 +++++++--- tests/compat/v0_3/test_rest_transport.py | 6 ------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/a2a/compat/v0_3/rest_transport.py b/src/a2a/compat/v0_3/rest_transport.py index 9e938812e..8d13b88f4 100644 --- a/src/a2a/compat/v0_3/rest_transport.py +++ b/src/a2a/compat/v0_3/rest_transport.py @@ -9,6 +9,7 @@ from google.protobuf.json_format import MessageToDict, Parse, ParseDict +from a2a.client.card_resolver import parse_agent_card from a2a.client.client import ClientCallContext from a2a.client.errors import A2AClientError from a2a.client.transports.base import ClientTransport @@ -46,6 +47,7 @@ ) from a2a.utils.constants import PROTOCOL_VERSION_0_3, VERSION_HEADER from a2a.utils.errors import JSON_RPC_ERROR_CODE_MAP, MethodNotFoundError +from a2a.utils.proto_utils import warn_on_missing_required_fields from a2a.utils.telemetry import SpanKind, trace_class @@ -331,9 +333,11 @@ async def get_extended_agent_card( response_data = await self._execute_request( 'GET', '/v1/card', context=context ) - card = conversions.to_core_agent_card( - types_v03.AgentCard.model_validate(response_data) - ) + # v0.3 servers send the card as JSON. parse_agent_card reads it like + # the card resolver does, and still accepts a card missing required + # fields, which only gets a warning. + card = parse_agent_card(response_data) + warn_on_missing_required_fields(card, 'v0.3 extended agent card') self.agent_card = card return card diff --git a/tests/compat/v0_3/test_rest_transport.py b/tests/compat/v0_3/test_rest_transport.py index 4ec3a5b5b..3ff9cac68 100644 --- a/tests/compat/v0_3/test_rest_transport.py +++ b/tests/compat/v0_3/test_rest_transport.py @@ -179,13 +179,7 @@ async def test_compat_rest_transport_get_extended_agent_card(transport): async def mock_send_request(*args, **kwargs): return { 'name': 'ExtendedAgent', - 'description': 'Extended', - 'version': '1.0.0', - 'url': 'http://example.com', 'capabilities': {}, - 'defaultInputModes': ['text/plain'], - 'defaultOutputModes': ['text/plain'], - 'skills': [], 'supportsAuthenticatedExtendedCard': True, }