From 156f5198acfcdf891a8ed929e5b462f250f0bb24 Mon Sep 17 00:00:00 2001 From: Krishna Thota Date: Mon, 28 Jul 2025 16:59:19 -0700 Subject: [PATCH 1/3] feat: support for authenticated extended card method --- src/a2a/server/apps/jsonrpc/fastapi_app.py | 1 + src/a2a/server/apps/jsonrpc/jsonrpc_app.py | 26 +++++- src/a2a/server/apps/jsonrpc/starlette_app.py | 1 + .../request_handlers/jsonrpc_handler.py | 35 ++++++++ src/a2a/types.py | 81 ++++++++++++++++++- .../request_handlers/test_jsonrpc_handler.py | 60 ++++++++++++++ tests/server/test_integration.py | 5 +- 7 files changed, 203 insertions(+), 6 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index 421c3784..a3034857 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -89,6 +89,7 @@ def add_routes_to_app( )(self._handle_requests) app.get(agent_card_url)(self._handle_get_agent_card) + # TODO: deprecated endpoint to be removed in a future release if self.agent_card.supports_authenticated_extended_card: app.get(extended_agent_card_url)( self._handle_get_authenticated_extended_agent_card diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index e149b4d1..7eec8837 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -32,6 +32,7 @@ AgentCard, CancelTaskRequest, DeleteTaskPushNotificationConfigRequest, + GetAuthenticatedExtendedCardRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, InternalError, @@ -39,6 +40,7 @@ JSONParseError, JSONRPCError, JSONRPCErrorResponse, + JSONRPCRequest, JSONRPCResponse, ListTaskPushNotificationConfigRequest, SendMessageRequest, @@ -142,7 +144,9 @@ def __init__( self.agent_card = agent_card self.extended_agent_card = extended_agent_card self.handler = JSONRPCHandler( - agent_card=agent_card, request_handler=http_handler + agent_card=agent_card, + request_handler=http_handler, + extended_agent_card=extended_agent_card, ) if ( self.agent_card.supports_authenticated_extended_card @@ -212,7 +216,16 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911 try: body = await request.json() + if isinstance(body, dict): + request_id = body.get('id') + + # First, validate the basic JSON-RPC structure. This is crucial + # because the A2ARequest model is a discriminated union where some + # request types have default values for the 'method' field + JSONRPCRequest.model_validate(body) + a2a_request = A2ARequest.model_validate(body) + call_context = self._context_builder.build(request) request_id = a2a_request.root.id @@ -352,6 +365,13 @@ async def _process_non_streaming_request( context, ) ) + case GetAuthenticatedExtendedCardRequest(): + handler_result = ( + await self.handler.get_authenticated_extended_card( + request_obj, + context, + ) + ) case _: logger.error( f'Unhandled validated request type: {type(request_obj)}' @@ -440,6 +460,10 @@ async def _handle_get_authenticated_extended_agent_card( self, request: Request ) -> JSONResponse: """Handles GET requests for the authenticated extended agent card.""" + logger.warning( + 'HTTP GET for authenticated extended card has been called by a client. ' + 'This endpoint is deprecated in favor of agent/authenticatedExtendedCard JSON-RPC method and will be removed in a future release.' + ) if not self.agent_card.supports_authenticated_extended_card: return JSONResponse( {'error': 'Extended agent card not supported or not enabled.'}, diff --git a/src/a2a/server/apps/jsonrpc/starlette_app.py b/src/a2a/server/apps/jsonrpc/starlette_app.py index fb03d99b..38c0004a 100644 --- a/src/a2a/server/apps/jsonrpc/starlette_app.py +++ b/src/a2a/server/apps/jsonrpc/starlette_app.py @@ -86,6 +86,7 @@ def routes( ), ] + # TODO: deprecated endpoint to be removed in a future release if self.agent_card.supports_authenticated_extended_card: app_routes.append( Route( diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index 88a169fe..a0657859 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -7,12 +7,16 @@ from a2a.server.request_handlers.response_helpers import prepare_response_object from a2a.types import ( AgentCard, + AuthenticatedExtendedCardNotConfiguredError, CancelTaskRequest, CancelTaskResponse, CancelTaskSuccessResponse, DeleteTaskPushNotificationConfigRequest, DeleteTaskPushNotificationConfigResponse, DeleteTaskPushNotificationConfigSuccessResponse, + GetAuthenticatedExtendedCardRequest, + GetAuthenticatedExtendedCardResponse, + GetAuthenticatedExtendedCardSuccessResponse, GetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigResponse, GetTaskPushNotificationConfigSuccessResponse, @@ -57,15 +61,18 @@ def __init__( self, agent_card: AgentCard, request_handler: RequestHandler, + extended_agent_card: AgentCard | None = None, ): """Initializes the JSONRPCHandler. Args: agent_card: The AgentCard describing the agent's capabilities. request_handler: The underlying `RequestHandler` instance to delegate requests to. + extended_agent_card: An optional, distinct Extended AgentCard to be served """ self.agent_card = agent_card self.request_handler = request_handler + self.extended_agent_card = extended_agent_card async def on_message_send( self, @@ -395,3 +402,31 @@ async def delete_push_notification_config( id=request.id, error=e.error if e.error else InternalError() ) ) + + async def get_authenticated_extended_card( + self, + request: GetAuthenticatedExtendedCardRequest, + context: ServerCallContext | None = None, + ) -> GetAuthenticatedExtendedCardResponse: + """Handles the 'agent/authenticatedExtendedCard' JSON-RPC method. + + Args: + request: The incoming `GetAuthenticatedExtendedCardRequest` object. + context: Context provided by the server. + + Returns: + A `GetAuthenticatedExtendedCardResponse` object containing the config or a JSON-RPC error. + """ + if self.extended_agent_card is None: + return GetAuthenticatedExtendedCardResponse( + root=JSONRPCErrorResponse( + id=request.id, + error=AuthenticatedExtendedCardNotConfiguredError(), + ) + ) + + return GetAuthenticatedExtendedCardResponse( + root=GetAuthenticatedExtendedCardSuccessResponse( + id=request.id, result=self.extended_agent_card + ) + ) diff --git a/src/a2a/types.py b/src/a2a/types.py index d90715b6..4a02a43e 100644 --- a/src/a2a/types.py +++ b/src/a2a/types.py @@ -172,6 +172,27 @@ class AgentSkill(A2ABaseModel): """ +class AuthenticatedExtendedCardNotConfiguredError(A2ABaseModel): + """ + An A2A-specific error indicating that the agent does not have an + Authenticated Extended Card configured + """ + + code: Literal[-32007] = -32007 + """ + The error code for when an authenticated extended card is not configured. + """ + data: Any | None = None + """ + A primitive or structured value containing additional information about the error. + This may be omitted. + """ + message: str | None = 'Authenticated Extended Card not configured' + """ + The error message. + """ + + class AuthorizationCodeOAuthFlow(A2ABaseModel): """ Defines configuration details for the OAuth 2.0 Authorization Code flow. @@ -375,6 +396,27 @@ class FileWithUri(A2ABaseModel): """ +class GetAuthenticatedExtendedCardRequest(A2ABaseModel): + """ + Represents a JSON-RPC request for the `agent/authenticatedExtendedCard` method. + """ + + id: str | int + """ + The identifier for this request. + """ + jsonrpc: Literal['2.0'] = '2.0' + """ + The version of the JSON-RPC protocol. MUST be exactly "2.0". + """ + method: Literal['agent/authenticatedExtendedCard'] = ( + 'agent/authenticatedExtendedCard' + ) + """ + The method name. Must be 'agent/authenticatedExtendedCard'. + """ + + class GetTaskPushNotificationConfigParams(A2ABaseModel): """ Defines parameters for fetching a specific push notification configuration for a task. @@ -999,6 +1041,7 @@ class A2AError( | UnsupportedOperationError | ContentTypeNotSupportedError | InvalidAgentResponseError + | AuthenticatedExtendedCardNotConfiguredError ] ): root: ( @@ -1013,6 +1056,7 @@ class A2AError( | UnsupportedOperationError | ContentTypeNotSupportedError | InvalidAgentResponseError + | AuthenticatedExtendedCardNotConfiguredError ) """ A discriminated union of all standard JSON-RPC and A2A-specific error types. @@ -1170,6 +1214,7 @@ class JSONRPCErrorResponse(A2ABaseModel): | UnsupportedOperationError | ContentTypeNotSupportedError | InvalidAgentResponseError + | AuthenticatedExtendedCardNotConfiguredError ) """ An object describing the error that occurred. @@ -1625,6 +1670,7 @@ class A2ARequest( | TaskResubscriptionRequest | ListTaskPushNotificationConfigRequest | DeleteTaskPushNotificationConfigRequest + | GetAuthenticatedExtendedCardRequest ] ): root: ( @@ -1637,6 +1683,7 @@ class A2ARequest( | TaskResubscriptionRequest | ListTaskPushNotificationConfigRequest | DeleteTaskPushNotificationConfigRequest + | GetAuthenticatedExtendedCardRequest ) """ A discriminated union representing all possible JSON-RPC 2.0 requests supported by the A2A specification. @@ -1750,6 +1797,25 @@ class AgentCard(A2ABaseModel): """ +class GetAuthenticatedExtendedCardSuccessResponse(A2ABaseModel): + """ + Represents a successful JSON-RPC response for the `agent/authenticatedExtendedCard` method. + """ + + id: str | int | None = None + """ + The identifier established by the client. + """ + jsonrpc: Literal['2.0'] = '2.0' + """ + The version of the JSON-RPC protocol. MUST be exactly "2.0". + """ + result: AgentCard + """ + The result is an Agent Card object. + """ + + class Task(A2ABaseModel): """ Represents a single, stateful operation or conversation between a client and an agent. @@ -1769,7 +1835,7 @@ class Task(A2ABaseModel): """ id: str """ - A unique identifier for the task, generated by the client for a new task or provided by the agent. + A unique identifier for the task, generated by the server for a new task. """ kind: Literal['task'] = 'task' """ @@ -1804,6 +1870,17 @@ class CancelTaskSuccessResponse(A2ABaseModel): """ +class GetAuthenticatedExtendedCardResponse( + RootModel[ + JSONRPCErrorResponse | GetAuthenticatedExtendedCardSuccessResponse + ] +): + root: JSONRPCErrorResponse | GetAuthenticatedExtendedCardSuccessResponse + """ + Represents a JSON-RPC response for the `agent/authenticatedExtendedCard` method. + """ + + class GetTaskSuccessResponse(A2ABaseModel): """ Represents a successful JSON-RPC response for the `tasks/get` method. @@ -1889,6 +1966,7 @@ class JSONRPCResponse( | GetTaskPushNotificationConfigSuccessResponse | ListTaskPushNotificationConfigSuccessResponse | DeleteTaskPushNotificationConfigSuccessResponse + | GetAuthenticatedExtendedCardSuccessResponse ] ): root: ( @@ -1901,6 +1979,7 @@ class JSONRPCResponse( | GetTaskPushNotificationConfigSuccessResponse | ListTaskPushNotificationConfigSuccessResponse | DeleteTaskPushNotificationConfigSuccessResponse + | GetAuthenticatedExtendedCardSuccessResponse ) """ A discriminated union representing all possible JSON-RPC 2.0 responses diff --git a/tests/server/request_handlers/test_jsonrpc_handler.py b/tests/server/request_handlers/test_jsonrpc_handler.py index 5f3930c5..ef43b05f 100644 --- a/tests/server/request_handlers/test_jsonrpc_handler.py +++ b/tests/server/request_handlers/test_jsonrpc_handler.py @@ -27,11 +27,15 @@ AgentCapabilities, AgentCard, Artifact, + AuthenticatedExtendedCardNotConfiguredError, CancelTaskRequest, CancelTaskSuccessResponse, DeleteTaskPushNotificationConfigParams, DeleteTaskPushNotificationConfigRequest, DeleteTaskPushNotificationConfigSuccessResponse, + GetAuthenticatedExtendedCardRequest, + GetAuthenticatedExtendedCardResponse, + GetAuthenticatedExtendedCardSuccessResponse, GetTaskPushNotificationConfigParams, GetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigResponse, @@ -1189,3 +1193,59 @@ async def test_on_delete_push_notification_error(self) -> None: # Assert self.assertIsInstance(response.root, JSONRPCErrorResponse) self.assertEqual(response.root.error, UnsupportedOperationError()) # type: ignore + + async def test_get_authenticated_extended_card_success(self) -> None: + """Test successful retrieval of the authenticated extended agent card.""" + # Arrange + mock_request_handler = AsyncMock(spec=DefaultRequestHandler) + mock_extended_card = AgentCard( + name='Extended Card', + description='More details', + url='http://agent.example.com/api', + version='1.1', + capabilities=AgentCapabilities(), + default_input_modes=['text/plain'], + default_output_modes=['application/json'], + skills=[], + ) + handler = JSONRPCHandler( + self.mock_agent_card, + mock_request_handler, + extended_agent_card=mock_extended_card, + ) + request = GetAuthenticatedExtendedCardRequest(id='ext-card-req-1') + call_context = ServerCallContext(state={'foo': 'bar'}) + + # Act + response: GetAuthenticatedExtendedCardResponse = ( + await handler.get_authenticated_extended_card(request, call_context) + ) + + # Assert + self.assertIsInstance( + response.root, GetAuthenticatedExtendedCardSuccessResponse + ) + self.assertEqual(response.root.id, 'ext-card-req-1') + self.assertEqual(response.root.result, mock_extended_card) + + async def test_get_authenticated_extended_card_not_configured(self) -> None: + """Test error when authenticated extended agent card is not configured.""" + # Arrange + mock_request_handler = AsyncMock(spec=DefaultRequestHandler) + handler = JSONRPCHandler( + self.mock_agent_card, mock_request_handler, extended_agent_card=None + ) + request = GetAuthenticatedExtendedCardRequest(id='ext-card-req-2') + call_context = ServerCallContext(state={'foo': 'bar'}) + + # Act + response: GetAuthenticatedExtendedCardResponse = ( + await handler.get_authenticated_extended_card(request, call_context) + ) + + # Assert + self.assertIsInstance(response.root, JSONRPCErrorResponse) + self.assertEqual(response.root.id, 'ext-card-req-2') + self.assertIsInstance( + response.root.error, AuthenticatedExtendedCardNotConfiguredError + ) diff --git a/tests/server/test_integration.py b/tests/server/test_integration.py index a5554ead..9268e11c 100644 --- a/tests/server/test_integration.py +++ b/tests/server/test_integration.py @@ -779,10 +779,7 @@ def test_invalid_request_structure(client: TestClient): """Test handling an invalid request structure.""" response = client.post( '/', - json={ - # Missing required fields - 'id': '123' - }, + json={'id': '123'}, ) assert response.status_code == 200 data = response.json() From 8871d9877d236291c67e00258666f04af6b65ce5 Mon Sep 17 00:00:00 2001 From: Krishna Thota Date: Mon, 28 Jul 2025 17:01:27 -0700 Subject: [PATCH 2/3] feat: support for authenticated extended card method --- tests/server/test_integration.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/server/test_integration.py b/tests/server/test_integration.py index 9268e11c..a5554ead 100644 --- a/tests/server/test_integration.py +++ b/tests/server/test_integration.py @@ -779,7 +779,10 @@ def test_invalid_request_structure(client: TestClient): """Test handling an invalid request structure.""" response = client.post( '/', - json={'id': '123'}, + json={ + # Missing required fields + 'id': '123' + }, ) assert response.status_code == 200 data = response.json() From d20477e33f7d88d430be545370b0845119d830ea Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Tue, 29 Jul 2025 18:15:35 +0100 Subject: [PATCH 3/3] Refresh from committed version --- src/a2a/types.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/a2a/types.py b/src/a2a/types.py index 4a02a43e..d64a9fab 100644 --- a/src/a2a/types.py +++ b/src/a2a/types.py @@ -398,7 +398,7 @@ class FileWithUri(A2ABaseModel): class GetAuthenticatedExtendedCardRequest(A2ABaseModel): """ - Represents a JSON-RPC request for the `agent/authenticatedExtendedCard` method. + Represents a JSON-RPC request for the `agent/getAuthenticatedExtendedCard` method. """ id: str | int @@ -409,11 +409,11 @@ class GetAuthenticatedExtendedCardRequest(A2ABaseModel): """ The version of the JSON-RPC protocol. MUST be exactly "2.0". """ - method: Literal['agent/authenticatedExtendedCard'] = ( - 'agent/authenticatedExtendedCard' + method: Literal['agent/getAuthenticatedExtendedCard'] = ( + 'agent/getAuthenticatedExtendedCard' ) """ - The method name. Must be 'agent/authenticatedExtendedCard'. + The method name. Must be 'agent/getAuthenticatedExtendedCard'. """ @@ -1799,7 +1799,7 @@ class AgentCard(A2ABaseModel): class GetAuthenticatedExtendedCardSuccessResponse(A2ABaseModel): """ - Represents a successful JSON-RPC response for the `agent/authenticatedExtendedCard` method. + Represents a successful JSON-RPC response for the `agent/getAuthenticatedExtendedCard` method. """ id: str | int | None = None @@ -1877,7 +1877,7 @@ class GetAuthenticatedExtendedCardResponse( ): root: JSONRPCErrorResponse | GetAuthenticatedExtendedCardSuccessResponse """ - Represents a JSON-RPC response for the `agent/authenticatedExtendedCard` method. + Represents a JSON-RPC response for the `agent/getAuthenticatedExtendedCard` method. """