Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/google/adk/a2a/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"A2aCardRequestConfig",
"A2AClientError",
"A2aRemoteAgentConfig",
"ADK_A2A_ALLOW_INSECURE_HTTP",
"AgentCardResolutionError",
"CardRequestInterceptor",
"ParametersConfig",
Expand All @@ -33,6 +34,7 @@ def __getattr__(name: str) -> object:
"A2aCardRequestConfig",
"A2AClientError",
"A2aRemoteAgentConfig",
"ADK_A2A_ALLOW_INSECURE_HTTP",
"AgentCardResolutionError",
"CardRequestInterceptor",
"ParametersConfig",
Expand All @@ -45,6 +47,7 @@ def __getattr__(name: str) -> object:
from ._remote_a2a_agent import RemoteA2aAgent
from .config import A2aCardRequestConfig
from .config import A2aRemoteAgentConfig
from .config import ADK_A2A_ALLOW_INSECURE_HTTP
from .config import CardRequestInterceptor
from .config import ParametersConfig
from .config import RequestInterceptor
Expand All @@ -55,6 +58,8 @@ def __getattr__(name: str) -> object:
return A2AClientError
elif name == "A2aRemoteAgentConfig":
return A2aRemoteAgentConfig
elif name == "ADK_A2A_ALLOW_INSECURE_HTTP":
return ADK_A2A_ALLOW_INSECURE_HTTP
elif name == "AgentCardResolutionError":
return AgentCardResolutionError
elif name == "CardRequestInterceptor":
Expand Down
56 changes: 47 additions & 9 deletions src/google/adk/a2a/agent/_remote_a2a_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
from ...flows.llm_flows.functions import REQUEST_INPUT_FUNCTION_CALL_NAME
from ...sessions.session import Session
from ...utils.context_utils import Aclosing
from ...utils.env_utils import is_env_enabled
from ..converters.event_converter import convert_a2a_message_to_event
from ..converters.event_converter import convert_a2a_task_to_event
from ..converters.event_converter import convert_event_to_a2a_message
Expand All @@ -84,6 +85,7 @@
from ..logs.log_utils import build_a2a_response_log
from .config import A2aCardRequestConfig
from .config import A2aRemoteAgentConfig
from .config import ADK_A2A_ALLOW_INSECURE_HTTP
from .config import CardRequestInterceptor
from .config import ParametersConfig
from .config import RequestInterceptor
Expand All @@ -95,6 +97,7 @@

__all__ = [
"A2AClientError",
"ADK_A2A_ALLOW_INSECURE_HTTP",
"AGENT_CARD_WELL_KNOWN_PATH",
"AgentCardResolutionError",
"RemoteA2aAgent",
Expand Down Expand Up @@ -680,6 +683,7 @@ def __init__(
auth_scheme: Optional[AuthScheme] = None,
auth_credential: Optional[AuthCredential] = None,
credential_key: Optional[str] = None,
allow_insecure_http: bool = False,
**kwargs: Any,
) -> None:
"""Initialize RemoteA2aAgent.
Expand Down Expand Up @@ -714,6 +718,10 @@ def __init__(
`auth_scheme` is None.
credential_key: Optional key under which the resolved credential is
cached. Defaults to a digest of the scheme and the credential.
allow_insecure_http: If True, allow plaintext HTTP for agent card
resolution and RPC targets (e.g. within a service mesh with mTLS).
Defaults to False. Can also be enabled via config or the
``ADK_A2A_ALLOW_INSECURE_HTTP=1`` environment variable.
**kwargs: Additional arguments passed to BaseAgent

Raises:
Expand Down Expand Up @@ -742,7 +750,10 @@ def __init__(
self._a2a_request_meta_provider = a2a_request_meta_provider
self._full_history_when_stateless_param = full_history_when_stateless
self._context_builder = context_builder
self._allow_insecure_http_param = allow_insecure_http
self._config = config or A2aRemoteAgentConfig()
if allow_insecure_http:
self._config.allow_insecure_http = True

if not use_legacy:
if self._config.request_interceptors is None:
Expand Down Expand Up @@ -818,6 +829,23 @@ def _full_history_when_stateless(self) -> bool:
def _full_history_when_stateless(self, value: bool) -> None:
self._full_history_when_stateless_param = value

@property
def _allow_insecure_http(self) -> bool:
return (
self._allow_insecure_http_param
or self._config.allow_insecure_http
or is_env_enabled(ADK_A2A_ALLOW_INSECURE_HTTP)
)

@_allow_insecure_http.setter
def _allow_insecure_http(self, value: bool) -> None:
self._allow_insecure_http_param = value
self._config.allow_insecure_http = value

@property
def allow_insecure_http(self) -> bool:
return self._allow_insecure_http

async def _resolve_auth_credential(
self, ctx: InvocationContext
) -> Optional[Event]:
Expand Down Expand Up @@ -956,11 +984,16 @@ async def _resolve_agent_card(
# The card request interceptors attach the credential resolved for this
# invocation, so the scheme is checked before the fetch rather than with
# the card's RPC targets afterwards -- by then the credential has already
# gone out on the wire. Plain http stays allowed on a loopback host, the
# same carve-out `_validate_card_rpc_targets` applies.
# gone out on the wire. Plain http stays allowed on a loopback host, or
# when allow_insecure_http is True.
parsed_source = urlparse(agent_card_source)
if parsed_source.scheme.lower() != "https" and not _is_loopback_host(
parsed_source.hostname
scheme = parsed_source.scheme.lower()
if scheme != "https" and not (
scheme == "http"
and (
self._allow_insecure_http
or _is_loopback_host(parsed_source.hostname)
)
):
raise AgentCardResolutionError(
"Agent card URL must use https, or http on a loopback host:"
Expand Down Expand Up @@ -995,9 +1028,9 @@ def _validate_card_rpc_targets(self, agent_card: AgentCard) -> None:

Every URL the card offers is checked, not only the one this ADK version
would select, because the client factory negotiates the endpoint across
the card's whole interface list. Each must be https and share the origin
the card was fetched from; plain http stays allowed on a loopback host,
the local-development shape the A2A helpers emit.
the card's whole interface list. Each must be https (or http if
allow_insecure_http is True or on a loopback host) and share the origin
the card was fetched from.

A card passed in directly or read from a local file did not come off the
network here, so its target is left to the caller.
Expand All @@ -1015,8 +1048,13 @@ def _validate_card_rpc_targets(self, agent_card: AgentCard) -> None:

for card_url in _compat.agent_card_rpc_urls(agent_card):
parsed_card = urlparse(card_url)
if parsed_card.scheme.lower() != "https" and not _is_loopback_host(
parsed_card.hostname
card_scheme = parsed_card.scheme.lower()
if card_scheme != "https" and not (
card_scheme == "http"
and (
self._allow_insecure_http
or _is_loopback_host(parsed_card.hostname)
)
):
raise AgentCardResolutionError(
"Agent card RPC URL must use https, or http on a loopback host:"
Expand Down
10 changes: 9 additions & 1 deletion src/google/adk/a2a/agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from a2a.server.events import Event as A2AEvent
from a2a.types import Message as A2AMessage
from pydantic import BaseModel
from pydantic import Field
from typing_extensions import Self

from .. import _compat
Expand All @@ -40,8 +41,11 @@
from ...a2a.converters.to_adk_event import convert_a2a_task_to_event
from ...agents.invocation_context import InvocationContext
from ...events.event import Event
from ...utils.env_utils import is_env_enabled
from .._compat import A2AClientEvent

ADK_A2A_ALLOW_INSECURE_HTTP = "ADK_A2A_ALLOW_INSECURE_HTTP"


class ParametersConfig(BaseModel):
"""Configuration for the parameters passed to the A2A send_message request."""
Expand Down Expand Up @@ -141,6 +145,10 @@ class A2aRemoteAgentConfig(BaseModel):
forward_session_id_as_context_id: bool = False
"""Whether to forward the local session ID as context_id when no context_id is present."""

allow_insecure_http: bool = Field(
default_factory=lambda: is_env_enabled(ADK_A2A_ALLOW_INSECURE_HTTP)
)

def __deepcopy__(
self, memo: dict[int, Any] | None = None
) -> A2aRemoteAgentConfig:
Expand All @@ -149,7 +157,7 @@ def __deepcopy__(
cls = self.__class__
copied_values: dict[str, Any] = {}
for k, v in self.__dict__.items():
if not k.startswith('_'):
if not k.startswith("_"):
if callable(v):
copied_values[k] = v
else:
Expand Down
2 changes: 2 additions & 0 deletions src/google/adk/agents/remote_a2a_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@

from ..a2a.agent._remote_a2a_agent import A2A_METADATA_PREFIX as A2A_METADATA_PREFIX
from ..a2a.agent._remote_a2a_agent import A2AClientError as A2AClientError
from ..a2a.agent._remote_a2a_agent import ADK_A2A_ALLOW_INSECURE_HTTP as ADK_A2A_ALLOW_INSECURE_HTTP
from ..a2a.agent._remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH as AGENT_CARD_WELL_KNOWN_PATH
from ..a2a.agent._remote_a2a_agent import AgentCardResolutionError as AgentCardResolutionError
from ..a2a.agent._remote_a2a_agent import DEFAULT_TIMEOUT as DEFAULT_TIMEOUT
from ..a2a.agent._remote_a2a_agent import RemoteA2aAgent as RemoteA2aAgent

__all__ = [
"A2AClientError",
"ADK_A2A_ALLOW_INSECURE_HTTP",
"AGENT_CARD_WELL_KNOWN_PATH",
"AgentCardResolutionError",
"RemoteA2aAgent",
Expand Down
Loading
Loading