From 5e249bb7591de0e43386f79ff6fc1b7db84a2b0d Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 23 Jun 2025 10:01:28 +0200 Subject: [PATCH 1/8] fix: Resolve `APIKeySecurityScheme` parsing failed Fixes #220 --- src/a2a/server/apps/jsonrpc/jsonrpc_app.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index ff6998cd..32c33419 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -375,7 +375,11 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse: # The public agent card is a direct serialization of the agent_card # provided at initialization. return JSONResponse( - self.agent_card.model_dump(mode='json', exclude_none=True) + self.agent_card.model_dump( + mode='json', + exclude_none=True, + by_alias=True, + ) ) async def _handle_get_authenticated_extended_agent_card( @@ -392,7 +396,9 @@ async def _handle_get_authenticated_extended_agent_card( if self.extended_agent_card: return JSONResponse( self.extended_agent_card.model_dump( - mode='json', exclude_none=True + mode='json', + exclude_none=True, + by_alias=True, ) ) # If supportsAuthenticatedExtendedCard is true, but no specific From 9fa62e08524c19562a18f1c74512350ef076599c Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 23 Jun 2025 10:44:56 +0200 Subject: [PATCH 2/8] Fix test for asyncio --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac6375bd..1ddc9887 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,11 +50,13 @@ testpaths = ["tests"] python_files = "test_*.py" python_functions = "test_*" addopts = "-ra --strict-markers" -asyncio_mode = "strict" markers = [ "asyncio: mark a test as a coroutine that should be run by pytest-asyncio", ] +[tool.pytest-asyncio] +mode = "strict" + [build-system] requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" From 475ba50e3d1f467d7860947ed322499b316ad65f Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 23 Jun 2025 11:07:54 +0200 Subject: [PATCH 3/8] Change AgentCard.model_validate to also use `by_alias` --- src/a2a/client/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index e29ef8a7..68bc5456 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -98,7 +98,9 @@ async def get_agent_card( target_url, agent_card_data, ) - agent_card = AgentCard.model_validate(agent_card_data) + agent_card = AgentCard.model_validate( + agent_card_data, by_alias=True + ) except httpx.HTTPStatusError as e: raise A2AClientHTTPError( e.response.status_code, From da2855c5356b1945ba5542a3a82add2badae16af Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 23 Jun 2025 11:09:38 +0200 Subject: [PATCH 4/8] Change to remove model_dump='json' --- src/a2a/client/client.py | 4 +--- src/a2a/server/apps/jsonrpc/jsonrpc_app.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index 68bc5456..e29ef8a7 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -98,9 +98,7 @@ async def get_agent_card( target_url, agent_card_data, ) - agent_card = AgentCard.model_validate( - agent_card_data, by_alias=True - ) + agent_card = AgentCard.model_validate(agent_card_data) except httpx.HTTPStatusError as e: raise A2AClientHTTPError( e.response.status_code, diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index 32c33419..0b78cc1c 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -376,7 +376,6 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse: # provided at initialization. return JSONResponse( self.agent_card.model_dump( - mode='json', exclude_none=True, by_alias=True, ) @@ -396,7 +395,6 @@ async def _handle_get_authenticated_extended_agent_card( if self.extended_agent_card: return JSONResponse( self.extended_agent_card.model_dump( - mode='json', exclude_none=True, by_alias=True, ) From 4df272778713bc829edc2850fb8e28ab56d47915 Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 23 Jun 2025 11:20:44 +0200 Subject: [PATCH 5/8] Change AgentCard to use by_alias --- src/a2a/client/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index e29ef8a7..68bc5456 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -98,7 +98,9 @@ async def get_agent_card( target_url, agent_card_data, ) - agent_card = AgentCard.model_validate(agent_card_data) + agent_card = AgentCard.model_validate( + agent_card_data, by_alias=True + ) except httpx.HTTPStatusError as e: raise A2AClientHTTPError( e.response.status_code, From 53d58290c55b74436f5c89b64c56ff9ac7f82465 Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 23 Jun 2025 11:49:19 +0200 Subject: [PATCH 6/8] Add Unit Tests --- .../server/apps/jsonrpc/test_serialization.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/server/apps/jsonrpc/test_serialization.py diff --git a/tests/server/apps/jsonrpc/test_serialization.py b/tests/server/apps/jsonrpc/test_serialization.py new file mode 100644 index 00000000..ea3da1c0 --- /dev/null +++ b/tests/server/apps/jsonrpc/test_serialization.py @@ -0,0 +1,94 @@ +from unittest import mock + +import pytest +from starlette.testclient import TestClient + +from a2a.server.apps import A2AFastAPIApplication, A2AStarletteApplication +from a2a.types import ( + APIKeySecurityScheme, + AgentCapabilities, + AgentCard, + In, + SecurityScheme, +) +from pydantic import ValidationError + + +@pytest.fixture +def agent_card_with_api_key(): + """Provides an AgentCard with an APIKeySecurityScheme for testing serialization.""" + # This data uses the alias 'in', which is correct for creating the model. + api_key_scheme_data = { + 'type': 'apiKey', + 'name': 'X-API-KEY', + 'in': 'header', + } + api_key_scheme = APIKeySecurityScheme.model_validate(api_key_scheme_data) + + agent_card = AgentCard( + name='APIKeyAgent', + description='An agent that uses API Key auth.', + url='http://example.com/apikey-agent', + version='1.0.0', + capabilities=AgentCapabilities(), + defaultInputModes=['text/plain'], + defaultOutputModes=['text/plain'], + skills=[], + securitySchemes={'api_key_auth': SecurityScheme(root=api_key_scheme)}, + security=[{'api_key_auth': []}], + ) + return agent_card + + +def test_starlette_agent_card_with_api_key_scheme_alias( + agent_card_with_api_key: AgentCard, +): + """ + Tests that the A2AStarletteApplication endpoint correctly serializes aliased fields. + + This verifies the fix for `APIKeySecurityScheme.in_` being serialized as `in_` instead of `in`. + """ + handler = mock.AsyncMock() + app_instance = A2AStarletteApplication(agent_card_with_api_key, handler) + client = TestClient(app_instance.build()) + + response = 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 + + try: + parsed_card = AgentCard.model_validate(response_data) + parsed_scheme_wrapper = parsed_card.securitySchemes['api_key_auth'] + assert isinstance(parsed_scheme_wrapper.root, APIKeySecurityScheme) + assert parsed_scheme_wrapper.root.in_ == In.header + except ValidationError as e: + pytest.fail( + f"AgentCard.model_validate failed on the server's response: {e}" + ) + + +def test_fastapi_agent_card_with_api_key_scheme_alias( + agent_card_with_api_key: AgentCard, +): + """ + Tests that the A2AFastAPIApplication endpoint correctly serializes aliased fields. + + This verifies the fix for `APIKeySecurityScheme.in_` being serialized as `in_` instead of `in`. + """ + handler = mock.AsyncMock() + app_instance = A2AFastAPIApplication(agent_card_with_api_key, handler) + client = TestClient(app_instance.build()) + + response = 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 'in_' not in security_scheme_json + assert security_scheme_json['in'] == 'header' From 2f134792d1cb009dcafd9aa4bfed3b00a2bd89db Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 23 Jun 2025 11:57:04 +0200 Subject: [PATCH 7/8] Revert fix to test unit tests --- src/a2a/client/client.py | 4 +--- src/a2a/server/apps/jsonrpc/jsonrpc_app.py | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index 68bc5456..e29ef8a7 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -98,9 +98,7 @@ async def get_agent_card( target_url, agent_card_data, ) - agent_card = AgentCard.model_validate( - agent_card_data, by_alias=True - ) + agent_card = AgentCard.model_validate(agent_card_data) except httpx.HTTPStatusError as e: raise A2AClientHTTPError( e.response.status_code, diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index 0b78cc1c..ff6998cd 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -375,10 +375,7 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse: # The public agent card is a direct serialization of the agent_card # provided at initialization. return JSONResponse( - self.agent_card.model_dump( - exclude_none=True, - by_alias=True, - ) + self.agent_card.model_dump(mode='json', exclude_none=True) ) async def _handle_get_authenticated_extended_agent_card( @@ -395,8 +392,7 @@ async def _handle_get_authenticated_extended_agent_card( if self.extended_agent_card: return JSONResponse( self.extended_agent_card.model_dump( - exclude_none=True, - by_alias=True, + mode='json', exclude_none=True ) ) # If supportsAuthenticatedExtendedCard is true, but no specific From 9edb079ac5cd5ceeb480ae3201f04ec00af57cea Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Mon, 23 Jun 2025 11:58:22 +0200 Subject: [PATCH 8/8] Add model_dump changes from server --- src/a2a/server/apps/jsonrpc/jsonrpc_app.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index ff6998cd..0b78cc1c 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -375,7 +375,10 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse: # The public agent card is a direct serialization of the agent_card # provided at initialization. return JSONResponse( - self.agent_card.model_dump(mode='json', exclude_none=True) + self.agent_card.model_dump( + exclude_none=True, + by_alias=True, + ) ) async def _handle_get_authenticated_extended_agent_card( @@ -392,7 +395,8 @@ async def _handle_get_authenticated_extended_agent_card( if self.extended_agent_card: return JSONResponse( self.extended_agent_card.model_dump( - mode='json', exclude_none=True + exclude_none=True, + by_alias=True, ) ) # If supportsAuthenticatedExtendedCard is true, but no specific