fix(a2a): reject malformed protocolVersion suffixes while keeping semver prereleases

This commit is contained in:
mateo-berri 2026-07-21 13:24:58 -07:00
parent 062e58fb1d
commit 2310211531
3 changed files with 27 additions and 4 deletions

View file

@ -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)

View file

@ -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),

View file

@ -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."""