Skip to content

Commit 686643b

Browse files
committed
AgentCard filter 0.3 protocol endpoints.
1 parent 40ddcb9 commit 686643b

11 files changed

Lines changed: 287 additions & 45 deletions

File tree

src/a2a/client/client_factory.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from a2a.client.transports.jsonrpc import JsonRpcTransport
1717
from a2a.client.transports.rest import RestTransport
1818
from a2a.client.transports.tenant_decorator import TenantTransportDecorator
19+
from a2a.compat.v0_3.versions import is_legacy_version
1920
from a2a.types.a2a_pb2 import (
2021
AgentCapabilities,
2122
AgentCard,
@@ -111,7 +112,7 @@ def jsonrpc_transport_producer(
111112
else PROTOCOL_VERSION_CURRENT
112113
)
113114

114-
if ClientFactory._is_legacy_version(version):
115+
if is_legacy_version(version):
115116
from a2a.compat.v0_3.jsonrpc_transport import ( # noqa: PLC0415
116117
CompatJsonRpcTransport,
117118
)
@@ -150,7 +151,7 @@ def rest_transport_producer(
150151
else PROTOCOL_VERSION_CURRENT
151152
)
152153

153-
if ClientFactory._is_legacy_version(version):
154+
if is_legacy_version(version):
154155
from a2a.compat.v0_3.rest_transport import ( # noqa: PLC0415
155156
CompatRestTransport,
156157
)
@@ -197,7 +198,7 @@ def grpc_transport_producer(
197198
)
198199

199200
if (
200-
ClientFactory._is_legacy_version(version)
201+
is_legacy_version(version)
201202
and CompatGrpcTransport is not None
202203
):
203204
return CompatGrpcTransport.create(card, url, config)
@@ -215,21 +216,6 @@ def grpc_transport_producer(
215216
grpc_transport_producer,
216217
)
217218

218-
@staticmethod
219-
def _is_legacy_version(version: str | None) -> bool:
220-
"""Determines if the given version is a legacy protocol version (>=0.3 and <1.0)."""
221-
if not version:
222-
return False
223-
try:
224-
v = Version(version)
225-
return (
226-
Version(PROTOCOL_VERSION_0_3)
227-
<= v
228-
< Version(PROTOCOL_VERSION_1_0)
229-
)
230-
except InvalidVersion:
231-
return False
232-
233219
@staticmethod
234220
def _find_best_interface(
235221
interfaces: list[AgentInterface],

src/a2a/compat/v0_3/conversions.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from google.protobuf.json_format import MessageToDict, ParseDict
1010

1111
from a2a.compat.v0_3 import types as types_v03
12+
from a2a.compat.v0_3.versions import is_legacy_version
1213
from a2a.server.models import PushNotificationConfigModel, TaskModel
1314
from a2a.types import a2a_pb2 as pb2_v10
15+
from a2a.utils import constants, errors
1416

1517

1618
_COMPAT_TO_CORE_TASK_STATE: dict[types_v03.TaskState, Any] = {
@@ -676,7 +678,7 @@ def to_core_agent_interface(
676678
return pb2_v10.AgentInterface(
677679
url=compat_interface.url,
678680
protocol_binding=compat_interface.transport,
679-
protocol_version='0.3.0', # Defaulting for legacy
681+
protocol_version=constants.PROTOCOL_VERSION_0_3, # Defaulting for legacy
680682
)
681683

682684

@@ -857,7 +859,8 @@ def to_core_agent_card(compat_card: types_v03.AgentCard) -> pb2_v10.AgentCard:
857859
primary_interface = pb2_v10.AgentInterface(
858860
url=compat_card.url,
859861
protocol_binding=compat_card.preferred_transport or 'JSONRPC',
860-
protocol_version=compat_card.protocol_version or '0.3.0',
862+
protocol_version=compat_card.protocol_version
863+
or constants.PROTOCOL_VERSION_0_3,
861864
)
862865
core_card.supported_interfaces.append(primary_interface)
863866

@@ -918,21 +921,23 @@ def to_core_agent_card(compat_card: types_v03.AgentCard) -> pb2_v10.AgentCard:
918921
def to_compat_agent_card(core_card: pb2_v10.AgentCard) -> types_v03.AgentCard:
919922
# Map supported interfaces back to legacy layout
920923
"""Convert agent card to v0.3 compat type."""
921-
primary_interface = (
922-
core_card.supported_interfaces[0]
923-
if core_card.supported_interfaces
924-
else pb2_v10.AgentInterface(
925-
url='', protocol_binding='JSONRPC', protocol_version='0.3.0'
924+
compat_interfaces = [
925+
interface
926+
for interface in core_card.supported_interfaces
927+
if (
928+
(not interface.protocol_version)
929+
or is_legacy_version(interface.protocol_version)
926930
)
927-
)
928-
additional_interfaces = (
929-
[
930-
to_compat_agent_interface(i)
931-
for i in core_card.supported_interfaces[1:]
932-
]
933-
if len(core_card.supported_interfaces) > 1
934-
else None
935-
)
931+
]
932+
if not compat_interfaces:
933+
raise errors.VersionNotSupportedError(
934+
'AgentCard must have at least one interface with compatible protocol version.'
935+
)
936+
937+
primary_interface = compat_interfaces[0]
938+
additional_interfaces = [
939+
to_compat_agent_interface(i) for i in compat_interfaces[1:]
940+
]
936941

937942
compat_cap = to_compat_agent_capabilities(core_card.capabilities)
938943
supports_authenticated_extended_card = (
@@ -948,7 +953,7 @@ def to_compat_agent_card(core_card: pb2_v10.AgentCard) -> types_v03.AgentCard:
948953
url=primary_interface.url,
949954
preferred_transport=primary_interface.protocol_binding,
950955
protocol_version=primary_interface.protocol_version,
951-
additional_interfaces=additional_interfaces,
956+
additional_interfaces=additional_interfaces or None,
952957
provider=to_compat_agent_provider(core_card.provider)
953958
if core_card.HasField('provider')
954959
else None,

src/a2a/compat/v0_3/versions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Utility functions for protocol version comparison and validation."""
2+
3+
from packaging.version import InvalidVersion, Version
4+
5+
from a2a.utils.constants import PROTOCOL_VERSION_0_3, PROTOCOL_VERSION_1_0
6+
7+
8+
def is_legacy_version(version: str | None) -> bool:
9+
"""Determines if the given version is a legacy protocol version (>=0.3 and <1.0)."""
10+
if not version:
11+
return False
12+
try:
13+
v = Version(version)
14+
return (
15+
Version(PROTOCOL_VERSION_0_3) <= v < Version(PROTOCOL_VERSION_1_0)
16+
)
17+
except InvalidVersion:
18+
return False

src/a2a/server/request_handlers/response_helpers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ def agent_card_to_dict(card: AgentCard) -> dict[str, Any]:
8787
"""Convert AgentCard to dict and inject backward compatibility fields."""
8888
result = MessageToDict(card)
8989

90-
compat_card = to_compat_agent_card(card)
91-
compat_dict = compat_card.model_dump(exclude_none=True)
90+
try:
91+
compat_card = to_compat_agent_card(card)
92+
compat_dict = compat_card.model_dump(exclude_none=True)
93+
except VersionNotSupportedError:
94+
compat_dict = {}
9295

9396
# Do not include supportsAuthenticatedExtendedCard if false
9497
if not compat_dict.get('supportsAuthenticatedExtendedCard'):

tests/client/transports/test_rest_client.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,6 @@ async def test_send_message_with_default_extensions(
258258
mock_response.status_code = 200
259259
mock_httpx_client.send.return_value = mock_response
260260

261-
262261
context = ClientCallContext(
263262
service_parameters={
264263
'X-A2A-Extensions': 'https://example.com/test-ext/v1,https://example.com/test-ext/v2'
@@ -301,7 +300,6 @@ async def test_send_message_streaming_with_new_extensions(
301300
mock_event_source
302301
)
303302

304-
305303
context = ClientCallContext(
306304
service_parameters={
307305
'X-A2A-Extensions': 'https://example.com/test-ext/v2'
@@ -402,7 +400,6 @@ async def test_get_card_with_extended_card_support_with_extensions(
402400

403401
request = GetExtendedAgentCardRequest()
404402

405-
406403
context = ClientCallContext(
407404
service_parameters={HTTP_EXTENSION_HEADER: extensions_str}
408405
)
@@ -691,7 +688,6 @@ async def test_rest_get_task_prepend_empty_tenant(
691688
@pytest.mark.asyncio
692689
@patch('a2a.client.transports.http_helpers.aconnect_sse')
693690
async def test_rest_streaming_methods_prepend_tenant( # noqa: PLR0913
694-
695691
self,
696692
mock_aconnect_sse,
697693
method_name,

tests/compat/v0_3/test_conversions.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
from a2a.server.models import PushNotificationConfigModel, TaskModel
8282
from cryptography.fernet import Fernet
8383
from a2a.types import a2a_pb2 as pb2_v10
84+
from a2a.utils.errors import VersionNotSupportedError
8485

8586

8687
def test_text_part_conversion():
@@ -986,7 +987,7 @@ def test_security_scheme_mtls_minimal():
986987
def test_agent_interface_conversion():
987988
v03_int = types_v03.AgentInterface(url='http', transport='JSONRPC')
988989
v10_expected = pb2_v10.AgentInterface(
989-
url='http', protocol_binding='JSONRPC', protocol_version='0.3.0'
990+
url='http', protocol_binding='JSONRPC', protocol_version='0.3'
990991
)
991992
v10_int = to_core_agent_interface(v03_int)
992993
assert v10_int == v10_expected
@@ -1131,7 +1132,7 @@ def test_agent_card_conversion():
11311132
url='u1', protocol_binding='JSONRPC', protocol_version='0.3.0'
11321133
),
11331134
pb2_v10.AgentInterface(
1134-
url='u2', protocol_binding='HTTP', protocol_version='0.3.0'
1135+
url='u2', protocol_binding='HTTP', protocol_version='0.3'
11351136
),
11361137
]
11371138
)
@@ -2014,3 +2015,24 @@ def test_push_notification_config_persistence_conversion_with_encryption():
20142015
assert v10_restored.id == v10_config.id
20152016
assert v10_restored.url == v10_config.url
20162017
assert v10_restored.token == v10_config.token
2018+
2019+
2020+
def test_to_compat_agent_card_unsupported_version():
2021+
card = pb2_v10.AgentCard(
2022+
name='Modern Agent',
2023+
description='Only supports 1.0',
2024+
version='1.0.0',
2025+
supported_interfaces=[
2026+
pb2_v10.AgentInterface(
2027+
url='http://grpc.v10.com',
2028+
protocol_binding='GRPC',
2029+
protocol_version='1.0.0',
2030+
),
2031+
],
2032+
capabilities=pb2_v10.AgentCapabilities(),
2033+
)
2034+
with pytest.raises(
2035+
VersionNotSupportedError,
2036+
match='AgentCard must have at least one interface with compatible protocol version.',
2037+
):
2038+
to_compat_agent_card(card)

tests/compat/v0_3/test_grpc_handler.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ def sample_agent_card() -> a2a_pb2.AgentCard:
3434
name='Test Agent',
3535
description='A test agent',
3636
version='1.0.0',
37+
supported_interfaces=[
38+
a2a_pb2.AgentInterface(
39+
url='http://jsonrpc.v03.com',
40+
protocol_binding='JSONRPC',
41+
protocol_version='0.3',
42+
),
43+
],
3744
)
3845

3946

@@ -434,8 +441,9 @@ async def test_get_agent_card_success(
434441
expected_res = a2a_v0_3_pb2.AgentCard(
435442
name='Test Agent',
436443
description='A test agent',
444+
url='http://jsonrpc.v03.com',
437445
version='1.0.0',
438-
protocol_version='0.3.0',
446+
protocol_version='0.3',
439447
preferred_transport='JSONRPC',
440448
capabilities=a2a_v0_3_pb2.AgentCapabilities(),
441449
)

tests/compat/v0_3/test_rest_transport.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,6 @@ async def test_compat_rest_transport_subscribe_post_405_get_405_fails(
337337

338338
method_count = {}
339339

340-
method_count = {}
341340
async def mock_stream(method, path, context=None, json=None):
342341
method_count[method] = method_count.get(method, 0) + 1
343342
if method == 'POST':
@@ -449,6 +448,7 @@ async def mock_stream(method, path, context=None, json=None):
449448
if False:
450449
yield
451450
create_405_error()
451+
452452
transport._send_stream_request = mock_stream
453453

454454
req = SubscribeToTaskRequest(id='task-123')

tests/compat/v0_3/test_versions.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Tests for version utility functions."""
2+
3+
import pytest
4+
5+
from a2a.compat.v0_3.versions import is_legacy_version
6+
7+
8+
@pytest.mark.parametrize(
9+
'version, expected',
10+
[
11+
('0.3', True),
12+
('0.3.0', True),
13+
('0.9', True),
14+
('0.9.9', True),
15+
('1.0', False),
16+
('1.0.0', False),
17+
('1.1', False),
18+
('0.2', False),
19+
('0.2.9', False),
20+
(None, False),
21+
('', False),
22+
('invalid', False),
23+
('v0.3', True),
24+
],
25+
)
26+
def test_is_legacy_version(version, expected):
27+
assert is_legacy_version(version) == expected

tests/server/request_handlers/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)