Skip to content

[Feat]: Agent card validation hook #975

Open

Description

@ishymko

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.

UPD: see #1023

Describe alternatives you've considered

The PR referenced implements such validation unconditionally, however using A2A for internal addresses is a valid scenario.

Activity

  1. added theissue type on Apr 16, 2026
  2. mtsmyassin commented on Apr 17, 2026

    @mtsmyassin

    Proposed Implementation

    Here's a concrete sketch of the agent card validation hook:

    1. Validator types (src/a2a/client/card_validators.py)

    """Built-in validators for agent cards."""
    from __future__ import annotations
    
    import ipaddress
    from collections.abc import Callable
    from urllib.parse import urlparse
    
    from a2a.types import AgentCard
    
    # Type alias: a validator raises on invalid, returns None on success
    CardValidator = Callable[[AgentCard], None]
    
    
    class InvalidAgentCardError(ValueError):
        """Raised when an agent card fails validation."""
    
    
    def reject_non_https_urls(card: AgentCard) -> None:
        """Rejects cards whose primary URL is not https://."""
        parsed = urlparse(card.url)
        if parsed.scheme != "https":
            raise InvalidAgentCardError(
                f"Agent card URL must use https, got: {card.url!r}"
            )
        # Also validate any interface URLs if present
        for iface in getattr(card, "additional_interfaces", None) or []:
            if urlparse(iface.url).scheme != "https":
                raise InvalidAgentCardError(
                    f"Additional interface URL must use https, got: {iface.url!r}"
                )
    
    
    def reject_private_urls(card: AgentCard) -> None:
        """Rejects cards that point to private/loopback/link-local addresses."""
        _check_not_private(card.url)
        for iface in getattr(card, "additional_interfaces", None) or []:
            _check_not_private(iface.url)
    
    
    def _check_not_private(url: str) -> None:
        parsed = urlparse(url)
        host = parsed.hostname
        if host is None:
            raise InvalidAgentCardError(f"URL has no hostname: {url!r}")
        try:
            ip = ipaddress.ip_address(host)
        except ValueError:
            # Not a raw IP - skip (DNS-level SSRF protection needs separate handling)
            return
        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
            raise InvalidAgentCardError(
                f"URL points to private/loopback address: {url!r}"
            )

    2. Client factory integration (src/a2a/client/client_factory.py)

    class ClientFactory:
        def __init__(
            self,
            ...,
            card_validators: list[CardValidator] | None = None,
        ) -> None:
            ...
            self._card_validators = card_validators or []
    
        async def create_from_url(self, url: str, ...) -> Client:
            card = await self._fetch_agent_card(url)
            self._validate_card(card)
            return self.create(card=card, ...)
    
        def create(self, card: AgentCard, ...) -> Client:
            self._validate_card(card)
            ...