From 4acaf12647df1ec09d8a0bb915437f80423c0143 Mon Sep 17 00:00:00 2001 From: Aarav Mittal <137450929+a2105z@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:01:50 -0500 Subject: [PATCH] fix(skills): skip invalid catalog hits in GCP skill search search_skills constructed Frontmatter for every Agent Registry hit, so one dotted first-party name raised ValidationError and emptied discovery. Skip entries that fail client-side validation and return the remaining hits. Fixes #6838 --- .../skill_registry/gcp_skill_registry.py | 28 +++++++-- .../skill_registry/test_gcp_skill_registry.py | 57 +++++++++++++++++++ 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/google/adk/integrations/skill_registry/gcp_skill_registry.py b/src/google/adk/integrations/skill_registry/gcp_skill_registry.py index 4f72973e3e8..9f604696e67 100644 --- a/src/google/adk/integrations/skill_registry/gcp_skill_registry.py +++ b/src/google/adk/integrations/skill_registry/gcp_skill_registry.py @@ -17,6 +17,7 @@ from __future__ import annotations import asyncio +import logging import os import ssl import tempfile @@ -35,6 +36,9 @@ from google.auth.transport import mtls from google.auth.transport import requests as auth_requests import httpx +from pydantic import ValidationError + +logger = logging.getLogger("google_adk." + __name__) class GCPSkillRegistry(SkillRegistry): @@ -215,6 +219,10 @@ async def get_skill(self, *, name: str) -> models.Skill: async def search_skills(self, *, query: str) -> list[models.Frontmatter]: """Searches for skills in the registry. + Catalog entries that fail client-side Frontmatter validation (for + example names with dots that ``get_skill`` also rejects) are skipped + so one non-conforming hit cannot fail the whole search. + Args: query: The search query. @@ -234,10 +242,18 @@ async def search_skills(self, *, query: str) -> list[models.Frontmatter]: results = [] for s in response_data.get("skills", []): - results.append( - models.Frontmatter( - name=s.get("name", "").split("/")[-1], - description=s.get("description", "") or "", - ) - ) + raw_name = s.get("name", "") + try: + results.append( + models.Frontmatter( + name=raw_name.split("/")[-1], + description=s.get("description", "") or "", + ) + ) + except ValidationError: + logger.warning( + "Skipping skill search result that failed frontmatter" + " validation: %s", + raw_name, + ) return results diff --git a/tests/unittests/integrations/skill_registry/test_gcp_skill_registry.py b/tests/unittests/integrations/skill_registry/test_gcp_skill_registry.py index e6d0c75cada..01a9e7e338d 100644 --- a/tests/unittests/integrations/skill_registry/test_gcp_skill_registry.py +++ b/tests/unittests/integrations/skill_registry/test_gcp_skill_registry.py @@ -184,6 +184,63 @@ async def test_search_skills_success(): ) +@pytest.mark.asyncio +async def test_search_skills_skips_invalid_frontmatter_names(): + """A non-conforming catalog name is skipped; valid hits are still returned.""" + registry = gcp_skill_registry.GCPSkillRegistry() + invalid_name = ( + "projects/test-project/locations/us-central1/skills/" + "cloud.google.com-agent-platform-eval-flywheel" + ) + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "skills": [ + { + "name": invalid_name, + "description": "First-party catalog entry with dots in the name.", + }, + { + "name": ( + "projects/test-project/locations/us-central1/skills/valid-skill" + ), + "description": "A loadable skill.", + }, + ] + } + + with mock.patch("httpx.AsyncClient.get", return_value=mock_response): + results = await registry.search_skills(query="query") + + assert len(results) == 1 + assert results[0].name == "valid-skill" + assert results[0].description == "A loadable skill." + + +@pytest.mark.asyncio +async def test_search_skills_returns_empty_when_all_hits_are_invalid(): + """Search still succeeds when every catalog hit fails Frontmatter validation.""" + registry = gcp_skill_registry.GCPSkillRegistry() + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "skills": [ + { + "name": ( + "projects/test-project/locations/us-central1/skills/" + "cloud.google.com-agent-platform-eval-flywheel" + ), + "description": "Dots are not allowed in Frontmatter.name.", + }, + ] + } + + with mock.patch("httpx.AsyncClient.get", return_value=mock_response): + results = await registry.search_skills(query="query") + + assert results == [] + + @pytest.mark.asyncio async def test_registry_requests_identify_adk(): """Registry calls carry the ADK client label.