diff --git a/src/a2a/compat/v0_3/rest_transport.py b/src/a2a/compat/v0_3/rest_transport.py index 362946736..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,12 +333,11 @@ 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) - ) + # 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 e627b444f..3ff9cac68 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, @@ -191,6 +192,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,