From fa39eb54bf0248954c45aacaeaa2f8b5c8591fee Mon Sep 17 00:00:00 2001 From: Wondr Date: Sun, 25 Jan 2026 17:57:58 +0100 Subject: [PATCH 1/5] fix: normalize api key scheme in alias --- src/a2a/types.py | 2 +- tests/server/apps/rest/test_serialization.py | 66 ++++++++++++++++++++ tests/test_types.py | 13 ++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/server/apps/rest/test_serialization.py diff --git a/src/a2a/types.py b/src/a2a/types.py index 918a06b5e..cc1cbcb16 100644 --- a/src/a2a/types.py +++ b/src/a2a/types.py @@ -34,7 +34,7 @@ class APIKeySecurityScheme(A2ABaseModel): """ An optional description for the security scheme. """ - in_: In + in_: In = Field(..., alias='in') """ The location of the API key. """ diff --git a/tests/server/apps/rest/test_serialization.py b/tests/server/apps/rest/test_serialization.py new file mode 100644 index 000000000..b76761643 --- /dev/null +++ b/tests/server/apps/rest/test_serialization.py @@ -0,0 +1,66 @@ +from unittest import mock + +import pytest + +from httpx import ASGITransport, AsyncClient + +from a2a.server.apps.rest.fastapi_app import A2ARESTFastAPIApplication +from a2a.types import ( + APIKeySecurityScheme, + AgentCapabilities, + AgentCard, + In, + SecurityScheme, +) + + +@pytest.fixture +def agent_card_with_api_key() -> AgentCard: + api_key_scheme_data = { + 'type': 'apiKey', + 'name': 'X-API-KEY', + 'in': 'header', + } + api_key_scheme = APIKeySecurityScheme.model_validate(api_key_scheme_data) + + return AgentCard( + name='APIKeyAgent', + description='An agent that uses API Key auth.', + url='http://example.com/apikey-agent', + version='1.0.0', + capabilities=AgentCapabilities(), + default_input_modes=['text/plain'], + default_output_modes=['text/plain'], + skills=[], + security_schemes={'api_key_auth': SecurityScheme(root=api_key_scheme)}, + security=[{'api_key_auth': []}], + ) + + +@pytest.mark.anyio +async def test_rest_agent_card_with_api_key_scheme_alias( + agent_card_with_api_key: AgentCard, +): + """Ensures REST agent card serialization uses the 'in' alias.""" + handler = mock.AsyncMock() + app_instance = A2ARESTFastAPIApplication(agent_card_with_api_key, handler) + app = app_instance.build( + agent_card_url='/.well-known/agent.json', rpc_url='' + ) + + async with AsyncClient( + transport=ASGITransport(app=app), base_url='http://test' + ) as client: + response = await client.get('/.well-known/agent.json') + + assert response.status_code == 200 + response_data = response.json() + + security_scheme_json = response_data['securitySchemes']['api_key_auth'] + assert 'in' in security_scheme_json + assert security_scheme_json['in'] == 'header' + assert 'in_' not in security_scheme_json + + parsed_card = AgentCard.model_validate(response_data) + parsed_scheme_wrapper = parsed_card.security_schemes['api_key_auth'] + assert parsed_scheme_wrapper.root.in_ == In.header diff --git a/tests/test_types.py b/tests/test_types.py index 73e6af7bb..ce7084be5 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -190,6 +190,19 @@ def test_security_scheme_valid(): assert scheme.root.name == 'X-API-KEY' +def test_security_scheme_accepts_in_field_name(): + scheme = SecurityScheme.model_validate( + { + 'type': 'apiKey', + 'in_': 'header', + 'name': 'X-API-KEY', + } + ) + assert isinstance(scheme.root, APIKeySecurityScheme) + assert scheme.root.in_ == In.header + assert scheme.model_dump(mode='json', exclude_none=True)['in'] == 'header' + + def test_security_scheme_invalid(): with pytest.raises(ValidationError): APIKeySecurityScheme( From 133be7145dc73d6a0a3648fdd2e647673bb9f937 Mon Sep 17 00:00:00 2001 From: Wondr Date: Sun, 25 Jan 2026 18:02:37 +0100 Subject: [PATCH 2/5] test: parametrize api key in field handling --- tests/test_types.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/test_types.py b/tests/test_types.py index ce7084be5..563bf5e2f 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -182,25 +182,22 @@ # --- Test Functions --- -def test_security_scheme_valid(): - scheme = SecurityScheme.model_validate(MINIMAL_AGENT_SECURITY_SCHEME) +@pytest.mark.parametrize('in_field_name', ['in', 'in_']) +def test_security_scheme_in_field_handling(in_field_name: str) -> None: + scheme_data = { + 'type': 'apiKey', + 'name': 'X-API-KEY', + in_field_name: 'header', + } + scheme = SecurityScheme.model_validate(scheme_data) assert isinstance(scheme.root, APIKeySecurityScheme) assert scheme.root.type == 'apiKey' assert scheme.root.in_ == In.header assert scheme.root.name == 'X-API-KEY' - -def test_security_scheme_accepts_in_field_name(): - scheme = SecurityScheme.model_validate( - { - 'type': 'apiKey', - 'in_': 'header', - 'name': 'X-API-KEY', - } - ) - assert isinstance(scheme.root, APIKeySecurityScheme) - assert scheme.root.in_ == In.header - assert scheme.model_dump(mode='json', exclude_none=True)['in'] == 'header' + serialized_data = scheme.model_dump(mode='json', exclude_none=True) + assert serialized_data.get('in') == 'header' + assert 'in_' not in serialized_data def test_security_scheme_invalid(): From 9cfb80b76e316c3615b8457cdc51cd6942f3a154 Mon Sep 17 00:00:00 2001 From: Wondr Date: Sun, 25 Jan 2026 18:10:20 +0100 Subject: [PATCH 3/5] test: rename rest serialization test --- .../rest/{test_serialization.py => test_rest_serialization.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/server/apps/rest/{test_serialization.py => test_rest_serialization.py} (100%) diff --git a/tests/server/apps/rest/test_serialization.py b/tests/server/apps/rest/test_rest_serialization.py similarity index 100% rename from tests/server/apps/rest/test_serialization.py rename to tests/server/apps/rest/test_rest_serialization.py From e1f78a85053594ce787c746fd532e640ad2e0e8b Mon Sep 17 00:00:00 2001 From: Wondr Date: Sun, 25 Jan 2026 18:19:37 +0100 Subject: [PATCH 4/5] fix: annotate telemetry __enter__ with Self --- src/a2a/utils/telemetry.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/a2a/utils/telemetry.py b/src/a2a/utils/telemetry.py index c73d2ac92..77d2b3e9f 100644 --- a/src/a2a/utils/telemetry.py +++ b/src/a2a/utils/telemetry.py @@ -62,6 +62,12 @@ def internal_method(self): from typing import TYPE_CHECKING, Any +try: + from typing import Self +except ImportError: # pragma: no cover - for Python < 3.11 + from typing_extensions import Self + + if TYPE_CHECKING: from opentelemetry.trace import SpanKind as SpanKindType else: @@ -86,7 +92,7 @@ class _NoOp: def __call__(self, *args: Any, **kwargs: Any) -> Any: return self - def __enter__(self) -> '_NoOp': + def __enter__(self) -> Self: return self def __exit__(self, *args: object, **kwargs: Any) -> None: From c2b0c98b420b22acd6689771794bb74b75421d18 Mon Sep 17 00:00:00 2001 From: Wondr Date: Sun, 25 Jan 2026 18:23:19 +0100 Subject: [PATCH 5/5] fix: use typing_extensions Self for telemetry --- src/a2a/utils/telemetry.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/a2a/utils/telemetry.py b/src/a2a/utils/telemetry.py index 77d2b3e9f..22d1e8730 100644 --- a/src/a2a/utils/telemetry.py +++ b/src/a2a/utils/telemetry.py @@ -61,11 +61,7 @@ def internal_method(self): from collections.abc import Callable from typing import TYPE_CHECKING, Any - -try: - from typing import Self -except ImportError: # pragma: no cover - for Python < 3.11 - from typing_extensions import Self +from typing_extensions import Self if TYPE_CHECKING: