-
Notifications
You must be signed in to change notification settings - Fork 429
Expand file tree
/
Copy pathcard_resolver.py
More file actions
131 lines (110 loc) · 4.56 KB
/
card_resolver.py
File metadata and controls
131 lines (110 loc) · 4.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import json
import logging
from collections.abc import Callable
from typing import Any
import httpx
from pydantic import ValidationError
from a2a.client.errors import (
A2AClientHTTPError,
A2AClientJSONError,
)
from a2a.types import (
AgentCard,
)
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
from a2a.utils.url_validation import (
A2ASSRFValidationError,
validate_agent_card_url,
)
logger = logging.getLogger(__name__)
class A2ACardResolver:
"""Agent Card resolver."""
def __init__(
self,
httpx_client: httpx.AsyncClient,
base_url: str,
agent_card_path: str = AGENT_CARD_WELL_KNOWN_PATH,
) -> None:
"""Initializes the A2ACardResolver.
Args:
httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient).
base_url: The base URL of the agent's host.
agent_card_path: The path to the agent card endpoint, relative to the base URL.
"""
self.base_url = base_url.rstrip('/')
self.agent_card_path = agent_card_path.lstrip('/')
self.httpx_client = httpx_client
async def get_agent_card(
self,
relative_card_path: str | None = None,
http_kwargs: dict[str, Any] | None = None,
signature_verifier: Callable[[AgentCard], None] | None = None,
) -> AgentCard:
"""Fetches an agent card from a specified path relative to the base_url.
If relative_card_path is None, it defaults to the resolver's configured
agent_card_path (for the public agent card).
Args:
relative_card_path: Optional path to the agent card endpoint,
relative to the base URL. If None, uses the default public
agent card path. Use `'/'` for an empty path.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.get request.
signature_verifier: A callable used to verify the agent card's signatures.
Returns:
An `AgentCard` object representing the agent's capabilities.
Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON,
validated against the AgentCard schema, or fails SSRF URL validation.
"""
if not relative_card_path:
path_segment = self.agent_card_path
else:
path_segment = relative_card_path.lstrip('/')
target_url = f'{self.base_url}/{path_segment}'
try:
response = await self.httpx_client.get(
target_url,
**(http_kwargs or {}),
)
response.raise_for_status()
agent_card_data = response.json()
logger.info(
'Successfully fetched agent card data from %s: %s',
target_url,
agent_card_data,
)
agent_card = AgentCard.model_validate(agent_card_data)
# Validate card.url before returning (fix for A2A-SSRF-01).
# Without this check, any caller who controls the card endpoint
# can redirect all subsequent RPC calls to an internal address.
try:
validate_agent_card_url(agent_card.url)
# Also validate any additional transport URLs declared in the card.
for iface in agent_card.additional_interfaces or []:
validate_agent_card_url(iface.url)
except A2ASSRFValidationError as e:
raise A2AClientJSONError(
f'AgentCard from {target_url} failed SSRF URL validation: {e}'
) from e
if signature_verifier:
signature_verifier(agent_card)
except httpx.HTTPStatusError as e:
raise A2AClientHTTPError(
e.response.status_code,
f'Failed to fetch agent card from {target_url}: {e}',
) from e
except json.JSONDecodeError as e:
raise A2AClientJSONError(
f'Failed to parse JSON for agent card from {target_url}: {e}'
) from e
except httpx.RequestError as e:
raise A2AClientHTTPError(
503,
f'Network communication error fetching agent card from {target_url}: {e}',
) from e
except ValidationError as e:
raise A2AClientJSONError(
f'Failed to validate agent card structure from {target_url}: {e.json()}'
) from e
return agent_card