Skip to content

Commit 3b581d1

Browse files
authored
Merge branch 'main' into auto-update-a2a-types-e162c0c6c4f609d2f4eef9042466d176ec75ebda
2 parents 9ea0fde + b567e80 commit 3b581d1

7 files changed

Lines changed: 206 additions & 5 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def add_routes_to_app(
9696
self.handle_deprecated_agent_card_path
9797
)
9898

99+
# TODO: deprecated endpoint to be removed in a future release
99100
if self.agent_card.supports_authenticated_extended_card:
100101
app.get(extended_agent_card_url)(
101102
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,
@@ -143,7 +145,9 @@ def __init__(
143145
self.agent_card = agent_card
144146
self.extended_agent_card = extended_agent_card
145147
self.handler = JSONRPCHandler(
146-
agent_card=agent_card, request_handler=http_handler
148+
agent_card=agent_card,
149+
request_handler=http_handler,
150+
extended_agent_card=extended_agent_card,
147151
)
148152
if (
149153
self.agent_card.supports_authenticated_extended_card
@@ -213,7 +217,16 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
213217

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

219232
request_id = a2a_request.root.id
@@ -353,6 +366,13 @@ async def _process_non_streaming_request(
353366
context,
354367
)
355368
)
369+
case GetAuthenticatedExtendedCardRequest():
370+
handler_result = (
371+
await self.handler.get_authenticated_extended_card(
372+
request_obj,
373+
context,
374+
)
375+
)
356376
case _:
357377
logger.error(
358378
f'Unhandled validated request type: {type(request_obj)}'
@@ -450,6 +470,10 @@ async def _handle_get_authenticated_extended_agent_card(
450470
self, request: Request
451471
) -> JSONResponse:
452472
"""Handles GET requests for the authenticated extended agent card."""
473+
logger.warning(
474+
'HTTP GET for authenticated extended card has been called by a client. '
475+
'This endpoint is deprecated in favor of agent/authenticatedExtendedCard JSON-RPC method and will be removed in a future release.'
476+
)
453477
if not self.agent_card.supports_authenticated_extended_card:
454478
return JSONResponse(
455479
{'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
@@ -98,6 +98,7 @@ def routes(
9898
)
9999
)
100100

101+
# TODO: deprecated endpoint to be removed in a future release
101102
if self.agent_card.supports_authenticated_extended_card:
102103
app_routes.append(
103104
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+
)

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

tests/server/test_integration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,8 @@ def test_invalid_request_structure(client: TestClient):
836836
'/',
837837
json={
838838
# Missing required fields
839-
'id': '123'
839+
'id': '123',
840+
'method': 'foo/bar',
840841
},
841842
)
842843
assert response.status_code == 200

tests/test_types.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
FileWithUri,
2525
GetTaskPushNotificationConfigParams,
2626
GetTaskPushNotificationConfigRequest,
27+
GetAuthenticatedExtendedCardRequest,
28+
GetAuthenticatedExtendedCardResponse,
29+
GetAuthenticatedExtendedCardSuccessResponse,
2730
GetTaskPushNotificationConfigResponse,
2831
GetTaskPushNotificationConfigSuccessResponse,
2932
GetTaskRequest,
@@ -968,6 +971,21 @@ def test_a2a_request_root_model() -> None:
968971
assert isinstance(a2a_req_task_resubscribe_req.root.params, TaskIdParams)
969972
assert a2a_req_task_resubscribe_req.root.method == 'tasks/resubscribe'
970973

974+
# GetAuthenticatedExtendedCardRequest
975+
get_auth_card_req_data: dict[str, Any] = {
976+
'jsonrpc': '2.0',
977+
'method': 'agent/getAuthenticatedExtendedCard',
978+
'id': 2,
979+
}
980+
a2a_req_get_auth_card = A2ARequest.model_validate(get_auth_card_req_data)
981+
assert isinstance(
982+
a2a_req_get_auth_card.root, GetAuthenticatedExtendedCardRequest
983+
)
984+
assert (
985+
a2a_req_get_auth_card.root.method
986+
== 'agent/getAuthenticatedExtendedCard'
987+
)
988+
971989
# Invalid method case
972990
invalid_req_data: dict[str, Any] = {
973991
'jsonrpc': '2.0',
@@ -1055,6 +1073,14 @@ def test_a2a_request_root_model_id_validation() -> None:
10551073
with pytest.raises(ValidationError):
10561074
A2ARequest.model_validate(task_resubscribe_req_data)
10571075

1076+
# GetAuthenticatedExtendedCardRequest
1077+
get_auth_card_req_data: dict[str, Any] = {
1078+
'jsonrpc': '2.0',
1079+
'method': 'agent/getAuthenticatedExtendedCard',
1080+
}
1081+
with pytest.raises(ValidationError):
1082+
A2ARequest.model_validate(get_auth_card_req_data) # missing id
1083+
10581084

10591085
def test_content_type_not_supported_error():
10601086
# Test ContentTypeNotSupportedError
@@ -1544,11 +1570,11 @@ def test_camelCase() -> None:
15441570
description='Just a hello world agent',
15451571
url='http://localhost:9999/',
15461572
version='1.0.0',
1547-
defaultInputModes=['text'],
1548-
defaultOutputModes=['text'],
1573+
defaultInputModes=['text'], # type: ignore
1574+
defaultOutputModes=['text'], # type: ignore
15491575
capabilities=AgentCapabilities(streaming=True),
15501576
skills=[skill],
1551-
supportsAuthenticatedExtendedCard=True,
1577+
supportsAuthenticatedExtendedCard=True, # type: ignore
15521578
)
15531579

15541580
# Test setting an attribute via camelCase alias
@@ -1568,3 +1594,56 @@ def test_camelCase() -> None:
15681594
assert agent_card.supports_authenticated_extended_card is False
15691595
assert default_input_modes == ['text']
15701596
assert agent_card.default_input_modes == ['text']
1597+
1598+
1599+
def test_get_authenticated_extended_card_request() -> None:
1600+
req_data: dict[str, Any] = {
1601+
'jsonrpc': '2.0',
1602+
'method': 'agent/getAuthenticatedExtendedCard',
1603+
'id': 5,
1604+
}
1605+
req = GetAuthenticatedExtendedCardRequest.model_validate(req_data)
1606+
assert req.method == 'agent/getAuthenticatedExtendedCard'
1607+
assert req.id == 5
1608+
# This request has no params, so we don't check for that.
1609+
1610+
with pytest.raises(ValidationError): # Wrong method literal
1611+
GetAuthenticatedExtendedCardRequest.model_validate(
1612+
{**req_data, 'method': 'wrong/method'}
1613+
)
1614+
1615+
with pytest.raises(ValidationError): # Missing id
1616+
GetAuthenticatedExtendedCardRequest.model_validate(
1617+
{'jsonrpc': '2.0', 'method': 'agent/getAuthenticatedExtendedCard'}
1618+
)
1619+
1620+
1621+
def test_get_authenticated_extended_card_response() -> None:
1622+
resp_data: dict[str, Any] = {
1623+
'jsonrpc': '2.0',
1624+
'result': MINIMAL_AGENT_CARD,
1625+
'id': 'resp-1',
1626+
}
1627+
resp = GetAuthenticatedExtendedCardResponse.model_validate(resp_data)
1628+
assert resp.root.id == 'resp-1'
1629+
assert isinstance(resp.root, GetAuthenticatedExtendedCardSuccessResponse)
1630+
assert isinstance(resp.root.result, AgentCard)
1631+
assert resp.root.result.name == 'TestAgent'
1632+
1633+
with pytest.raises(ValidationError): # Result is not an AgentCard
1634+
GetAuthenticatedExtendedCardResponse.model_validate(
1635+
{'jsonrpc': '2.0', 'result': {'wrong': 'data'}, 'id': 1}
1636+
)
1637+
1638+
resp_data_err: dict[str, Any] = {
1639+
'jsonrpc': '2.0',
1640+
'error': JSONRPCError(**TaskNotFoundError().model_dump()),
1641+
'id': 'resp-1',
1642+
}
1643+
resp_err = GetAuthenticatedExtendedCardResponse.model_validate(
1644+
resp_data_err
1645+
)
1646+
assert resp_err.root.id == 'resp-1'
1647+
assert isinstance(resp_err.root, JSONRPCErrorResponse)
1648+
assert resp_err.root.error is not None
1649+
assert isinstance(resp_err.root.error, JSONRPCError)

0 commit comments

Comments
 (0)