From 869241a263b35c43d62bb9960b688b6833602924 Mon Sep 17 00:00:00 2001 From: Ingmar Date: Tue, 17 Mar 2026 02:56:42 -0700 Subject: [PATCH 1/2] feat: add DNS-AID integration for agent discovery via DNS Add native DNS-AID tools that enable AI agents to discover, publish, and unpublish other agents using DNS SVCB records (IETF draft-mozleywilliams-dnsop-dnsaid-01). DNS-AID provides decentralized agent discovery without centralized registries, using existing DNS infrastructure. Requires: dns-aid>=0.12.0 Co-Authored-By: Claude Opus 4.6 --- src/google/adk/tools/dns_aid_a2a_bridge.py | 40 ++++++ src/google/adk/tools/dns_aid_tool.py | 146 +++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 src/google/adk/tools/dns_aid_a2a_bridge.py create mode 100644 src/google/adk/tools/dns_aid_tool.py diff --git a/src/google/adk/tools/dns_aid_a2a_bridge.py b/src/google/adk/tools/dns_aid_a2a_bridge.py new file mode 100644 index 0000000000..90109eaaf7 --- /dev/null +++ b/src/google/adk/tools/dns_aid_a2a_bridge.py @@ -0,0 +1,40 @@ +"""Bridge between DNS-AID discovered agents and Google ADK A2A. + +When DNS-AID discovers an agent published with protocol='a2a', the agent's +endpoint serves an A2A Agent Card at /.well-known/agent-card.json. +This module provides utilities to convert those records to ADK-compatible refs. +""" + +from __future__ import annotations + +from typing import Any + + +async def a2a_agent_from_record(agent_record: Any) -> dict[str, Any]: + """Convert a DNS-AID AgentRecord with A2A protocol to an ADK-compatible reference. + + Args: + agent_record: An AgentRecord from dns_aid.discover() with protocol=a2a. + + Returns: + Dictionary with agent metadata suitable for ADK RemoteAgent construction. + + Raises: + ValueError: If the agent record is not using A2A protocol. + """ + from dns_aid.core.models import Protocol + + if agent_record.protocol != Protocol.A2A: + raise ValueError( + f"Agent {agent_record.name} uses protocol {agent_record.protocol}, not A2A" + ) + + base_url = agent_record.endpoint_url.rstrip("/") + return { + "name": agent_record.name, + "url": agent_record.endpoint_url, + "capabilities": agent_record.capabilities or [], + "description": agent_record.description, + "protocol": "a2a", + "a2a_card_url": f"{base_url}/.well-known/agent-card.json", + } diff --git a/src/google/adk/tools/dns_aid_tool.py b/src/google/adk/tools/dns_aid_tool.py new file mode 100644 index 0000000000..a230c1ee38 --- /dev/null +++ b/src/google/adk/tools/dns_aid_tool.py @@ -0,0 +1,146 @@ +"""DNS-AID tools for Google ADK. + +Google ADK wraps plain async functions as FunctionTool objects. +Tool descriptions are derived from docstrings. +""" + +from __future__ import annotations + +import json +from typing import Any, Optional + + +def _resolve_backend(backend_name: str | None) -> Any: + """Resolve a DNS backend by name, or return None.""" + if not backend_name: + return None + from dns_aid.backends import create_backend + + return create_backend(backend_name) + + +async def discover_agents( + domain: str, + protocol: Optional[str] = None, + name: Optional[str] = None, + require_dnssec: bool = False, +) -> str: + """Discover AI agents at a domain via DNS-AID SVCB records. + + Queries DNS to find published agents, optionally filtering by protocol or name. + Returns JSON with agent names, endpoints, capabilities, and protocols. + + Args: + domain: Domain to search for agents (e.g. 'agents.example.com'). + protocol: Filter by protocol: 'a2a', 'mcp', 'https', or None for all. + name: Filter by specific agent name. + require_dnssec: Require DNSSEC-validated responses. + """ + import dns_aid + + result = await dns_aid.discover( + domain=domain, protocol=protocol, name=name, require_dnssec=require_dnssec + ) + return json.dumps(result.model_dump(), default=str) + + +async def publish_agent( + agent_name: str, + domain: str, + protocol: str = "mcp", + endpoint: str = "", + port: int = 443, + capabilities: Optional[list[str]] = None, + version: str = "1.0.0", + description: Optional[str] = None, + ttl: int = 3600, + backend_name: Optional[str] = None, +) -> str: + """Publish an AI agent to DNS using DNS-AID protocol. + + Creates SVCB and TXT records so the agent becomes discoverable. + + Args: + agent_name: Agent identifier in DNS label format. + domain: Domain to publish under. + protocol: Protocol ('a2a', 'mcp', 'https'). + endpoint: Hostname where the agent is reachable. + port: Port number. + capabilities: List of agent capabilities. + version: Agent version. + description: Human-readable description. + ttl: DNS TTL in seconds. + backend_name: DNS backend name (e.g. 'route53', 'cloudflare'). + """ + import dns_aid + + result = await dns_aid.publish( + name=agent_name, + domain=domain, + protocol=protocol, + endpoint=endpoint, + port=port, + capabilities=capabilities, + version=version, + description=description, + ttl=ttl, + backend=_resolve_backend(backend_name), + ) + return json.dumps(result.model_dump(), default=str) + + +async def unpublish_agent( + agent_name: str, + domain: str, + protocol: str = "mcp", + backend_name: Optional[str] = None, +) -> str: + """Remove an AI agent's DNS-AID records. + + Args: + agent_name: Agent identifier to remove. + domain: Domain the agent is published under. + protocol: Protocol. + backend_name: DNS backend name. + """ + import dns_aid + + deleted = await dns_aid.unpublish( + name=agent_name, domain=domain, protocol=protocol, backend=_resolve_backend(backend_name) + ) + if deleted: + return json.dumps( + {"success": True, "message": f"Agent '{agent_name}' unpublished from {domain}"} + ) + return json.dumps( + {"success": False, "message": f"Agent '{agent_name}' not found at {domain}"} + ) + + +def get_dns_aid_tools(backend_name: Optional[str] = None) -> list: + """Return DNS-AID tools wrapped as Google ADK FunctionTool objects. + + Args: + backend_name: Optional DNS backend for publish/unpublish operations. + """ + from google.adk.tools import FunctionTool + + tools = [FunctionTool(discover_agents)] + if backend_name: + # Create closures that bind the backend_name + async def _publish(**kwargs): # type: ignore[no-untyped-def] + return await publish_agent(backend_name=backend_name, **kwargs) + + async def _unpublish(**kwargs): # type: ignore[no-untyped-def] + return await unpublish_agent(backend_name=backend_name, **kwargs) + + _publish.__name__ = "publish_agent" + _publish.__doc__ = publish_agent.__doc__ + _unpublish.__name__ = "unpublish_agent" + _unpublish.__doc__ = unpublish_agent.__doc__ + tools.append(FunctionTool(_publish)) + tools.append(FunctionTool(_unpublish)) + else: + tools.append(FunctionTool(publish_agent)) + tools.append(FunctionTool(unpublish_agent)) + return tools From a7adb4ca0f27e1d0ca95a999fb6709299c1c89ff Mon Sep 17 00:00:00 2001 From: Ingmar Date: Tue, 17 Mar 2026 09:02:13 -0700 Subject: [PATCH 2/2] fix: address Gemini Code Assist review feedback - Fix critical **kwargs issue in get_dns_aid_tools() wrappers: _publish and _unpublish now have explicit parameter signatures so FunctionTool can introspect them for LLM tool schemas - Add proper type hints: AgentRecord instead of Any, list['FunctionTool'] return type - Move imports to module level in A2A bridge - Extract A2A_AGENT_CARD_PATH constant (no magic strings) --- src/google/adk/tools/dns_aid_a2a_bridge.py | 11 ++++-- src/google/adk/tools/dns_aid_tool.py | 45 ++++++++++++++++++---- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/google/adk/tools/dns_aid_a2a_bridge.py b/src/google/adk/tools/dns_aid_a2a_bridge.py index 90109eaaf7..58e247d5ec 100644 --- a/src/google/adk/tools/dns_aid_a2a_bridge.py +++ b/src/google/adk/tools/dns_aid_a2a_bridge.py @@ -9,8 +9,13 @@ from typing import Any +from dns_aid.core.models import AgentRecord, Protocol -async def a2a_agent_from_record(agent_record: Any) -> dict[str, Any]: +# Well-known path for A2A agent cards per the A2A specification. +A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json" + + +async def a2a_agent_from_record(agent_record: AgentRecord) -> dict[str, Any]: """Convert a DNS-AID AgentRecord with A2A protocol to an ADK-compatible reference. Args: @@ -22,8 +27,6 @@ async def a2a_agent_from_record(agent_record: Any) -> dict[str, Any]: Raises: ValueError: If the agent record is not using A2A protocol. """ - from dns_aid.core.models import Protocol - if agent_record.protocol != Protocol.A2A: raise ValueError( f"Agent {agent_record.name} uses protocol {agent_record.protocol}, not A2A" @@ -36,5 +39,5 @@ async def a2a_agent_from_record(agent_record: Any) -> dict[str, Any]: "capabilities": agent_record.capabilities or [], "description": agent_record.description, "protocol": "a2a", - "a2a_card_url": f"{base_url}/.well-known/agent-card.json", + "a2a_card_url": f"{base_url}{A2A_AGENT_CARD_PATH}", } diff --git a/src/google/adk/tools/dns_aid_tool.py b/src/google/adk/tools/dns_aid_tool.py index a230c1ee38..a1e37c1381 100644 --- a/src/google/adk/tools/dns_aid_tool.py +++ b/src/google/adk/tools/dns_aid_tool.py @@ -117,7 +117,7 @@ async def unpublish_agent( ) -def get_dns_aid_tools(backend_name: Optional[str] = None) -> list: +def get_dns_aid_tools(backend_name: Optional[str] = None) -> list["FunctionTool"]: """Return DNS-AID tools wrapped as Google ADK FunctionTool objects. Args: @@ -127,12 +127,43 @@ def get_dns_aid_tools(backend_name: Optional[str] = None) -> list: tools = [FunctionTool(discover_agents)] if backend_name: - # Create closures that bind the backend_name - async def _publish(**kwargs): # type: ignore[no-untyped-def] - return await publish_agent(backend_name=backend_name, **kwargs) - - async def _unpublish(**kwargs): # type: ignore[no-untyped-def] - return await unpublish_agent(backend_name=backend_name, **kwargs) + # Create closures that bind the backend_name with explicit signatures + # so FunctionTool can introspect parameters for the LLM tool schema. + async def _publish( + agent_name: str, + domain: str, + protocol: str = "mcp", + endpoint: str = "", + port: int = 443, + capabilities: Optional[list[str]] = None, + version: str = "1.0.0", + description: Optional[str] = None, + ttl: int = 3600, + ) -> str: + return await publish_agent( + backend_name=backend_name, + agent_name=agent_name, + domain=domain, + protocol=protocol, + endpoint=endpoint, + port=port, + capabilities=capabilities, + version=version, + description=description, + ttl=ttl, + ) + + async def _unpublish( + agent_name: str, + domain: str, + protocol: str = "mcp", + ) -> str: + return await unpublish_agent( + backend_name=backend_name, + agent_name=agent_name, + domain=domain, + protocol=protocol, + ) _publish.__name__ = "publish_agent" _publish.__doc__ = publish_agent.__doc__