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..58e247d5ec --- /dev/null +++ b/src/google/adk/tools/dns_aid_a2a_bridge.py @@ -0,0 +1,43 @@ +"""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 + +from dns_aid.core.models import AgentRecord, Protocol + +# 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: + 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. + """ + 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}{A2A_AGENT_CARD_PATH}", + } 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..a1e37c1381 --- /dev/null +++ b/src/google/adk/tools/dns_aid_tool.py @@ -0,0 +1,177 @@ +"""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["FunctionTool"]: + """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 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__ + _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