You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is your feature request related to a problem? Please describe.
#895 mentions lack of agent card validation on the client which may be used to trigger calls to private URLs when agent card is coming from an untrusted source.
Currently no hooks are provided for such validation, which is especially important for functions like create_client and create_from_url which support URL fetch.
Describe the solution you'd like
The SDK should not define the rules of validation and private URLs should not be forbidden as it can be a perfectly valid use case.
"""Built-in validators for agent cards."""from __future__ importannotationsimportipaddressfromcollections.abcimportCallablefromurllib.parseimporturlparsefroma2a.typesimportAgentCard# Type alias: a validator raises on invalid, returns None on successCardValidator=Callable[[AgentCard], None]
classInvalidAgentCardError(ValueError):
"""Raised when an agent card fails validation."""defreject_non_https_urls(card: AgentCard) ->None:
"""Rejects cards whose primary URL is not https://."""parsed=urlparse(card.url)
ifparsed.scheme!="https":
raiseInvalidAgentCardError(
f"Agent card URL must use https, got: {card.url!r}"
)
# Also validate any interface URLs if presentforifaceingetattr(card, "additional_interfaces", None) or []:
ifurlparse(iface.url).scheme!="https":
raiseInvalidAgentCardError(
f"Additional interface URL must use https, got: {iface.url!r}"
)
defreject_private_urls(card: AgentCard) ->None:
"""Rejects cards that point to private/loopback/link-local addresses."""_check_not_private(card.url)
forifaceingetattr(card, "additional_interfaces", None) or []:
_check_not_private(iface.url)
def_check_not_private(url: str) ->None:
parsed=urlparse(url)
host=parsed.hostnameifhostisNone:
raiseInvalidAgentCardError(f"URL has no hostname: {url!r}")
try:
ip=ipaddress.ip_address(host)
exceptValueError:
# Not a raw IP - skip (DNS-level SSRF protection needs separate handling)returnifip.is_privateorip.is_loopbackorip.is_link_localorip.is_reserved:
raiseInvalidAgentCardError(
f"URL points to private/loopback address: {url!r}"
)
Validators are a list[Callable[[AgentCard], None]] rather than a single callback, so users can compose multiple policies.
Validators raise exceptions rather than returning bool, so error messages can be descriptive.
Using InvalidAgentCardError(ValueError) subclass so it can be caught specifically but also by generic ValueError handlers.
Applied at both create() and create_from_url() entry points.
Default is empty list = no validation = backwards compatible, matching the issue's concern about internal-address use cases.
Related
This also overlaps with #786 (SSRF protection). A DNS-resolving validator (resolving the hostname and checking the resolved IP) would close the full SSRF loop but is more invasive since it requires a network call. Probably best to keep DNS resolution out of this hook and handle it at the HTTP transport layer with a custom resolver, as #786 discusses.
I'd be happy to draft a PR against client_factory.py with the above if this design looks reasonable.
Planning to open a PR adding opt-in agent card validation hooks on ClientFactory / create_client (composable validators, default no-op for backward compatibility). Will include built-in helpers and unit tests.
Planning to open a PR adding opt-in card validation hooks on ClientFactory / create_client (composable validators, default no-op for backward compatibility). Will include built-in helpers and unit tests.
Hi @piyushbag, thank you for your PR, please note that the issue is labeled with maintainers-only, we're not accepting external contributions for it at this point.
Hi @ishymko — this is exactly the shape we ended up with in the A2A ecosystem's Go SDK, so I'd like to align on the hook design before proposing an implementation.
Reference pattern (a2a-go, a2acrypto package): the SDK defines a KeyResolver interface and the verifier delegates all trust decisions to it. The default resolver is safe-by-default (only fetches from a verifier-configured JWKS URL), but callers can inject their own resolver for private-network deployments. The SDK never hardcodes validation rules — it provides the extension point.
Mapping that to a2a-python:
classAgentCardValidator(Protocol):
defvalidate(self, agent_card: AgentCard) ->None: # raises on rejection
...
client=create_client(
url=...,
agent_card_validator=MyValidator(), # optional; None = no validation
)
Key decisions to confirm:
Hook vs. rule set.feat(client): add opt-in card validation hooks #1142 was closed unmerged — was the objection that unconditional validation (SDK-defined rules) is wrong, or something else in the approach? The hook-as-protocol shape above avoids SDK-defined rules entirely; the SDK just calls the injected validator when fetching a card from an untrusted source (create_client / create_from_url).
Sync vs. async. Card fetch is async; a sync validator keeps the hook simple, but network-based validation (e.g. JWKS fetch) needs async. Proposal: support both (AgentCardValidator sync, or an AsyncAgentCardValidator that takes precedence when provided).
Happy to write the implementation if this direction looks right — the composable URL-validation utilities from #1114 can stay as-is, and the hook just sits on top.
Activity
mtsmyassin commentedon Apr 17, 2026
Proposed Implementation
Here's a concrete sketch of the agent card validation hook:
1. Validator types (
src/a2a/client/card_validators.py)2. Client factory integration (
src/a2a/client/client_factory.py)3. Usage
Design notes
list[Callable[[AgentCard], None]]rather than a single callback, so users can compose multiple policies.bool, so error messages can be descriptive.InvalidAgentCardError(ValueError)subclass so it can be caught specifically but also by genericValueErrorhandlers.create()andcreate_from_url()entry points.Related
This also overlaps with #786 (SSRF protection). A DNS-resolving validator (resolving the hostname and checking the resolved IP) would close the full SSRF loop but is more invasive since it requires a network call. Probably best to keep DNS resolution out of this hook and handle it at the HTTP transport layer with a custom resolver, as #786 discusses.
I'd be happy to draft a PR against
client_factory.pywith the above if this design looks reasonable.piyushbag commentedon Jul 21, 2026
Planning to open a PR adding opt-in agent card validation hooks on
ClientFactory/create_client(composable validators, default no-op for backward compatibility). Will include built-in helpers and unit tests.piyushbag commentedon Jul 21, 2026
Planning to open a PR adding opt-in card validation hooks on
ClientFactory/create_client(composable validators, default no-op for backward compatibility). Will include built-in helpers and unit tests.ishymko commentedon Jul 22, 2026
Hi @piyushbag, thank you for your PR, please note that the issue is labeled with
maintainers-only, we're not accepting external contributions for it at this point.kuangmi-bit commentedon Aug 6, 2026
Hi @ishymko — this is exactly the shape we ended up with in the A2A ecosystem's Go SDK, so I'd like to align on the hook design before proposing an implementation.
Reference pattern (a2a-go,
a2acryptopackage): the SDK defines aKeyResolverinterface and the verifier delegates all trust decisions to it. The default resolver is safe-by-default (only fetches from a verifier-configured JWKS URL), but callers can inject their own resolver for private-network deployments. The SDK never hardcodes validation rules — it provides the extension point.Mapping that to a2a-python:
Key decisions to confirm:
create_client/create_from_url).AgentCardValidatorsync, or anAsyncAgentCardValidatorthat takes precedence when provided).Happy to write the implementation if this direction looks right — the composable URL-validation utilities from #1114 can stay as-is, and the hook just sits on top.
feat(utils): add composable UrlValidator for URL screening