From 6cfe832586f4b3d5cd509123f6e60ae0cf5c79e6 Mon Sep 17 00:00:00 2001 From: "J. DiMare" Date: Sat, 1 Aug 2026 17:39:33 -0400 Subject: [PATCH] demo: compose capability descriptors via metaDataUrl + ANS_DNSAID Fixes #95 Signed-off-by: J. DiMare --- scripts/demo/payable-endpoints/README.md | 196 ++++++++++++++++++ .../testdata/agent-card.json | 1 + .../testdata/expected-svcb.txt | 1 + scripts/demo/payable-endpoints/verify.sh | 185 +++++++++++++++++ 4 files changed, 383 insertions(+) create mode 100644 scripts/demo/payable-endpoints/README.md create mode 100644 scripts/demo/payable-endpoints/testdata/agent-card.json create mode 100644 scripts/demo/payable-endpoints/testdata/expected-svcb.txt create mode 100755 scripts/demo/payable-endpoints/verify.sh diff --git a/scripts/demo/payable-endpoints/README.md b/scripts/demo/payable-endpoints/README.md new file mode 100644 index 0000000..6484b40 --- /dev/null +++ b/scripts/demo/payable-endpoints/README.md @@ -0,0 +1,196 @@ +# Composing capability descriptors via `metaDataUrl` + +ANS discovers and cryptographically verifies agents by name. Once a caller has +done that, a natural next question is *what can I actually do with this agent, +and how?* — where "how" may mean calling it, authenticating to it, or paying it. + +This demo shows that the answer is already in the schema. An ANS-registered +agent can advertise arbitrary capabilities through the existing optional +`AgentEndpoint.metaDataUrl`, pinned with `metaDataHash` and published as a `cap` +locator by the default `ANS_DNSAID` discovery profile. No new `Protocol` enum +member, no new `DiscoveryProfile`, no wire change, no Go change. + +Payment is used as the worked example below because it is the case people most +often assume needs new protocol machinery. It does not. + +``` +scripts/demo/payable-endpoints/verify.sh # offline, fixtures only +scripts/demo/payable-endpoints/verify.sh --live # also compare the live descriptor +``` + +No running ANS stack is required to *run* this demo — it asserts against +committed fixtures. Those fixtures were produced by running the registration +below against `ans-ra` from `scripts/demo/start.sh`; see "Reproducing the +fixtures" at the end. + +## The gap this closes + +The `Protocol` enum is deliberately transport-only — `A2A`, `MCP`, `HTTP_API`. +`internal/catalog/generate.go` says so directly: + +```go +// The protocol enum is A2A/MCP/HTTP-API; +// there is no PAYMENT. +``` + +That is the right call. Payment is not a discovery transport, and baking a rail +into the enum would drag ANS toward settlement concerns it should not own. But +it leaves the follow-on question unanswered in the docs, and the absence reads +to integrators as a missing feature rather than a deliberate boundary. + +## The mechanism: capability is metadata, not protocol + +`metaDataUrl` (`spec/api-spec-v2.yaml`, `components.schemas.AgentEndpoint`) is an +optional `https` URI whose only job is to locate a richer descriptor for the +endpoint. Nothing constrains what that descriptor advertises. If the descriptor +declares a capability, then following `metaDataUrl` is how a caller learns of +it — through machinery that already ships. + +Two implementation constraints shape any such composition: + +1. **Catalog eligibility is `A2A`/`MCP` only.** `protocolMediaType` + (`internal/catalog/generate.go`) yields an artifact media type only for `A2A` + and `MCP`; `HTTP_API` endpoints are catalog-ineligible + (`NO_ELIGIBLE_ENDPOINT`). A descriptor must ride an `A2A` or `MCP` endpoint to + be both discoverable *and* capability-bearing. An `HTTP_API` endpoint can + still carry a `cap` over DNS-AID, but will not surface in the ARD catalog. +2. **`metaDataUrl` host must equal `agentHost`.** The emit-side URL policy + (`internal/catalog/generate.go`, `internal/finder/project/project.go`, and the + spec text) requires absolute `https`, no userinfo/query/fragment, and + `host == agentHost`. An off-host `metaDataUrl` fails catalog eligibility and + is published without an integrity pin unless `metaDataHash` is supplied. + **Host the descriptor on the same origin as the agent.** + +## Worked example: an A2A card advertising payment tooling + +The committed fixture — `testdata/agent-card.json` — is an A2A agent-card whose +skills include `get_payment_requirements` (which returns an x402 `accepts` +block), alongside `resolve_alias`, `discover_agent`, `check_alias_available`, and +`get_agent_registration_info`. It is served from the same host registered as +`agentHost`, so the same-host policy holds and the endpoint stays +catalog-eligible. + +`verify.sh` prints the registration body this produces: + +```json +{ + "agentHost": "api.dnsofmoney.com", + "discoveryProfiles": ["ANS_DNSAID"], + "endpoints": [ + { + "protocol": "A2A", + "agentUrl": "https://api.dnsofmoney.com/a2a/v1", + "metaDataUrl": "https://api.dnsofmoney.com/.well-known/agent-card.json", + "metaDataHash": "SHA256:23133f65cc200a06f79009d3b43e65dd743fb7d0a914109274a6c5b5a9c0d41b", + "transports": ["JSON_RPC"] + } + ] +} +``` + +Wire keys are capital-D `metaDataUrl` / `metaDataHash`, matching +`scripts/demo/register.sh`. + +Nothing here is payment-specific except the contents of the descriptor. Swap the +card for one advertising a different capability and every field above is +unchanged. + +## What `ANS_DNSAID` emits + +With `discoveryProfiles` defaulting to `["ANS_DNSAID"]`, the RA provisions +RFC 9460 SVCB rows (ServiceMode `1 .` at the agent FQDN) via +`internal/adapter/discovery/ans/dnsaid.go`. + +This is the row `ans-ra` **actually emitted** for the registration above, +captured verbatim from `scripts/demo/dns-records.sh --json` and committed as +`testdata/expected-svcb.txt`: + +``` +1 . alpn=a2a port=443 key65400=https://api.dnsofmoney.com/.well-known/agent-card.json key65401=IxM_ZcwgCgb3kAnTtD5l3XQ_t9CpFBCSdKbFtanA1Bs key65402=a2a key65409=agent-card.json +``` + +| SvcParam | Value | Source | +|---|---|---| +| `alpn` | `a2a` | from the endpoint's `protocol` | +| `port` | `443` | default | +| `key65400` (`cap`) | the `metaDataUrl` | the locator a resolver follows | +| `key65401` (`cap-sha256`) | `IxM_ZcwgCgb3kAnTtD5l3XQ_t9CpFBCSdKbFtanA1Bs` | base64url of the **raw** digest bytes, not the hex text | +| `key65402` (`bap`) | `a2a` | agent protocol | +| `key65409` (`well-known`) | `agent-card.json` | emitted because `metaDataUrl` is a `https://{fqdn}/.well-known/` URL | + +`verify.sh` **derives** `cap-sha256` from the fixture and asserts it against this +recorded row. The check is therefore against reference-implementation output, not +against a value the script re-computed for itself — if the emit path changes +shape, the demo fails rather than agreeing with its own arithmetic. + +The same registration also emits a `_ans-badge` TXT record and a `_443._tcp` +TLSA record. Those are the standard `ANS_DNSAID` record set and are unrelated to +the capability composition; they are omitted from the fixture deliberately, since +the badge URL embeds a per-run `agentId`. + +A resolver that already speaks `ANS_DNSAID` needs no new capability to reach an +integrity-pinned capability descriptor. + +## The integrity pin + +The fixture's SHA-256 is: + +``` +23133f65cc200a06f79009d3b43e65dd743fb7d0a914109274a6c5b5a9c0d41b +``` + +`verify.sh` asserts the committed fixture against this value. That assertion is +offline and deterministic, so this example stays verifiable regardless of what +the live endpoint does later. + +`--live` additionally fetches the real descriptor and compares. **A mismatch +there is reported, not failed.** A changed descriptor is either drift or a +legitimate new version, and a digest alone cannot distinguish the two — which is +precisely why the teaching example is pinned to a fixture rather than to +whatever the network happens to return. + +That distinction is the practical lesson of `metaDataHash`: it detects change, +and change is the operator's signal to re-verify and re-register. It is not a +liveness check. + +## Scope and non-goals + +**In scope:** documenting that ANS already supports capability-bearing agents via +`metaDataUrl` + `metaDataHash` + `ANS_DNSAID`, with a runnable, verifiable +example. + +**Non-goals:** + +- No new `Protocol` enum member. The enum stays transport-only, as the code + comment intends. +- No new `DiscoveryProfile` value; the enum stays `[ANS_DNSAID, ANS_TXT]`. +- No change to the RA, TL, verifier, or the wire contract. Zero Go changes. +- No claim that ANS settles, custodies, or routes anything. The capability lives + entirely in the descriptor the caller fetches; ANS's role ends at verified + discovery. + +## Reproducing the fixtures + +`testdata/expected-svcb.txt` is recorded output, not a hand-written expectation. +To regenerate it against a local stack: + +```bash +scripts/demo/start.sh +# register the endpoint with the metaDataUrl + metaDataHash shown above +# (register.sh does not send metaDataHash, so POST /v2/ans/agents directly), +# then drive it to PENDING_DNS: +# POST /v2/ans/agents/{agentId}/verify-acme +scripts/demo/dns-records.sh --json | jq -r '.[] | select(.type=="SVCB") | .value' \ + > scripts/demo/payable-endpoints/testdata/expected-svcb.txt +scripts/demo/stop.sh --clean +``` + +Recorded against `upstream/main` at `d8ed4bb` on 2026-08-01. + +## References + +- `spec/api-spec-v2.yaml` — `AgentEndpoint` (`metaDataUrl`, `metaDataHash`), `DiscoveryProfile`. +- `internal/catalog/generate.go` — catalog eligibility, the "no PAYMENT" comment, host policy. +- `internal/adapter/discovery/ans/dnsaid.go` — `cap` / `cap-sha256` / `well-known` SvcParam emission. +- `scripts/demo/register.sh` — the registration body shape this example matches. +- RFC 9460 (SVCB), the A2A agent-card spec, and x402 (HTTP 402 agent payments) for the worked example. diff --git a/scripts/demo/payable-endpoints/testdata/agent-card.json b/scripts/demo/payable-endpoints/testdata/agent-card.json new file mode 100644 index 0000000..8ec9077 --- /dev/null +++ b/scripts/demo/payable-endpoints/testdata/agent-card.json @@ -0,0 +1 @@ +{"name":"DNS of Money — Financial Address Resolution Agent","description":"Deterministic financial address resolution for AI agents: resolve pay: aliases to payment endpoints, discover agent-commerce capabilities, and quote x402 payment requirements. Read-only and non-custodial — the intelligence lives in the calling agent; this agent serves verifiable, deterministic answers and never moves money. Send a message whose data part is {\"tool\": , \"arguments\": {...}}.","version":"1.0.0","protocolVersion":"0.3.0","url":"https://api.dnsofmoney.com/a2a/v1","preferredTransport":"JSONRPC","supportedInterfaces":[{"url":"https://api.dnsofmoney.com/a2a/v1","protocolBinding":"JSONRPC","protocolVersion":"1.0"},{"url":"https://api.dnsofmoney.com/a2a/v1","protocolBinding":"JSONRPC","protocolVersion":"0.3.0"}],"provider":{"organization":"DNS of Money","url":"https://dnsofmoney.com"},"documentationUrl":"https://github.com/sparkmastergrape/ucap/blob/main/docs/mcp-quickstart.md","capabilities":{"streaming":false,"pushNotifications":false,"stateTransitionHistory":false},"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Optional. Anonymous callers get tier-redacted resolution results; keyed callers see the fields their tier allows."}},"security":[],"defaultInputModes":["application/json","text/plain"],"defaultOutputModes":["application/json"],"skills":[{"id":"resolve_alias","name":"resolve_alias","description":"Resolve a human-readable financial alias (pay: address) to payment endpoints and routing metadata. Call this before initiating any payment to a pay: address. Returns the preferred rail, fallback endpoints, an ISO 20022 hint, and a compliance signal. Anonymous calls return status only; a free API key (no credit card) unlocks the endpoint set — which rails and currencies the alias accepts — via POST /api/v1/access-requests. You do NOT need a key to pay: get_payment_requirements returns a payable address to any caller. Example: resolve_alias('pay:acme.user').","tags":["resolution","discovery","payments"],"examples":["{\"tool\": \"resolve_alias\", \"arguments\": {\"alias_uri\": \"pay:acme.user\"}}"],"inputModes":["application/json","text/plain"],"outputModes":["application/json"]},{"id":"discover_agent","name":"discover_agent","description":"Get the AP2/A2A agent-commerce block for a pay: address — the DNS of Money equivalent of an A2A agent-card lookup. Use this during agent discovery before initiating a mandate flow. Returns agent capabilities only; NO payment credentials. Example: discover_agent('pay:acme.user').","tags":["discovery","agent-commerce"],"examples":["{\"tool\": \"discover_agent\", \"arguments\": {\"alias_uri\": \"pay:acme.user\"}}"],"inputModes":["application/json","text/plain"],"outputModes":["application/json"]},{"id":"check_alias_available","name":"check_alias_available","description":"Check whether a pay: alias can still be claimed. Returns exactly one of three states: 'available' (free to register), 'taken' (already registered), or 'reserved' (protected namespace). When available, also returns the number of founding spots remaining. Public — no API key required. Read-only: this never reserves or registers the name. Example: check_alias_available('pay:acme.user').","tags":["payments"],"examples":[],"inputModes":["application/json","text/plain"],"outputModes":["application/json"]},{"id":"get_payment_requirements","name":"get_payment_requirements","description":"Get x402 PaymentRequirements for paying a pay: alias. Returns the 402 `accepts` block (scheme, network, payTo, asset, amount, invoiceId) an x402 agent needs to construct and submit the payment. currency='XRP' settles on the XRP Ledger (amount in drops); currency='USDC' settles USDC on Algorand via the GoPlausible facilitator (amount in atomic micro-USDC, feePayer in `extra`). The deterministic resolver chooses the rail; this tool never moves money. Example: get_payment_requirements('pay:acme.user', amount='1.5').","tags":["payments","x402","quoting"],"examples":["{\"tool\": \"get_payment_requirements\", \"arguments\": {\"alias_uri\": \"pay:acme.user\", \"currency\": \"USDC\"}}"],"inputModes":["application/json","text/plain"],"outputModes":["application/json"]},{"id":"get_agent_registration_info","name":"get_agent_registration_info","description":"Learn how to self-register a FREE pay:agent.