An inside look at DNS-AID — the open-source system that lets AI agents discover, trust, and talk to each other using the internet's oldest naming system: DNS.
Before we look under the hood, let's understand the problem it solves.
Imagine you've built an AI agent that's great at writing code. Your colleague has one that's great at reviewing code. Wouldn't it be great if they could work together?
Right now, connecting them means hardcoding URLs, sharing API keys over Slack, and hoping nothing changes. It's like having to memorize everyone's phone number instead of looking them up in a directory.
The internet already has a universal directory: DNS. When you visit google.com, DNS translates that name into an address your computer can reach. DNS-AID adds AI agents to this same directory.
DNS-AID publishes a rich "contact card" for each AI agent into DNS. Here's what it looks like:
_chat._mcp._agents.example.com
The agent's name in the directory — "chat agent, speaks MCP, at example.com"
target: mcp.example.com
Where to find the agent (its address)
alpn="mcp"
Which "language" the agent speaks
port=443
Which "door" to knock on
key65400=https://…/cap.json
Link to a detailed profile of what the agent can do
This uses a DNS record type called SVCB — a modern record type designed to carry rich connection information in a single lookup.
Register your agent in DNS so others can find it — like listing your business in the Yellow Pages.
Find agents at any domain — one DNS query returns everything you need to connect.
Check that an agent is who it claims to be — inspecting DNSSEC, TLS, and DANE security proofs.
Here's the entire workflow in Python — six lines of code:
import dns_aid
# Publish your agent to DNS
await dns_aid.publish(
name="my-agent",
domain="example.com",
protocol="mcp",
endpoint="agent.example.com"
)
# Discover agents at a domain
agents = await dns_aid.discover("example.com")
# Verify trust
result = await dns_aid.verify(agents[0].fqdn)
Load the DNS-AID library so we can use it.
Register an agent named "my-agent" in DNS. It lives at agent.example.com and speaks the MCP protocol. Anyone querying example.com will now find it.
Ask DNS: "Who are the agents at example.com?" Returns a list of every agent registered there.
For the first agent found, run a full security check: Is the DNS signed? Is the TLS valid? Does everything check out? Returns a score from 0 to 100.
DNS-AID is like a film crew — each module has a specific role. Let's meet them.
Think of DNS-AID as a talent agency for AI agents. Different departments handle different jobs:
The talent agent who creates your listing — turns your agent's details into DNS records
The scout who finds agents by querying DNS and reading their contact cards
The security guard who checks IDs — verifies DNSSEC, TLS, and DANE before trusting anyone
The messenger who actually delivers requests — handles auth, policy checks, and protocol details
The postal workers who write records to specific DNS providers (AWS, Cloudflare, BIND9, etc.)
Every agent in the system is represented by an AgentRecord — the data structure that holds all the information from its DNS contact card.
class AgentRecord(BaseModel):
name: str
domain: str
protocol: Protocol
target_host: str
port: int = 443
capabilities: list[str]
cap_uri: str | None
policy_uri: str | None
This is the "blueprint" for an agent's identity card.
name — the agent's unique name, like "chat" or "code-reviewer"
domain — the organization it belongs to, like "example.com"
protocol — which "language" it speaks (MCP, A2A, or HTTPS)
target_host — the actual server address where it lives
port — which "door number" to knock on (usually 443, the standard HTTPS door)
capabilities — what it can do, like ["chat", "code-review"]
cap_uri — link to a detailed capability profile document
policy_uri — link to the rules about who's allowed to call this agent
Every agent gets a structured DNS name built from three pieces:
_{name}
The agent's name
_{protocol}
How it communicates
_agents.{domain}
The "agents" namespace
_chat._mcp._agents.example.com
The _agents label acts as a namespace — it keeps agent records separate from regular website records. No collisions, no confusion.
Trace what actually happens when you publish an agent and when another agent discovers it.
When you call dns_aid.publish(), here's the journey your data takes — like dropping a letter into a mailbox and watching it get sorted, stamped, and delivered.
Name, domain, protocol, endpoint — the raw ingredients
Your details get organized into a structured object
Reads DNS_AID_BACKEND env var → loads Route53, Cloudflare, DDNS, etc.
Standard SVCB params (alpn, port) go to SVCB record. Custom DNS-AID params get "demoted" to TXT records because most providers reject them
SVCB record + TXT record created at _name._protocol._agents.domain
DNS-AID supports 7 different DNS providers. Instead of a giant if/else chain, it uses a factory pattern:
_BACKEND_CLASSES = {
"route53": ("dns_aid.backends.route53", "Route53Backend"),
"cloudflare": ("dns_aid.backends.cloudflare", "CloudflareBackend"),
"ddns": ("dns_aid.backends.ddns", "DDNSBackend"),
"mock": ("dns_aid.backends.mock", "MockBackend"),
}
def create_backend(name):
module_path, class_name = _BACKEND_CLASSES[name]
module = importlib.import_module(module_path)
return getattr(module, class_name)()
A lookup table mapping backend names to their code locations. Think of it as a directory: "route53" → go to this file, use this class.
Given a name like "route53", look up where its code lives, load it, and create a ready-to-use backend object. The lazy import means boto3 is only loaded if you actually use Route53.
Now the reverse — how an agent finds other agents. Like a traveler arriving in a new city and looking up the local directory.
After discovery, the AgentClient handles the actual call. Here's the pipeline:
DNS-AID speaks to 7 different DNS providers through one uniform interface — like an international adapter for power outlets.
Every backend must implement the same set of operations. This is the abstract base class:
Write the agent's SVCB "contact card" into the DNS zone
Write supplementary text records (capabilities, custom params)
Remove an agent's records when it's unpublished
Check that the DNS zone exists before trying to write to it
DNS-AID adds custom fields to SVCB records (like key65400 for capability URIs). But here's the catch:
Route53, Cloudflare, and Cloud DNS only accept standard SvcParamKeys. DNS-AID uses private-use keys (65400–65408) that these providers will reject with an error.
The solution? Parameter demotion. The base class automatically splits params:
Standard params (alpn, port) → SVCB record
Custom params (cap, policy, realm) → TXT record
The Discoverer knows to check both SVCB and TXT when reading an agent's details. Everything round-trips cleanly.
For local development, DNS-AID can talk to a BIND9 server using RFC 2136 dynamic updates:
class DDNSBackend(DNSBackend):
def __init__(self, server, key_name,
key_secret, key_algorithm):
self._keyring = dns.tsigkeyring.from_text(
{self.key_name: self.key_secret}
)
This backend talks directly to a DNS server (like BIND9).
When created, it needs the server address plus a secret key — like a signed letter that proves you have authority to make changes.
The TSIG keyring holds the shared secret for authenticating update messages.
How DNS-AID ensures agents are who they say they are — and enforces rules about who can call whom.
DNS-AID uses a layered security model — like airport security with multiple checkpoints, each catching different threats:
DNSSEC validates the DNS response wasn't tampered with. DANE pins the TLS certificate. Security score: 0–100.
Pluggable auth handlers: Bearer tokens, API keys, OAuth2 client credentials, AWS SigV4, HTTP Message Signatures. Each handler knows how to add the right credentials to a request.
16 built-in rule types + custom CEL expressions. Checked at three enforcement points: caller-side, target-side, and DNS-level.
Auth handlers are registered in a dictionary — like a toolkit where each tool handles a different type of lock:
_REGISTRY = {
"none": _build_noop,
"api_key": _build_api_key,
"bearer": _build_bearer,
"oauth2": _build_oauth2,
"http_msg_sig": _build_http_msg_sig,
"sigv4": _build_sigv4,
}
def resolve_auth_handler(auth_type,
auth_config,
credentials):
factory = _REGISTRY[auth_type]
return factory(auth_config, credentials)
A registry of auth handler factories, keyed by type name.
"none" — no auth needed (public agents)
"api_key" — adds an X-API-Key header
"bearer" — adds Authorization: Bearer {token}
"oauth2" — full OAuth2 client_credentials flow
"http_msg_sig" — RFC 9421 HTTP Message Signatures
"sigv4" — AWS Signature V4 (for VPC Lattice)
Given an auth type string, look up the right factory function and call it with the config + credentials to get a ready-to-use auth handler.
The Validator computes a 0–100 security score by checking five things:
@property
def security_score(self) -> int:
score = 0
if self.record_exists: score += 20
if self.svcb_valid: score += 20
if self.dnssec_valid: score += 30
if self.dane_valid: score += 15
if self.endpoint_reachable: score += 15
return score
This calculates the security score like a report card:
Start at zero.
+20 points if the DNS record exists at all (basic presence)
+20 points if the SVCB record is properly formatted
+30 points if DNSSEC proves the record hasn't been tampered with (biggest weight — this is the hardest to fake)
+15 points if DANE pins the TLS certificate correctly
+15 points if the agent's server actually responds
Return the total. A score of 100 means bulletproof.
Policy documents are JSON files served at the agent's policy_uri. They can enforce rules like:
"Only accept calls from agents using OAuth2 or bearer tokens"
"Only agents from *.trusted.com can call me"
"I'm only available 9am–5pm UTC on weekdays"
"Max 100 requests per minute per caller"
Build your debugging intuition — common failure modes and where to look when something goes wrong.
When an agent call fails, the error usually lives in one of these layers. Think of it like a medical triage — the symptoms tell you which system to examine:
The agent isn't published or the index is stale. Run dns-aid index sync to rebuild the index from ground truth, then try discovery again.
No backend configured. DNS-AID has no silent fallback to mock — this is intentional to prevent accidentally testing against fake data in production. Set the env var.
The target agent's policy rejected your call. Check: are you calling from an allowed domain? Using the required auth type? Within the availability window?
Auth failed. The auth_type from discovery might not match your credentials. Check the agent's .well-known/agent-card.json for the expected auth type.
Run dns-aid verify {fqdn} to see which checks failed. Most common: DNSSEC not enabled (costs you 30 points) or no DANE/TLSA record (15 points).
DNS-AID includes a built-in diagnostic tool. Like a doctor's checkup, it examines your environment and reports what's healthy and what needs attention:
dns-aid doctor
Checks env vars, backend connectivity, Python version, dependencies
dns-aid doctor --domain example.com
Also tests DNS resolution and agent discovery for that domain
dns-aid verify _chat._mcp._agents.example.com
Deep security audit: DNSSEC chain, TLS version, DANE cert pinning
DNS-AID's policy engine fails open on fetch errors — if it can't reach the policy document, it allows the call. This is a deliberate design choice: a broken policy server shouldn't take down all agent communication. But in strict mode, it fails closed.
dns-aid discover returns nothing. The SVCB record exists when you check manually. What's the most likely issue?Zoom out and see how all the pieces fit together — and where DNS-AID fits in the AI ecosystem.
DNS-AID is split into two distinct tiers — like a library with a front desk and a delivery service:
When you discover multiple agents that can do the same thing, the Signal Collector helps you pick the best one. The ranking formula:
40%
Reliability
Did it succeed?
30%
Latency
How fast?
15%
Cost
How expensive?
15%
Freshness
How recent?
DNS-AID can be used three ways — same engine, different steering wheels:
dns-aid publish / discover / verify / message — for humans in a terminal
Exposes all operations as MCP tools that AI agents can call directly
import dns_aid — the async API for embedding in your own code
DNS-AID isn't just a library — it's the reference implementation for IETF draft-mozleywilliams-dnsop-dnsaid-01, a proposed internet standard. The naming convention:
If this becomes an IETF standard, every DNS provider will natively support these records. Any AI agent — regardless of vendor, framework, or protocol — will be discoverable through the same universal directory: DNS.
You now understand how DNS-AID works — from the DNS naming convention to the backend factory pattern to the three-layer security model.
Built from the dns-aid-core codebase.