Skip to content

Commit c03e35c

Browse files
committed
feat: add DNS-AID integration for agent discovery via DNS
Adds google.adk.integrations.dns_aid as the decentralized counterpart to integrations/agent_registry: instead of pulling agents from a centralized cloud registry, agents are discovered (and published) via DNS SVCB records under _<name>._<protocol>._agents.<domain>. Public surface: from google.adk.integrations.dns_aid import ( discover_agents, # async; query DNS SVCB publish_agent, # async; create SVCB+TXT records unpublish_agent, # async; remove records get_dns_aid_tools, # FunctionTool wrappers for an LlmAgent remote_a2a_agent_from_record, # bridge to RemoteA2aAgent mcp_toolset_from_record, # bridge to McpToolset ) Install: pip install "google-adk[dns-aid]" The extra pulls dns-aid>=0.18,<1 and google-adk[a2a] (for the A2A bridge). Implementation notes: - Lives in integrations/, not tools/, per integrations/README.md and matching the PR #5094 SecretManagerClient move pattern. Old import paths (google.adk.tools.dns_aid_tool, .dns_aid_a2a_bridge) remain as re-export shims that emit DeprecationWarning, scheduled for removal in a future release. - dns_aid imports are lazy-wrapped per integrations/README.md rule 4 so a missing extra produces "pip install google-adk[dns-aid]" rather than a raw ModuleNotFoundError. - LLM-callable args use pydantic Field constraints (RFC 1035 pattern for agent_name, ge=1/le=65535 for port, ge=60 for ttl) and Literal types for protocol and backend_name so the FunctionTool schema constrains the model up front instead of relying on runtime ValueErrors. - discover_agents returns the structured DiscoveryResult dict directly; the previous json.dumps() wrapper forced the LLM to parse JSON and silently stripped type info on datetimes/UUIDs. - unpublish_agent translates dns_aid exceptions into a typed status payload (not_found / permission_denied / backend_unavailable / throttled / error) so the LLM can decide whether to retry rather than being told "not found" for every failure mode. - get_dns_aid_tools binds backend_name via inspect.signature + functools.wraps so adding a parameter upstream (e.g. policy_uri in Phase 6) doesn't silently desync the FunctionTool schema. A parity test in tests/unittests/integrations/dns_aid/test_dns_aid.py guards this contract. - a2a_bridge.remote_a2a_agent_from_record returns a real RemoteA2aAgent (not the prior dict[str, Any]); RemoteA2aAgent fetches the card lazily on first invocation, so the bridge is sync. - mcp_bridge.mcp_toolset_from_record is the symmetric piece for protocol=mcp, mirroring how integrations/agent_registry uses StreamableHTTPConnectionParams. - Logger uses the project's required form logging.getLogger("google_adk." + __name__) so the check-file-contents CI gate doesn't reject it. Tests: 26 new tests under tests/unittests/integrations/dns_aid/ cover each rule path (validation, error translation, signature parity, bridge type assertions). All pass with -p asyncio (asyncio_mode=auto). README.md: full setup, per-backend credential table (route53, cloudflare, cloud-dns, ns1, infoblox/nios, ddns, mock), bridge examples, and a Future section flagging where trust verification, cap docs, and policy enforcement will plug in (kept out of v1 to keep this PR focused).
1 parent 3e282d2 commit c03e35c

12 files changed

Lines changed: 1302 additions & 3 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# AI Coding Assistant Context
22

3-
This document provides context for AI coding assistants (Claude Code, Gemini
4-
CLI, GitHub Copilot, Cursor, etc.) to understand the ADK Python project and
5-
assist with development.
3+
This document provides context for AI coding assistants (Gemini CLI, GitHub
4+
Copilot, Cursor, etc.) to understand the ADK Python project and assist with
5+
development.
66

77
## Project Overview
88

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ optional-dependencies.dev = [
9696
"pyink>=25.12",
9797
"pylint>=2.6",
9898
]
99+
optional-dependencies.dns-aid = [
100+
"dns-aid>=0.18,<1",
101+
"google-adk[a2a]",
102+
]
99103
optional-dependencies.docs = [
100104
"autodoc-pydantic",
101105
"furo",
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# DNS-AID
2+
3+
Decentralized agent discovery for Google ADK via DNS SVCB records — the
4+
counterpart to [`agent_registry`](../agent_registry/), which uses Google
5+
Cloud's centralized agent registry.
6+
7+
## What is DNS-AID?
8+
9+
DNS-AID encodes agent metadata into DNS SVCB records (RFC 9460) under the
10+
naming convention `_<name>._<protocol>._agents.<domain>` (for example
11+
`_chat._mcp._agents.example.com`). The
12+
[`dns-aid`](https://pypi.org/project/dns-aid/) Python SDK (`dns-aid-core`)
13+
handles SVCB encoding/decoding, backend zone updates, and discovery
14+
queries; this integration is a thin ADK-shaped wrapper over it.
15+
16+
Discovery is decentralized by design: each operator publishes to whatever
17+
DNS provider they already own (Route 53, Cloud DNS, Cloudflare, NS1,
18+
Infoblox NIOS, on-prem BIND/Knot via RFC 2136). There is no single trust
19+
root and no shared registry — which makes DNS-AID a natural fit for
20+
multi-cloud, federated, and air-gapped deployments where a centralized
21+
service is undesirable or unavailable.
22+
23+
## Install
24+
25+
```bash
26+
pip install "google-adk[dns-aid]"
27+
```
28+
29+
This pulls in `dns_aid>=0.18,<1` and the `[a2a]` extra (needed for the
30+
A2A bridge).
31+
32+
## Quickstart — discover agents
33+
34+
```python
35+
import asyncio
36+
from google.adk.integrations.dns_aid import discover_agents
37+
38+
39+
async def main():
40+
result = await discover_agents(domain='agents.example.com')
41+
for record in result['agents']:
42+
print(record['name'], record['protocol'], record['endpoint'])
43+
44+
45+
asyncio.run(main())
46+
```
47+
48+
Filter by protocol or by name with `protocol='a2a'` / `name='chat'`. Pass
49+
`require_dnssec=True` to reject any result where end-to-end DNSSEC
50+
validation fails.
51+
52+
## Quickstart — publish an agent
53+
54+
```python
55+
from google.adk.integrations.dns_aid import publish_agent
56+
57+
await publish_agent(
58+
agent_name='chat',
59+
domain='agents.example.com',
60+
protocol='mcp',
61+
endpoint='chat.internal.example.com',
62+
port=443,
63+
backend_name='route53',
64+
)
65+
```
66+
67+
Omit `backend_name` to use whatever default the host resolver / `dns_aid`
68+
configuration provides. See [Backend credentials](#backend-credentials)
69+
for the supported values.
70+
71+
## Quickstart — bridge to RemoteA2aAgent
72+
73+
```python
74+
from google.adk.integrations.dns_aid import discover_agents
75+
from google.adk.integrations.dns_aid import remote_a2a_agent_from_record
76+
77+
result = await discover_agents(domain='agents.example.com', protocol='a2a')
78+
agents = [
79+
remote_a2a_agent_from_record(record)
80+
for record in result['agents']
81+
]
82+
```
83+
84+
`remote_a2a_agent_from_record` is synchronous — it does no I/O.
85+
`RemoteA2aAgent` fetches its agent card lazily on first invocation.
86+
87+
## Quickstart — bridge to McpToolset
88+
89+
```python
90+
from google.adk.integrations.dns_aid import discover_agents
91+
from google.adk.integrations.dns_aid import mcp_toolset_from_record
92+
93+
result = await discover_agents(domain='agents.example.com', protocol='mcp')
94+
toolsets = [
95+
mcp_toolset_from_record(record)
96+
for record in result['agents']
97+
]
98+
```
99+
100+
Like its A2A counterpart, `mcp_toolset_from_record` is synchronous; the
101+
underlying `McpToolset` connects on first use.
102+
103+
## Use as ADK FunctionTools
104+
105+
```python
106+
from google.adk.agents import LlmAgent
107+
from google.adk.integrations.dns_aid import get_dns_aid_tools
108+
109+
agent = LlmAgent(
110+
model='gemini-2.5-flash',
111+
tools=get_dns_aid_tools(backend_name='route53'),
112+
)
113+
```
114+
115+
`get_dns_aid_tools(backend_name=None)` returns FunctionTool wrappers for
116+
`discover_agents`, `publish_agent`, and `unpublish_agent` with
117+
`backend_name` pre-bound (so the LLM-facing schema does not include it).
118+
Omit `backend_name` to fall back to the default `dns_aid` configuration.
119+
120+
## Backend credentials
121+
122+
`backend_name` must match the exact spelling that `dns_aid` expects
123+
(hyphens, not underscores). Credentials live wherever the underlying
124+
provider SDK looks for them:
125+
126+
| `backend_name` | Auth source | Required env vars / config |
127+
|---|---|---|
128+
| `route53` | AWS SDK default chain | `AWS_PROFILE` or `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (or instance profile) |
129+
| `cloudflare` | API token | `CLOUDFLARE_API_TOKEN` |
130+
| `cloud-dns` | Google ADC | `GOOGLE_APPLICATION_CREDENTIALS` (or `gcloud auth application-default login`) |
131+
| `ns1` | API key | `NS1_API_KEY` |
132+
| `infoblox` / `nios` | NIOS API user | `INFOBLOX_HOST`, `INFOBLOX_USERNAME`, `INFOBLOX_PASSWORD` |
133+
| `ddns` | RFC 2136 TSIG | `DDNS_TSIG_KEYFILE`, `DDNS_SERVER` |
134+
| `mock` | n/a — in-memory only | (no creds; for tests) |
135+
136+
> Credentials are resolved by `dns_aid` itself, not by ADK. ADK does not
137+
> read these env vars directly — it just hands the `backend_name` string
138+
> to `dns_aid.backends.create_backend(...)`. If a publish or unpublish
139+
> call fails with a backend-specific authentication error, check the
140+
> `dns-aid-core` docs for that provider.
141+
142+
## Programmatic API
143+
144+
| Symbol | Description |
145+
|---|---|
146+
| `discover_agents` | Async; query DNS SVCB records under a domain. |
147+
| `publish_agent` | Async; create SVCB + TXT records via the named backend. |
148+
| `unpublish_agent` | Async; remove the records. Returns a structured status dict that distinguishes `not_found`, `permission_denied`, `backend_unavailable`, and `throttled`. |
149+
| `get_dns_aid_tools` | Build the FunctionTool list for an `LlmAgent`. |
150+
| `remote_a2a_agent_from_record` | Bridge to `RemoteA2aAgent` (protocol `a2a`). |
151+
| `mcp_toolset_from_record` | Bridge to `McpToolset` (protocol `mcp`). |
152+
153+
## Future
154+
155+
The current integration ships discovery, publish, unpublish, and the two
156+
bridges. Future revisions can layer:
157+
158+
- **Cap-doc fetch and verify**`dns-aid-core` exposes `cap_fetcher` for
159+
pulling the SVCB-referenced capability document.
160+
- **Trust verification** — DNSSEC, DANE, and `cap-sha256` checks via
161+
`dns_aid.core.validator`.
162+
- **Policy enforcement** — Phase 6 of `dns-aid-core` provides
163+
`PolicyEvaluator` for evaluating policies referenced by `policy_uri`
164+
in the SVCB record.
165+
166+
## References
167+
168+
- DNS-AID spec: `draft-mozleywilliams-dnsop-dnsaid` (IETF Internet-Draft)
169+
- `dns-aid-core`: the [`dns-aid`](https://pypi.org/project/dns-aid/)
170+
PyPI package
171+
- A2A spec: [Agent-to-Agent protocol](https://google.github.io/A2A/)
172+
- ADK FunctionTool: [function tools documentation](https://google.github.io/adk-docs/tools/function-tools/)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""DNS-AID integration for Google ADK.
16+
17+
DNS-AID enables agents to discover and publish themselves via DNS SVCB
18+
records. This integration is the decentralized counterpart to
19+
``integrations/agent_registry`` (which uses Google Cloud's centralized
20+
agent registry).
21+
22+
Install: ``pip install "google-adk[dns-aid]"``
23+
"""
24+
25+
from .a2a_bridge import remote_a2a_agent_from_record
26+
from .dns_aid import discover_agents
27+
from .dns_aid import get_dns_aid_tools
28+
from .dns_aid import publish_agent
29+
from .dns_aid import unpublish_agent
30+
from .mcp_bridge import mcp_toolset_from_record
31+
32+
__all__ = [
33+
'discover_agents',
34+
'get_dns_aid_tools',
35+
'mcp_toolset_from_record',
36+
'publish_agent',
37+
'remote_a2a_agent_from_record',
38+
'unpublish_agent',
39+
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Bridge from DNS-AID AgentRecord to ADK RemoteA2aAgent."""
16+
17+
from __future__ import annotations
18+
19+
try:
20+
from dns_aid.core.models import AgentRecord
21+
from dns_aid.core.models import Protocol
22+
except ImportError as e:
23+
raise ImportError(
24+
'dns_aid is not installed. Please install it with '
25+
'`pip install "google-adk[dns-aid]"`.'
26+
) from e
27+
28+
try:
29+
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
30+
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
31+
except ImportError as e:
32+
raise ImportError(
33+
'a2a-sdk is not installed. Please install it with '
34+
'`pip install "google-adk[a2a]"`. The dns-aid extra pulls this in '
35+
'transitively but a partial install can still hit this.'
36+
) from e
37+
38+
39+
def remote_a2a_agent_from_record(record: AgentRecord) -> RemoteA2aAgent:
40+
"""Convert a DNS-AID AgentRecord (protocol=a2a) into a RemoteA2aAgent.
41+
42+
RemoteA2aAgent fetches the agent card lazily on first invocation, so this
43+
function performs no I/O and is intentionally synchronous.
44+
45+
Args:
46+
record: An AgentRecord from dns_aid.discover() with protocol=a2a.
47+
48+
Returns:
49+
A RemoteA2aAgent ready to be added to an ADK runner.
50+
51+
Raises:
52+
ValueError: If the record's protocol is not A2A.
53+
"""
54+
if record.protocol != Protocol.A2A:
55+
raise ValueError(
56+
f'Agent {record.name} uses protocol {record.protocol}, not A2A'
57+
)
58+
agent_card_url = (
59+
f'{record.endpoint_url.rstrip("/")}{AGENT_CARD_WELL_KNOWN_PATH}'
60+
)
61+
return RemoteA2aAgent(
62+
name=record.name,
63+
agent_card=agent_card_url,
64+
description=record.description or '',
65+
)
66+
67+
68+
__all__ = ['remote_a2a_agent_from_record']

0 commit comments

Comments
 (0)