ADR-006: Verified service agents
- Status: Proposed, awaiting owner sign-off. No Phase 11 code is written until the owner answers the questions at the end.
- Date: 2026-10-31
Context
Section titled “Context”Phase 11 lets a business such as a dental clinic run an agent that strangers can find and book with. Owner decision D3 sets two tiers. Verified website is automatic: the business publishes its agent’s key on its own domain, and every node checks that itself. Verified profession is a manual check of licence and registration by the Stelloz team, issued as a signed attestation that nodes verify. Decision D4 puts the directory and the verifier next to the relay. There are no payments.
ADR-005 gives every agent a signed profile card and an opt-in public directory at /v1/listings, where cards are labelled “self-described”.
This ADR adds what can be checked on top of a card. The safety code stays the identity check, both owners still consent to every connection, and a badge never changes policy by itself.
Decision
Section titled “Decision”1. Business profiles (P11.1)
Section titled “1. Business profiles (P11.1)”A business card is an ADR-005 card with v: 2, because v1 rejects unknown fields by design. Nodes from P11.1 accept both. A v1-only node treats a v2 card as unreadable, as ADR-005 does for any card it rejects.
| New field | Rule |
|---|---|
business.org |
Trading name, 1 to 120 characters. |
business.profession |
One code from a fixed profession list in packages/schemas, such as dentist. Each code maps to one ADR-005 category. Optional. |
business.location |
{ city, region, country }: city up to 60 characters, ISO 3166-2 region, ISO 3166-1 country. An optional street of up to 120 characters. |
business.services |
Up to 10 phrases of 60 characters. |
website |
A hostname the agent claims to represent, as a lowercase A-label (xn-- form for IDNs), no port, path or IP literal. Optional. |
templates |
Up to 5 service task types the agent accepts (section 5). Optional. |
Signing, verification, the 4 KB limit and distribution are unchanged, and a card still never carries the owner’s personal details.
2. Verified website (P11.2)
Section titled “2. Verified website (P11.2)”Location and format. The business serves https://<host>/.well-known/agentlink.json:
{ "v": 1, "agents": [ { "address": "al1...", "label": "Front desk" } ]}agents holds 1 to 20 entries. label (optional, 60 characters) is shown only to the business. Unknown fields are rejected. The file is unsigned: control of the host is the proof.
What it binds. The address is the agent’s identity key (ADR-003), so listing the address binds the host to the key. The badge needs both directions: the agent’s signed card claims website, and that host’s file lists the address.
How the node fetches. Through the SSRF-safe outbound fetcher that cross-node handle lookup adds to apps/api (ADR-008): HTTPS on port 443 only, public IPs only, checked after DNS and pinned for the connection, no redirects, a 3-second timeout and a 16 KB cap. A 200 with application/json and a valid body is the only success.
When the node checks.
- On receiving a card whose
websiteis new or changed, whether in a connection request, aprofile.cardmessage or a directory result about to be shown. - Every 24 hours for contacts, and from a “check now” button, at most once per 10 minutes.
- The whole file is cached per host: a success for 24 hours, a definite failure for 6 hours.
- Discover checks only the 20 cards on screen, at most 4 fetches at once.
Failure, expiry and revocation.
| Result | Badge |
|---|---|
| File lists the address | Shown, with “checked 3 hours ago”. |
404, 410, a valid file without the address, or a card without website |
Removed at once. |
Timeout, DNS failure, 5xx, bad TLS, redirect, oversized or invalid body |
Kept for 72 hours from the last success, marked “could not re-check”, then removed. |
The business revokes an agent by removing its address from the file. Nodes drop the badge within 24 hours, or at once on “check now”.
Subdomains. The exact host only: book.mapledental.ca needs its own file. A host that is itself a public suffix (co.uk, github.io) is refused, using the Public Suffix List bundled with each release.
IDN and homographs. Cards and caches hold A-labels only. The dashboard shows the Unicode form only when every label passes the UTS #39 “highly restrictive” profile (one script per label), and the A-label otherwise. The registrable part of the host is shown in bold so mapledental.ca.example.net cannot pass for mapledental.ca.
What the badge shows. “Website verified: mapledental.ca”, and on hover or tap: “mapledental.ca lists this agent. Stelloz did not check who runs the website.” It proves control of a domain, not who the business is.
3. Verified profession (P11.3)
Section titled “3. Verified profession (P11.3)”Who can apply. The owner, from the dashboard, for an agent that already has a verified website. The request is signed by the agent key under its own tag, ALVR1.
What the team checks.
- The licence or registration is active on the regulator’s public register, in the jurisdiction claimed.
- The name on the register matches
business.org, or the register lists the licensee at that business. - The applicant speaks for the business: the team calls or emails the contact the register or the verified website publishes, never one the applicant supplies, and confirms the agent’s safety code.
Attestation format. A signed JSON object, sent beside the card (in the connection request, the acceptance, profile.card, and the directory listing), never inside it.
| Field | Rule |
|---|---|
v |
1. |
id |
16 random bytes, base64url, for revocation. |
address |
The agent’s al1 address. |
subject |
The name as it appears on the register, up to 120 characters. |
profession |
A code from the same fixed list as cards. |
jurisdiction |
ISO 3166-2, such as CA-ON. |
regulator |
The register’s name, up to 120 characters. |
registration |
The public licence number. |
website |
The host verified at check time. |
checkedAt |
Date of the last manual check, YYYY-MM-DD. |
issuedAt, expiresAt |
UTC, to the second, at most 7 days apart. |
issuer |
The issuing key’s certificate, with its kid (below). |
sig |
Ed25519 over AL1-ATTEST, a zero byte, and the canonical JSON of every field except sig. |
Canonical JSON is the existing canonicalJson in packages/crypto, with NFC strings and integer numbers, as for cards.
A node shows the profession badge only if the signature verifies, issuer chains to a pinned root, neither key nor id is revoked, it has not expired, address matches, and the website badge is shown for the same host.
The badge reads “Licensed dentist, Ontario”, with the regulator, licence number and check date on tap, so anyone can confirm it on the register.
Verifier keys and custody.
- Root key. Ed25519, generated offline and never on a networked machine. The owner holds it on two hardware tokens kept apart. Its public key is pinned in each node release.
AGENTLINK_VERIFIER_ROOT_KEYSoverrides it for test networks. - Issuing key. The root signs an issuing key certificate
{ v, kid, publicKey, notBefore, notAfter }underAL1-VKEY, valid 90 days. The issuing key lives in the verifier service beside the relay, sealed under that service’s master key as node keys are (ADR-002). It signs only attestations and the revocation list. - Rotation. A new issuing key starts 7 days before the old one ends. Each release pins the current and the next root, so a root rotation needs no flag day.
- Issuing. The team records checks, denials and revocations with the verifier CLI on the verifier host. The service re-signs each active record’s 7-day attestation daily, and the agent’s node fetches its own from
GET /v1/attestations/:addressdaily.
Revocation. Short-lived attestations plus a list. The service publishes GET /v1/verifier/status, signed by the issuing key, listing revoked attestation IDs until they would have expired. A separate root-signed document with a growing seq, GET /v1/verifier/keys, lists revoked issuing keys. Nodes fetch both hourly. If a node cannot fetch them, badges stay until their attestation expires, with “revocation list last updated” shown after 24 hours.
The team revokes when a re-check finds the licence lapsed or suspended, when the business asks, when the owner reports the agent key lost, or on fraud. Every record is re-checked against the register at least once every 12 months.
Compromise plan.
| Lost | Response | Exposure |
|---|---|---|
| Issuing key or verifier host | The root revokes its kid; a new issuing key re-signs every active record within an hour. |
Forged badges until nodes next poll, at most an hour. |
| Root key | A release drops it and pins the next root; a notice goes on the docs site. | Until nodes upgrade, which is why the root stays offline. |
Appeals. A denial or revocation states a reason code. The applicant can appeal within 30 days to a published address. A different team member reviews it, or the owner while the team is one person, and answers within 14 days. A revocation that follows the register stays until the register changes.
Data the team keeps. Per record: the attestation, the register URL, each check’s date, result and checker, the contact used to confirm, and an email for renewals and appeals. The check uses public registers, so the team asks for no ID documents or licence scans, and deletes any sent anyway. Denied applications are deleted after 90 days.
4. Impersonation warnings (P11.4)
Section titled “4. Impersonation warnings (P11.4)”Look-alike rule. The node computes a skeleton for each name and host: NFKC, case folding, the UTS #39 confusables mapping, without spaces or punctuation. Two skeletons match if equal, or within one edit when both have at least 8 characters. The node compares against verified agents it knows and asks the directory, GET /v1/listings/verified?skeleton=, re-verifying every card returned.
| Case | Warning |
|---|---|
Unverified agent whose name or business.org matches a verified one |
“Not verified. Its name looks like Maple Dental, which is verified at mapledental.ca.” |
Card claims website, but the host does not list it |
“mapledental.ca does not list this agent.” |
| Verified host that looks like another verified host | “rnapledental.ca is verified, but looks like mapledental.ca. Check the safety code.” |
| New address matching a contact’s name or host | “This is a different agent from the Maple Dental in your contacts.” |
The address is the key, so a key change is a new address. The last case interrupts before the first message or task to it. Warnings appear in Needs you, Discover and the contact page. The agent API returns them as codes such as lookalike_name, and an agent cannot dismiss one.
Copy guidance. Say what was checked and what was not. Name the real verified business when there is one. End with one action, usually “compare the safety code with the business by phone”. Never write “safe”, “trusted”, “fake” or “scam”. Never rely on colour alone. Warn only on a specific match, so warnings stay rare and read.
5. Service task templates (P11.5), protocol effects only
Section titled “5. Service task templates (P11.5), protocol effects only”Two new task types, appointment.booking and document.request, join dinner.coordination, each with a strict proposal schema in packages/schemas.
New disclosure capabilities cover the new fields: disclose.identity (name, date of birth), disclose.contact, disclose.health and disclose.financial (insurer, plan). Like every disclose.*, they start on ask. The dashboard sets health and financial grants to allow only one contact at a time, and warns when that contact has no profession badge.
A new simulated action, appointment.book, works like reservation.book: owner approval bound to its exact params. document.request carries only text fields. File transfer needs its own ADR.
6. Open to new clients
Section titled “6. Open to new clients”A listed, verified business may turn on standing consent, so its node accepts connection requests without a manual Accept. This is owner consent given in advance, like an invite link. It is off by default and only available to listed agents. Rate limits, blocking and per-request disclosure rules still apply, and the requesting owner still consents on their side.
Alternatives considered
Section titled “Alternatives considered”- DNS TXT records instead of a well-known file. Rejected for now: the Node.js resolver does not validate DNSSEC, and many small businesses can upload a file but not edit DNS.
- The directory checks websites and nodes trust it. Rejected by D3: every node checks itself.
- One online verifier key pinned directly. Rejected: a host compromise would need a node release to recover.
- Year-long attestations with only a revocation list. Rejected: a node that misses the list shows a revoked badge for months.
Threat model
Section titled “Threat model”| Threat | Defence | Residual risk |
|---|---|---|
| Agent claims a domain it does not control | The host’s file must list the address | A compromised website |
SSRF through a website claim |
Shared fetcher: HTTPS, public IPs, pinned, no redirects, caps | Small fetch load on a victim host, cached 6 hours |
| Homograph domain | A-labels; UTS #39 display rule; bold registrable domain | Owners who ignore the warning |
| Forged attestation | Pinned root, issuing key chain, Ed25519 | None while the root is safe |
| Revoked licence still shown | 7-day life, hourly revocation list | Up to an hour, or 7 days for a node offline from the verifier |
| Social engineering of the team | Contact taken only from the register or verified site; safety code confirmed | A compromised register entry |
| Unverified look-alike | Skeleton rule, warnings, interstitial | Names that differ by more than one edit |
| Operator issues false badges | Badges show the regulator and licence number, which anyone can check | Same operator runs relay, directory and verifier |
Rollout, tests and gates
Section titled “Rollout, tests and gates”Each unit ends with an independent security audit and does not close while a high-severity finding is open. Docs, AGENTS.md and the API reference change with the code.
- P11.1 Business profiles. Tests: v1 and v2 accepted; bad, oversized and unknown fields rejected.
- P11.2 Verified website. Tests: SSRF suite (private, loopback, rebinding, redirects, oversize, slow body); a file without the address shows no badge; the grace window; IDN, subdomain and public-suffix rules.
- P11.3 Verified profession. Tests: forged, expired, wrong-address, revoked-ID and revoked-key attestations show no badge; rotation keeps badges continuous.
- P11.4 Impersonation warnings. Tests: the four cases, confusable and one-edit names, and no warning for distinct names.
- P11.5 Service templates. Tests: health and financial fields wait on
ask; changed params fail approval.
Phase gate: the dental booking demo. Two nodes. “Maple Dental” lists its agent and serves the file on a demo domain, and holds a profession attestation from a test root, never the production root. Alice finds it on Discover, sees both badges, and both owners consent. Her agent proposes an appointment.booking for a cleaning. Health notes wait for Alice’s approval. The clinic accepts, Alice approves appointment.book (simulated), and both tasks end confirmed. An unverified “Maple Dentall” shows the look-alike warning.
Consequences
Section titled “Consequences”- Owners can tell a business’s real agent from a copy without trusting the directory.
- Website badges are automatic. Profession badges cost team time, which caps how many are issued.
- Nodes fetch business websites, bounded by one shared fetcher.
- The verifier root becomes the most sensitive secret in the project.
Decisions for the owner
Section titled “Decisions for the owner”- Does Verified profession require a verified website first? (a) Yes, it gives the team a contact to confirm and ties the badge to a domain. (b) No, a register call-back alone. Recommended: (a).
- Attestation lifetime. (a) 7 days, re-signed daily, plus an hourly revocation list. (b) One year with a revocation list only. (c) 7 days with no list. Recommended: (a).
- Verifier key custody. (a) An offline root you hold on two hardware tokens, signing 90-day online issuing keys. (b) One online key pinned in releases. Recommended: (a).
- Website re-checks. (a) Every 24 hours, badge kept 72 hours through outages, dropped at once on a definite failure. (b) Every 7 days with no grace. Recommended: (a).
- Subdomains. (a) Exact host only. (b) A domain’s file also covers its subdomains. Recommended: (a).
- Look-alike warnings. (a) Warn and require one extra confirmation, never block. (b) Block connections to unverified look-alikes. Recommended: (a).
- Open to new clients. (a) A listed, verified business may turn on standing consent, so connection requests from anyone are accepted automatically. Rate limits, blocking and per-request disclosure rules still apply. (b) Every request needs a manual Accept. Recommended: (a).
Stelloz's source code is not public yet. An open source GitHub link may come later.