From 8c32f340c12e3ad21c873769124aff26b4dd4580 Mon Sep 17 00:00:00 2001 From: Ashfaq <105435085+Ashfaqbs@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:16:21 +0530 Subject: [PATCH] fix(a2a): do not cache remote agent card when validation fails RemoteA2aAgent._ensure_resolved() assigned self._agent_card before calling _validate_agent_card(). When validation raised (e.g. an RPC URL that fails the https/loopback check), the invalid card stayed cached. The next invocation's `if not self._agent_card:` guard then evaluated to False, so resolution and validation were both skipped and the previously-rejected card was reused to build the A2A client. Only assign self._agent_card after validation succeeds, so a failed validation is retried (and re-validated) on the next call instead of silently bypassed. Fixes #6901 --- src/google/adk/agents/remote_a2a_agent.py | 9 ++-- .../unittests/agents/test_remote_a2a_agent.py | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 52eb5b59c0a..929b0ac62d8 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -990,10 +990,13 @@ async def _ensure_resolved( if not self._agent_card: # Resolve agent card if needed - self._agent_card = await self._resolve_agent_card(ctx) + resolved_agent_card = await self._resolve_agent_card(ctx) - # Validate agent card - await self._validate_agent_card(self._agent_card) + # Validate agent card before caching it. If validation fails, the + # card must not be cached, or a subsequent invocation would skip + # resolution and validation entirely and reuse the invalid card. + await self._validate_agent_card(resolved_agent_card) + self._agent_card = resolved_agent_card # Update description if empty if not self.description and self._agent_card.description: diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index 975b4ae0583..5a41a7fbda8 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -697,6 +697,47 @@ async def test_ensure_resolved_caches_card_without_interceptor(self): assert mock_resolve.await_count == 1 + @pytest.mark.asyncio + async def test_ensure_resolved_does_not_cache_card_on_validation_failure( + self, + ): + """A card that fails validation must not be cached. + + Otherwise a later invocation sees `self._agent_card` already set, skips + resolution and validation entirely, and reuses the invalid card. + """ + agent = RemoteA2aAgent( + name="test_agent", + agent_card="https://example.com/agent.json", + ) + + with patch.object( + agent, "_resolve_agent_card", new_callable=AsyncMock + ) as mock_resolve: + mock_resolve.return_value = self.agent_card + with patch.object( + agent, "_validate_agent_card", new_callable=AsyncMock + ) as mock_validate: + mock_validate.side_effect = ValueError("invalid rpc url") + + with pytest.raises(AgentCardResolutionError): + await agent._ensure_resolved(Mock()) + + assert agent._agent_card is None + + mock_validate.side_effect = None + with patch.object(agent, "_ensure_httpx_client") as mock_ensure: + mock_ensure.return_value = AsyncMock() + mock_factory = Mock() + mock_factory.create.return_value = Mock() + agent._a2a_client_factory = mock_factory + + await agent._ensure_resolved(Mock()) + + assert mock_resolve.await_count == 2 + assert mock_validate.await_count == 2 + assert agent._agent_card is self.agent_card + @pytest.mark.asyncio async def test_ensure_resolved_without_ctx_uses_cached_path(self): """_ensure_resolved() is callable with no ctx (backward compatible)."""