diff --git a/litellm/proxy/a2a/agent_card.py b/litellm/proxy/a2a/agent_card.py index 129ad8f0a96..29a689a32de 100644 --- a/litellm/proxy/a2a/agent_card.py +++ b/litellm/proxy/a2a/agent_card.py @@ -7,6 +7,7 @@ the base; specific fields are replaced so all traffic flows through the proxy and uses LiteLLM auth. """ +import re from copy import deepcopy from typing import Any, Dict, List, Literal, Mapping @@ -20,16 +21,25 @@ SUPPORTED_A2A_PROTOCOL_VERSIONS: tuple[SupportedA2AVersion, ...] = ("0.3", "1.0" LITELLM_A2A_PROTOCOL_VERSION = "1.0" +_PROTOCOL_VERSION_PATTERN = re.compile( + r"^(\d+\.\d+)(?:\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)?$" +) + + def normalize_protocol_version(version: object) -> SupportedA2AVersion | None: """Map a raw ``protocolVersion`` value to the supported canonical major.minor version. - Semver strings the A2A spec and Google a2a-sdk emit (e.g. ``"0.3.0"``, ``"1.0.1"``) - canonicalize to their major.minor (``"0.3"``, ``"1.0"``). Anything outside the - supported set, including non-strings, yields ``None``. + Accepts the bare major.minor convention of the 1.0 spec (``"0.3"``, ``"1.0"``) and the + full semver forms older SDKs emit (``"0.3.0"``, ``"1.0.1"``, including prerelease and + build suffixes like ``"0.3.0-rc1"``). Malformed strings, versions outside the + supported set, and non-strings yield ``None``. """ if not isinstance(version, str): return None - major_minor = ".".join(version.split(".")[:2]) + match = _PROTOCOL_VERSION_PATTERN.match(version) + if match is None: + return None + major_minor = match.group(1) return next((supported for supported in SUPPORTED_A2A_PROTOCOL_VERSIONS if supported == major_minor), None) diff --git a/tests/test_litellm/proxy/a2a/test_agent_card.py b/tests/test_litellm/proxy/a2a/test_agent_card.py index 32e211b45be..dfa848e335e 100644 --- a/tests/test_litellm/proxy/a2a/test_agent_card.py +++ b/tests/test_litellm/proxy/a2a/test_agent_card.py @@ -219,9 +219,16 @@ def test_strips_additional_interfaces_to_prevent_backend_url_leak(): ("1.0", "1.0"), ("1.0.0", "1.0"), ("1.0.1", "1.0"), + ("0.3.0-rc1", "0.3"), + ("1.0.0-rc.1+build.5", "1.0"), ("0.2.6", None), ("2.0", None), ("0.30", None), + ("0.3.garbage", None), + ("0.3.", None), + ("1.0.not-semver", None), + ("0.3.0.0", None), + ("0.3-rc1", None), ("garbage", None), ("", None), (None, None), diff --git a/tests/test_litellm/proxy/agent_endpoints/test_endpoints.py b/tests/test_litellm/proxy/agent_endpoints/test_endpoints.py index d4228f799d5..bcd3333baf9 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_endpoints.py @@ -580,6 +580,12 @@ class TestAgentProtocolVersionValidation: assert "Unsupported protocolVersion '0.2.6'" in resp.json()["detail"] self.mock_registry.add_agent_to_db.assert_not_awaited() + def test_malformed_protocol_version_is_rejected(self): + resp = self._create_agent_with_protocol_version("0.3.garbage") + assert resp.status_code == 400 + assert "Unsupported protocolVersion '0.3.garbage'" in resp.json()["detail"] + self.mock_registry.add_agent_to_db.assert_not_awaited() + class TestCheckAgentManagementPermission: """Unit tests for the _check_agent_management_permission helper."""