feat(experimental): EDNS(0) agent-hint signaling [experimental]#123
Open
nicknacnic wants to merge 5 commits into
Open
feat(experimental): EDNS(0) agent-hint signaling [experimental]#123nicknacnic wants to merge 5 commits into
nicknacnic wants to merge 5 commits into
Conversation
0f30fc9 to
63a5998
Compare
Introduce an experimental EDNS(0) option (agent-hint, code 65430, private use)
that lets a DNS-AID client signal selector filters with each outgoing query.
Any hop on the resolution path that understands the option may use the hint
to narrow the response or short-circuit with a cached pre-filtered match.
Stock authoritative servers treat the option as inert per RFC 6891 §6.1.1.
The design accommodates three loci of processing:
1. In-process client cache (EdnsAwareResolver — shipped in this PR)
2. Hint-aware forwarder / recursive resolver (out of scope, future work)
3. Hint-aware authoritative DNS server (out of scope, future work)
Wire format carries up to 255 selectors of (1B code + 1B length + UTF-8 value),
with a soft 512-byte payload cap. v0 defines four selector codes matching the
existing Path A filter taxonomy: capabilities (0x01), intent (0x02),
transport (0x03), auth_type (0x04). A response-side echo (VERSION 0x80) lets
hint-aware hops advertise which selectors they honoured — absence is meaningful
(no upstream filtering happened, client should fall back to local filtering).
Publisher advertisement uses two complementary channels:
- Channel 1 (JSON): `edns_signaling` block in cap-doc / agent-card / agents-index
JSON. Lifted onto CapabilityDocument, A2AAgentCard, and HttpIndexAgent as an
optional dict field. Forward-compat on unknown shapes (silent).
- Channel 2 (DNS): response-side OPT echo (preferred), or an optional SVCB
advertisement param key65409 = "agent-hint" (reserved, not emitted in v0).
Establishes the `src/dns_aid/experimental/` namespace convention:
- Experimental APIs never re-exported from dns_aid/__init__.py — import
explicitly: `from dns_aid.experimental import AgentHint`.
- Per-feature env flag (DNS_AID_EXPERIMENTAL_EDNS_HINTS=1) gates runtime
behaviour. Without the flag, code paths stay dormant.
- CLI commands print `[experimental]` banner on stderr; exit non-zero when
the flag isn't set.
- Design docs live in docs/experimental/ with section headers that map to
future IETF-draft structure for clean migration to LF spec material later.
Files:
- src/dns_aid/experimental/{__init__,edns_hint,edns_cache}.py — new subpackage
- src/dns_aid/core/_edns_hint_ctx.py — private contextvar helper shared by
discoverer and indexer (avoids circular import while keeping the hint
threaded through the discovery chain)
- src/dns_aid/core/discoverer.py — agent_hint= kwarg on discover(); body
extracted into _discover_body() so the contextvar try/finally wraps cleanly
- src/dns_aid/core/indexer.py — applies the hint on the index TXT query path
- src/dns_aid/core/{cap_fetcher,a2a_card,http_index}.py — lift the optional
edns_signaling advertisement from JSON
- src/dns_aid/cli/main.py — new edns-probe command with --show-wire
- tests/unit/test_edns_hint.py + test_edns_cache.py — 37 new unit tests
- tests/testbed/smoke_edns.sh — manual tcpdump-based wire verification against
the BIND9 testbed
- docs/experimental/{README,edns-signaling.md,edns-signaling.abnf} — full
design doc with Overview / Motivation / Conceptual model / Wire format /
Advertisement / Privacy / Security / Open questions / Future work
- docs/api-reference.md — new "Experimental: EDNS(0) signaling" section
- docs/architecture.md — new "Experimental namespace" section
- README.md — Experimental Features pointer in the Documentation list
Out of scope (forward work):
- Reference hint-aware authoritative server
- IANA option-code reservation
- Recursive/forwarder reference at Locus 2
- Echo authentication (DNSSEC doesn't cover OPT records)
Signed-off-by: Layer8 <NWillAU900@gmail.com>
…ew fixes
Replaces the v1 selector set (capabilities / intent / transport / auth_type)
with a structured two-axis taxonomy after design review. The v1 selectors were
the wrong layer — capabilities and intent live in cap-doc JSON, not in SVCB,
so an authoritative server cannot filter on them without breaking DNS latency
budgets. Moves those to Channel 1 JSON (post-fetch local filter) and reshapes
the wire option around what the substrate actually has access to.
Wire format (option code 65430, RFC 6891 private use):
Axis 1 — substrate filters (0x01–0x0F). Participate in the cache key.
0x01 realm — multi-tenant scope; SVCB realm= param
0x02 transport — "mcp" | "a2a" | "https"
0x03 policy_required — only records carrying a policy= URI ("1" or absent)
0x04 min_trust — "signed" | "dnssec" | "signed+dnssec"
0x05 jurisdiction — ISO region tag ("eu", "us-east", ...)
Axis 2 — metering / lifecycle (0x10–0x1F). Drive request policy; do NOT
fragment the cache.
0x10 client_intent_class — "discovery" | "invocation"
0x11 max_age — UTF-8 decimal seconds
0x12 parallelism — sibling-query count (fan-out signal)
0x13 deadline_ms — wait budget; hint-only, no SLA refuse in v0
0x20+ reserved. client_cookie (0x20) and correlation_id (0x21) documented
as future selectors; not coded in v0.
The two-axis split is structural: AgentHint.signature() includes Axis 1 only.
Two queries differing only in metering (parallelism, deadline_ms, etc.) hit
the same cache entry — the answer set is the same, the policy applied to the
request is different. Locked in by test_axis2_only_differences_still_hit_cache.
Self-audit fixes against Igor's prior DCV review (CLAUDE.md / memory):
- First-wins on duplicate selector codes. Mirrors _parse_txt_value in dcv.py.
Defends against a hostile forwarder appending an overriding selector after
a legitimate one. Verified live: payload with realm=prod, realm=evil now
decodes to realm="prod" (was "evil" pre-fix).
- Empty Axis-1 string values on decode treated as field-not-set. Matches the
encode-side semantics (encode skips empty strings via `if self.realm:`) and
prevents a forged empty-value payload from fragmenting the cache key under
a value the legitimate client would never produce.
- New tests/unit/test_edns_hint_ctx.py (18 tests). Covers the most security-
relevant runtime gate — no wire emission without DNS_AID_EXPERIMENTAL_EDNS_
HINTS=1. Parametrized truthy/non-truthy env-value matrix, exception-swallow
(experimental crash MUST NOT propagate into stable core discovery), and
contextvar reset scoping.
Conceptual model widened in the design doc: Locus 1 is now framed as "any
in-client programmable hop the SDK provides" rather than just the resolver
wrapper — covers the SDK growing into a real agentic cache, or a small DNS-
like cache process co-located with the agent runtime. Axis-aware code-range
numbering documented as a deliberate trade-off vs flat numbering (§9.3).
Async fan-out is now the primary worked example in §4.4.
Tests: 70 experimental (was 37 in v1). ruff format/check, mypy --strict, full
unit suite (1569 passing) all clean before commit.
Signed-off-by: Layer8 <NWillAU900@gmail.com>
- Pre-install tcpdump in agents/Dockerfile so smoke_edns.sh doesn't need external network access at runtime (the testbed agent containers are intentionally pinned to the internal DNS topology with no upstream resolver, so apt-get install at smoke time can't fetch packages). - Update the smoke_edns.sh probe call to use the new v2 CLI flags (--realm / --transport / --min-trust / --intent-class / --parallelism / --deadline-ms), matching the two-axis selector taxonomy. - Tighten the result-check shell logic so `set -euo pipefail` doesn't bail early when grep doesn't find the marker on a real failure. End-to-end verified locally against bind-orga (stock BIND9 9.20). The agent-hint OPT record (code 0xff96 = 65430) reaches the authoritative on all three discovery query paths — index TXT, SVCB, capabilities TXT — and the wire payload matches the design doc bit-for-bit: 0xff96 (option-code) 0x002b (length=43) 0x00 (VERSION) 0x06 (SELECTOR-COUNT) 01 04 "prod" realm 02 03 "mcp" transport 04 06 "signed" min_trust 10 0a "invocation" client_intent_class 12 01 "4" parallelism 13 05 "30000" deadline_ms Stock BIND9 returns the answer set without an AgentHintEcho — correct behaviour for an inert-to-the-option authoritative, and the client's "no upstream filtering happened" fallback path engages cleanly. Signed-off-by: Layer8 <NWillAU900@gmail.com>
63a5998 to
57c25ad
Compare
Rework the main README to position DNS-AID as a neutral DNS-layer substrate rather than a centralized aggregator or competitor to other agent-discovery efforts: - Replace 'Companion services' hosted-directory billing with an 'Ecosystem and Integrations' section that names no operator. - Path B example: drop trust_tier / min_security_score from the lead snippet; frame the directory as opt-in convenience with the DNS substrate as the authoritative trust gate. - Telemetry section: remove fetch_rankings() from the intro; keep local rank() and configurable HTTP push; mention community rankings only as caller-configured. - CLI: dns-aid search / dns-aid submit use placeholder directory URLs and a --to flag. - Delete the 'Server-Side: Agent Directory Pipeline' ASCII diagram (CRAWLING / CURATION / INDEXING / SERVING with trust_score / TSVECTOR / Rankings); replace with a brief paragraph stating directory/indexing services are out of scope and free to define their own scoring and ranking. - Replace 'Why DNS-AID? / vs Competing Proposals / The Sovereignty Question / Google's Agent Ecosystem / .agent comparison table' with a single 'How DNS-AID Relates to Other Efforts' section in standards-org voice. Names ANS, A2A, AgentDNS, NANDA, ai.txt / llms.txt, and the .agent gTLD as parallel work at different layers of the stack rather than competitors. - Soften three 'ANS-compatible' repetitions to 'format aligns with the ANS schema'. Net: -145 / +48 lines. Signed-off-by: Layer8 <NWillAU900@gmail.com>
Signed-off-by: Layer8 <NWillAU900@gmail.com>
This was referenced May 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Experimental DNS-AID extension — an EDNS(0) option (
agent-hint, code 65430 in the RFC 6891 private-use range) that lets clients attach selector filters to outgoing DNS queries. Any hop on the resolution path that understands the option may use the hint to narrow the response or short-circuit with a cached pre-filtered match. Stock authoritative servers treat the option as inert per RFC 6891 §6.1.1 — graceful degradation.Forward-looking design + client-side reference implementation, not a shipping feature. The primary deliverable is the design doc at docs/experimental/edns-signaling.md, written with section headers that map cleanly to future IETF-draft structure so content lifts when an LF spec home arrives.
Two-axis selector taxonomy
Axis 1 — substrate filters (0x01–0x0F). What records to return. Participate in the cache key.
realmrealm=param (multi-tenant scope)transportmcp/a2a/https_{proto}._agents+alpnpolicy_required"1"(or absent)policy=URImin_trustsigned/dnssec/signed+dnssecsigparam + DNSSEC chainjurisdictionAxis 2 — metering / lifecycle (0x10–0x1F). How to handle the request. Do NOT fragment the cache.
client_intent_classdiscovery/invocationmax_ageparallelismdeadline_ms0x20+ reserved.
client_cookie(0x20) andcorrelation_id(0x21) are documented as future selectors with explicit privacy / threat-model caveats; not coded in v0.Not DNS-layer selectors.
capabilitiesandintentlive in Channel 1 JSON advertisement (edns_signaling.honored_selectorsin cap-doc / agent-card), used for post-fetch local filtering. The reason is layering: SVCB doesn't carry capability strings — those live in cap-doc JSON that an auth would have to dereference per-query, breaking DNS latency budgets.Load-bearing invariant: Axis 1 ⊆ cache key
AgentHint.signature()includes Axis 1 only. Two queries that differ only in metering — say one withparallelism=4and another withparallelism=64— hit the same cache entry. Same answer set, different request policy. Locked in bytest_axis2_only_differences_still_hit_cacheandtest_signature_includes_axis1_only.Three loci of processing (forward-looking)
The wire format and advertisement schema support hint-aware processing at any hop along the resolution path:
EdnsAwareResolver. Long-term: the SDK growing into a real agentic cache, or a small DNS-like cache process co-located with the agent runtime. Always usable; ships in this PR.What's in this PR
New
experimental/namespace convention (established here; documented indocs/architecture.md): code insrc/dns_aid/experimental/, never re-exported from the top-level package, env-flag-gated runtime,[experimental]stderr banner on CLI commands, design docs indocs/experimental/.Code:
src/dns_aid/experimental/{__init__,edns_hint,edns_cache}.py—AgentHint,AgentHintEcho,EdnsSignalingAdvertisement,EdnsAwareResolversrc/dns_aid/core/_edns_hint_ctx.py— private contextvar helper shared bydiscoverer.pyandindexer.py(avoids circular import while keeping the hint threaded through discovery)src/dns_aid/core/discoverer.py—agent_hint=kwarg ondiscover(), body extracted so the contextvartry/finallywraps cleanlysrc/dns_aid/core/indexer.py— applies the hint on the_index._agents.{domain}TXT query pathsrc/dns_aid/core/{cap_fetcher,a2a_card,http_index}.py— lift the optionaledns_signalingadvertisement from JSON (forward-compat on unknown shapes)src/dns_aid/cli/main.py— newdns-aid edns-probe <domain>command, env-flag-gated,[experimental]bannerTests (70 experimental):
test_edns_hint.py(40) — wire format round-trip both axes, malformed-input rejection, signature stability, Axis-1-only invariant, first-wins on duplicate selector codes (3 adversarial regression tests), empty Axis-1 values treated as field-not-set (3 tests)test_edns_cache.py(12) — cache hit/miss, TTL, hint-mismatch, echo surfacing, Axis-2-only-differences-still-hit-cachetest_edns_hint_ctx.py(18) — env-flag gating, truthy/non-truthy matrix, exception-swallow (experimental crash MUST NOT propagate into stable core discovery), contextvar reset scopingEnd-to-end wire verification (local):
tests/testbed/smoke_edns.shagainst the BIND9 testbed.tcpdumpcapture confirms the option (code 0xff96) reaches the authoritative on all three discovery query paths (index TXT / SVCB / capabilities TXT), with wire bytes matching the design doc bit-for-bit. Stock BIND9 returns no echo — correct behaviour for an inert authoritative, and the client's local-filter fallback engages cleanly.tcpdumpis now pre-installed in the agent Dockerfile so the smoke script is self-contained.Docs:
docs/experimental/edns-signaling.md— full design doc (Overview / Motivation incl. async fan-out / Conceptual model / Wire format / Advertisement / Privacy / Security / Open Questions / Future Work)docs/experimental/edns-signaling.abnf— wire-format ABNF with axis-encoded code rangesdocs/experimental/README.md— index + namespace conventionsREADME.md— Experimental Features pointerdocs/api-reference.md— new "Experimental: EDNS(0) signaling" sectiondocs/architecture.md— new "Experimental namespace" sectionSelf-audit against prior DCV review
_parse_txt_value)realm=prod+realm=evil→realm=="prod"None, matching encode-sideif self.realm:skipValueErrorMAX_OPTION_PAYLOAD=512,MAX_SELECTOR_VALUE_LEN=255TYPE_CHECKINGor function-body imports onlyMagicMock(notAsyncMock) for resolver in tests_make_upstreamhelperruff format,ruff check,mypyall cleanCI gates verified locally
ruff format --check src/dns_aid— cleanruff check src/dns_aid— cleanmypy src/dns_aid— Success: no issues found in 84 source filespytest tests/unit/— 1569 passed; same 35 pre-existing CEL/ML-DSA failures, no new regressionstests/testbed/smoke_edns.sh— option appears on the wire to BIND9 (manual; requires Docker)Test plan
mypy,ruff format --check,ruff checkcleanfrom dns_aid import discoverdoes not triggerdns_aid.experimentalimportDNS_AID_EXPERIMENTAL_EDNS_HINTS=1,dns-aid edns-probeprints env-var instruction and exits non-zerotests/testbed/smoke_edns.sh— option 0xff96 appeared on the wire across all three discovery query pathsOut of scope (forward work)
deadline_msenforcement (currently hint-only; structured SLA-refuse RCODE is §10 future work)Branch
Three commits on `experimental/edns-signaling`:
Squash at merge time is fine if preferred.
Acknowledgments
Thanks to John Zinky (Akamai) for design-level input on the experimental EDNS(0)
agent-hintsignaling work — particularly on how hint-aware hops could fit into the broader resolver ecosystem.