DNS-AID API Reference

July 30, 2026 · View on GitHub

Complete API documentation for DNS-AID - DNS-based Agent Identification and Discovery.

Relationship to IETF

This document describes the API of the DNS-AID reference implementation.

The DNS-AID specification is defined in the IETF draft: https://datatracker.ietf.org/doc/draft-mozleywilliams-dnsop-dnsaid/.

Table of Contents


Quick Start

import asyncio
from dns_aid import publish, discover, verify, Protocol

async def main():
    # Publish an agent
    result = await publish(
        name="my-agent",
        domain="example.com",
        protocol="mcp",
        endpoint="agent.example.com",
        capabilities=["chat", "code-review"],
    )

    # Discover agents at a domain
    discovery = await discover("example.com", protocol=Protocol.MCP)

    # Verify an agent's DNS records
    verification = await verify("my-agent.example.com")

asyncio.run(main())

Core Functions

publish()

Publish an AI agent to DNS using this implementation of the DNS-AID specification.

async def publish(
    name: str,
    domain: str,
    protocol: str | Protocol,
    endpoint: str,
    port: int = 443,
    capabilities: list[str] | None = None,
    version: str = "1.0.0",
    description: str | None = None,
    ttl: int = 3600,
    backend: DNSBackend | None = None,
    cap_uri: str | None = None,
    cap_sha256: str | None = None,
    bap: list[str] | None = None,
    policy_uri: str | None = None,
    realm: str | None = None,
    ipv4_hint: str | None = None,
    ipv6_hint: str | None = None,
) -> PublishResult

Parameters

ParameterTypeRequiredDefaultDescription
namestrYes-Agent identifier (e.g., "chat", "network-specialist"). Must be DNS label format: lowercase alphanumeric with hyphens, 1-63 chars.
domainstrYes-Domain to publish under (e.g., "example.com")
protocolstr | ProtocolYes-Communication protocol: "mcp" or "a2a"
endpointstrYes-Hostname where agent is reachable
portintNo443Port number (1-65535)
capabilitieslist[str]No[]List of agent capabilities
versionstrNo"1.0.0"Agent version (semver format)
descriptionstrNoNoneHuman-readable description
ttlintNo3600DNS record TTL in seconds (60-86400)
backendDNSBackendNoNoneDNS backend to use (uses default if None)
cap_uristrNoNoneURI to capability document (DNS-AID custom param)
cap_sha256strNoNoneBase64url SHA-256 digest of capability descriptor
baplist[str]NoNoneSupported protocols with versions (e.g., ["mcp/1", "a2a/1"])
policy_uristrNoNoneURI to agent policy document
realmstrNoNoneMulti-tenant scope identifier
ipv4_hintstrNoNoneIPv4 address hint for SVCB record (reduces A query round trips)
ipv6_hintstrNoNoneIPv6 address hint for SVCB record (reduces AAAA query round trips)

Returns

PublishResult - Contains the published agent and created DNS records.

Example

from dns_aid import publish

result = await publish(
    name="network-specialist",
    domain="example.com",
    protocol="mcp",
    endpoint="mcp.example.com",
    capabilities=["ipam", "dns", "vpn"],
    ttl=300,
)

if result.success:
    print(f"Published: {result.agent.fqdn}")
    print(f"Records: {result.records_created}")
else:
    print(f"Failed: {result.message}")

DNS Records Created

  • SVCB: {name}.{domain} (draft-02 flat primary owner) → Service binding record
  • SVCB (AliasMode, optional): {name}._agents.{domain} → walkable AliasMode pointer at the flat primary owner (suppressible via publish_walkable_alias=False)
  • TXT: {name}.{domain} → Capabilities and metadata (alongside the primary SVCB)

discover()

Discover AI agents at a domain using this implementation of the DNS-AID specification (Path A — DNS substrate).

async def discover(
    domain: str,
    protocol: str | Protocol | None = None,
    name: str | None = None,
    require_dnssec: bool = False,
    use_http_index: bool = False,
    enrich_endpoints: bool = True,
    verify_signatures: bool = False,
    trust_dnssec_pointers: bool = False,
    verify_dane: bool = False,
    *,
    # Path A in-memory filter kwargs (v0.19.0+)
    capabilities: list[str] | None = None,
    capabilities_any: list[str] | None = None,
    auth_type: str | None = None,
    intent: str | None = None,
    transport: str | None = None,
    realm: str | None = None,
    min_dnssec: bool = False,
    text_match: str | None = None,
    require_signed: bool = False,
    require_signature_algorithm: list[str] | None = None,
) -> DiscoveryResult

Parameters

Substrate parameters:

ParameterTypeRequiredDefaultDescription
domainstrYes-Domain to search for agents
protocolstr | ProtocolNoNoneFilter by protocol (None for all)
namestrNoNoneFilter by specific agent name (case-insensitive per RFC 1035)
require_dnssecboolNoFalseRequire every DNS-plane agent's DNS response to carry the resolver AD flag; raises DNSSECError if any does not. ARD / HTTP-catalog agents are exempt (no DNS SVCB record — their trust is catalog_trust). Exposed on SDK, CLI (--require-dnssec), and MCP.
use_http_indexboolNoFalseUse HTTP index endpoint instead of DNS-only discovery
enrich_endpointsboolNoTrueFetch cap docs / agent cards to enrich AgentRecords
verify_signaturesboolNoFalseFetch JWKS and verify per-agent JWS signatures
trust_dnssec_pointersboolNoFalseOpt-in. Also follow an off-domain ARD catalog pointer when its pointer record is DNSSEC-validated (AD flag). Off by default — the AD flag is only trustworthy with a validating resolver over a secure path. Exposed on SDK, CLI (--trust-dnssec-pointers), and MCP.
verify_daneboolNoFalseOpt-in. Check each resolved agent endpoint's TLS certificate against its DANE/TLSA record — defense-in-depth on the endpoint that does not change the catalog/pointer trust decision. Demoted to None unless the agent's DNS response is DNSSEC-validated (DANE without DNSSEC carries no integrity guarantee, RFC 6698 §10.1). Surfaced on AgentRecord.dane_verified. SDK / CLI (--verify-dane) / MCP.

Filter kwargs (v0.19.0+, all keyword-only, all default no-op):

ParameterTypeDescription
capabilitieslist[str] | NoneAll-of capability match — every entry must be present on the agent. Empty list = no-match (explicit).
capabilities_anylist[str] | NoneAny-of capability match — at least one entry must be present. Empty list = no-match.
auth_typestr | NoneCase-insensitive exact match against agent.auth_type.
intentstr | NoneMatch against agent.category; falls back to substring match across capabilities.
transportstr | NoneMatch against the agent's protocol identifier (Path A surfaces protocol, not wire transport).
realmstr | NoneExact match against agent.realm.
min_dnssecboolWhen True, only records whose DNS response was DNSSEC-validated (AD flag) pass. ARD / HTTP-catalog agents are exempt (no DNS SVCB record — their trust is catalog_trust) and pass through rather than being dropped.
text_matchstr | NoneCase-insensitive substring match across description, use_cases, and capabilities. Empty string raises ValueError.
require_signedboolWhen True, only records whose JWS signature verified pass. Auto-enables verify_signatures=True.
require_signature_algorithmlist[str] | NoneRestrict require_signed matches to records whose verified algorithm is in this allow-list. Requires require_signed=True.

All filter predicates compose with logical AND. None / False means "no constraint". When every filter is unset, the input list is returned unchanged with no allocation (no-op fast path keeps existing callers free of overhead).

Discovery Methods

MethodEndpointUse Case
DNS (default)_index._agents.{domain} TXT recordDecentralized, cached, minimal round trips
HTTP Indexhttps://_index._aiagents.{domain}/index-wellknownANS-compatible, rich metadata (descriptions, model cards)
ARD ai-cataloghttps://{domain}/.well-known/ai-catalog.jsonARD catalogs, auto-detected via use_http_index=True; carries publisher trust manifests

With use_http_index=True the fetcher probes the legacy index locations first and the ARD well-known location last, then auto-detects the document format (specVersion: "1.0" + entries[] → ARD; keyed object → legacy). ARD entries whose artifact type is application/mcp-server-card+json or application/a2a-agent-card+json become agents (protocol inferred from the media type); inline nested catalogs recurse (depth ≤ 3); registry entries and non-agent artifacts are skipped. An entry's trustManifest is preserved on AgentRecord.trust_manifest (pass-through — dns-aid does not verify signatures, attestation digests, or identity↔publisher alignment).

Returns

DiscoveryResult - Contains list of discovered agents and query metadata.

Raises

  • ValueErrortext_match is an empty string, or require_signature_algorithm is set without require_signed=True.
  • DNSSECErrorrequire_dnssec=True but a DNS-plane agent's response is not authenticated (AD flag unset). ARD / HTTP-catalog agents are exempt.

Example

from dns_aid import discover, Protocol

# Discover all agents (pure DNS - default)
result = await discover("example.com")

# Discover MCP agents only
result = await discover("example.com", protocol=Protocol.MCP)

# Discover specific agent
result = await discover("example.com", protocol="mcp", name="chat")

# Discover via HTTP index (ANS-compatible, richer metadata)
result = await discover("example.com", use_http_index=True)

# Filter by capabilities + auth type (v0.19.0+)
result = await discover(
    "example.com",
    capabilities=["payment-processing", "fraud-detection"],
    auth_type="oauth2",
    realm="prod",
)

# Trust-gated discovery: only signed agents with allow-listed algorithms
result = await discover(
    "example.com",
    require_signed=True,
    require_signature_algorithm=["ES256", "Ed25519"],
)

for agent in result.agents:
    print(f"{agent.name}: {agent.endpoint_url}")
    print(f"  Capabilities: {', '.join(agent.capabilities)}")
    if agent.description:
        print(f"  Description: {agent.description}")

verify()

Verify DNS-AID records as interpreted by this implementation, with security validation.

async def verify(fqdn: str) -> VerifyResult

Parameters

ParameterTypeRequiredDescription
fqdnstrYesFully qualified domain name (e.g., "chat.example.com")

Returns

VerifyResult - Contains security validation results and score.

Example

from dns_aid import verify

result = await verify("chat.example.com")

print(f"Record exists: {result.record_exists}")
print(f"DNSSEC valid: {result.dnssec_valid}")
print(f"Security Score: {result.security_score}/100")
print(f"Rating: {result.security_rating}")

Domain Control Validation (DCV)

DCV is a stateless challenge/verify primitive that lets one party prove control of a domain to another using a short-lived TXT record at _agents-challenge.{domain}. It implements draft-ietf-dnsop-domain-verification-techniques-12 plus the bnd-req binding extension from draft-mozleywilliams-dnsop-dnsaid-02.

Role split:

  • Challenger — calls issue() and verify(); no DNS write credentials required.
  • Claimant — calls place() and revoke(); needs backend write credentials for the domain.

Wire format (space-separated key=value at _agents-challenge.{domain} TXT):

token=<32-char-base32>  [domain=<domain>]  [bnd-req=svc:<agent>@<issuer>]  expiry=<RFC3339Z>
from dns_aid.core import dcv

# Challenger
challenge = dcv.issue("example.com", agent_name="assistant", issuer_domain="orga.test")
# ... deliver challenge to claimant out-of-band ...

# Claimant
await dcv.place(challenge.domain, challenge.token, bnd_req=challenge.bnd_req)

# Challenger
result = await dcv.verify(challenge.domain, challenge.token,
                          expected_bnd_req=challenge.bnd_req)
if result.verified:
    await dcv.revoke(challenge.domain, token=challenge.token)  # claimant cleanup

dcv.issue()

Generate a stateless DCV challenge. Nothing is written to DNS — the returned DCVChallenge is delivered to the claimant out-of-band.

def issue(
    domain: str,
    *,
    agent_name: str | None = None,
    issuer_domain: str | None = None,
    ttl_seconds: int = 3600,
) -> DCVChallenge

Parameters

ParameterTypeRequiredDefaultDescription
domainstrYes-Domain the claimant must prove control of
agent_namestrNoNoneAgent name to scope the bnd-req field
issuer_domainstrNoNoneIssuer domain to scope the bnd-req field
ttl_secondsintNo3600Challenge validity window (30–86400)

Returns a DCVChallenge. Raises ValueError if ttl_seconds is out of range; ValidationError for invalid domain or agent name.

dcv.place()

Write the DCV challenge TXT record to DNS via the configured backend.

async def place(
    domain: str,
    token: str,
    *,
    bnd_req: str | None = None,
    expiry_seconds: int = 3600,
    ttl: int = 300,
    backend: DNSBackend | None = None,
) -> DCVPlaceResult

Parameters

ParameterTypeRequiredDefaultDescription
domainstrYes-Zone to write the challenge into
tokenstrYes-Token from the challenger (32-char lowercase base32)
bnd_reqstrNoNoneBinding scope from the issued challenge
expiry_secondsintNo3600Placed-record validity (30–86400). Prefer aligning with DCVChallenge.expiry.
ttlintNo300DNS record TTL — keep short for quick cleanup
backendDNSBackendNoNoneDefaults to DNS_AID_BACKEND env var

Returns a DCVPlaceResult.

dcv.verify()

Resolve _agents-challenge.{domain} and confirm the token is present, unexpired, and (optionally) bound to the expected scope.

async def verify(
    domain: str,
    token: str,
    *,
    nameserver: str | None = None,
    port: int = 53,
    expected_bnd_req: str | None = None,
    require_dnssec: bool = False,
) -> DCVVerifyResult

Parameters

ParameterTypeRequiredDefaultDescription
domainstrYes-Domain to check
tokenstrYes-Token originally issued by the challenger
nameserverstrNoNoneOperator-trusted nameserver IP (testbeds only)
portintNo53DNS port
expected_bnd_reqstrNoNoneWhen set, record bnd-req must match exactly
require_dnssecboolNoFalseWhen True, resolver must set AD flag (silently downgraded when nameserver= is used)

Fail-closed contract

ConditionResult
Missing expiry= fieldverified=False
Malformed expiry=verified=False
Bare token (no token= prefix)Not matched
domain= mismatched with queried domainRecord skipped
Invalid nameserver IPDCVVerifyResult(verified=False) — never raises
require_dnssec=True + no AD flagverified=False
>10 challenge records (DoS guard)verified=False

Returns a DCVVerifyResult.

Security: The nameserver parameter accepts any syntactically valid IP including loopback, link-local (169.254/16), and RFC1918 ranges. It is operator-trusted and intentionally omitted from the MCP tool surface. Do not expose it to untrusted callers.

dcv.revoke()

Delete the DCV challenge TXT record. Should be called immediately after a successful verify() to prevent token reuse within the validity window.

async def revoke(
    domain: str,
    *,
    token: str,
    backend: DNSBackend | None = None,
) -> DCVRevokeResult

Parameters

ParameterTypeRequiredDefaultDescription
domainstrYes-Zone to remove the challenge from
tokenstrYes-Token that was placed (must match the record in DNS)
backendDNSBackendNoNoneDefaults to DNS_AID_BACKEND env var

Returns a DCVRevokeResult. The token must be present in DNS before deletion (check-then-delete reduces racing with concurrent challengers; not atomic — expiry= remains the true security gate).

DCVChallenge

Issued challenge, delivered to the claimant out-of-band.

FieldTypeDescription
tokenstrBase32-encoded nonce to place in DNS
domainstrDomain being challenged
fqdnstrFull owner name (_agents-challenge.{domain})
txt_valuestrVerbatim TXT RDATA to place
expirydatetimeUTC expiry time
bnd_reqstr | NoneBinding scope (svc:<agent>@<issuer>), if set

DCVPlaceResult

FieldTypeDescription
fqdnstrFull owner name where the challenge was placed
domainstrZone domain
expires_atdatetimeUTC time the placed challenge expires

DCVVerifyResult

FieldTypeDescription
verifiedboolTrue if a valid, unexpired matching record was found
domainstrDomain that was queried
tokenstrToken that was checked
fqdnstrFull owner name queried
expiredboolTrue if a matching record was found but past expiry
dnssec_validatedboolTrue if require_dnssec=True and resolver set AD=1
errorstr | NoneHuman-readable failure reason when verified=False

DCVRevokeResult

FieldTypeDescription
removedboolTrue if the challenge record was deleted
domainstrZone domain
fqdnstrFull owner name that was targeted

Data Models

AgentRecord

Represents an AI agent published via DNS-AID.

from dns_aid import AgentRecord, Protocol

agent = AgentRecord(
    name="network-specialist",
    domain="example.com",
    protocol=Protocol.MCP,
    target_host="mcp.example.com",
    port=443,
    capabilities=["ipam", "dns", "vpn"],
    version="1.0.0",
    description="Network automation agent",
    ttl=3600,
)

Fields

FieldTypeRequiredDefaultDescription
namestrYes-Agent identifier (1-63 chars, DNS label format)
domainstrYes-Domain where agent is published
protocolProtocolYes-Communication protocol
target_hoststrYes-Hostname where agent is reachable
portintNo443Port number
ipv4_hintstrNoNoneIPv4 address hint
ipv6_hintstrNoNoneIPv6 address hint
capabilitieslist[str]No[]Agent capabilities
versionstrNo"1.0.0"Agent version
descriptionstrNoNoneDescription
ttlintNo3600DNS TTL (60-86400)
cap_uristrNoNoneURI to capability document (DNS-AID)
cap_sha256strNoNoneSHA-256 digest of capability descriptor
baplist[str]No[]Supported protocols with versions
policy_uristrNoNoneURI to agent policy document
realmstrNoNoneMulti-tenant scope identifier
capability_sourcestrNoNoneWhere capabilities came from: cap_uri, well_known, agent_card, http_index, ard_catalog, txt_fallback, none
endpoint_sourcestrNoNoneWhere endpoint came from: dns_svcb, dns_svcb_enriched, http_index, http_index_fallback, ard_card (real endpoint from a fetched ARD agent/server card), ard_inline, direct, directory
trust_manifestTrustManifestNoNonePublisher trust claims from an ARD ai-catalog entry (identity, attestations, provenance, signature) — pass-through, not verified
catalog_truststr | NoneNoNoneARD-sourced records only — how the catalog was trusted: tls_domain (on-domain), dnssec (DNSSEC-validated off-domain pointer), or jws (JWS-signed off-domain). None for pure-DNS records.
dnssec_validatedboolNoFalseTrue when this agent's DNS response carried the resolver AD flag. Set for DNS-plane agents when require_dnssec / min_dnssec / verify_dane is used; ARD / HTTP-catalog agents are exempt and stay False. AD-flag based — not independent DNSKEY→DS→RRSIG chain validation.
dane_verifiedbool | NoneNoNoneDANE/TLSA endpoint-certificate binding (opt-in verify_dane=True): True = endpoint cert matched its DNSSEC-anchored TLSA record; False = TLSA mismatch; None = not checked, no TLSA record, or not DNSSEC-anchored.

The ARD ai-catalog guide covers the full discovery flow (DNS pointer → catalog → per-agent DNS-first → card dereferencing), publishing the host-anywhere _catalog._agents / _index._agents pointer (dns-aid index publish-catalog, publish_catalog_pointer library/MCP), and the trust model.

Properties

PropertyTypeDescription
fqdnstrFull DNS-AID record name (draft-02 flat primary owner): {name}.{domain}
walkable_fqdnstrOptional walkable AliasMode form: {name}._agents.{domain}
legacy_fqdnstrLegacy -01 form: _{name}._{protocol}._agents.{domain} (used only by the back-compat discovery path)
endpoint_urlstrFull URL: https://{target_host}:{port}
svcb_targetstrSVCB target with trailing dot

Methods

MethodReturnsDescription
to_svcb_params()dict[str, str]SVCB record parameters
to_txt_values()list[str]TXT record values

DiscoveryResult

Result of a DNS-AID discovery query.

Fields

FieldTypeDescription
querystrDNS query made
domainstrDomain that was queried
agentslist[AgentRecord]Discovered agents
dnssec_validatedboolWhether DNSSEC was verified
cachedboolWhether result was cached
query_time_msfloatQuery latency in milliseconds

Properties

PropertyTypeDescription
countintNumber of agents discovered

PublishResult

Result of publishing an agent to DNS.

Fields

FieldTypeDescription
agentAgentRecordThe published agent
records_createdlist[str]DNS records created
zonestrDNS zone used
backendstrDNS backend used
successboolWhether publish succeeded
messagestr | NoneStatus message

VerifyResult

Result of verifying an agent's DNS records.

Fields

FieldTypeDescription
fqdnstrFQDN that was verified
record_existsboolDNS record exists
svcb_validboolSVCB record is valid
dnssec_validboolDNSSEC chain validated
dane_validbool | NoneDANE/TLSA verified
endpoint_reachableboolEndpoint responds
endpoint_latency_msfloat | NoneResponse time

Properties

PropertyTypeDescription
security_scoreintSecurity score (0-100)
security_ratingstr"Excellent", "Good", "Fair", or "Poor"

Security Scoring

CheckPoints
Record exists20
SVCB valid20
DNSSEC valid30
DANE valid15
Endpoint reachable15
Total100

Protocol

Enumeration of supported agent communication protocols.

from dns_aid import Protocol

Protocol.MCP   # Model Context Protocol (Anthropic)
Protocol.A2A   # Agent-to-Agent (Google)
Protocol.HTTPS # Standard HTTPS

Backends

DNSBackend Interface

Abstract base class for DNS providers.

from dns_aid.backends.base import DNSBackend

class CustomBackend(DNSBackend):
    @property
    def name(self) -> str:
        return "custom"

    async def create_svcb_record(self, zone, name, priority, target, params, ttl) -> str:
        ...

    async def create_txt_record(self, zone, name, values, ttl) -> str:
        ...

    async def delete_record(self, zone, name, record_type) -> bool:
        ...

    async def list_records(self, zone, name_pattern, record_type):
        ...

    async def zone_exists(self, zone) -> bool:
        ...

Route53Backend

AWS Route 53 implementation.

from dns_aid.backends.route53 import Route53Backend

# Auto-discover zones from AWS
backend = Route53Backend()

# Or specify zone ID directly
backend = Route53Backend(zone_id="Z1234567890ABC")

# Use with publish
from dns_aid import publish
from dns_aid.core.publisher import set_default_backend

set_default_backend(backend)
result = await publish(name="my-agent", ...)

Requirements: pip install dns-aid[route53] and AWS credentials configured.

InfobloxBloxOneBackend

Infoblox UDDI (Universal DDI) implementation.

from dns_aid.backends.infoblox import InfobloxBloxOneBackend

# From environment variables (recommended)
backend = InfobloxBloxOneBackend()

# Or with explicit configuration
backend = InfobloxBloxOneBackend(
    api_key="your-api-key",
    dns_view="default",  # DNS view name
    base_url="https://csp.infoblox.com",  # Optional
)

# Use as context manager
async with InfobloxBloxOneBackend() as backend:
    zones = await backend.list_zones()
    print(zones)

Environment Variables:

VariableRequiredDefaultDescription
INFOBLOX_API_KEYYes-Infoblox UDDI API key
INFOBLOX_DNS_VIEWNodefaultDNS view name
INFOBLOX_BASE_URLNohttps://csp.infoblox.comAPI URL

DNS-AID Compliance: Infoblox UDDI is not fully compliant with the DNS-AID draft. It only supports alias mode SVCB (priority 0) and lacks alpn, port, and mandatory parameters. For full compliance, use Route53Backend, InfobloxNIOSBackend, NS1Backend, or DDNSBackend.

InfobloxNIOSBackend

Infoblox NIOS on-premise WAPI implementation. Supports full ServiceMode SVCB with custom DNS-AID parameters.

from dns_aid.backends.infoblox import InfobloxNIOSBackend

# From environment variables (recommended)
backend = InfobloxNIOSBackend()

# Or with explicit configuration
backend = InfobloxNIOSBackend(
    host="nios.example.com",
    username="admin",
    password="your-password",
    dns_view="default",       # DNS view name
    wapi_version="2.13.7",    # WAPI version
    verify_ssl=False,         # TLS certificate verification
)

Environment Variables:

VariableRequiredDefaultDescription
NIOS_HOSTYes-Grid Manager hostname or IP
NIOS_USERNAMEYes-WAPI username
NIOS_PASSWORDYes-WAPI password
NIOS_DNS_VIEWNodefaultDNS view name
NIOS_WAPI_VERSIONNo2.13.7WAPI version
NIOS_VERIFY_SSLNofalseVerify TLS certificate

DNS-AID Compliance: NIOS WAPI supports ServiceMode SVCB records (priority > 0) with full SVC parameters including custom DNS-AID keys (key65400key65409). NIOS natively supports private-use SVCB keys via the supports_private_svcb_keys property.

NS1Backend

NS1 (IBM NS1 Connect) REST API v1 implementation.

from dns_aid.backends.ns1 import NS1Backend

backend = NS1Backend()  # reads NS1_API_KEY from env

# Or with explicit configuration
backend = NS1Backend(
    api_key="your-api-key",
    base_url="https://api.nsone.net/v1",  # default
)
VariableRequiredDefaultDescription
NS1_API_KEYYes-NS1 API key with DNS read/write permissions
NS1_BASE_URLNohttps://api.nsone.net/v1API base URL (for private/dedicated deployments)

DNS-AID Compliance: NS1 supports ServiceMode SVCB records with full SVC parameters including private-use keys (key65400key65409). NS1 natively accepts private-use SVCB keys — cap_uri, policy_uri, and realm go directly into the SVCB record without TXT demotion.

AkamaiEdgeDNSBackend

Akamai Edge DNS implementation using the Config DNS API v2 with EdgeGrid authentication.

from dns_aid.backends.akamai_edgedns import AkamaiEdgeDNSBackend

backend = AkamaiEdgeDNSBackend()  # reads AKAMAI_* env vars or ~/.edgerc

# Or with explicit configuration
backend = AkamaiEdgeDNSBackend(
    host="akab-xxxx.luna.akamaiapis.net",
    client_token="...",
    client_secret="...",
    access_token="...",
)
VariableRequiredDefaultDescription
AKAMAI_HOSTNo*-EdgeGrid API hostname
AKAMAI_CLIENT_TOKENNo*-EdgeGrid client token
AKAMAI_CLIENT_SECRETNo*-EdgeGrid client secret
AKAMAI_ACCESS_TOKENNo*-EdgeGrid access token
AKAMAI_EDGERCNo~/.edgercPath to .edgerc credentials file
AKAMAI_EDGERC_SECTIONNodefaultSection within .edgerc

* Either all four AKAMAI_* credential variables or an ~/.edgerc file. Partial env credentials raise an error naming the missing variables rather than silently falling back.

DNS-AID Compliance: Akamai Edge DNS supports ServiceMode SVCB records with full SVC parameters including private-use keys (key65400key65408) via the supports_private_svcb_keys property — cap_uri, policy_uri, bap, and realm go directly into the SVCB record without TXT demotion.

Concurrency: Akamai serializes modifications per zone. The backend serializes its own writes per zone and automatically retries transient 409 concurrentZoneModification responses with exponential backoff, so concurrent publish_agent() calls are safe.

CloudflareBackend

Cloudflare DNS REST API v4 implementation.

from dns_aid.backends.cloudflare import CloudflareBackend

backend = CloudflareBackend()  # reads CLOUDFLARE_API_TOKEN from env

# Or with explicit configuration
backend = CloudflareBackend(
    api_token="your-api-token",
    zone_id="optional-zone-id",  # auto-discovered from domain if omitted
)
VariableRequiredDefaultDescription
CLOUDFLARE_API_TOKENYes-API token with Zone → DNS → Edit (and Read) permissions
CLOUDFLARE_ZONE_IDNo-Zone ID (auto-discovered by zone name if omitted)

DNS-AID Compliance: Cloudflare supports ServiceMode SVCB records (priority > 0) with full SVC parameters including private-use keys (key65400key65409). Cloudflare natively accepts private-use SVCB keys via the supports_private_svcb_keys property — cap_uri, cap_sha256, bap, policy_uri, and realm go directly into the SVCB record without TXT demotion. TXT records are still written for human-readable metadata (capabilities, version, description), each value stored as its own RFC 1035 character-string.

DDNSBackend

RFC 2136 Dynamic DNS implementation. Works with BIND, Windows DNS, PowerDNS, Knot DNS, and any RFC 2136 compliant server.

from dns_aid.backends.ddns import DDNSBackend

# From environment variables (recommended)
backend = DDNSBackend()

# Or with explicit configuration
backend = DDNSBackend(
    server="ns1.example.com",
    key_name="dns-aid-key",
    key_secret="YourBase64SecretHere==",
    key_algorithm="hmac-sha256",  # default
    port=53,                       # default
    timeout=10.0,                  # default
)

# Or load from BIND key file
backend = DDNSBackend(key_file="/etc/bind/dns-aid-key.conf")

# Use as context manager
async with DDNSBackend() as backend:
    exists = await backend.zone_exists("example.com")

Environment Variables:

VariableRequiredDefaultDescription
DDNS_SERVERYes-DNS server hostname or IP
DDNS_KEY_NAMEYes-TSIG key name
DDNS_KEY_SECRETYes-TSIG key secret (base64)
DDNS_KEY_ALGORITHMNohmac-sha256TSIG algorithm
DDNS_PORTNo53DNS server port
DDNS_TIMEOUTNo10Query timeout in seconds

Supported TSIG Algorithms:

  • hmac-sha256 (recommended)
  • hmac-sha384
  • hmac-sha512
  • hmac-sha224
  • hmac-md5 (legacy)

Full DNS-AID Compliance: DDNSBackend supports ServiceMode SVCB records (priority > 0) with all required parameters (alpn, port, mandatory).

MockBackend

In-memory backend for testing.

from dns_aid.backends.mock import MockBackend

backend = MockBackend()

# Pre-populate zones
backend = MockBackend(zones={"example.com": {}})

JWS Signatures

Application-layer signature verification as an alternative to DNSSEC.

generate_keypair()

Generate an EC P-256 keypair for signing DNS records.

from dns_aid.core.jwks import generate_keypair

private_key, public_key = generate_keypair()
# private_key: EllipticCurvePrivateKey
# public_key: EllipticCurvePublicKey

export_jwks()

Export public key as JWKS JSON for hosting at .well-known/dns-aid-jwks.json.

from dns_aid.core.jwks import export_jwks

jwks_dict = export_jwks(public_key, kid="dns-aid-2024")
# {
#   "keys": [{
#     "kty": "EC",
#     "crv": "P-256",
#     "kid": "dns-aid-2024",
#     "use": "sig",
#     "x": "...",
#     "y": "..."
#   }]
# }

sign_record()

Sign a DNS record payload with a private key.

from dns_aid.core.jwks import sign_record, RecordPayload

payload = RecordPayload(
    fqdn="payment.example.com",
    target="payment.example.com",
    port=443,
    alpn="mcp",
)

jws_compact = sign_record(payload, private_key)
# Returns: "eyJhbGciOiJFUzI1NiIs..."

verify_signature()

Verify a JWS signature against a public key.

from dns_aid.core.jwks import verify_signature

is_valid, payload = verify_signature(jws_compact, public_key)
# is_valid: bool
# payload: RecordPayload if valid, None if invalid

Publishing with Signatures

from dns_aid import publish

result = await publish(
    name="payment",
    domain="example.com",
    protocol="mcp",
    endpoint="payment.example.com",
    sign=True,
    private_key_path="./keys/private.pem",
)

Discovery with Verification

from dns_aid import discover

agents = await discover(
    "example.com",
    verify_signatures=True,  # Verify JWS sig= parameter
)

Validation Utilities

Input validation functions for security compliance.

from dns_aid.utils.validation import (
    validate_agent_name,
    validate_domain,
    validate_protocol,
    validate_endpoint,
    validate_port,
    validate_ttl,
    validate_capabilities,
    validate_fqdn,
    ValidationError,
)

Functions

FunctionInputReturnsDescription
validate_agent_name(name)strstrValidate/normalize agent name
validate_domain(domain)strstrValidate/normalize domain
validate_protocol(protocol)strLiteral["mcp", "a2a"]Validate protocol
validate_endpoint(endpoint)strstrValidate endpoint hostname
validate_port(port)intintValidate port (1-65535)
validate_ttl(ttl)intintValidate TTL (60-604800)
validate_capabilities(caps)list[str]list[str]Validate capability list
validate_fqdn(fqdn)strstrValidate DNS-AID FQDN

ValidationError

Custom exception with structured error details.

try:
    validate_agent_name("INVALID NAME!")
except ValidationError as e:
    print(f"Field: {e.field}")
    print(f"Message: {e.message}")
    print(f"Value: {e.value}")

CLI Reference

The dns-aid CLI provides command-line access to all DNS-AID functions.

Commands

# Publish an agent (auto-updates index)
dns-aid publish --name my-agent --domain example.com --protocol mcp \
    --endpoint agent.example.com --capability chat --capability code

# Publish without updating index
dns-aid publish --name internal-bot --domain example.com --protocol mcp \
    --endpoint bot.example.com --no-update-index

# Discover agents (pure DNS - default)
dns-aid discover example.com
dns-aid discover example.com --protocol mcp

# Discover via HTTP index (ANS-compatible)
dns-aid discover example.com --use-http-index

# Verify an agent
dns-aid verify my-agent.example.com

# List all agents in a zone
dns-aid list example.com

# Delete an agent (auto-removes from index)
dns-aid delete --name my-agent --domain example.com --protocol mcp --force

# Delete without updating index
dns-aid delete --name my-agent --domain example.com --protocol mcp --force --no-update-index

# List available DNS zones
dns-aid zones

# Agent Index Commands
dns-aid index list example.com           # List agents in domain's index
dns-aid index sync example.com           # Sync index with actual DNS records

Agent Communication Commands

# Send a message to an A2A agent (discover-first: DNS → agent card → invoke)
dns-aid message --domain ai.infoblox.com --name security-analyzer \
    "Analyze security of marketing.ai.infoblox.com"

# Send a message to an A2A agent (direct endpoint)
dns-aid message --endpoint https://security-analyzer.ai.infoblox.com \
    "Analyze DNS-AID security posture"

# JSON output
dns-aid message --endpoint https://chat.example.com "Hello" --json

# Custom timeout (seconds)
dns-aid message --domain example.com --name chat "Hello" --timeout 60
OptionDescription
--domainDomain for DNS discovery (used with --name)
--nameAgent name for DNS discovery (used with --domain)
--endpointDirect endpoint URL (skips discovery)
--jsonOutput raw JSON response
--timeoutRequest timeout in seconds (default: 30)
# List tools on a remote MCP agent (discover-first)
dns-aid list-tools --domain example.com --name network-specialist

# List tools via direct endpoint
dns-aid list-tools --endpoint https://mcp.example.com/mcp

# Call a tool on a remote MCP agent
dns-aid call --endpoint https://mcp.example.com/mcp search_flights \
    --arguments '{"origin": "SFO", "destination": "JFK"}'

# Call with discover-first
dns-aid call --domain example.com --name network-specialist get_subnets \
    --arguments '{"network": "10.0.0.0/8"}'

Environment Variables

General:

VariableDescription
DNS_AID_BACKENDDefault backend: "route53", "cloudflare", "ns1", "infoblox", "nios", "akamai-edgedns", "ddns", or "mock"
DNS_AID_LOG_LEVELLogging level: DEBUG, INFO, WARNING, ERROR

AWS Route 53:

Route 53 uses boto3's credential chain. No env vars are required if ~/.aws/credentials or an IAM role is configured.

VariableRequiredDescription
AWS_ACCESS_KEY_IDNoAWS access key (or use aws configure / IAM role)
AWS_SECRET_ACCESS_KEYNoAWS secret key
AWS_DEFAULT_REGIONNoAWS region (default: us-east-1)
AWS_PROFILENoNamed profile from ~/.aws/credentials

Infoblox UDDI:

VariableDescription
INFOBLOX_API_KEYInfoblox UDDI API key (required)
INFOBLOX_DNS_VIEWDNS view name (default: "default")
INFOBLOX_BASE_URLAPI URL (default: https://csp.infoblox.com)

Infoblox NIOS (On-Prem):

VariableDescription
NIOS_HOSTGrid Manager hostname or IP (required)
NIOS_USERNAMEWAPI username (required)
NIOS_PASSWORDWAPI password (required)
NIOS_DNS_VIEWDNS view name (default: "default")
NIOS_WAPI_VERSIONWAPI version (default: "2.13.7")
NIOS_VERIFY_SSLVerify TLS certificate (default: "false")

DDNS (RFC 2136):

VariableDescription
DDNS_SERVERDNS server hostname or IP (required)
DDNS_KEY_NAMETSIG key name (required)
DDNS_KEY_SECRETTSIG key secret, base64 (required)
DDNS_KEY_ALGORITHMTSIG algorithm (default: hmac-sha256)
DDNS_PORTDNS server port (default: 53)

Akamai Edge DNS:

Akamai supports two credential sources. All four AKAMAI_* vars take precedence over ~/.edgerc when set.

VariableDescription
AKAMAI_HOSTEdgeGrid API hostname (e.g. akab-xxxx.luna.akamaiapis.net)
AKAMAI_CLIENT_TOKENEdgeGrid client token
AKAMAI_CLIENT_SECRETEdgeGrid client secret
AKAMAI_ACCESS_TOKENEdgeGrid access token
AKAMAI_EDGERCPath to .edgerc credentials file (default: ~/.edgerc)
AKAMAI_EDGERC_SECTIONSection within .edgerc (default: default)

MCP Server

The MCP server (dns-aid-mcp) exposes DNS-AID as tools for AI assistants.

Starting the Server

# Stdio transport (for Claude Desktop)
dns-aid-mcp

# HTTP transport (for remote access)
dns-aid-mcp --transport http --port 8000

# HTTP with custom host binding
dns-aid-mcp --transport http --host 0.0.0.0 --port 8000

Available Tools

ToolDescription
publish_agent_to_dnsPublish an agent to DNS (auto-updates index)
discover_agents_via_dnsDiscover agents at a domain (supports use_http_index param)
verify_agent_dnsVerify agent DNS records
list_published_agentsList all agents in a zone
delete_agent_from_dnsDelete an agent from DNS (auto-updates index)
list_agent_indexList agents in domain's index
sync_agent_indexSync index with actual DNS records
send_a2a_messageSend a message to an A2A agent. Accepts domain + name (discover-first) or endpoint (direct).

Health Endpoints (HTTP Transport)

EndpointMethodDescription
/GETServer info and available tools
/healthGETBasic health check
/readyGETReadiness check (DNS backend available)

Claude Desktop Integration

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "dns-aid": {
      "command": "dns-aid-mcp"
    }
  }
}

Error Handling

All functions may raise exceptions. Recommended pattern:

from dns_aid import publish, discover
from dns_aid.utils.validation import ValidationError

try:
    result = await publish(
        name="my-agent",
        domain="example.com",
        protocol="mcp",
        endpoint="agent.example.com",
    )
    if not result.success:
        print(f"Publish failed: {result.message}")
except ValidationError as e:
    print(f"Invalid input: {e.field} - {e.message}")
except Exception as e:
    print(f"Unexpected error: {e}")

Invocation Module (core/invoke.py)

Single source of truth for agent invocation. Both CLI and MCP server delegate to these functions.

send_a2a_message()

Send a message to an A2A agent using discover-first or direct endpoint.

from dns_aid.core.invoke import send_a2a_message, InvokeResult

# Discover-first (DNS → agent card → invoke)
result: InvokeResult = await send_a2a_message(
    message="Analyze DNS-AID security posture",
    domain="ai.infoblox.com",
    name="security-analyzer",
    timeout=30.0,
)

# Direct endpoint (skip discovery)
result = await send_a2a_message(
    message="Hello",
    endpoint="https://chat.example.com",
)

print(result.text)       # Extracted text response
print(result.raw)        # Full JSON-RPC response dict
print(result.error)      # Error message if failed (None on success)
ParameterTypeRequiredDescription
messagestrYesMessage text to send
domainstrNoDomain for DNS discovery (used with name)
namestrNoAgent name for DNS discovery (used with domain)
endpointstrNoDirect endpoint URL (skips discovery). Either domain+name or endpoint required.
timeoutfloatNoRequest timeout in seconds (default: 30)

call_mcp_tool()

Call a tool on a remote MCP agent via JSON-RPC tools/call.

from dns_aid.core.invoke import call_mcp_tool

result = await call_mcp_tool(
    endpoint="https://mcp.example.com/mcp",
    tool_name="search_flights",
    arguments={"origin": "SFO", "destination": "JFK"},
)

list_mcp_tools()

List available tools on a remote MCP agent via JSON-RPC tools/list.

from dns_aid.core.invoke import list_mcp_tools

result = await list_mcp_tools(
    endpoint="https://mcp.example.com/mcp",
)

resolve_a2a_endpoint()

Resolve an A2A agent endpoint via DNS discovery and agent card fetch.

from dns_aid.core.invoke import resolve_a2a_endpoint

endpoint_url = await resolve_a2a_endpoint(
    domain="ai.infoblox.com",
    name="security-analyzer",
)
# Returns: "https://security-analyzer.ai.infoblox.com:443"

Resolution chain:

  1. DNS discovery (discover(domain, protocol="a2a", name=name))
  2. Agent card fetch (/.well-known/agent-card.json) for canonical URL
  3. Host mismatch protection: if agent card URL hostname differs from DNS endpoint, DNS wins

InvokeResult

Returned by all invocation functions.

FieldTypeDescription
textstr | NoneExtracted text from response
rawdict | NoneFull response payload
errorstr | NoneError message if invocation failed

SDK: Invocation & Telemetry

The Tier 1 SDK provides agent invocation with automatic telemetry capture, and community-wide ranking queries.

Top-Level Functions

invoke()

async def invoke(
    agent: AgentRecord,
    *,
    method: str | None = None,
    arguments: dict | None = None,
    timeout: float | None = None,
    config: SDKConfig | None = None,
) -> InvocationResult

One-shot agent invocation with telemetry. Creates an AgentClient, calls the agent, returns the result with an attached signal.

Examples:

import dns_aid

# MCP agent: list tools
result = await dns_aid.discover("example.com", protocol="mcp")
resp = await dns_aid.invoke(result.agents[0], method="tools/list")
print(resp.signal.invocation_latency_ms)  # 148.2

# A2A agent: send a message (standard JSON-RPC message/send)
result = await dns_aid.discover("ai.infoblox.com", protocol="a2a")
resp = await dns_aid.invoke(
    result.agents[0],
    method="message/send",
    arguments={
        "message": {
            "messageId": "unique-id",
            "role": "user",
            "parts": [{"kind": "text", "text": "What is DNS-AID?"}],
        }
    },
)
# Standard A2A methods (message/send, tasks/get, etc.) are automatically
# wrapped in a JSON-RPC 2.0 envelope by the A2A protocol handler.

rank()

async def rank(
    agents: list[AgentRecord],
    *,
    method: str | None = None,
    arguments: dict | None = None,
    config: SDKConfig | None = None,
) -> list[RankedAgent]

Invoke multiple agents and rank by telemetry performance (composite score).

Protocol Handlers

The SDK routes invocations through protocol-specific handlers:

ProtocolHandlerWire Format
MCPMCPProtocolHandlerMCP Streamable HTTP (modern, spec 2025-03-26+) for tools/call and tools/list, with transparent legacy plain JSON-RPC POST fallback when the target rejects the modern transport
A2AA2AProtocolHandlerJSON-RPC 2.0 for standard methods (message/send, tasks/get); generic payload for custom methods
HTTPSHTTPSProtocolHandlerHTTP POST with JSON body

MCP Protocol Handler delegates transport to the official mcp Python SDK (mcp.client.streamable_http.streamablehttp_client and mcp.ClientSession) for the modern path. The handler injects a per-invocation telemetry adapter (latency, TTFB, response size, cost headers, TLS version) and propagates the dns-aid X-DNS-AID-Caller-Domain header on every request when DNS_AID_CALLER_DOMAIN is set. On transport mismatch (HTTP 405/406, refused initialize via JSON-RPC -32601) the handler transparently falls back to the legacy plain JSON-RPC POST path; the fallback event is logged as a structured warning (transport.legacy_fallback) carrying the endpoint, the failure reason, and the modern attempt latency.

A2A Protocol Handler automatically wraps standard A2A methods in a JSON-RPC 2.0 envelope:

# Standard methods (message/send, message/stream, tasks/get, tasks/cancel, etc.)
# are wrapped in: {"jsonrpc": "2.0", "method": "...", "params": {...}, "id": "..."}

# Non-standard/custom methods use generic format for backward compatibility:
# {"method": "custom_task", ...arguments}

AgentClient

The main SDK class. Use as async context manager for connection reuse.

from dns_aid.sdk import AgentClient, SDKConfig

config = SDKConfig(timeout_seconds=30.0, caller_id="my-app")

async with AgentClient(config=config) as client:
    result = await client.invoke(agent, method="tools/list")
    ranked = client.rank()

Methods:

MethodDescription
invoke(agent, method, arguments, timeout, credentials, credential_provider, auth_handler)Invoke agent, return InvocationResult
rank(strategy)Rank all invoked agents by composite score
fetch_rankings(fqdns, limit)Fetch community-wide rankings from telemetry API
signalsProperty: list of all collected InvocationSignal objects

invoke() — credential resolution (v0.21.0+)

AgentClient.invoke() resolves authentication credentials in this precedence order (the first non-empty source wins; subsequent sources are not consulted):

  1. auth_handler — explicit AuthHandler instance for full caller control
  2. credentials — pre-fetched credentials dict (existing behavior)
  3. credential_provider — async callback awaited lazily at invoke time (v0.21.0+)
  4. No-auth fallback when all three are absent

The credential_provider parameter accepts an async callable that takes the target AgentRecord and returns a credentials dict. It enables short-lived delegation tokens (RFC 8693 token exchange), per-target credential scoping, AWS STS assume-role per invocation, and dynamic secret stores (Vault, KMS).

from dns_aid.core.models import AgentRecord

async def per_target_provider(agent: AgentRecord) -> dict[str, str]:
    # Provider receives the AgentRecord — can derive credentials from
    # agent.fqdn, agent.realm, agent.connect_meta, etc.
    return {"token": await mint_jwt_for(agent.fqdn)}

async with AgentClient(config=config) as client:
    resp = await client.invoke(
        agent,
        method="tools/list",
        credential_provider=per_target_provider,
    )

SDKConfig.credential_provider_timeout (default 30s, env var DNS_AID_CREDENTIAL_PROVIDER_TIMEOUT) bounds the await. Hanging providers surface as CredentialProviderError with the underlying TimeoutError preserved as __cause__. Provider exceptions never leak credential values into logs or the wrapped error's serialised surface.

See security-credentials.md for the per-handler security matrix and the full credential-handling posture.

fetch_rankings()

async def fetch_rankings(
    self,
    fqdns: list[str] | None = None,
    limit: int = 50,
) -> list[dict]

Fetch community-wide rankings from a configured telemetry API endpoint. Returns pre-computed composite scores based on aggregated telemetry.

Parameters:

ParameterTypeDefaultDescription
fqdnslist[str] | NoneNoneFilter rankings to specific agent FQDNs
limitint50Maximum number of rankings to return

Returns: List of ranking dictionaries with agent_fqdn, composite_score, etc.

Example:

async with AgentClient(config) as client:
    # Get all rankings
    rankings = await client.fetch_rankings()

    # Get rankings for specific agents only
    rankings = await client.fetch_rankings(
        fqdns=["booking.example.com"],
        limit=10
    )

    for r in rankings:
        print(f"{r['agent_fqdn']}: {r['composite_score']}")

Note: Requires telemetry_api_url to be configured in SDKConfig. Returns empty list if not configured.

SDKConfig

from dns_aid.sdk import SDKConfig

config = SDKConfig(
    timeout_seconds=30.0,            # Default request timeout
    caller_id="my-app",              # Caller identifier for signals
    persist_signals=False,           # Auto-save signals to PostgreSQL
    database_url=None,               # DB URL (falls back to DATABASE_URL env)
    otel_enabled=False,              # Enable OpenTelemetry export (v0.23.0+)
    otel_endpoint=None,              # OTLP endpoint URL — use http:// (plaintext) or https:// (TLS)
    otel_export_format="otlp",       # "otlp" | "console" | "noop"
    otel_sampler=None,               # Sampler name; None = OTEL default (v0.23.0+)
    otel_environment=None,           # deployment.environment resource attr (v0.23.0+)
    otel_metric_labels=[],           # Opt-in high-cardinality labels: fqdn|caller|tool (v0.23.0+)
    http_push_url=None,              # POST signals to remote telemetry API
    directory_api_url=None,          # Base URL for AgentClient.search() and fetch_rankings()
    telemetry_api_url=None,          # Deprecated alias for directory_api_url
    credential_provider_timeout=30.0,  # Max seconds to wait for credential_provider callback (v0.21.0+)
)

# Or from environment variables:
config = SDKConfig.from_env()

Environment Variables:

VariableDefaultDescription
DNS_AID_SDK_TIMEOUT30.0Request timeout in seconds
DNS_AID_SDK_CALLER_IDNoneCaller identifier
DNS_AID_SDK_PERSIST_SIGNALSfalseEnable DB persistence
DATABASE_URLNonePostgreSQL connection URL
DNS_AID_SDK_OTEL_ENABLEDfalseEnable OpenTelemetry (v0.23.0+)
DNS_AID_SDK_OTEL_ENDPOINTNoneOTLP collector URL — http:// plaintext, https:// TLS
DNS_AID_SDK_OTEL_EXPORT_FORMATotlpotlp | console | noop
DNS_AID_SDK_OTEL_SAMPLERNoneSampler name; lower precedence than OTEL_TRACES_SAMPLER (v0.23.0+)
DNS_AID_SDK_OTEL_ENVIRONMENTNoneSets deployment.environment resource attr (v0.23.0+)
DNS_AID_SDK_OTEL_METRIC_LABELSNoneComma-separated opt-in labels: fqdn,caller,tool (v0.23.0+)
DNS_AID_SDK_HTTP_PUSH_URLNonePOST signals to this URL
DNS_AID_SDK_DIRECTORY_API_URLNoneBase URL for AgentClient.search() + fetch_rankings() (v0.19.0+)
DNS_AID_SDK_TELEMETRY_API_URLNoneDeprecated alias for DNS_AID_SDK_DIRECTORY_API_URL

Standard OpenTelemetry env vars are also honored: OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, OTEL_PROPAGATORS, OTEL_RESOURCE_ATTRIBUTES, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_ENDPOINT. See docs/integrations/opentelemetry.md for the full guide (sampling, propagation, managed-collector auth, failure modes).

The resolved_directory_url property returns directory_api_url when set, falling back to telemetry_api_url for backwards compatibility. Using the legacy alias emits a DeprecationWarning once per process.


AgentClient.search() — Path B cross-domain search (v0.19.0+)

async def search(
    self,
    q: str | None = None,
    *,
    protocol: Literal["mcp", "a2a", "https"] | None = None,
    domain: str | None = None,
    capabilities: list[str] | None = None,
    min_security_score: int | None = None,
    verified_only: bool = False,
    intent: str | None = None,
    auth_type: str | None = None,
    transport: str | None = None,
    realm: str | None = None,
    limit: int = 20,
    offset: int = 0,
) -> SearchResponse

Issues GET {directory_api_url}/api/v1/search and returns a typed SearchResponse. Path B is opt-in: invoking search() without directory_api_url configured raises DirectoryConfigError immediately, before any network work.

Parameters

ParameterTypeDefaultDescription
qstr | NoneNoneFree-text query, or None for browse-all-with-filters mode.
protocolLiteral["mcp","a2a","https"] | NoneNoneRestrict to a protocol.
domainstr | NoneNoneRestrict to a single domain.
capabilitieslist[str] | NoneNoneAll-of capability match.
min_security_scoreint | NoneNoneMinimum security score (0–100).
verified_onlyboolFalseRestrict to DCV-verified domains.
intentstr | NoneNoneAction intent filter (query / command / transaction / subscription).
auth_typestr | NoneNoneAuth type filter.
transportstr | NoneNoneTransport filter (streamable-http, https, sse, stdio).
realmstr | NoneNoneMulti-tenant realm filter.
limitint20Page size, 1–10000.
offsetint0Pagination offset.

Returns

SearchResponse — query echo, ranked results, pagination state.

Raises

  • DirectoryConfigErrordirectory_api_url not configured.
  • DirectoryAuthError — Directory rejected credentials (HTTP 401/403).
  • DirectoryRateLimitedError — Directory rate-limited (HTTP 429); the retry_after_seconds detail mirrors the Retry-After header.
  • DirectoryUnavailableError — Transient: connect refused, timeout, 5xx, 404, unexpected redirect, oversized response, or response shape the SDK can't validate.
  • RuntimeError — Client not in an async context manager.

Example

from dns_aid.sdk import AgentClient, SDKConfig

async with AgentClient(config=SDKConfig.from_env()) as client:
    response = await client.search(
        "payment processing",
        protocol="mcp",
        capabilities=["payment-processing"],
        min_security_score=70,
    )
    for result in response.results:
        print(f"{result.score:.2f}  {result.agent.fqdn}  T{result.trust.trust_tier}")
        print(f"   sec={result.trust.security_score}  trust={result.trust.trust_score}")

    # Paginate
    while response.has_more:
        response = await client.search(q="payment", offset=response.next_offset)

Composition pattern (zero-trust)

Path B returns directory-attested candidates. Path A re-verifies via DNS substrate before invoking — directory is opt-in convenience, never a trust bottleneck.

from dns_aid.sdk import AgentClient
from dns_aid.core.discoverer import discover

async with AgentClient() as client:
    # 1. Cross-domain candidate discovery
    response = await client.search(q="fraud detection", min_security_score=70)

    for candidate in response.results:
        # 2. DNS-substrate re-verification with signature requirement
        verified = await discover(
            candidate.agent.domain,
            name=candidate.agent.name,
            require_signed=True,
            require_signature_algorithm=["ES256", "Ed25519"],
        )
        if verified.agents:
            # 3. Safe to invoke
            ...

SearchResponse / SearchResult / TrustAttestation / Provenance (v0.19.0+)

Typed result models in dns_aid.sdk.search. All immutable (frozen=True) and forward-compatible (extra="ignore") so directory schema additions don't break SDK consumers.

class SearchResponse(BaseModel):
    query: str | None              # Echo of the q parameter from the directory.
    results: list[SearchResult]    # Ranked results, length <= limit.
    total: int                     # Matches across all pages (after skip-and-log).
    limit: int
    offset: int

    @property
    def has_more(self) -> bool: ...
    @property
    def next_offset(self) -> int | None: ...

class SearchResult(BaseModel):
    agent: AgentRecord
    score: float                       # Raw relevance score, not normalized.
    trust: TrustAttestation            # Defaults to all-zero if directory omits.
    provenance: Provenance | None      # Built when first_seen / last_seen present.

class TrustAttestation(BaseModel):
    security_score: int = 0            # 0–100
    trust_score: int = 0               # 0–100
    popularity_score: int = 0          # 0–100
    trust_tier: int = 0                # 0–3 (untiered / basic / enhanced / continuous)
    safety_status: Literal["active", "blocked"] = "active"
    dnssec_valid: bool | None = None
    dane_valid: bool | None = None
    svcb_valid: bool | None = None
    endpoint_reachable: bool | None = None
    protocol_verified: bool | None = None
    threat_flags: dict[str, Any] = {}
    breakdown: dict[str, Any] | None = None   # Directory's trust_breakdown.
    badges: list[str] | None = None           # Directory's trust_badges.

class Provenance(BaseModel):
    discovery_level: int = 0           # 0 observed / 1 beacon / 2 manifest / 3 federated
    first_seen: datetime
    last_seen: datetime
    last_verified: datetime | None = None
    company: dict[str, Any] | None = None

Directory exceptions (v0.19.0+)

from dns_aid.sdk import (
    DirectoryError,                # Base exception class.
    DirectoryConfigError,          # directory_api_url not configured (78 EX_CONFIG).
    DirectoryUnavailableError,     # Transient (75 EX_TEMPFAIL).
    DirectoryRateLimitedError,     # 429; carries retry_after_seconds detail.
    DirectoryAuthError,            # 401/403 (77 EX_NOPERM); does NOT inherit from Unavailable.
)

Every exception carries a details: dict[str, Any] attribute with structured fields (directory_url, status_code, underlying, etc.) so callers can dispatch on type and inspect details for richer error handling.

InvocationResult

Returned by invoke(). Contains response data and telemetry signal.

FieldTypeDescription
successboolWhether invocation succeeded
datadict | str | NoneResponse payload
signalInvocationSignalTelemetry signal for this call
error_messagestr | NoneError description if failed

InvocationSignal

Per-call telemetry captured automatically.

FieldTypeDescription
idUUIDUnique signal identifier
agent_fqdnstrAgent DNS-AID FQDN
agent_endpointstrEndpoint URL used
protocolstrProtocol (mcp, a2a, https)
methodstrMethod called
statusInvocationStatussuccess, error, timeout, refused
invocation_latency_msfloatTotal invocation time
ttfb_msfloatTime to first byte
http_status_codeintHTTP response status
cost_unitsfloatCost from X-Cost-Units header
cost_currencystrCurrency from X-Cost-Currency header
response_size_bytesintResponse payload size
tls_versionstrTLS version used
timestampdatetimeWhen the call was made
caller_idstrCaller identifier from config

Ranking

ranked = client.rank()  # Default: WeightedCompositeStrategy

for r in ranked:
    print(f"{r.agent_fqdn}: {r.composite_score:.1f}")

Scoring Formula (WeightedComposite):

composite = 0.40 * reliability   (success_rate * 100)
          + 0.30 * latency       (100 * (1 - avg_latency/5000))
          + 0.15 * cost          (relative to cheapest)
          + 0.15 * freshness     (recency weighted)

Available Strategies:

  • WeightedCompositeStrategy (default)
  • LatencyFirstStrategy — prioritizes lowest latency
  • ReliabilityFirstStrategy — prioritizes highest success rate

HTTP Telemetry Push (Optional)

The SDK can optionally push signals to an external telemetry collection endpoint:

config = SDKConfig(
    http_push_url="https://your-telemetry-server.example.com/signals"
)

async with AgentClient(config=config) as client:
    # Signals automatically pushed in a background thread
    await client.invoke(agent, method="tools/list")

Disabled by default (http_push_url=None). Configure via SDKConfig or the DNS_AID_SDK_HTTP_PUSH_URL environment variable.


Version

import dns_aid
print(dns_aid.__version__)  # "0.6.0"

See Also