Skip to content

Commit 156f519

Browse files
committed
feat: support for authenticated extended card method
1 parent ddbfff9 commit 156f519

File tree

7 files changed

+203
-6
lines changed

7 files changed

+203
-6
lines changed

src/a2a/server/apps/jsonrpc/fastapi_app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def add_routes_to_app(
8989
)(self._handle_requests)
9090
app.get(agent_card_url)(self._handle_get_agent_card)
9191

92+
# TODO: deprecated endpoint to be removed in a future release
9293
if self.agent_card.supports_authenticated_extended_card:
9394
app.get(extended_agent_card_url)(
9495
self._handle_get_authenticated_extended_agent_card

src/a2a/server/apps/jsonrpc/jsonrpc_app.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
AgentCard,
3333
CancelTaskRequest,
3434
DeleteTaskPushNotificationConfigRequest,
35+
GetAuthenticatedExtendedCardRequest,
3536
GetTaskPushNotificationConfigRequest,
3637
GetTaskRequest,
3738
InternalError,
3839
InvalidRequestError,
3940
JSONParseError,
4041
JSONRPCError,
4142
JSONRPCErrorResponse,
43+
JSONRPCRequest,
4244
JSONRPCResponse,
4345
ListTaskPushNotificationConfigRequest,
4446
SendMessageRequest,
@@ -142,7 +144,9 @@ def __init__(
142144
self.agent_card = agent_card
143145
self.extended_agent_card = extended_agent_card
144146
self.handler = JSONRPCHandler(
145-
agent_card=agent_card, request_handler=http_handler
147+
agent_card=agent_card,
148+
request_handler=http_handler,
149+
extended_agent_card=extended_agent_card,
146150
)
147151
if (
148152
self.agent_card.supports_authenticated_extended_card
@@ -212,7 +216,16 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
212216

213217
try:
214218
body = await request.json()
219+
if isinstance(body, dict):
220+
request_id = body.get('id')
221+
222+
# First, validate the basic JSON-RPC structure. This is crucial
223+
# because the A2ARequest model is a discriminated union where some
224+
# request types have default values for the 'method' field
225+
JSONRPCRequest.model_validate(body)
226+
215227
a2a_request = A2ARequest.model_validate(body)
228+
216229
call_context = self._context_builder.build(request)
217230

218231
request_id = a2a_request.root.id
@@ -352,6 +365,13 @@ async def _process_non_streaming_request(
352365
context,
353366
)
354367
)
368+
case GetAuthenticatedExtendedCardRequest():
369+
handler_result = (
370+
await self.handler.get_authenticated_extended_card(
371+
request_obj,
372+
context,
373+
)
374+
)
355375
case _:
356376
logger.error(
357377
f'Unhandled validated request type: {type(request_obj)}'
@@ -440,6 +460,10 @@ async def _handle_get_authenticated_extended_agent_card(
440460
self, request: Request
441461
) -> JSONResponse:
442462
"""Handles GET requests for the authenticated extended agent card."""
463+
logger.warning(
464+
'HTTP GET for authenticated extended card has been called by a client. '
465+
'This endpoint is deprecated in favor of agent/authenticatedExtendedCard JSON-RPC method and will be removed in a future release.'
466+
)
443467
if not self.agent_card.supports_authenticated_extended_card:
444468
return JSONResponse(
445469
{'error': 'Extended agent card not supported or not enabled.'},

src/a2a/server/apps/jsonrpc/starlette_app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def routes(
8686
),
8787
]
8888

89+
# TODO: deprecated endpoint to be removed in a future release
8990
if self.agent_card.supports_authenticated_extended_card:
9091
app_routes.append(
9192
Route(

src/a2a/server/request_handlers/jsonrpc_handler.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
from a2a.server.request_handlers.response_helpers import prepare_response_object
88
from a2a.types import (
99
AgentCard,
10+
AuthenticatedExtendedCardNotConfiguredError,
1011
CancelTaskRequest,
1112
CancelTaskResponse,
1213
CancelTaskSuccessResponse,
1314
DeleteTaskPushNotificationConfigRequest,
1415
DeleteTaskPushNotificationConfigResponse,
1516
DeleteTaskPushNotificationConfigSuccessResponse,
17+
GetAuthenticatedExtendedCardRequest,
18+
GetAuthenticatedExtendedCardResponse,
19+
GetAuthenticatedExtendedCardSuccessResponse,
1620
GetTaskPushNotificationConfigRequest,
1721
GetTaskPushNotificationConfigResponse,
1822
GetTaskPushNotificationConfigSuccessResponse,
@@ -57,15 +61,18 @@ def __init__(
5761
self,
5862
agent_card: AgentCard,
5963
request_handler: RequestHandler,
64+
extended_agent_card: AgentCard | None = None,
6065
):
6166
"""Initializes the JSONRPCHandler.
6267
6368
Args:
6469
agent_card: The AgentCard describing the agent's capabilities.
6570
request_handler: The underlying `RequestHandler` instance to delegate requests to.
71+
extended_agent_card: An optional, distinct Extended AgentCard to be served
6672
"""
6773
self.agent_card = agent_card
6874
self.request_handler = request_handler
75+
self.extended_agent_card = extended_agent_card
6976

7077
async def on_message_send(
7178
self,
@@ -395,3 +402,31 @@ async def delete_push_notification_config(
395402
id=request.id, error=e.error if e.error else InternalError()
396403
)
397404
)
405+
406+
async def get_authenticated_extended_card(
407+
self,
408+
request: GetAuthenticatedExtendedCardRequest,
409+
context: ServerCallContext | None = None,
410+
) -> GetAuthenticatedExtendedCardResponse:
411+
"""Handles the 'agent/authenticatedExtendedCard' JSON-RPC method.
412+
413+
Args:
414+
request: The incoming `GetAuthenticatedExtendedCardRequest` object.
415+
context: Context provided by the server.
416+
417+
Returns:
418+
A `GetAuthenticatedExtendedCardResponse` object containing the config or a JSON-RPC error.
419+
"""
420+
if self.extended_agent_card is None:
421+
return GetAuthenticatedExtendedCardResponse(
422+
root=JSONRPCErrorResponse(
423+
id=request.id,
424+
error=AuthenticatedExtendedCardNotConfiguredError(),
425+
)
426+
)
427+
428+
return GetAuthenticatedExtendedCardResponse(
429+
root=GetAuthenticatedExtendedCardSuccessResponse(
430+
id=request.id, result=self.extended_agent_card
431+
)
432+
)

src/a2a/types.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,27 @@ class AgentSkill(A2ABaseModel):
172172
"""
173173

174174

175+
class AuthenticatedExtendedCardNotConfiguredError(A2ABaseModel):
176+
"""
177+
An A2A-specific error indicating that the agent does not have an
178+
Authenticated Extended Card configured
179+
"""
180+
181+
code: Literal[-32007] = -32007
182+
"""
183+
The error code for when an authenticated extended card is not configured.
184+
"""
185+
data: Any | None = None
186+
"""
187+
A primitive or structured value containing additional information about the error.
188+
This may be omitted.
189+
"""
190+
message: str | None = 'Authenticated Extended Card not configured'
191+
"""
192+
The error message.
193+
"""
194+
195+
175196
class AuthorizationCodeOAuthFlow(A2ABaseModel):
176197
"""
177198
Defines configuration details for the OAuth 2.0 Authorization Code flow.
@@ -375,6 +396,27 @@ class FileWithUri(A2ABaseModel):
375396
"""
376397

377398

399+
class GetAuthenticatedExtendedCardRequest(A2ABaseModel):
400+
"""
401+
Represents a JSON-RPC request for the `agent/authenticatedExtendedCard` method.
402+
"""
403+
404+
id: str | int
405+
"""
406+
The identifier for this request.
407+
"""
408+
jsonrpc: Literal['2.0'] = '2.0'
409+
"""
410+
The version of the JSON-RPC protocol. MUST be exactly "2.0".
411+
"""
412+
method: Literal['agent/authenticatedExtendedCard'] = (
413+
'agent/authenticatedExtendedCard'
414+
)
415+
"""
416+
The method name. Must be 'agent/authenticatedExtendedCard'.
417+
"""
418+
419+
378420
class GetTaskPushNotificationConfigParams(A2ABaseModel):
379421
"""
380422
Defines parameters for fetching a specific push notification configuration for a task.
@@ -999,6 +1041,7 @@ class A2AError(
9991041
| UnsupportedOperationError
10001042
| ContentTypeNotSupportedError
10011043
| InvalidAgentResponseError
1044+
| AuthenticatedExtendedCardNotConfiguredError
10021045
]
10031046
):
10041047
root: (
@@ -1013,6 +1056,7 @@ class A2AError(
10131056
| UnsupportedOperationError
10141057
| ContentTypeNotSupportedError
10151058
| InvalidAgentResponseError
1059+
| AuthenticatedExtendedCardNotConfiguredError
10161060
)
10171061
"""
10181062
A discriminated union of all standard JSON-RPC and A2A-specific error types.
@@ -1170,6 +1214,7 @@ class JSONRPCErrorResponse(A2ABaseModel):
11701214
| UnsupportedOperationError
11711215
| ContentTypeNotSupportedError
11721216
| InvalidAgentResponseError
1217+
| AuthenticatedExtendedCardNotConfiguredError
11731218
)
11741219
"""
11751220
An object describing the error that occurred.
@@ -1625,6 +1670,7 @@ class A2ARequest(
16251670
| TaskResubscriptionRequest
16261671
| ListTaskPushNotificationConfigRequest
16271672
| DeleteTaskPushNotificationConfigRequest
1673+
| GetAuthenticatedExtendedCardRequest
16281674
]
16291675
):
16301676
root: (
@@ -1637,6 +1683,7 @@ class A2ARequest(
16371683
| TaskResubscriptionRequest
16381684
| ListTaskPushNotificationConfigRequest
16391685
| DeleteTaskPushNotificationConfigRequest
1686+
| GetAuthenticatedExtendedCardRequest
16401687
)
16411688
"""
16421689
A discriminated union representing all possible JSON-RPC 2.0 requests supported by the A2A specification.
@@ -1750,6 +1797,25 @@ class AgentCard(A2ABaseModel):
17501797
"""
17511798

17521799

1800+
class GetAuthenticatedExtendedCardSuccessResponse(A2ABaseModel):
1801+
"""
1802+
Represents a successful JSON-RPC response for the `agent/authenticatedExtendedCard` method.
1803+
"""
1804+
1805+
id: str | int | None = None
1806+
"""
1807+
The identifier established by the client.
1808+
"""
1809+
jsonrpc: Literal['2.0'] = '2.0'
1810+
"""
1811+
The version of the JSON-RPC protocol. MUST be exactly "2.0".
1812+
"""
1813+
result: AgentCard
1814+
"""
1815+
The result is an Agent Card object.
1816+
"""
1817+
1818+
17531819
class Task(A2ABaseModel):
17541820
"""
17551821
Represents a single, stateful operation or conversation between a client and an agent.
@@ -1769,7 +1835,7 @@ class Task(A2ABaseModel):
17691835
"""
17701836
id: str
17711837
"""
1772-
A unique identifier for the task, generated by the client for a new task or provided by the agent.
1838+
A unique identifier for the task, generated by the server for a new task.
17731839
"""
17741840
kind: Literal['task'] = 'task'
17751841
"""
@@ -1804,6 +1870,17 @@ class CancelTaskSuccessResponse(A2ABaseModel):
18041870
"""
18051871

18061872

1873+
class GetAuthenticatedExtendedCardResponse(
1874+
RootModel[
1875+
JSONRPCErrorResponse | GetAuthenticatedExtendedCardSuccessResponse
1876+
]
1877+
):
1878+
root: JSONRPCErrorResponse | GetAuthenticatedExtendedCardSuccessResponse
1879+
"""
1880+
Represents a JSON-RPC response for the `agent/authenticatedExtendedCard` method.
1881+
"""
1882+
1883+
18071884
class GetTaskSuccessResponse(A2ABaseModel):
18081885
"""
18091886
Represents a successful JSON-RPC response for the `tasks/get` method.
@@ -1889,6 +1966,7 @@ class JSONRPCResponse(
18891966
| GetTaskPushNotificationConfigSuccessResponse
18901967
| ListTaskPushNotificationConfigSuccessResponse
18911968
| DeleteTaskPushNotificationConfigSuccessResponse
1969+
| GetAuthenticatedExtendedCardSuccessResponse
18921970
]
18931971
):
18941972
root: (
@@ -1901,6 +1979,7 @@ class JSONRPCResponse(
19011979
| GetTaskPushNotificationConfigSuccessResponse
19021980
| ListTaskPushNotificationConfigSuccessResponse
19031981
| DeleteTaskPushNotificationConfigSuccessResponse
1982+
| GetAuthenticatedExtendedCardSuccessResponse
19041983
)
19051984
"""
19061985
A discriminated union representing all possible JSON-RPC 2.0 responses

tests/server/request_handlers/test_jsonrpc_handler.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@
2727
AgentCapabilities,
2828
AgentCard,
2929
Artifact,
30+
AuthenticatedExtendedCardNotConfiguredError,
3031
CancelTaskRequest,
3132
CancelTaskSuccessResponse,
3233
DeleteTaskPushNotificationConfigParams,
3334
DeleteTaskPushNotificationConfigRequest,
3435
DeleteTaskPushNotificationConfigSuccessResponse,
36+
GetAuthenticatedExtendedCardRequest,
37+
GetAuthenticatedExtendedCardResponse,
38+
GetAuthenticatedExtendedCardSuccessResponse,
3539
GetTaskPushNotificationConfigParams,
3640
GetTaskPushNotificationConfigRequest,
3741
GetTaskPushNotificationConfigResponse,
@@ -1189,3 +1193,59 @@ async def test_on_delete_push_notification_error(self) -> None:
11891193
# Assert
11901194
self.assertIsInstance(response.root, JSONRPCErrorResponse)
11911195
self.assertEqual(response.root.error, UnsupportedOperationError()) # type: ignore
1196+
1197+
async def test_get_authenticated_extended_card_success(self) -> None:
1198+
"""Test successful retrieval of the authenticated extended agent card."""
1199+
# Arrange
1200+
mock_request_handler = AsyncMock(spec=DefaultRequestHandler)
1201+
mock_extended_card = AgentCard(
1202+
name='Extended Card',
1203+
description='More details',
1204+
url='http://agent.example.com/api',
1205+
version='1.1',
1206+
capabilities=AgentCapabilities(),
1207+
default_input_modes=['text/plain'],
1208+
default_output_modes=['application/json'],
1209+
skills=[],
1210+
)
1211+
handler = JSONRPCHandler(
1212+
self.mock_agent_card,
1213+
mock_request_handler,
1214+
extended_agent_card=mock_extended_card,
1215+
)
1216+
request = GetAuthenticatedExtendedCardRequest(id='ext-card-req-1')
1217+
call_context = ServerCallContext(state={'foo': 'bar'})
1218+
1219+
# Act
1220+
response: GetAuthenticatedExtendedCardResponse = (
1221+
await handler.get_authenticated_extended_card(request, call_context)
1222+
)
1223+
1224+
# Assert
1225+
self.assertIsInstance(
1226+
response.root, GetAuthenticatedExtendedCardSuccessResponse
1227+
)
1228+
self.assertEqual(response.root.id, 'ext-card-req-1')
1229+
self.assertEqual(response.root.result, mock_extended_card)
1230+
1231+
async def test_get_authenticated_extended_card_not_configured(self) -> None:
1232+
"""Test error when authenticated extended agent card is not configured."""
1233+
# Arrange
1234+
mock_request_handler = AsyncMock(spec=DefaultRequestHandler)
1235+
handler = JSONRPCHandler(
1236+
self.mock_agent_card, mock_request_handler, extended_agent_card=None
1237+
)
1238+
request = GetAuthenticatedExtendedCardRequest(id='ext-card-req-2')
1239+
call_context = ServerCallContext(state={'foo': 'bar'})
1240+
1241+
# Act
1242+
response: GetAuthenticatedExtendedCardResponse = (
1243+
await handler.get_authenticated_extended_card(request, call_context)
1244+
)
1245+
1246+
# Assert
1247+
self.assertIsInstance(response.root, JSONRPCErrorResponse)
1248+
self.assertEqual(response.root.id, 'ext-card-req-2')
1249+
self.assertIsInstance(
1250+
response.root.error, AuthenticatedExtendedCardNotConfiguredError
1251+
)

0 commit comments

Comments
 (0)