Specifications / agent-authorization

Agent Authorization

A DNS-based standard that lets a domain explicitly opt in to being contacted by autonomous agents — over email or any other channel — and scope which agents or platforms it accepts, per channel.

Status: Draft standard, version AGENTS1. Vendor-neutral and provider-agnostic: the record, the tags, and the matching rules name no provider and assume no single channel. primitive.dev appears only as an example provider identifier and email only as the first defined channel, the same way example.com appears as an example domain.

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, MAY, RECOMMENDED, and NOT RECOMMENDED are used per RFC 2119/RFC 8174.

Quick start

If you just want to let agents on a given platform email your domain, publish one DNS TXT record at _agents on your domain (using primitive.dev as the example provider):

_agents.example.com.  IN  TXT  "v=AGENTS1; p=accept; channel=email; allow=provider:primitive.dev"

No record means no permission, and you can change or remove it at any time. The rest of this document defines the grammar, the trust model, and the exact evaluation rules.

Abstract

Authentication standards answer "is this message really from the domain it claims?" — SPF, DKIM, and DMARC for email, and analogues elsewhere. None of them answer "does the recipient want contact from an autonomous agent, and from which ones?"

As autonomous agents become common — and as they reach domains over more than one channel — recipient domains need a single, public, machine-readable signal: "I accept agent contact, here is who I accept it from, and over which channels." Agent Authorization is that signal: one DNS TXT record the recipient publishes at _agents on its own domain.

It is the mirror image of authorization-to-send standards like SPF. SPF authorizes servers to send as a domain. This record authorizes agents to make contact with a domain. Control of the domain's DNS authenticates the publisher's opt-in intent — only the domain owner can publish the record. It does not, by itself, authenticate the agent or provider making contact; that is the asserting provider's responsibility today, with hooks reserved (see Security considerations) to layer in verifiable proof without a breaking change.

The standard is split into two layers:

  • A channel-agnostic core — the record, its grammar, the provider/principal trust model, the version-selection and evaluation algorithm. None of it is email-specific.
  • One or more channel bindings — each defines a channel token (e.g. email) and the two channel-specific details the core leaves open: what the channel principal is, and how principal values are compared. Email is the first binding.

Terminology

The standard deliberately separates identities that channel authentication usually conflates:

  • Agent — an autonomous program that initiates contact (sends mail, opens a session, posts a message) without a human composing each exchange. For evaluation an agent is described by an open set of attributes its provider asserts and has verified. AGENTS1 defines two attributes, provider and the channel principal; future versions MAY define more (e.g. a per-agent id). Consumers MUST treat the attribute set as open, not as a fixed tuple, so new attributes are additive.
  • Provider (agent platform) — a service that hosts agents, identified by a provider identifier: a domain name it controls, e.g. primitive.dev. The identifier is channel-independent: trusting a provider means trusting it to vouch for the agents it hosts, on any channel.
  • Channel — a transport over which an agent reaches a domain (email today; others may be registered later). Each channel has a registered channel token and a binding (see Channels).
  • Channel principal — the per-channel identity an agent presents and the channel has verified. What the principal is, and how it is compared, are defined by the channel binding — it is a domain for some channels, but MAY be a public key, a handle, or another identifier for others. The core never assumes its shape. For the email binding the principal is the DKIM-aligned From domain, and the domain: token (below) is its principal-grant form.
  • Publisher (recipient domain) — the domain that publishes the _agents record and thereby opts in. The "you" in this document.

The provider/principal split is the core of the trust model. A publisher can trust a whole platform ("any agent Primitive hosts is fine") or a specific principal ("only agents presenting acme.com, whoever hosts them"), and these are different decisions — on any channel.

The record

A publisher opts in by publishing one or more TXT records at the _agents label of the domain that will be contacted:

_agents.example.com.  IN  TXT  "v=AGENTS1; p=accept; channel=email; allow=provider:primitive.dev"

Lookup procedure

A consumer evaluating contact to <domain> over channel C:

  1. Queries TXT at the exact name _agents.<domain>. AGENTS1 performs no ancestor tree-walk: absence at the exact name means not opted in for <domain>. Subtree inheritance is reserved for a future sp= tag, whose absence MUST be treated as sp=strict (no inheritance). Naming this default now means already-published apex records keep a stable meaning when sp= ships.
  2. Consumers MUST support EDNS0 and TCP fallback. A truncated or oversized answer MUST be retried over TCP, never silently treated as "no record."
  3. NODATA (the name exists but has no TXT) is treated identically to NXDOMAIN: not opted in. An empty or whitespace-only TXT string is a malformed record and is ignored (net: not opted in). A CNAME at _agents.<domain> is followed normally by the resolver, and the resulting TXT set is evaluated as if published at _agents.<domain>.

Version selection

The v tag carries the format version (AGENTS1). Versioning and the multiple-record rule are defined together so they never fight:

  • A consumer MUST ignore any record whose v it does not recognize.
  • Version selection runs first. Among records it recognizes that name channel C, the consumer determines the highest v it understands and discards all records naming C with a lower v. Records that differ only in v are NOT a collision — this is how a publisher runs AGENTS1 and a future AGENTS2 side by side at one name during a transition. The same-version duplicate check (below) then applies only to the surviving highest-version records; a duplicate at a superseded version never affects C.
  • v=AGENTS1 is reserved for breaking grammar changes only. Backward- compatible evolution uses new tags (ignored by older consumers) and the critical-tag mechanism (below), not a version bump.

Multiple records and channels

A domain MAY publish multiple v=AGENTS1 records at _agents, each governing a different channel (or set of channels). Evaluation selects by channel:

  • Records that do not name channel C are ignored for C, so an unrelated channel's record never blocks C.
  • If exactly one AGENTS1 record names C, it governs C.
  • If more than one same-version record names C, the configuration is ambiguous and fails closed for C (not authorized). Fail-closed is chosen deliberately: under DNS forgery, denial is the safe failure, whereas a permissive merge would let a forged duplicate broaden access. Because this is also a griefing/tamper vector (someone who can inject one extra record can deny a channel), consumers SHOULD log/alert on duplicate- channel records rather than denying silently, MUST NOT cache the duplicate state as a sticky negative, and publishers SHOULD deploy DNSSEC.
  • A record naming multiple channels (channel=email chat) asserts that its allow set is identical for every channel listed. A publisher needing different allow/policy per channel MUST use separate records.
  • When a record names several channel tokens, a consumer evaluates it for any listed token it understands and ignores listed tokens it does not. An unrecognized channel token alongside a recognized one (channel=email foo, foo unknown) does not invalidate the record for the recognized channel.

DNS encoding constraints

  • A record value longer than 255 octets MUST be split into multiple <character-string>s within a single RR. Consumers MUST concatenate all character-strings of an RR with no separator before parsing (the DKIM / RFC 1035 rule). This is the single most common TXT implementation error; consumers that read only the first string will misparse long records.
  • Keep the full _agents response small enough to fit a single UDP/EDNS0 answer where practical. Publishers with large allow enumerations SHOULD use the reserved .well-known/agents companion (see Future extensions) rather than growing the TXT set unboundedly.
  • This standard uses the TXT RRTYPE exclusively. A dedicated RRTYPE is out of scope and SHOULD NOT be introduced; the deprecated SPF RRTYPE (RFC 7208 §3.1) is the cautionary precedent.

Syntax

The record value is a list of key=value tags separated by semicolons, following the same lexical conventions as SPF and DMARC:

record = "v=AGENTS1" *( OWS ";" OWS tag )
tag    = [ "!" ] key "=" value
key    = ALPHA *( ALPHA / DIGIT / "_" )
value  = *( %x09 / %x20-3A / %x3C-7E )  ; any printable ASCII + SP/HTAB, except ";" (%x3B)
OWS    = *( SP / HTAB )           ; ALPHA/DIGIT/SP/HTAB are [RFC 5234] core rules
  • Tokenization is two levels. A record is split into tags at each ;. A tag's value runs from the = to the next ; or end of record; leading and trailing OWS in a value is stripped. There is no escape mechanism in AGENTS1, so a value MUST NOT contain ;. For list-valued tags (channel, allow), the stripped value is then split into tokens on runs of ASCII space, HTAB, and/or comma; interior whitespace in a list value is a separator, not data.
  • Tag keys are case-insensitive, and the ! critical marker is case-irrelevant (!REQ!req). Value case rules are per-tag: channel tokens and provider identifiers are ASCII case-insensitive; principal values are compared per the channel binding (which may be case-sensitive, e.g. a public key). "Case-insensitive" in this document means ASCII case- folding only.
  • The v=AGENTS1 tag MUST come first. A record whose first tag (after OWS trimming) is not v=AGENTS1 is malformed and MUST be ignored — consumers do not reorder to find v.
  • A tag whose key appears more than once makes the record malformed; the record MUST be ignored (fail-closed; avoids first-wins/last-wins divergence).
  • Unknown ordinary tags MUST be ignored (forward compatibility).
  • Critical tags — a tag key prefixed with ! is critical. A consumer that does not understand a critical tag MUST treat the record as not authorized rather than ignoring the tag. This reserves the ability to ship a tightening change (e.g. !req=) that older consumers cannot silently bypass — the escape hatch a blanket "ignore unknowns" rule otherwise forecloses.

Tags

Tag Required Meaning
v yes Format version. MUST be AGENTS1.
p yes Policy: accept opts in, reject is an explicit opt-out.
channel yes One or more registered channel tokens (whitespace- or comma-separated) this record governs, e.g. email.
allow when p=accept Whitespace- or comma-separated authorization tokens (below). An empty allow= under p=accept authorizes nothing (deny-all) and is not malformed. Absent allow under p=accept makes the record malformed (ignored); use explicit allow= for deny-all. allow under p=reject is ignored, not malformed.
req (critical-capable; written !req) no (reserved) Critical strictness requirements (e.g. dnssec, signed, verified-provider). Reserved; defined by a future version. Because it is written critical (!req), an older consumer that sees it fails closed rather than honoring a weaker policy.
policy no Absolute https:// URL to a human-oriented acceptance policy. Advisory and never authoritative for the accept/reject decision. A malformed value MUST be ignored without invalidating the record.
contact no Abuse / operations contact as a mailto: URI. Advisory; malformed value ignored without invalidating the record.

A record whose channel tag is missing, empty (channel=), or names no registered channel the consumer understands is, for that consumer, simply not applicable to any channel it can evaluate; it MUST be ignored (not malformed, not treated as governing some default channel).

Any p value other than accept is treated as reject — for both the authorization decision and caching — so an unknown/typo'd policy fails safe.

allow tokens

Each token in allow is a typed grant. The type prefix distinguishes a provider grant from a principal grant:

Token Grants
* Any compliant agent on any provider. NOT RECOMMENDED — the broadest opt-in and a standing spam target; prefer enumerated grants. Tooling SHOULD warn when a publisher selects it.
provider:<provider-id> Any agent hosted by the named provider. Trust the platform.
domain:<principal> The email binding's principal grant: any agent whose verified channel principal (DKIM-aligned From domain) is the named domain, regardless of provider. Other bindings define their own principal-grant token(s).
agent:<id> Reserved for a future per-agent identity axis. Will be namespaced (agent:<id>@<provider>) and cryptographically attested. Unrecognized in AGENTS1 and MUST be ignored.

Rules:

  • provider: and principal grants (domain:) are orthogonal; a publisher may list any mix.
  • * is matched literally and is NOT canonicalized. * carrying an ignored |-qualifier (*|rate=10) still reduces to the base token * and authorizes all in AGENTS1 (qualifiers are advisory here).
  • A token with no recognized type prefix (a bare primitive.dev) MUST be ignored, so future token types degrade safely.
  • The vertical bar | is reserved as a future per-token qualifier delimiter (e.g. domain:acme.com|rate=10). AGENTS1 consumers MUST ignore everything from the first | in a token onward and match only the base token. Reserving it now keeps future per-grant scoping additive instead of colliding with the ; tag separator.
  • Canonicalization before comparison (provider ids and domain:-style domain principals): strip a single trailing ., trim surrounding whitespace, ASCII-lowercase, then convert to IDNA2008 A-label (punycode) form. Reject the token — drop that grant only, do not invalidate the record — if it is empty or fails IDNA2008 A-label conversion (matching the bare-token degradation above). The same canonicalization is applied to the incoming agent.provider and to a domain-shaped agent.principal before comparison, so both sides are A-labels and comparison is octet-equality. Non-domain principals are canonicalized and compared per their binding.

Channels

A channel is a transport with a registered channel token and a binding that supplies the two details the core leaves open: what the channel principal is, and how principal values are compared. Everything else — record, grammar, trust tokens, version selection, evaluation, caching — is shared across all channels.

Channel tokens and allow token-type prefixes live in shared registries (see IANA considerations). Tokens containing a . or prefixed x- are reserved for private use and MUST NOT be registered, so experimentation cannot squat the global namespace.

Email (channel token: email)

The first defined binding.

  • Channel principal = the agent's DKIM-aligned From domain (the RFC 5321 / RFC 5322 author domain whose DKIM signature passes and aligns). A domain:acme.com grant authorizes agents that send DKIM-aligned mail from acme.com.
  • Principal comparison = domain canonicalization above (A-label, ASCII case-insensitive).
  • The record is consulted on the outbound path, keyed on the recipient's domain (the part after @). The principal SHOULD be one the channel authenticates end-to-end (the receiving side can re-verify DKIM alignment), not merely the asserting provider's self-report.

Future channels register a token and define their own principal and comparison. The core does not change.

Evaluation (authorizing contact)

A contacting agent is described by provider-asserted, channel-verified attributes; AGENTS1 reads two:

  • agent.provider — the provider identifier the agent runs on.
  • agent.principal — the agent's verified channel principal, per the binding for the channel in use.

To decide whether contact to <domain> over channel C is authorized:

  1. Resolve TXT at _agents.<domain> per Lookup procedure.
  2. If resolution is inconclusive (SERVFAIL, timeout, truncation that TCP retry did not resolve): the result is unknown. Treat as not authorized for this decision, but MUST NOT cache it as a negative (see Caching); retry later.
  3. Discard records whose v is unrecognized or malformed (per Syntax). From the remainder, take the records naming C, and select the single governing record per Version selection and Multiple records and channels (zero → not authorized; same-version duplicates → not authorized).
  4. If any critical tag (!-prefixed) in the selected record is not understood → not authorized. If p is not accept (including unknown/typo'd values) → not authorized. If p=accept but allow is absent → the record is malformed → not authorized (an explicit empty allow= is the deny-all form, and authorizes nothing).
  5. Contact is authorized iff any allow token matches (after canonicalization):
    • token is *, OR
    • token is provider:X and X == agent.provider, OR
    • token is a principal grant (domain:Y for email) and Y == agent.principal under the binding's comparison.

Fail-closed is the rule throughout: absence, ambiguity, malformed records, unknown critical tags, and lookup failure all resolve to not authorized. Opting in is an explicit, unambiguous act.

Pre-flight / dry-run mode

A consumer MAY evaluate without a bound agent identity (a debugger asking "what would happen?"). With agent.provider/agent.principal unknown, * and known-value checks that don't depend on the missing attribute may still resolve, but a provider:/domain: decision that depends on an unknown attribute is indeterminate and MUST be reported as such (treated as not authorized), never silently skipped or guessed. If any token resolves to a definite match (e.g. *), report authorized; report indeterminate only when no token definitely matches and at least one depends on a missing attribute.

Example

_agents.example.com. TXT "v=AGENTS1; p=accept; channel=email; allow=provider:primitive.dev domain:acme.com"
_agents.example.com. TXT "v=AGENTS1; p=accept; channel=chat;  allow=*"

(chat is hypothetical, used only to illustrate multi-channel publishing; email is the one channel AGENTS1 defines.) Evaluating an email contact (only the first record names email):

  • Agent on primitive.dev, principal bot.thing.io → authorized (matches provider:primitive.dev).
  • Agent on another platform, principal acme.com → authorized (matches domain:acme.com).
  • Agent on another platform, principal random.netnot authorized.

Caching

The record is read on the hot contact path, so implementations SHOULD cache, keyed on the publisher domain (cache the full _agents record set once and select by channel in memory — not per channel):

  • Honor the record's DNS TTL, with a sane floor and a ceiling. For p=accept the positive-cache ceiling SHOULD be short (≤1h) so revocation takes effect quickly; reject/absence fail safe and MAY be cached longer.
  • Negative results (no record / reject) MAY be cached but MUST honor the zone SOA negative-TTL as an upper bound (RFC 2308) and SHOULD default short. If the resolver does not surface the SOA negative-TTL (common with high-level/DoH resolvers), fall back to the short default and never cache a negative longer than that default.
  • Inconclusive lookups (SERVFAIL, timeout) MUST NOT be cached as negative; retry, so a transient resolver blip never becomes a sticky "this domain rejected you."
  • On a credible abuse signal for a publisher, a consumer SHOULD re-resolve that publisher's record immediately, bypassing cache.

Implementation note: DNS-over-HTTPS hides SERVFAIL behind an HTTP-200 envelope with Status: 2 and an empty answer set. Consumers MUST read the response Status, not the answer count, to honor the inconclusive rule above.

Security considerations

  • DNS control authenticates intent, not the contacting party. Publishing the record proves the publisher opted in. It does not prove the agent or provider is who it claims; in AGENTS1 those are the asserting provider's word (see below). Treat the record as the publisher's consent, layered with provider-side and channel-native authentication.
  • Forgery can over-permit without DNSSEC. An on-path or cache-poisoning attacker on an unsigned zone can fabricate a more permissive record (allow=*) and manufacture consent the publisher never gave. So the worst case is not merely denial — without DNSSEC it includes over-permission. DNSSEC is RECOMMENDED; consumers SHOULD prefer authenticated (AD-bit) results and MAY require them (future !req=dnssec) for * grants or any grant used to bypass an otherwise-applicable sender restriction.
  • The asserter is the beneficiary. In the email binding the consumer running evaluation is often the sending provider itself, asserting "this is DKIM-aligned." The decision SHOULD be anchored to a receiver-verifiable result (the receiving side re-checks alignment), not a self-report. A future signed-assertion mechanism (reserved via !req=signed and a proof axis) lets a relying party verify the (provider, principal) binding without trusting the provider's word.
  • Provider-identifier impersonation. Nothing in AGENTS1 binds provider:X to the entity controlling X; a bad actor can self-assert any provider id. A future version will require proof of control of the provider domain (a provider-published record at a reserved label and/or a discoverable signing key). Until then, publishers SHOULD understand provider: trust is only as strong as the ecosystem's provider verification.
  • Class-based, not per-message. A match authorizes a category on a channel, not a specific message or session, and does not replace per-contact abuse controls (rate limiting, content policy, suppression). The reserved per-agent axis (agent:) and per-token qualifier delimiter (|) are the path to finer scoping.
  • allow=* is the open relay of agent auth and makes a domain a soft target; it is NOT RECOMMENDED and MAY be deprecated in a future version.
  • The record is a public recon oracle. _agents.<domain> advertises, to adversaries too, who accepts agent contact and how loosely. Publishers SHOULD avoid *, use a role/abuse address for contact that can absorb load, and prefer narrow grants.
  • Revocation is cache-bounded. A compromise-then-opt-in attacker's runway is the positive-cache TTL; the short p=accept ceiling and abuse-triggered re-resolve above exist to bound it.

Reserved for future hardening (no breaking change required)

Unknown ordinary tags are ignored and unknown critical tags fail closed, so the following can be added to AGENTS1 records without a version bump:

  1. !req= strictness (dnssec / signed / verified-provider) — critical, so older consumers can't silently honor a weaker policy.
  2. A proof/signature axis for verifiable (provider, principal) assertions.
  3. A provider proof-of-control contract at a reserved provider-side label.
  4. Namespaced, attested per-agent identity (agent:<id>@<provider>).
  5. A machine-readable abuse/revocation feedback hook.

Conformance

A compliant consumer implements the lookup procedure, version selection, multiple-record and critical-tag rules, canonicalization, and fail-closed evaluation above. A compliant provider asserts only (provider, principal) pairs it has actually verified for the channel in use, honors the publisher's contact, and does not assert a provider id it does not control. "Compliant" in allow=* and elsewhere refers to this profile; it is a behavioral commitment, not a certification, and provides no security on its own — see Security considerations.

Future extensions (non-normative)

Reserved for later versions; an AGENTS1 consumer ignores ordinary ones and fails closed on critical ones it does not understand:

  • !req= strictness; signed assertions / proof axis; provider proof-of-control.
  • agent:<id>@<provider> per-agent identity; | per-token qualifiers (rate=, for=).
  • sp= subdomain policy (default strict today) and an org-boundary tree-walk.
  • Additional channel bindings beyond email.
  • A .well-known/agents HTTPS companion for large or signed policies. If introduced it is advisory only unless a future version explicitly makes it authoritative via a critical pointer tag; the TXT record remains the authoritative accept/reject source for AGENTS1.

IANA considerations (registries)

To keep a vendor-neutral namespace from fragmenting, this standard anticipates three registries (registration policy: Specification Required / Expert Review):

  • Channel tokens (e.g. email).
  • allow token-type prefixes (e.g. provider, domain, agent).
  • Tag keys (e.g. v, p, channel, allow, req, policy, contact), noting which are critical-capable.

Names containing a . or prefixed x- are reserved for private use in all three and MUST NOT be registered.

Provider implementation note (non-normative)

A contacting provider integrates the standard by, on each outbound contact, resolving _agents.<recipient-domain>, selecting the record for the channel in use, and evaluating it against the agent's asserted-and-verified attributes per the algorithm above. Practical guidance learned from the email binding:

  • Resolve off the hot commit/transaction path (as SPF is checked outside the message-commit critical section); pass the resolved record set into the evaluation as data so the matcher does no network I/O.
  • Carry the lookup as a three-state result — found / empty / inconclusive — not a nullable list, and map DoH Status / resolver error codes into it so the "don't cache inconclusive as negative" rule is implementable.
  • An inconclusive lookup is a retryable condition, not a denial; it MUST NOT be recorded as a permanent rejection of the recipient.

Because the check sits alongside a provider's existing contact authorization, it is one additional path to "allowed", not a replacement for sender-side requirements (a verified identity, authenticated sending, etc.).