From 6dba23f2e3d750805eaf1204c5acddd9a3f083ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 16 Jun 2026 18:50:58 +0000 Subject: [PATCH 1/2] Add DNS-AID publishing automation and operator documentation Netlify DNS cannot create SVCB/HTTPS records required for DNS-AID agent discovery. Add Cloudflare-oriented publish/verify scripts, machine-readable record definitions (_index, _mcp, _a2a), a GitHub Actions workflow, and operator docs covering full-zone migration or _agents subdomain delegation. Co-authored-by: Edmund Miller --- .github/workflows/publish-dns-aid.yml | 35 +++ README.md | 4 + dns/README.md | 86 +++++++ dns/records.json | 31 +++ netlify/dns-aid-records.example | 9 +- scripts/publish-dns-aid.sh | 322 ++++++++++++++++++++++++++ 6 files changed, 485 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-dns-aid.yml create mode 100644 dns/README.md create mode 100644 dns/records.json create mode 100755 scripts/publish-dns-aid.sh diff --git a/.github/workflows/publish-dns-aid.yml b/.github/workflows/publish-dns-aid.yml new file mode 100644 index 0000000..82caa91 --- /dev/null +++ b/.github/workflows/publish-dns-aid.yml @@ -0,0 +1,35 @@ +name: Publish DNS-AID records + +on: + workflow_dispatch: + inputs: + command: + description: "publish-dns-aid.sh command" + required: true + default: publish + type: choice + options: + - publish + - verify + - dnssec + +permissions: + contents: read + +jobs: + dns-aid: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Run DNS-AID script + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} + DNS_AID_DOMAIN: rewrites.bio + run: | + chmod +x scripts/publish-dns-aid.sh + ./scripts/publish-dns-aid.sh "${{ github.event.inputs.command }}" diff --git a/README.md b/README.md index 10d5181..df99275 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ All manifesto content lives in the Astro source files (`src/pages/index.astro` a Configured for Netlify (see `netlify.toml`). Push to `main` to deploy. +## DNS for AI Discovery (DNS-AID) + +Agent discovery via DNS requires SVCB/HTTPS records under `_agents.rewrites.bio` plus DNSSEC. Netlify DNS does not support those record types — see [`dns/README.md`](dns/README.md) and run `./scripts/publish-dns-aid.sh` with Cloudflare (or delegate `_agents.rewrites.bio` to a provider that supports SVCB). + ## License Open source under the [MIT License](LICENSE). diff --git a/dns/README.md b/dns/README.md new file mode 100644 index 0000000..d0486e4 --- /dev/null +++ b/dns/README.md @@ -0,0 +1,86 @@ +# DNS for AI Discovery (DNS-AID) + +Publish [DNS-AID](https://datatracker.ietf.org/doc/draft-mozleywilliams-dnsop-dnsaid/) records so agents can discover rewrites.bio endpoints through DNS before any HTTP request. + +## Why this is not only a site change + +DNS-AID records are **authoritative DNS data**, not files served by the Astro site. The isitagentready scanner queries: + +- `SVCB` / `HTTPS` `_index._agents.rewrites.bio` +- `SVCB` / `HTTPS` `_a2a._agents.rewrites.bio` +- `SVCB` / `HTTPS` `_mcp._agents.rewrites.bio` + +Each record must be in **ServiceMode** (`priority >= 1`) with at least `alpn` and `port`, and the zone must be **DNSSEC-signed** for `checks.discoverability.dnsAid` to pass. + +## Netlify DNS limitation + +rewrites.bio uses **Netlify DNS** (NS1 under the hood). Netlify's DNS UI and API only support A, AAAA, CNAME, MX, NS, TXT, and related types — **not SVCB or HTTPS** (RFC 9460). + +Choose one of the paths below. + +## Option A — Cloudflare DNS for rewrites.bio (recommended) + +1. Add `rewrites.bio` to Cloudflare and point registrar nameservers to Cloudflare. +2. Recreate existing Netlify DNS records in Cloudflare (A/ALIAS/CNAME for the site, etc.). +3. Publish DNS-AID records: + +```sh +export CLOUDFLARE_API_TOKEN="..." # Zone.DNS Edit + Zone.DNS Settings +export CLOUDFLARE_ZONE_ID="..." +./scripts/publish-dns-aid.sh publish +./scripts/publish-dns-aid.sh dnssec +``` + +4. If the registrar is not Cloudflare, add the DS records printed by `dnssec`. +5. Verify: + +```sh +./scripts/publish-dns-aid.sh verify +``` + +## Option B — Delegate only `_agents.rewrites.bio` + +Keep Netlify DNS for the apex zone and delegate the `_agents` label to a provider with SVCB/HTTPS support (Cloudflare, Route 53, NS1, deSEC, etc.). + +1. Create a child zone for `_agents.rewrites.bio` at the provider. +2. Delegate from Netlify: + +```sh +export NETLIFY_AUTH_TOKEN="..." +export DELEGATE_NS="ada.ns.cloudflare.com bob.ns.cloudflare.com" # example +./scripts/publish-dns-aid.sh delegate +``` + +3. In the **child zone**, publish records named `_index`, `_mcp`, and `_a2a` (see `dns/records.json`). +4. Enable DNSSEC on the child zone and add the resulting **DS** record in Netlify DNS for `_agents.rewrites.bio`. +5. Run `./scripts/publish-dns-aid.sh verify`. + +## Record definitions + +Canonical machine-readable config: [`dns/records.json`](records.json) + +Zone-file style (for manual import): + +```sh +./scripts/publish-dns-aid.sh show +``` + +Human-readable reference: [`netlify/dns-aid-records.example`](../netlify/dns-aid-records.example) + +## GitHub Actions + +Workflow [`.github/workflows/publish-dns-aid.yml`](../.github/workflows/publish-dns-aid.yml) can publish records when these repository secrets are set: + +| Secret | Purpose | +| --- | --- | +| `CLOUDFLARE_API_TOKEN` | Cloudflare API token | +| `CLOUDFLARE_ZONE_ID` | Zone ID for `rewrites.bio` or delegated child zone | + +Trigger manually via **Actions → Publish DNS-AID records → Run workflow**. + +## References + +- [DNS-AID skill](https://isitagentready.com/.well-known/agent-skills/dns-aid/SKILL.md) +- [DNS-AID draft](https://datatracker.ietf.org/doc/draft-mozleywilliams-dnsop-dnsaid/) +- [RFC 9460 (SVCB/HTTPS)](https://www.rfc-editor.org/rfc/rfc9460) +- [Agent readiness spec — DNS-AID](https://specification.website/spec/agent-readiness/dns-aid/) diff --git a/dns/records.json b/dns/records.json new file mode 100644 index 0000000..a6c0ee1 --- /dev/null +++ b/dns/records.json @@ -0,0 +1,31 @@ +{ + "domain": "rewrites.bio", + "target": "rewrites.bio.", + "ttl": 3600, + "records": [ + { + "name": "_index._agents", + "type": "HTTPS", + "priority": 1, + "target": "rewrites.bio.", + "params": "alpn=\"h3,h2\" port=443 mandatory=alpn,port", + "description": "Site index and discovery entrypoint" + }, + { + "name": "_mcp._agents", + "type": "HTTPS", + "priority": 1, + "target": "rewrites.bio.", + "params": "alpn=\"h3,h2\" port=443 mandatory=alpn,port", + "description": "WebMCP tools and MCP server card on the homepage" + }, + { + "name": "_a2a._agents", + "type": "SVCB", + "priority": 1, + "target": "rewrites.bio.", + "params": "alpn=\"h3,h2\" port=443 mandatory=alpn,port key65000=\"/.well-known/agent.md\"", + "description": "Agent-to-agent discovery via public content endpoints" + } + ] +} diff --git a/netlify/dns-aid-records.example b/netlify/dns-aid-records.example index 180feac..80ad248 100644 --- a/netlify/dns-aid-records.example +++ b/netlify/dns-aid-records.example @@ -1,8 +1,13 @@ ; DNS for AI Discovery (DNS-AID) records for rewrites.bio -; Add these at your DNS provider and sign the zone with DNSSEC. +; Generated reference — canonical source: dns/records.json +; Netlify DNS does not support SVCB/HTTPS; use Cloudflare or delegate _agents.rewrites.bio. +; Sign the zone with DNSSEC so validating resolvers return authenticated data. ; ; _index._agents — site index and discovery entrypoint _index._agents.rewrites.bio. 3600 IN HTTPS 1 rewrites.bio. alpn="h3,h2" port=443 mandatory=alpn,port -; _a2a._agents — agent-to-agent discovery (content endpoints, no auth) +; _mcp._agents — WebMCP tools and MCP server card on the homepage +_mcp._agents.rewrites.bio. 3600 IN HTTPS 1 rewrites.bio. alpn="h3,h2" port=443 mandatory=alpn,port + +; _a2a._agents — agent-to-agent discovery (public content endpoints) _a2a._agents.rewrites.bio. 3600 IN SVCB 1 rewrites.bio. alpn="h3,h2" port=443 mandatory=alpn,port key65000="/.well-known/agent.md" diff --git a/scripts/publish-dns-aid.sh b/scripts/publish-dns-aid.sh new file mode 100755 index 0000000..4692dc2 --- /dev/null +++ b/scripts/publish-dns-aid.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash +# Publish DNS for AI Discovery (DNS-AID) records for rewrites.bio. +# +# Netlify DNS does not support SVCB/HTTPS record types in its UI or API. +# Use one of: +# 1. Cloudflare (recommended): host rewrites.bio on Cloudflare DNS, then run: +# CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ZONE_ID=... ./scripts/publish-dns-aid.sh publish +# 2. Subdomain delegation: delegate _agents.rewrites.bio to a provider with SVCB support, +# then publish into the child zone (see dns/README.md). +# +# Requires: curl, jq, python3 + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RECORDS_FILE="${RECORDS_FILE:-$ROOT/dns/records.json}" +DOMAIN="${DNS_AID_DOMAIN:-rewrites.bio}" +DOH_RESOLVER="${DOH_RESOLVER:-https://cloudflare-dns.com/dns-query}" +SCAN_URL="${SCAN_URL:-https://isitagentready.com/api/scan}" + +usage() { + cat <<'EOF' +Usage: publish-dns-aid.sh + +Commands: + publish Upsert DNS-AID SVCB/HTTPS records (Cloudflare API) + delegate Add NS records at Netlify for _agents. delegation + dnssec Enable Cloudflare DNSSEC and print DS records for the registrar + verify Query DoH for DNS-AID records and run isitagentready scan + show Print zone-file style records from dns/records.json + +Environment: + CLOUDFLARE_API_TOKEN Cloudflare API token (Zone.DNS Edit + Zone.DNS Settings) + CLOUDFLARE_ZONE_ID Cloudflare zone ID for rewrites.bio (or delegated child zone) + NETLIFY_AUTH_TOKEN Netlify personal access token (for delegate command) + NETLIFY_ZONE_ID Netlify DNS zone ID (optional; auto-discovered) + DELEGATE_NS Space-separated child nameservers (for delegate command) + DNS_AID_DOMAIN Domain (default: rewrites.bio) +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +load_records() { + require_cmd jq + if [[ ! -f "$RECORDS_FILE" ]]; then + echo "error: records file not found: $RECORDS_FILE" >&2 + exit 1 + fi +} + +cf_api() { + local method="$1" + local path="$2" + local data="${3:-}" + + if [[ -z "${CLOUDFLARE_API_TOKEN:-}" ]]; then + echo "error: CLOUDFLARE_API_TOKEN is required" >&2 + exit 1 + fi + if [[ -z "${CLOUDFLARE_ZONE_ID:-}" ]]; then + echo "error: CLOUDFLARE_ZONE_ID is required" >&2 + exit 1 + fi + + local args=( + -sS + -X "$method" + "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}${path}" + -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" + -H "Content-Type: application/json" + ) + if [[ -n "$data" ]]; then + args+=(--data "$data") + fi + + local response + response="$(curl "${args[@]}")" + if ! echo "$response" | jq -e '.success == true' >/dev/null; then + echo "error: Cloudflare API request failed: $path" >&2 + echo "$response" | jq '.' >&2 || echo "$response" >&2 + exit 1 + fi + echo "$response" +} + +upsert_cloudflare_record() { + local name="$1" + local type="$2" + local priority="$3" + local target="$4" + local params="$5" + local ttl="$6" + + local list + list="$(cf_api GET "/dns_records?type=${type}&name=${name}.${DOMAIN}")" + local existing_id + existing_id="$(echo "$list" | jq -r --arg n "${name}.${DOMAIN}" '.result[] | select(.name == $n) | .id' | head -n1)" + + local payload + payload="$(jq -n \ + --arg type "$type" \ + --arg name "$name" \ + --argjson priority "$priority" \ + --arg target "$target" \ + --arg value "$params" \ + --argjson ttl "$ttl" \ + '{ + type: $type, + name: $name, + ttl: $ttl, + data: { + priority: $priority, + target: $target, + value: $value + } + }')" + + if [[ -n "$existing_id" && "$existing_id" != "null" ]]; then + echo "Updating ${type} ${name}.${DOMAIN} (id=${existing_id})" + cf_api PUT "/dns_records/${existing_id}" "$payload" >/dev/null + else + echo "Creating ${type} ${name}.${DOMAIN}" + cf_api POST "/dns_records" "$payload" >/dev/null + fi +} + +cmd_publish() { + load_records + local target ttl + target="$(jq -r '.target' "$RECORDS_FILE")" + ttl="$(jq -r '.ttl' "$RECORDS_FILE")" + + jq -c '.records[]' "$RECORDS_FILE" | while IFS= read -r record; do + local name type priority params + name="$(echo "$record" | jq -r '.name')" + type="$(echo "$record" | jq -r '.type')" + priority="$(echo "$record" | jq -r '.priority')" + params="$(echo "$record" | jq -r '.params')" + upsert_cloudflare_record "$name" "$type" "$priority" "$target" "$params" "$ttl" + done + + echo "DNS-AID records published to Cloudflare zone ${CLOUDFLARE_ZONE_ID}." + echo "Next: run './scripts/publish-dns-aid.sh dnssec' and add DS records at your registrar if needed." +} + +netlify_api() { + local method="$1" + local path="$2" + local data="${3:-}" + + if [[ -z "${NETLIFY_AUTH_TOKEN:-}" ]]; then + echo "error: NETLIFY_AUTH_TOKEN is required" >&2 + exit 1 + fi + + local args=( + -sS + -X "$method" + "https://api.netlify.com/api/v1${path}" + -H "Authorization: Bearer ${NETLIFY_AUTH_TOKEN}" + -H "Content-Type: application/json" + ) + if [[ -n "$data" ]]; then + args+=(--data "$data") + fi + + curl "${args[@]}" +} + +resolve_netlify_zone_id() { + if [[ -n "${NETLIFY_ZONE_ID:-}" ]]; then + echo "$NETLIFY_ZONE_ID" + return + fi + + local zones + zones="$(netlify_api GET "/dns_zones")" + local zone_id + zone_id="$(echo "$zones" | jq -r --arg d "$DOMAIN" '.[] | select(.name == $d or .domain == $d) | .id' | head -n1)" + if [[ -z "$zone_id" || "$zone_id" == "null" ]]; then + echo "error: could not find Netlify DNS zone for ${DOMAIN}" >&2 + exit 1 + fi + echo "$zone_id" +} + +cmd_delegate() { + require_cmd jq + if [[ -z "${DELEGATE_NS:-}" ]]; then + echo "error: DELEGATE_NS is required (space-separated child nameservers)" >&2 + echo "example: DELEGATE_NS='ada.ns.cloudflare.com bob.ns.cloudflare.com'" >&2 + exit 1 + fi + + local zone_id + zone_id="$(resolve_netlify_zone_id)" + local hostname="_agents.${DOMAIN}" + + for ns in $DELEGATE_NS; do + local ns_value="$ns" + if [[ "$ns_value" != *. ]]; then + ns_value="${ns_value}." + fi + + local payload + payload="$(jq -n --arg hostname "$hostname" --arg value "$ns_value" '{type:"NS",hostname:$hostname,value:$value,ttl:3600}')" + echo "Adding NS ${hostname} -> ${ns_value}" + local response + response="$(netlify_api POST "/dns_zones/${zone_id}/dns_records" "$payload")" + if ! echo "$response" | jq -e '.id' >/dev/null 2>&1; then + echo "warning: Netlify API response for NS ${ns_value}:" >&2 + echo "$response" | jq '.' >&2 || echo "$response" >&2 + fi + done + + echo "Delegated ${hostname} to child nameservers." + echo "Publish DNS-AID records in the child zone, then enable DNSSEC and add DS at Netlify." +} + +cmd_dnssec() { + local status + status="$(cf_api GET "")" + local current + current="$(echo "$status" | jq -r '.result.status // empty')" + + if [[ "$current" != "active" ]]; then + echo "Enabling Cloudflare DNSSEC..." + cf_api PATCH "" '{"status":"active"}' >/dev/null + else + echo "Cloudflare DNSSEC already active." + fi + + local ds + ds="$(cf_api GET "/dnssec")" + echo + echo "Add these DS records at your domain registrar (or parent zone) if DNS is not fully on Cloudflare:" + echo "$ds" | jq -r '.result.ds[]? | "\(.key_tag) \(.algorithm) \(.digest_type) \(.digest)"' + echo + echo "If using Netlify DNS as parent for a delegated _agents zone, add the DS record via Netlify DNS UI/API." +} + +doh_query() { + local qname="$1" + local qtype="$2" + curl -sS -G "$DOH_RESOLVER" \ + --data-urlencode "name=${qname}" \ + --data-urlencode "type=${qtype}" \ + --data-urlencode "do=1" \ + -H "accept: application/dns-json" +} + +cmd_verify() { + require_cmd jq + load_records + + local ok=0 + jq -r '.records[] | "\(.type)\t\(.name)"' "$RECORDS_FILE" | while IFS=$'\t' read -r type name; do + local fqdn="${name}.${DOMAIN}" + local response + response="$(doh_query "$fqdn" "$type")" + local status answers ad + status="$(echo "$response" | jq -r '.Status')" + answers="$(echo "$response" | jq -r '.Answer // [] | length')" + ad="$(echo "$response" | jq -r '.AD // false')" + if [[ "$status" == "0" && "$answers" -gt 0 ]]; then + echo "OK ${type} ${fqdn} (answers=${answers}, AD=${ad})" + echo "$response" | jq -r '.Answer[]? | " \(.type) \(.data)"' + else + echo "MISSING ${type} ${fqdn} (Status=${status})" + ok=1 + fi + done + + echo + echo "Running isitagentready scan for https://${DOMAIN} ..." + local scan + scan="$(curl -sS -X POST "$SCAN_URL" -H "Content-Type: application/json" -d "{\"url\":\"https://${DOMAIN}\"}")" + echo "$scan" | jq '{ + dnsAid: .checks.discoverability.dnsAid.status, + message: .checks.discoverability.dnsAid.message, + dnssecValidated: .checks.discoverability.dnsAid.details.dnssecValidated, + serviceRecordCount: .checks.discoverability.dnsAid.details.serviceRecordCount + }' +} + +cmd_show() { + load_records + echo "; DNS for AI Discovery (DNS-AID) records for ${DOMAIN}" + echo "; Generated from ${RECORDS_FILE}" + echo "; Sign the zone with DNSSEC so validating resolvers return authenticated data." + echo + jq -r --arg domain "$DOMAIN" ' + . as $root | + .records[] | + "\(.name).\($domain). \($root.ttl) IN \(.type) \(.priority) \(.target) \(.params)" + ' "$RECORDS_FILE" +} + +main() { + local command="${1:-}" + case "$command" in + publish) cmd_publish ;; + delegate) cmd_delegate ;; + dnssec) cmd_dnssec ;; + verify) cmd_verify ;; + show) cmd_show ;; + -h|--help|help|"") usage ;; + *) + echo "error: unknown command: $command" >&2 + usage + exit 1 + ;; + esac +} + +main "$@" From 12bbbd376ff3d69dbfc3ddd6e304a6b1368b6c4c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 16 Jun 2026 18:53:46 +0000 Subject: [PATCH 2/2] Fix prek CI: format dns/README.md with Prettier Co-authored-by: Edmund Miller --- dns/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dns/README.md b/dns/README.md index d0486e4..5b91b07 100644 --- a/dns/README.md +++ b/dns/README.md @@ -71,10 +71,10 @@ Human-readable reference: [`netlify/dns-aid-records.example`](../netlify/dns-aid Workflow [`.github/workflows/publish-dns-aid.yml`](../.github/workflows/publish-dns-aid.yml) can publish records when these repository secrets are set: -| Secret | Purpose | -| --- | --- | -| `CLOUDFLARE_API_TOKEN` | Cloudflare API token | -| `CLOUDFLARE_ZONE_ID` | Zone ID for `rewrites.bio` or delegated child zone | +| Secret | Purpose | +| ---------------------- | -------------------------------------------------- | +| `CLOUDFLARE_API_TOKEN` | Cloudflare API token | +| `CLOUDFLARE_ZONE_ID` | Zone ID for `rewrites.bio` or delegated child zone | Trigger manually via **Actions → Publish DNS-AID records → Run workflow**.