Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Plan D: Consolidated Approach SVCB + dnsRecordStyles#25

Open
kperry-godaddy wants to merge 9 commits into
mainfrom
feat/plan-d-svcb-on-main
Open

Plan D: Consolidated Approach SVCB + dnsRecordStyles#25
kperry-godaddy wants to merge 9 commits into
mainfrom
feat/plan-d-svcb-on-main

Conversation

@kperry-godaddy
Copy link
Copy Markdown

Summary

Replaces #13. Same set of changes, rebased directly onto main so it no longer depends on the dropped #12 (capabilities_hash) stack.

  • Plan D Consolidated Approach SVCB: emit + verify SVCB rows at the bare FQDN per ANS_SPEC.md §4.4.2 (alpn, port, wk, card-sha256).
  • Legacy _ans TXT family preserved, plus a new HTTPS RR alongside it.
  • New dnsRecordStyles field on the V2 register request: array of ANS_SVCB / ANS_TXT (CONSTANT_CASE), default ["ANS_SVCB"], set deduplicated, invalid elements → 422 INVALID_DNS_RECORD_STYLE. V1 lane pinned to ["ANS_TXT"].
  • Migration 006_agent_dns_record_styles.sql adds the dns_record_styles column (JSON array, json_valid CHECK), backfills pre-existing rows to ["ANS_TXT"]. Slot 006 reclaimed from the dropped [AI assisted] Plans A + C: capabilitiesHash sealing for the Trust Card #12.
  • Tests cover empty/single/multi/duplicate/invalid input through the service layer; the SVCB+verifier pairing is exercised at the domain layer where unregistered SvcParams are tractable.

make check clean, coverage 90.1% (above the 90% gate).

Differences from #13

  • Branched from main, not from [AI assisted] Plans A + C: capabilitiesHash sealing for the Trust Card #12. The dropped capabilitiesHash plumbing leaves domain.AgentRegistration.CapabilitiesHash as a read-only field consumed by the SVCB card-sha256= SvcParam; populator path lands in a follow-up PR.
  • All Scott's original commits preserved with attribution; commits cleaned up to satisfy CLAUDE.md hygiene (no Co-Authored-By: Claude trailers, signed off, GPG signed).

Test plan

  • make check passes locally
  • CI green on this branch
  • Re-run lifecycle quickstart end-to-end against dnsRecordStyles=["ANS_SVCB"], ["ANS_TXT"], and ["ANS_SVCB","ANS_TXT"]
  • Confirm 006_agent_dns_record_styles.sql applies cleanly to a fresh dev DB

Copilot AI review requested due to automatic review settings May 22, 2026 17:42
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “Plan D” DNS record style selection for agent registrations by adding Consolidated Approach SVCB support alongside the legacy _ans TXT family, persisting the chosen style set, updating DNS verification semantics, and extending the API/OpenAPI specs accordingly.

Changes:

  • Add dnsRecordStyles to V2 registration requests (defaulting to ["ANS_SVCB"]) and persist the chosen set on registrations (SQLite migration + store codec).
  • Extend required DNS record computation to emit SVCB records at the bare FQDN (and HTTPS RR for the legacy TXT family), plus add SVCB verification in the DNS lookup adapter.
  • Update OpenAPI schemas to include DNSRecordStyle and add SVCB to the DnsRecord.type enum; add unit tests across service/domain/adapter layers.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
spec/api-spec-v2.yaml Adds DNSRecordStyle + dnsRecordStyles and extends DnsRecord.type with SVCB.
internal/adapter/docsui/openapi/ra.yaml Mirrors the OpenAPI schema changes for the docs UI bundle.
internal/ra/handler/registration.go Accepts dnsRecordStyles in the HTTP request and converts to typed domain values.
internal/ra/service/registration.go Applies DNS record style selection onto the registration aggregate during register.
internal/ra/service/helpers.go Implements applyDNSRecordStyles normalization/validation/dedup logic.
internal/ra/service/helpers_test.go Adds unit tests for applyDNSRecordStyles.
internal/domain/agent.go Adds CapabilitiesHash and persists DNSRecordStyles on the aggregate.
internal/domain/dnsrecords.go Adds style enum + defaulting and emits legacy TXT/HTTPS and consolidated SVCB records.
internal/domain/dnsrecords_test.go Adds a comprehensive style/params matrix test and helper tests for SVCB params.
internal/adapter/dns/lookup.go Adds SVCB verification and propagates DNSSEC AD bit for HTTPS/SVCB.
internal/adapter/dns/dns_test.go Adds SVCB verifier tests and AD-bit propagation tests for HTTPS/SVCB.
internal/ra/service/lifecycle.go Extends DNSSEC mismatch hard-fail logic to include HTTPS and SVCB.
internal/port/dns.go Updates docs for DNSSECVerified to include HTTPS/SVCB in addition to TLSA.
internal/adapter/store/sqlite/migrations/006_agent_dns_record_styles.sql Adds/poplulates dns_record_styles column for persisted style sets.
internal/adapter/store/sqlite/agent.go Loads/saves dns_record_styles via JSON encode/decode helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread spec/api-spec-v2.yaml
Comment on lines +1085 to +1091
dnsRecordStyles:
type: array
items:
$ref: '#/components/schemas/DNSRecordStyle'
uniqueItems: true
minItems: 1
description: |
Comment on lines +1085 to +1091
dnsRecordStyles:
type: array
items:
$ref: '#/components/schemas/DNSRecordStyle'
uniqueItems: true
minItems: 1
description: |
Comment on lines +255 to +259
Type: DNSRecordSVCB,
Value: value,
Purpose: PurposeDiscovery,
Required: false,
TTL: 3600,
Comment thread internal/domain/dnsrecords.go Outdated
// and capability locators in a single DNS lookup. SvcParams from
// DNS-AID, ANS, and other agentic specs coexist in the same record
// per RFC 9460 §8 unknown-key ignore semantics. See ANS_SPEC.md
// §4.4.2 in github.com/gdcorp-engineering/ans-registry-poc.
Comment on lines +300 to +304
if r.Actual == "" {
r.Actual = got
}
if normalizeHTTPS(got) == wantNorm {
r.Found = true
scourtney-godaddy and others added 6 commits May 22, 2026 14:03
Extends domain.ComputeRequiredDNSRecords to emit one SVCB record per
protocol at the agent bare FQDN, alongside the existing _ans TXT
family. The SVCB row carries:

  alpn=PROTOCOL              from endpoint.Protocol
  port=443                   ServiceMode SvcPriority 1 at the FQDN
  wk=SUFFIX                  A2A: agent-card.json; MCP: mcp.json
  card-sha256=BASE64URL      base64url of reg.CapabilitiesHash when set

card-sha256 and capabilities_hash are the section 4.4.2 cross-check
encodings of the same SHA-256 (DNS uses base64url, TL uses hex). When
the operator did not submit agentCardContent, the SvcParam is absent
and verifiers fall back to TOFU on first Trust Card fetch.

Adds verifySVCB to LookupVerifier mirroring verifyHTTPS. Tests cover
present-matching, absent (zone has different name), and wrong-target
cases (AliasMode where ServiceMode was expected). Provisional SvcParams
(wk, card-sha256) are unit-tested at the domain layer because miekg/dns
rejects them in zone-file form until IANA registration; the verifier-
level test exercises only registered SvcParamKeys (alpn, port).

Required=false: section 4.4.2 marks Consolidated Approach SVCB as MAY,
opt-in during the _ans TXT transition.

Signed-off-by: kperry <[email protected]>
Adds dnsRecordStyle to the V2 RegistrationRequest with three values:
"consolidated" (default, recommended), "legacy" (original _ans TXT
shape), "both" (transition union). Empty -> consolidated. Invalid ->
422 INVALID_DNS_RECORD_STYLE.

The default points new integrations at the lean Consolidated Approach
shape per section 4.4.2 SHOULD: one SVCB record at the bare FQDN per
protocol, plus shared _ans-prefixed records and TLSA. Operators on
existing zone-edit tooling for _ans TXT pick "legacy" explicitly.
Migration operators set "both" for a defined window then flip back to
"consolidated".

V1 lane pins to "legacy" regardless of the request because V1 callers
predate the Consolidated Approach and their tooling expects the
original shape. V1 has no dnsRecordStyle field on the wire.

Migration 007 adds the dns_record_style column on agent_registrations.
Nullable for backwards compatibility with pre-Plan-D rows.

Tests:
- "both" emits 2x _ans TXT + 2x SVCB + shared records (existing test
  updated to set DNSRecordStyleBoth so it exercises the union path).
- New tests cover "consolidated" (no _ans TXT), "legacy" (no SVCB),
  and "both" (union); the SvcParam wk/card-sha256 tests already
  covered the consolidated path implicitly.
- Lint: extracted applyDNSRecordStyle helper to keep RegisterAgent
  under the funlen ceiling.

Signed-off-by: kperry <[email protected]>
Closes a long-standing spec/impl gap: ANS_SPEC.md section A.8.1 lists
the HTTPS RR (RFC 9460 type 65) at the agent FQDN as RA-generated
content the AHP provisions, but ComputeRequiredDNSRecords had never
emitted it. The DNSRecordHTTPS enum value and verifyHTTPS verifier
were already in place; this commit wires the emission.

Generated only for the legacy + both styles, not for consolidated:
the SVCB rows the consolidated form publishes already carry the same
alpn/port/ECH SvcParams the HTTPS RR would, so emitting both would
duplicate content and risk the two records drifting (section A.8.2
explicitly notes this). Operators on the consolidated path who still
want HTTPS-RR-aware clients (typically browsers) to see the metadata
can publish their own HTTPS RR as a side addition.

Required=false: HTTPS RR is blocked by CNAME at the agent FQDN per
RFC 1034 section 3.6.2. AHPs whose apex is fronted via CNAME cannot
publish it at the same name; the RA does not block verify-dns on
its absence.

Tests pin: legacy style includes HTTPS RR + no SVCB; consolidated
style includes SVCB + no HTTPS RR; both style includes both
families.

Signed-off-by: kperry <[email protected]>
…t reclaimed)

PR12 (capabilities_hash) shipped migration 006 in the upstream stack
that this branch was originally based on. Now that PR12 is dropped,
the 006 slot is open and this branch's only migration moves into it
to keep the migration sequence dense (001 → 006, no gap).

Filename also pluralized to dns_record_styles.sql to match the column
this PR ships (a JSON array of CONSTANT_CASE values, not a singleton
string).

Signed-off-by: kperry <[email protected]>
@kperry-godaddy kperry-godaddy force-pushed the feat/plan-d-svcb-on-main branch from 72cfbc8 to 9cc50da Compare May 22, 2026 19:04
kperry-godaddy added a commit that referenced this pull request May 22, 2026
The OpenAPI schemas declared minItems: 1 and uniqueItems: true, with
a description claiming duplicates are rejected. The server actually
defaults empty/missing to ["ANS_SVCB"] (in applyDNSRecordStyles) and
silently dedupes duplicates, so the spec was making promises the
service layer doesn't keep. Two paths to align: tighten the server
to match the spec, or relax the spec to match the server. The
defaulting is intentional design (operators who omit the field land
on the recommended Consolidated Approach), so the spec relaxes.

- Drop minItems and uniqueItems constraints; the server doesn't
  enforce them and we don't want OpenAPI validators rejecting bodies
  the service would have accepted and normalized.
- Reword the description: duplicates are "deduplicated server-side"
  (with a worked example), and "Omitted/missing/empty" all normalize
  to ["ANS_SVCB"] (was just "Omitted/missing").

Mirrors the same edits in spec/api-spec-v2.yaml and
internal/adapter/docsui/openapi/ra.yaml so Swagger UI matches the
canonical contract.

Addresses Copilot review feedback C1+C2 on PR #25.

Signed-off-by: kperry <[email protected]>
kperry-godaddy added a commit that referenced this pull request May 22, 2026
ComputeRequiredDNSRecords previously emitted SVCB rows with
Required=false unconditionally, with the comment justifying it as
"§4.4.2 marks the Consolidated Approach as MAY, opt-in alongside the
`_ans` TXT family during the transition." That assumption only holds
in union mode. The default style is ["ANS_SVCB"] (SVCB-sole), so the
default registration was emitting zero Required=true PurposeDiscovery
records. The badge TXT (PurposeBadge) keeps verify-dns from passing
on an empty zone, but a SVCB-sole agent could publish only the badge,
skip SVCB entirely, and verify-dns would still pass — a registered
agent that is undiscoverable via the discovery family the operator
opted into.

Fix: when ANS_SVCB is the only selected style, mark SVCB Required=true.
When emitted alongside ANS_TXT (the union/transition mode), keep
Required=false because the legacy `_ans` TXT family above carries
the required signal — that preserves the §4.4.2 MAY-during-transition
framing for operators running both families.

The TestComputeRequiredDNSRecords_StyleMatrix matrix gains a
wantSVCBRequired column covering both paths: SVCB-sole, default
(empty styles → ["ANS_SVCB"]), and the all-invalid fallback (also
["ANS_SVCB"]) all assert Required=true; the union case asserts
Required=false. The original WithoutCert test fixture uses union
mode and keeps Required=false with an updated comment pointing
readers at the matrix for the SVCB-sole path.

Addresses Copilot review feedback C3 on PR #25.

Signed-off-by: kperry <[email protected]>
kperry-godaddy added a commit that referenced this pull request May 22, 2026
verifySVCB previously did full normalized-string equality
(`normalizeHTTPS(got) == wantNorm`) while its docstring claimed
RFC 9460 §8 unknown-key ignore semantics. The two diverged: a live
record carrying extra SvcParams from a coexisting agentic spec
(DNS-AID, etc.) would not match, and under DNSSEC AD=true the
mismatch trips the SVCB_DNSSEC_MISMATCH hard fail in the lifecycle
layer — defeating the entire point of the Consolidated Approach
(multi-spec coexistence in a single SVCB row).

Switch to subset matching:
  - parseSVCBValue parses "<priority> <target> [k=v]..." into a
    structured (priority, target, params-map) form. Used by both the
    expected and actual sides.
  - matchesSVCBSubset returns true iff priority and target are equal
    and every expected SvcParam is present in the live record with an
    equal value. Additional SvcParams in the live record are ignored.
  - verifySVCB calls parseSVCBValue on rec.Value once, then
    parseSVCBValue+matchesSVCBSubset on each candidate SVCB rrset.

Tests added to TestLookupVerifier_SVCB:
  - extra-svcparams-tolerated-rfc9460-section-8: live record carries
    `mandatory=alpn` not in the expected; still matches.
  - missing-expected-param-fails-subset-match: live record omits
    expected `port=443`; does not match.

verifyHTTPS keeps strict-equality matching (its docstring is honest
about that): HTTPS RR is a companion to legacy `_ans` TXT, not the
multi-spec coexistence target. SVCB is where the §8 tolerance
matters.

Addresses Copilot review feedback C5 on PR #25.

Signed-off-by: kperry <[email protected]>
The OpenAPI schemas declare minItems: 1 and uniqueItems: true, and
their description says duplicates are rejected. The server previously
did neither — it normalized empty arrays to the default and silently
deduped duplicates — so the spec was making promises the service
layer didn't keep.

Tighten the service layer to match the contract:
  - Field omitted (nil) still defaults to ["ANS_SVCB"]; the field
    isn't in `required`, so omission is legal.
  - Field present but empty (`"dnsRecordStyles": []`) now returns 422
    INVALID_DNS_RECORD_STYLE. Matches `minItems: 1`. A caller who
    explicitly sends an empty list is signalling intent the schema
    doesn't permit; defaulting silently would mask a likely client
    bug.
  - Duplicates now return 422 INVALID_DNS_RECORD_STYLE. Matches
    `uniqueItems: true`. Silent dedup would persist a state the
    caller didn't quite request.
  - Invalid element behavior unchanged: 422 with the canonical valid
    set in the message.

The handler-side toDomainDNSRecordStyles now preserves the nil-vs-
empty distinction so applyDNSRecordStyles can tell field omission
(JSON null or missing) from explicit empty array (`[]`). JSON
unmarshal of null/missing yields a nil slice; explicit `[]` yields a
non-nil zero-length slice; the conversion preserves both shapes
unchanged for the service layer to discriminate on.

Tests updated:
  - v2_explicit_empty_slice_rejected (replaces
    v2_empty_slice_normalizes_to_default).
  - v2_duplicate_elements_rejected (replaces
    v2_duplicate_elements_deduped).
  - Existing v2_nil_normalizes_to_default and unset_schema cases
    still pass — they cover the field-omitted path.

Addresses Copilot review feedback C1+C2 on PR #25 by aligning
the server with the spec rather than relaxing the spec.
ComputeRequiredDNSRecords previously emitted SVCB rows with
Required=false unconditionally, with the comment justifying it as
"§4.4.2 marks the Consolidated Approach as MAY, opt-in alongside the
`_ans` TXT family during the transition." That assumption only holds
in union mode. The default style is ["ANS_SVCB"] (SVCB-sole), so the
default registration was emitting zero Required=true PurposeDiscovery
records. The badge TXT (PurposeBadge) keeps verify-dns from passing
on an empty zone, but a SVCB-sole agent could publish only the badge,
skip SVCB entirely, and verify-dns would still pass — a registered
agent that is undiscoverable via the discovery family the operator
opted into.

Fix: when ANS_SVCB is the only selected style, mark SVCB Required=true.
When emitted alongside ANS_TXT (the union/transition mode), keep
Required=false because the legacy `_ans` TXT family above carries
the required signal — that preserves the §4.4.2 MAY-during-transition
framing for operators running both families.

The TestComputeRequiredDNSRecords_StyleMatrix matrix gains a
wantSVCBRequired column covering both paths: SVCB-sole, default
(empty styles → ["ANS_SVCB"]), and the all-invalid fallback (also
["ANS_SVCB"]) all assert Required=true; the union case asserts
Required=false. The original WithoutCert test fixture uses union
mode and keeps Required=false with an updated comment pointing
readers at the matrix for the SVCB-sole path.

Addresses Copilot review feedback C3 on PR #25.

Signed-off-by: kperry <[email protected]>
verifySVCB previously did full normalized-string equality
(`normalizeHTTPS(got) == wantNorm`) while its docstring claimed
RFC 9460 §8 unknown-key ignore semantics. The two diverged: a live
record carrying extra SvcParams from a coexisting agentic spec
(DNS-AID, etc.) would not match, and under DNSSEC AD=true the
mismatch trips the SVCB_DNSSEC_MISMATCH hard fail in the lifecycle
layer — defeating the entire point of the Consolidated Approach
(multi-spec coexistence in a single SVCB row).

Switch to subset matching:
  - parseSVCBValue parses "<priority> <target> [k=v]..." into a
    structured (priority, target, params-map) form. Used by both the
    expected and actual sides.
  - matchesSVCBSubset returns true iff priority and target are equal
    and every expected SvcParam is present in the live record with an
    equal value. Additional SvcParams in the live record are ignored.
  - verifySVCB calls parseSVCBValue on rec.Value once, then
    parseSVCBValue+matchesSVCBSubset on each candidate SVCB rrset.

Tests added to TestLookupVerifier_SVCB:
  - extra-svcparams-tolerated-rfc9460-section-8: live record carries
    `mandatory=alpn` not in the expected; still matches.
  - missing-expected-param-fails-subset-match: live record omits
    expected `port=443`; does not match.

verifyHTTPS keeps strict-equality matching (its docstring is honest
about that): HTTPS RR is a companion to legacy `_ans` TXT, not the
multi-spec coexistence target. SVCB is where the §8 tolerance
matters.

Addresses Copilot review feedback C5 on PR #25.

Signed-off-by: kperry <[email protected]>
@kperry-godaddy kperry-godaddy force-pushed the feat/plan-d-svcb-on-main branch from 9af162d to e543cec Compare May 22, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants