Version: 0.8.3 Status: Draft Last Updated: 2026-07-31 Authors: Roland R. Rodriguez, Jr. rrrodzilla@proton.me
This document specifies the agent:// URI scheme for topology-independent agent identity in multi-agent systems. The scheme enables capability-based discovery through DHT key derivation, organizational scoping through trust roots, and cryptographic verification through PASETO attestation tokens.
This is a draft specification intended for community review and feedback. The specification is implemented by the reference implementation at https://crates.io/crates/agent-uri.
Feedback is welcome via GitHub issues or direct contact with the authors. Specific areas where feedback is requested:
- DHT participation incentive models
- Capability mapping service design
- Integration patterns with A2A protocol
- Introduction
- Terminology
- URI Syntax
- Component Semantics
- Normalization and Equivalence
- Discovery and Resolution
- Attestation
- Security Considerations
- IANA Considerations
- References
Appendix A: ABNF Grammar Appendix B: Test Vectors Appendix C: Length Constraints
Multi-agent systems require stable agent identity that survives infrastructure changes. Current approaches bind agent identity to network location—when agents migrate between providers, scale across instances, or federate across organizations, URI-based identity schemes break references, fragment audit trails, and require centralized coordination.
The agent:// URI scheme decouples identity from topology through three components:
- Trust root: Organizational authority vouching for the agent
- Capability path: Hierarchical, identity-defining description of agent capabilities
- Agent identifier: Globally unique, time-sortable reference
-
Topology independence: Agent identity MUST NOT change when the agent migrates between hosts, cloud providers, or geographic regions.
-
Capability semantics: The scheme MUST support discovery by capability ("find agents that can approve invoices").
-
Decentralized resolution: No single registry SHOULD be required for resolution.
-
Organizational scoping: Queries MUST be scopable to trust boundaries.
-
Verifiable claims: Capability claims MUST be cryptographically verifiable.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119].
Agent: A software entity capable of autonomous action in a multi-agent system.
Trust root: An organizational authority that vouches for agents' existence and capabilities by issuing attestation tokens.
Capability path: A hierarchical path describing what an agent can do.
The capability path is constitutive identity material. Moving the same implementation to a different capability path denotes a different agent and requires a newly minted Agent ID and attestation.
Agent identifier: A TypeID combining a semantic prefix with a UUIDv7 suffix.
Attestation: A cryptographically signed token binding an agent URI to capability claims.
DHT: Distributed Hash Table used for decentralized agent discovery.
Pointer: A small record naming an agent URI registered beneath a capability path. A pointer is a discovery hint. The agent's own registration record is the authority for everything about that agent.
Pointer page: One stored value holding a set of pointers for one capability path.
Shard level: The exponent L for which a capability path's pointers occupy
2^L pointer pages.
An agent URI follows RFC 3986 generic syntax with agent-specific constraints:
agent://trust-root/capability-path/agent-id[?query][#fragment]
Examples:
agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q
agent://acme.corp/workflow/approval/invoice/rule_fsm_01h5fskfsk4fpeqwnsyz5hj55t
agent://localhost:8472/debug/test/llm_01h455vb4pex5vsknk084sn02q
The complete grammar is provided in Appendix A. The top-level rule is:
agent-uri = scheme "://" trust-root "/" capability-path "/" agent-id
[ "?" query ] [ "#" fragment ]
scheme = "agent"| Component | Maximum Length | Notes |
|---|---|---|
| Total URI | 512 characters | Hard limit |
| Trust root | 128 characters | Including port |
| Capability path | 256 characters | All segments combined |
| Path segments | 32 count | Maximum number of segments |
| Each segment | 64 characters | Individual segment |
| Agent ID prefix | 63 characters | TypeID specification limit |
| Agent ID suffix | 26 characters | Fixed (UUIDv7 in Crockford Base32) |
Implementations MUST reject URIs exceeding these limits.
The component maxima bind before the total does: the longest constructible URI is
8 (agent://) + 128 + 1 + 256 + 1 + 90 = 484 characters, so the 512-character
cap is never the limit that refuses a well-formed URI. It is checked first
regardless, so that a hostile input is refused on its length before any component
is parsed. Vector len-002 carries the 484-character case.
The trust root identifies the organizational authority vouching for the agent.
Syntax:
trust-root = host [ ":" port ]
host = domain / ip-literal / ipv4-address
domain = label *( "." label )
label = 1*63( ALPHA / DIGIT / "-" )Requirements:
- The trust root MUST follow DNS hostname syntax or be a valid IP address.
- Domain labels MUST NOT start or end with a hyphen.
- The trust root MUST publish verification keys at a well-known endpoint (see Section 7.2).
- DNS trust roots are case-insensitive and MUST be normalized to lowercase. IPv4 addresses use dotted-decimal form and IPv6 literals use RFC 5952 canonical text in brackets. An explicit port is preserved; no default port is inferred or stripped.
A trailing dot MUST be rejected rather than stripped:
example.com.is not a valid trust root, and onlyexample.comnames that identity. - A trust root is ASCII. A host containing a non-ASCII character MUST be rejected rather than converted; a label beginning
xn--is treated as an opaque DNS label and is not decoded or validated as Punycode. Converting an internationalized name to A-label form is the responsibility of whatever accepts it from a person, and Section 8.11 states what this leaves undetected. - A host consisting of exactly four dot-separated labels that each contain only digits MUST be parsed as an
ipv4-address, never as adomain. Such a host MUST be rejected when it is not a valid dotted-decimal address: every octet MUST be in the range 0-255 and MUST NOT carry a leading zero, per thedec-octetrule in Appendix A. Hosts with any other number of labels, or with a non-numeric label, remain subject to thedomainrule.
Examples:
anthropic.com
agents.us-west-2.prod.acme.corp
localhost:8472
[::1]:8472
192.168.1.1:8080
The capability path describes what the agent does using hierarchical segments.
Syntax:
capability-path = segment *( "/" segment )
segment = 1*64( LOWER / DIGIT / "-" )Requirements:
- Capability paths MUST contain at least one segment.
- Capability paths MUST NOT exceed 32 segments.
- Each segment MUST be lowercase alphanumeric with hyphens permitted. Uppercase input MUST be rejected, not folded, and a segment MUST NOT carry percent-encoding:
ch%61tis rejected rather than decoded tochat. - Segments MUST NOT be empty (no consecutive slashes).
- Capability paths support prefix matching for discovery.
- An agent's capability path is immutable. A capability-path change MUST use a new Agent ID; implementations MUST reject reuse of a trust-root/Agent-ID pair under another path.
Examples:
assistant/chat
workflow/approval/invoice
tool/code-interpreter
financial/trading/equity/market-orders
Semantics:
Capability paths form a hierarchy. A query for /workflow/approval returns agents registered at:
/workflow/approval(exact match)/workflow/approval/invoice(child)/workflow/approval/expense(child)
But not:
/workflow(parent)/workflow/review(sibling)
The agent identifier is a TypeID providing globally unique, time-sortable reference.
Syntax:
agent-id = prefix "_" suffix
prefix = LOWER *( LOWER / "_" ) LOWER / LOWER
suffix = first-char 25base32char
first-char = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7"
base32char = DIGIT / "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h"
/ "j" / "k" / "m" / "n" / "p" / "q" / "r" / "s" / "t"
/ "v" / "w" / "x" / "y" / "z"The suffix uses Crockford Base32 encoding with the alphabet 0123456789abcdefghjkmnpqrstvwxyz (excludes i, l, o, u to avoid ambiguity). The first character is constrained to 0-7 to ensure the 128-bit UUIDv7 value fits in 26 characters (3 + 25×5 = 128 bits).
Requirements:
- The prefix MUST contain only lowercase letters and underscores.
- The prefix MUST start and end with a letter.
- The suffix MUST be exactly 26 characters of Crockford Base32.
- The suffix's first character MUST be in the range 0-7 (ensures 128-bit value fits).
- The suffix encodes a UUIDv7, providing time-ordering and uniqueness.
- The alphabet excludes i, l, o, u to avoid visual ambiguity with 1, 1, 0, v.
Prefix Semantics:
The prefix encodes the agent's implementation type:
| Prefix | Description |
|---|---|
llm |
Large language model agent |
llm_chat |
LLM specialized for conversation |
rule |
Rule-based agent |
rule_fsm |
Finite state machine agent |
hybrid |
Combined approaches |
Custom prefixes SHOULD follow the pattern type_modifier_modifier.
That pattern is a naming convention and not part of the syntax above. A prefix that does not read as a class followed by non-empty modifiers is still well formed: the prefix rule permits any arrangement of lowercase letters and underscores between the first and last letter, including consecutive underscores, exactly as the TypeID specification does. An implementation MUST accept llm__chat. Refusing it would reject identifiers that conforming TypeID implementations mint. An implementation that exposes the class and modifiers separately MUST NOT report a modifier for the empty run between consecutive underscores.
Examples:
llm_01h455vb4pex5vsknk084sn02q
llm_chat_streaming_01h5fskfsk4fpeqwnsyz5hj55t
rule_fsm_01h455vb4pex5vsknk084sn02q
The query string provides optional parameters for version negotiation and metadata.
Syntax:
query = *( pchar / "/" / "?" )Standard Parameters:
| Parameter | Type | Description |
|---|---|---|
version |
string | Capability version constraint |
ttl |
integer | Time-to-live hint in seconds |
attestation |
string | Inline PASETO token (discouraged; use headers) |
Requirements:
- Query parameters are NOT part of agent identity.
- Two URIs differing only in query string reference the same agent.
- Query parameters MUST be stripped for normalization and DHT key derivation.
- Percent-encoded octets in a query parameter value MUST be decoded as a byte sequence, and the decoded byte sequence MUST be valid UTF-8.
- A value whose decoded octets are not valid UTF-8 MUST be rejected. Implementations MUST NOT substitute U+FFFD, decode lossily, or interpret the octets as Latin-1.
- When serializing a query parameter value, any octet outside the unreserved set (
ALPHA/DIGIT/-/_/.) MUST be rendered as%followed by two uppercase hexadecimal digits.
The fragment provides optional sub-agent reference.
Syntax:
fragment = *( pchar / "/" / "?" )Requirements:
- Fragments are NOT part of agent identity.
- Fragments are reserved for future use in composite agent scenarios.
- Fragments MUST be stripped for normalization and DHT key derivation.
Two URIs denote the same agent if and only if their canonical forms are byte-equal.
An agent URI is identity material, so normalization is deliberately narrow: an implementation folds case only where a standard makes a component case-insensitive, and rejects every other departure from canonical form rather than repairing it. A repair maps two spellings onto one identity, and each such mapping is a place where two parties can disagree about which agent a string names. Rejection leaves exactly one spelling per identity.
Normalization Rules:
- Scheme: Compared case-insensitively, written lowercase (
agent).AGENT://is accepted and folded, per RFC 3986 Section 3.1. - Trust root: DNS names compared case-insensitively, written lowercase, per RFC 4343. IP addresses in canonical text form — for IPv6 that is RFC 5952, which drops leading zeros and compresses the longest run of zero groups. Explicit ports preserved.
- Capability path: Lowercase by grammar, with no trailing slash. Uppercase input is rejected, not folded.
- Agent ID: Lowercase by grammar. Uppercase input is rejected, not folded.
- Query and fragment: Stripped entirely. Where a query is preserved rather than stripped, its parameters serialize sorted by name.
Rejected rather than normalized:
| Input | Disposition | Why |
|---|---|---|
AGENT://example.com/chat/llm_01h4... |
Accepted, folded | Scheme is case-insensitive by RFC 3986 |
agent://Example.COM/chat/llm_01h4... |
Accepted, folded | DNS is case-insensitive by RFC 4343 |
agent://example.com./chat/llm_01h4... |
Rejected | A trailing dot is a different spelling, not a case difference |
agent://example.com/Chat/llm_01h4... |
Rejected | Capability segments are lowercase identity material |
agent://example.com/chat/LLM_01H4... |
Rejected | Agent IDs are lowercase identity material |
agent://example.com/ch%61t/llm_01h4... |
Rejected | Percent-encoding appears in the grammar only in query parameter values |
Rules 3 and 4 took effect in version 0.5; earlier revisions normalized uppercase
input in those components. The 0.4 vectors are preserved as
test-vectors-v0.4.json so prior references stay checkable.
Example:
Input: AGENT://Anthropic.COM/assistant/chat/llm_01h455vb4pex5vsknk084sn02q?version=1.0#task
Output: agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q
To compare two agent URIs for equivalence:
- Parse both URIs according to Section 3.
- Apply normalization rules from Section 5.1.
- Compare the resulting strings byte-by-byte.
- URIs are equivalent if and only if the comparison returns equal.
Implementations MUST use the canonical form for:
- DHT key derivation
- Attestation subject matching
- Cache keys
- Audit log references
Every DHT key in this specification is a SHA-256 digest, and all of them are computed over one canonical input string:
path_input = canonical(trust_root) || "/" || canonical(capability_path)
The key naming a capability path is the digest of that string on its own:
capability_key = SHA-256(path_input)
Properties:
-
Trust-root scoping: Different trust roots produce different keys for the same capability path, preventing cross-organization pollution.
-
Deterministic lookup: Any node can compute the key and query directly without metadata lookup.
Example:
Trust root: anthropic.com
Capability path: assistant/chat
Input string: "anthropic.com/assistant/chat"
Key: SHA-256("anthropic.com/assistant/chat")
= ee7f343128163eec1164fb5afc0a019df215fc73decb14bc58fef1a4966e8262
A store that bounds the size of one stored value cannot hold a capability path's
subtree at capability_key; Section 6.2
requirement 6 gives the measured limit. A backend on such a store spreads a
capability path over a family of keys instead, derived from the same
path_input under distinct domain separators. Each separator below is the
literal ASCII string shown followed by one 0x00 octet.
| Key | Holds | Derivation |
|---|---|---|
| Identity | one agent's registration | SHA-256("agent-uri/dht/identity/v1" 0x00 || canonical(agent_uri)) |
| Shard descriptor | a path's shard level | SHA-256("agent-uri/dht/shard-descriptor/v1" 0x00 || path_input) |
Pointer page n |
pointers beneath a path | SHA-256("agent-uri/dht/shard-page/v1" 0x00 || path_input || "#" || uint32be(n)) |
A publisher's page index at shard level L is derived from its Agent ID:
placement = uint32be(SHA-256("agent-uri/dht/shard-placement/v1" 0x00 || agent_id)[0..4])
page_for(agent_id, L) = placement AND (2^L - 1)
Requirements:
-
A sharded backend MUST use these derivations byte for byte. Two implementations that disagree on a key do not merely perform differently; they cannot see each other's registrations.
-
Domain separation is REQUIRED, not an optimization. Without it a pointer page and a registration can be made to land on the same key, and a node handed a value has nothing to tell it which of the two the value was written as.
-
The identity key MUST be derived from the canonical agent URI per Section 5.1. A reference carrying a query string or a fragment names a view of an agent rather than another agent, and MUST resolve to the same record.
-
Placement MUST be computed from the Agent ID alone and not from the full URI. An agent then occupies the same page index at its exact path and at every ancestor path, so a reader fanning out over one level sees each agent exactly once per path.
-
The page count MUST be a power of two and placement MUST be by bitwise mask. This is normative rather than an implementation preference. Under
page = placement mod Pfor arbitraryP, raisingPmoves nearly every publisher to a different page, and every pointer already written becomes unreadable until its publisher happens to rewrite it. Under a mask, raisingLby one splits each page in two: an agent either stays on the page it was on or moves to a page index that did not exist at the lower level, and a reader at the higher level visits both. Growth is therefore backward compatible. A specification that said only "spread overPpages" would admit an implementation that silently loses registrations every time a path grows. -
Implementations MUST impose a maximum shard level; 16 is RECOMMENDED, which is 65 536 pages under one path. A shard descriptor is unauthenticated (requirement 14 of Section 6.2), so without a cap a forged level directs every reader of that path to derive an unbounded number of keys.
-
A reader SHOULD bound the number of keys one lookup reads, independently of the level it read. The cap in requirement 6 bounds a forged descriptor; a per-lookup budget bounds the cost of an honest but very wide path.
An agent registers by publishing its record so that both an exact lookup by URI and a prefix lookup by capability path can find it. Two record models satisfy that, and which one applies is a property of the store rather than a free choice.
Direct model. Where one store holds the namespace and a stored value has no
practical size bound, as in an in-process index or a database-backed registry,
the registration is written at the capability_key for its exact capability path
and at the key for every ancestor path. This ancestor-key materialization makes
a prefix query one ordinary exact-key lookup.
Sharded model. Where the store bounds the size of one value, which is every Kademlia overlay, the registration is written once at its identity key, and each ancestor key holds pointers to it spread over pages (Section 6.1.1). A prefix query reads the path's descriptor, then its pages, then the registrations the pointers name.
The registration record is the same in both:
Registration {
agent_uri: AgentUri, // Full agent URI
agent_key: PublicKey, // Ed25519 key authorized to write this record
endpoints: Vec<Endpoint>, // Current network endpoints
attestation: Option<String>, // PASETO token
expires_at: Timestamp, // Registration TTL
registered_at: Timestamp, // Creation time; identifies the record instance
sequence: u64, // Position in this record's write history
}The sharded model adds two auxiliary records. Neither carries authority, and neither is signed:
Pointer {
agent_uri: AgentUri, // The agent this page points at
expires_at: Timestamp, // Not later than that agent's own expiry
}
ShardDescriptor {
level: u8, // The path's pointers occupy 2^level pages
}Requirements:
- Agents MUST register at the capability path encoded in their URI.
- Registration MUST include at least one endpoint.
- Registration MUST include a valid attestation token covering the URI path.
- DHT nodes MUST verify attestations before storing records.
- In the direct model, an implementation MUST write the record atomically to the exact key and all ancestor keys. In the sharded model there is one authoritative copy, at the identity key, and the ancestor keys hold pointers that are written and expire independently of it; an implementation MUST tolerate transient divergence between them. This specification does not require cross-node multi-key atomicity in either model.
- Registration write amplification is O(d), where d is path depth. A capability
key cannot hold its subtree wherever the store bounds the size of one value,
and that bound belongs to the protocol rather than to the deployment: on
libp2p-kadthe limit is 16 KiB per record, which this specification's reference workspace measured at 1 to 27 registrations. No amount of provisioning moves it, because the limit is enforced by the nodes that store the record and not by the one that writes it. A deployment on such a store MUST use the sharded model. - Registration MUST name an Ed25519
agent_key. That key, and only that key, is authorized to write the record. - Registration MUST carry a mutation proof over the record as submitted, and DHT nodes MUST verify it. The attestation says a trust root vouched for a key; the proof says the registering party holds it.
- Where an attestation is required, DHT nodes MUST reject a registration whose
agent_keydiffers from the attestation'sagent_keyclaim. A token attests one agent's key and cannot be presented for another. - An agent that has previously held this URI SHOULD open the new record at a
sequenceabove every sequence it has ever signed for that URI. See Section 6.6. - In the sharded model, a registration MUST be published at its identity key,
and MUST place a pointer to itself on page
page_for(agent_id, L)at its exact capability path and at every ancestor path, whereLis the shard level read from that path's descriptor. - A pointer's
expires_atMUST NOT be later than that of the registration it names. A stale pointer costs a reader one wasted dereference; one that outlives its registration costs that dereference on every lookup until it expires. - A publisher that finds a page at or above its capacity MAY raise that path's shard level. A raise MUST be by exactly one level, and an implementation MUST NOT lower a level it reads. Growth is opportunistic: a path over capacity keeps accepting pointers until some publisher widens it.
- Pointers and shard descriptors are unauthenticated, and implementations MUST NOT treat either as evidence of anything about an agent. See requirement 3 of Section 6.3 and Section 8.10.
- A node that bounds how many registrations a key may hold MUST charge a registration against the key derived from its own capability path, and MUST NOT refuse it for the occupancy of an ancestor key. In the direct model an ancestor holds its entire subtree, so charging it would make the population under a prefix decide whether an unrelated agent may register beneath it. See Section 8.4.
Exact lookup by agent URI is one read in either model: at the
capability_key for the agent's exact capability path in the direct model, at
the agent's identity key in the sharded model.
Prefix lookup, and exact lookup by capability path, proceed by model.
In the direct model:
-
Key derivation: Compute
capability_keyfrom trust root and capability path. -
DHT lookup: Perform one ordinary lookup at that key. Ancestor-key materialization makes the returned bucket the prefix subtree.
-
Result filtering: Verify attestations on returned records; filter by query parameters.
In the sharded model:
-
Descriptor read: Read the path's shard descriptor to learn its level
L. An absent descriptor means level 0, which is a single page. -
Page reads: Read pages
0through2^L - 1for that path and collect their pointers, discarding expired ones. -
Dereference: Read the identity record named by each remaining pointer.
-
Result filtering: Verify attestations and mutation proofs on returned records; discard any record whose own capability path does not satisfy the query; filter by query parameters.
Requirements:
-
A reader MUST verify each returned record independently. Being handed a record by the overlay makes it neither authentic nor current.
-
Where several copies of one record are returned, a reader MUST resolve them by the ordering in Section 6.6, the greatest
(registered_at, sequence), and MUST NOT prefer whichever copy arrived last. Replication and caching decide arrival order; they say nothing about which copy is newer. -
A reader MUST decide whether a record satisfies the query from the record's own
agent_uri, and MUST NOT infer it from the page the pointer was found on. Nothing signs a pointer, so any page can name any URI. -
A reader that truncates a lookup against a key budget (requirement 7 of Section 6.1.1) MUST report the result as incomplete. An empty page of results and an exhausted budget are different answers.
Prefix Matching (direct model):
A query for /workflow/approval reads only the depth-2 key:
Register: /workflow/approval/invoice
Writes: SHA-256("acme.com/workflow")
SHA-256("acme.com/workflow/approval")
SHA-256("acme.com/workflow/approval/invoice")
Query: /workflow/approval
Reads: SHA-256("acme.com/workflow/approval")
Prefix Matching (sharded model):
The same registration writes one record and three pointers, and the same query reads a descriptor, that path's pages, and one identity key per pointer:
Register: agent://acme.com/workflow/approval/invoice/llm_01h4...
Writes: identity key for the canonical URI (the record)
page_for(llm_01h4..., L) at "acme.com/workflow" (a pointer)
page_for(llm_01h4..., L) at "acme.com/workflow/approval" (a pointer)
page_for(llm_01h4..., L) at "acme.com/workflow/approval/invoice"
Query: /workflow/approval
Reads: descriptor key for "acme.com/workflow/approval" -> L
page keys 0..2^L for "acme.com/workflow/approval" -> pointers
identity key per surviving pointer -> records
Under the standard Kademlia routing-table and connectivity assumptions, one exact-key lookup is expected to require O(log N) overlay hops, where N is the number of DHT nodes. Every read named below is one exact-key lookup and carries that cost. What differs between operations is how many reads each one takes:
| Operation | Direct model | Sharded model |
|---|---|---|
| Exact lookup by agent URI | 1 read | 1 read |
| Exact lookup by capability path | 1 read | 1 + 2^L + m reads |
| Prefix lookup | 1 read | 1 + 2^L + m reads |
L is the queried path's shard level and m is the number of unexpired
pointers found. Result transfer remains proportional to the number and size of
returned records.
Two consequences are worth stating plainly. In the sharded model, exact lookup
by capability path costs exactly what a prefix lookup costs: both read the
same pointer pages, and they differ only in a local filter on the dereferenced
record's URI. What remains a single read is exact lookup by agent URI, which
is what a cached agent:// reference resolves through.
The 2^L page reads do not depend on each other and MAY be issued
concurrently, as MAY the m dereferences, so the expected latency of a sharded
lookup is nearer three sequential round trips than 1 + 2^L + m of them.
An implementation MAY cache a path's shard descriptor, which removes the first of those three from repeated lookups under one path. The cost of a stale cached level falls on readers and not on publishers, and it is coverage rather than correctness: a reader working from a level below the current one reads a subset of the path's pages, and misses any agent whose pointer landed on a page the higher level added. The cache lifetime bounds that window. A publisher working from a stale level is unaffected, because the page it writes to is one a higher-level reader still visits.
Corollary: Resolution cost is independent of migration history. An agent that has migrated 100 times has the same resolution cost as one that never migrated.
Propagation time after an update depends on the concrete DHT's replication, retry, churn, and cache policies. This specification does not claim a fixed upper bound.
Agent migration updates only the DHT record; the URI remains stable:
- Agent reads its current record to learn its
registered_atandsequence. - Agent signs a mutation proof over the new endpoint(s) at the next sequence.
- Agent submits the update with that proof; nodes verify it per Section 6.6.
- Kademlia replicates to k closest nodes.
- After propagation, lookups return new endpoint.
- Cached references continue to resolve correctly.
The agent's identity (URI) does not change for endpoint migration. A trust-root or capability-path change creates a new identity and requires a new Agent ID and attestation.
The record model does not change this sequence. In the sharded model a migration rewrites the identity record and nothing else: a pointer names the agent's URI and not its endpoints, so no ancestor key is touched and the write stays one record regardless of path depth.
Registration records are world-readable, so an agent URI identifies a record
but authorizes nothing. Every write that modifies or removes an existing
record MUST carry a mutation proof: an Ed25519 signature, made by the
record's agent_key, over the operation being requested.
Signed payload. The proof signs a domain-separated, length-prefixed encoding of:
| Field | Purpose |
|---|---|
| Domain separator | Prevents a signature minted elsewhere from authorizing a DHT write |
| Operation kind | Prevents a refresh proof from acting as a deregistration, or a registration proof from acting as either |
registered_at |
Identifies the record instance |
sequence |
Identifies the position in that record's history |
agent_uri |
Binds the proof to one record |
| Operation arguments | Binds the proof to the endpoints or TTL requested |
Resulting expires_at |
Binds the proof to the record the write produces, not only to the request that produced it |
For registration, which creates the record rather than changing one, the proof
covers the record as submitted: its agent_key, endpoints, and expires_at,
at the registered_at and sequence the record opens with.
Requirements:
- DHT nodes MUST reject a write whose proof does not verify under the record's
agent_key: the stored record's for a modification or removal, the submitted record's for a registration. - DHT nodes MUST reject a write whose
sequencedoes not exceed the stored record'ssequence, and MUST record the acceptedsequenceon every ancestor-key copy. - The signature MUST be checked before the sequence. Reporting a sequence mismatch for an unsigned write discloses the record's position to a party that has proven nothing.
- The encoding MUST be injective: every variable-length field length-prefixed, so that no two distinct operations produce the same signed bytes.
registered_atMUST NOT change over a record's lifetime. In particular, refreshing a registration extendsexpires_atonly.- The proof MUST cover the
expires_atthe write results in, and a node MUST store that value rather than one it derives itself. A refreshing agent therefore states the instant its record will expire, not only the lifetime it asked for, because only the signer knows when it signed.
Rationale. The sequence number orders writes within one record's life, so a
captured proof cannot be applied twice. The registration time identifies the
record instance, so a proof captured before a deregistration cannot reach the
record that replaces it. Two registrations of the same URI within the
resolution of registered_at share an instance identity; requirement 10 of
Section 6.2 removes that dependence on the clock
for agents that retain their sequence across re-registration.
Requirement 6 exists because a record that travels between nodes travels whole.
A field outside the signature is a field any node on the path may rewrite, and
expires_at is the field that decides how long a record outlives the agent's
intent to be found. Signing only the requested TTL, and letting each node
compute an instant from it, would leave an observer of a legitimate migration
free to republish the agent's own record with an expiry of their choosing.
Shortening one is the denial of service
Section 8.1 already scopes as a residual risk;
lengthening one holds a genuine, agent-signed record open past the
moment the agent chose to let it lapse. Requirement 6 makes both a signature
failure. It is also why a node that finds an expiry outside its acceptable
window MUST refuse the record rather than clamp it: clamping rewrites bytes the
signature covers, and the record would then fail to verify at the next hop.
Attestation tokens use PASETO v4.public (Ed25519 signatures):
v4.public.<payload>[.<footer>]
Claims:
| Claim | Type | Required | Description |
|---|---|---|---|
jti |
string | REQUIRED | Unique token identifier |
iss |
string | REQUIRED | Issuing trust root |
agent_uri |
string | REQUIRED | Canonical Agent URI being attested |
agent_key |
string | REQUIRED | Agent's own Ed25519 public key, base64-encoded |
iat |
datetime | REQUIRED | Issued-at timestamp |
exp |
datetime | REQUIRED | Expiration timestamp |
aud |
string | OPTIONAL | Audience restriction |
capabilities |
string[] | REQUIRED | Authorized capability paths |
Example Claims:
{
"jti": "01h455vb4pex5vsknk084sn02q",
"iss": "acme.com",
"agent_uri": "agent://acme.com/workflow/approval/invoice/rule_01h455vb4pex5vsknk084sn02q",
"agent_key": "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
"iat": "2026-01-20T00:00:00Z",
"exp": "2026-02-19T00:00:00Z",
"capabilities": ["workflow/approval/invoice"]
}Token Identifier:
The jti claim names one token. Two attestations of the same URI with the same
capabilities are otherwise indistinguishable, which means neither can be spoken
about: not in a log, not in an incident report, and not in the revocation list
Section 8.2 requires verifiers to consult. A
signature cannot be withdrawn, so an unnameable token is one that must be
honoured until it expires.
- Trust roots MUST include
jtiin every issued token. - The value MUST be unique per token. It SHOULD be a UUIDv7 rendered as a
26-character Crockford base32 string, which is the same encoding an
agent-idsuffix uses (Section 3.5); reusing it means an implementation needs one alphabet rather than two, and the v7 timestamp makes identifiers sort by issuance. - Verifiers MUST reject a token with no
jti. Treating it as optional would place every such token permanently beyond the reach of revocation.
Agent Key Binding:
The agent_key claim names the agent's own key, distinct from the trust root's
signing key. Without it a token would be a bearer credential: registration
records are world-readable and carry their token inline, so any lookup would
hand the reader a credential naming a URI and its capabilities with nothing
about who is entitled to present it.
- Trust roots MUST include
agent_keyin every issued token. - Trust roots SHOULD attest a key only after the agent has demonstrated possession of the matching private key. Attesting a key that was merely supplied vouches for whoever supplied it.
- Verifiers MUST reject a token whose
agent_keydoes not decode to a valid Ed25519 public key. Treating an unreadable key as absent would restore the bearer-credential behaviour this claim removes.
Trust roots MUST publish verification keys at a well-known endpoint:
GET https://{trust-root}/.well-known/agent-keys.json
Response Format:
{
"trust_root": "acme.com",
"keys": [{
"kid": "key-2026-01",
"algorithm": "Ed25519",
"public_key": "<base64-encoded public key>",
"not_before": "2026-01-01T00:00:00Z",
"not_after": "2027-01-01T00:00:00Z"
}],
"revoked_keys": [{
"public_key": "<base64-encoded public key>",
"kid": "key-2025-07"
}]
}Requirements:
- Trust roots MUST serve this endpoint over HTTPS.
- Multiple keys MAY be published for rotation.
- Keys MUST include validity periods (
not_before,not_after). - Revoked keys SHOULD be listed in
revoked_keys. Each entry MUST carrypublic_key, and MAY carry thekidthe key had while it was published. It is the key material that a verifier can act on: a revocation is checked against the key that signed the token, and a verifier that has already dropped the key it is being told about cannot turn a name back into one. - Verifiers MUST reject a token whose signing key is outside its published
validity period, evaluated against the verifier's current time rather than
against the token's
iat. Evaluating againstiatwould makenot_afteradvisory: a key withdrawn at noon would keep producing acceptable tokens for as long as anything it signed beforehand had left to live. - Verifiers MAY apply their clock-skew tolerance to both bounds, for the same
reason Section 7.4 applies it to
iatandexp. - A verifier MUST reject a document that names a
trust_rootother than the one whose endpoint served it, and MUST NOT follow a redirect away from that endpoint. What makes a document evidence at all is the authority it was fetched from; without both of these, any authority could publish keys for anybody's namespace. - A verifier MUST bound what it will read from the endpoint, and SHOULD treat an unreachable endpoint as an absence of keys rather than as a reason to accept anything.
What discovery does and does not establish:
Fetching a document establishes which keys an authority stands behind. It does
not establish that the authority should be trusted. A verifier that fetches
evil.example's keys learns exactly which keys evil.example signs with, and
nothing about whether to believe what it attests. Which trust roots matter is a
deployment decision, and this endpoint does not make it.
Rotation:
Requirement 5 is what makes the overlap in a published key list load-bearing
rather than decorative. A rotation that is safe for tokens already in flight
publishes the incoming key's not_before at or before the outgoing key's
not_after, with the gap between them at least as long as the longest token
lifetime the root issues:
outgoing: not_after = T + longest_ttl
incoming: not_before = T
Every token signed before T expires by T + longest_ttl, so no token is ever
refused for naming a key that was current when it was minted. Trust roots
SHOULD retain a rotated-out key for that interval and SHOULD then remove it, so
that the number of keys a verifier must try stays bounded by the rotation
schedule rather than by the root's history.
A validity period is not revocation. It is a schedule, published in advance, and a verifier that holds the key already holds the schedule. Revocation (Section 8.2) is unplanned news that has to reach the verifier. A compromised key needs both: removal from what the root publishes, and an entry on the revocation list for every verifier still holding a stale copy of the key document.
Every attested capability MUST first be constrained to the subject identity:
scoped(c, agent_uri) := c == uri_path || c.starts_with(uri_path + "/")
Within that scope, a grant may cover an equal or narrower requested operation:
covered(path, capabilities) := ∃c ∈ capabilities : path.starts_with(c)
Example:
For subject path /workflow/approval, an attestation with
capabilities: ["workflow/approval"] covers:
/workflow/approval(exact match)/workflow/approval/invoice(descendant)
But NOT:
/workflow(broader than the identity)/workflow/review(sibling)/financial(unrelated)
Complete verification of an agent presenting URI and attestation:
- Parse agent URI; extract
trust_root,capability_path,agent_id. - Fetch/cache verification key from trust root's well-known endpoint.
- Verify PASETO signature using the key, and reject if that key is outside the validity period the trust root published for it (Section 7.2).
- Check
exp> current time (not expired). - Check
iss==trust_rootfrom URI. - Check
agent_uri== the canonical full agent URI. - Check
agent_keydecodes to a valid Ed25519 public key. - Check
jtiis present, and that neither it nor the verifying key appears in the revocation list of Section 8.2. This step MUST follow step 3:jtiandissare claims, so consulting a list by them before the signature is checked lets the presenter choose which entry is looked up, and therefore choose one that is not listed. - Check every capability equals the URI path or is its descendant.
- Check at least one capability covers the requested operation or registration path.
- If
audis present, require an explicit, exact verifier audience match. - Where the presenter claims to be the agent, require proof of possession of
agent_key. Steps 1 to 11 authenticate the token; only this authenticates the presenter. For registration that proof is the mutation proof of Section 6.6.
All checks MUST pass. Failure at any step MUST reject the attestation.
A verifier that has no revocation list available cannot perform step 8, and MUST therefore reject rather than proceed. Accepting a token with a required check skipped would report it as verified on evidence that was never gathered; a deployment that does not revoke states that explicitly instead, which makes the choice auditable rather than indistinguishable from an oversight.
The optional aud claim restricts attestation validity to specific verifiers:
When to Use:
- High-value transactions (financial approvals, contract signing)
- Sensitive data access (personal information, trade secrets)
- Compliance-driven interactions requiring specific authorization
Verification Behavior:
- If
audis present, the verifier MUST match. - If
audis absent, any verifier MAY accept (subject to other checks).
Multiple Parties:
Agents interacting with multiple specific parties MAY hold multiple attestations with different aud values.
Threat: An adversary controlling nodes surrounding a capability key could return false registration records or suppress legitimate ones.
Mitigations:
-
Multi-path verification: Query from diverse network positions; consistent results across paths indicate authenticity.
-
Attestation verification: Signature verification against published keys rejects fraudulent attestations even if DHT returns attacker-controlled records.
-
Kademlia redundancy: Records are stored on k closest nodes; eclipsing requires controlling a significant fraction of the network.
Residual Risk: DHT manipulation can cause denial of service (hiding legitimate agents) but cannot cause acceptance of unauthorized agents.
Threat: A compromised signing key enables issuing fraudulent attestations for arbitrary agents under that trust root.
Mitigations:
-
Key revocation: Trust roots publish
revoked_keyslist; verifiers MUST check before accepting. -
Time-bounded attestations: The
expclaim limits blast radius. -
Key rotation with overlap: Rotate periodically with overlapping validity windows.
-
Hardware security modules: Protect signing keys with HSMs.
Revocation has two granularities, and both are needed:
| Revoke | By | Effect |
|---|---|---|
| One token | jti (Section 7.1) |
That attestation stops being honoured; everything else the root issued is unaffected |
| One key | The trust root's public key | Every token that key ever signed stops being honoured, including tokens never individually listed |
Per-token revocation is the tool for a grant issued in error. It is not
sufficient for compromise: an attacker holding a stolen signing key mints tokens
whose jti values the trust root has never seen, so a list of known token
identifiers can only ever name the attestations that were already known about.
Key revocation is what bounds that, because it does not require enumerating what
it refuses.
Requirements:
- Verifiers MUST check both a token's
jtiand the key that verified its signature against the revocation list before accepting. - Verifiers MUST perform this check after signature verification.
jtiandissare claims; consulting a list by them beforehand lets the presenter choose which entry is looked up. - A verifier with no revocation list available MUST reject. See Section 7.4.
- Where both a token and its signing key are revoked, a verifier SHOULD report the key. It is the larger fact, and a caller told only that one token was withdrawn may reasonably request another from the same root — which is precisely what a compromised key benefits from.
Residual Risk: Revocation is only as current as the list a verifier holds.
Between a compromise and the list reaching every verifier, tokens minted with
the stolen key are honoured. The exp claim bounds that window from the other
end, which is why the default token lifetime is short.
Scope Limitation: Compromise affects only the compromised trust root's agents. Cross-trust-root isolation prevents lateral movement.
Threat: An attacker claims a trust root domain they don't control.
Mitigation: Verifiers MUST fetch keys from the trust root's well-known endpoint over HTTPS. DNS and TLS provide domain authentication.
Threat: Early registrants claim broad capability paths, blocking legitimate agents.
Mitigation: Trust roots SHOULD implement governance for their namespace. DHT nodes MAY enforce attestation requirements before accepting registrations.
Subtree lockout: A node that bounds a key's occupancy turns squatting into something cheaper and broader if it charges a registration against ancestor keys. In the direct model an ancestor holds its whole subtree, so filling one shallow prefix refuses every path beneath it, including paths nobody has claimed and paths the squatter never named. The attacker does not have to guess which capability a competitor will want; a top-level prefix covers all of them. Requirement 15 of Section 6.2 forbids the accounting that allows this.
Threat: DHT queries reveal requester interest in specific capabilities.
Trade-offs:
| Approach | Privacy | Latency | Bandwidth |
|---|---|---|---|
| Onion routing | Strong | Higher | Normal |
| Query batching | Moderate | Normal | Higher |
| Local caching | Weak | Lower | Lower |
Deployments requiring query privacy SHOULD consider private information retrieval techniques.
Threat: Prefix-based discovery enables capability enumeration.
Mitigation: Trust roots MAY restrict prefix queries to authorized requesters.
Note on the sharded model: pointer pages list a path's agent URIs directly
and can be read without dereferencing anything, so enumeration there costs a
descriptor read and 2^L page reads. A deployment that treated the cost of
enumeration as a protection has less of one than it appears to.
Threat: An agent URI is public, so any party that can reach a storing node knows which record to write to. Without authorization on modification, that party can repoint an agent's endpoints at infrastructure it controls, or evict the agent entirely, without holding any key.
Mitigation: Every modification and removal carries a mutation proof signed
by the record's agent_key, per Section 6.6. The
proof covers the operation's arguments, so an intercepted migration cannot be
re-aimed at other endpoints while keeping the agent's signature.
Residual Risk: A captured proof that has already been applied cannot be applied again, but an adversary who suppresses a legitimate write can delay it. This is the denial of service described in Section 8.1, not a hijack: producing a record that points somewhere new requires a signature over those endpoints, which no capture supplies.
Threat: Registration records are world-readable and carry their attestation token inline, so any lookup returns one. A token that named only a URI and its capabilities would be a bearer credential: whoever read a record could re-register that URI, pointing at their own endpoints, for as long as the token remained valid.
Mitigations:
-
Agent key binding: the
agent_keyclaim names the key the trust root vouched for, and DHT nodes reject a registration whose record names a different one. A lifted token can only produce a record naming its rightful agent's key. -
Proof of possession: registration carries a mutation proof signed by that key, so keeping the rightful key in the record does not help either.
-
Endpoint binding in the proof, not the claims: the proof covers the endpoints, so a lifted token cannot be paired with substituted ones. Binding endpoints into the attestation instead would force re-issuance from the trust root on every migration, which Section 6.5 exists to avoid.
Residual Risk: A trust root that attests a key without first seeing the agent prove possession of it vouches for whoever supplied that key. The binding is only as good as the enrolment that precedes it.
Threat: In a multi-root deployment, a verifier trusts several trust roots. A valid signing key for one authority (e.g. marketing.acme.com) mints an attestation whose agent_uri is rooted at a different authority (e.g. finance.acme.com). Authenticating iss alone proves only which trusted key signed the token, not that the signer owns the attested namespace.
Mitigation: Verifiers MUST reject an attestation whose iss claim differs from the trust root (authority) of the attested agent URI. If the attested URI's authority cannot be determined, verifiers MUST reject the attestation (fail closed). This binding makes iss authoritative for the URI's namespace and preserves the cross-trust-root isolation described in Section 8.2: a key compromise or misuse cannot forge attestations for agents under a different trust root.
Threat: In the sharded model (Section 6.1.1), pointer pages and shard descriptors carry no signature. There is nothing for one to be signed by: a page is a set contributed to by every agent beneath a path, and no single party owns it. Any party that can write to the overlay can therefore put any agent URI on any page, or raise any path's shard level.
Mitigations:
-
A pointer confers nothing. A reader dereferences every pointer to the named agent's own registration and decides from that record's
agent_uriwhether it satisfies the query, per requirement 3 of Section 6.3. An injected pointer buys the attacker one wasted read on someone else's machine. -
Level capping. A maximum shard level bounds how many keys a forged descriptor can make a reader derive, and a per-lookup key budget bounds it again independently.
-
Union merge. Pages merge by union rather than by replacement, so a write cannot remove a pointer another publisher placed.
Residual Risk: Pointer pages are a denial-of-service surface rather than an authenticity one. Filling a page with pointers to agents that do not exist makes every lookup under that path slower and can push a reader against its key budget, hiding legitimate agents in the way Section 8.1 describes. Raising a shard level is not reversible by the same means, because descriptors take the greater level: a path can be pushed wide and stays wide.
Threat: A trust root is ASCII, and this specification performs no IDNA processing. An implementation therefore cannot distinguish a domain from one that merely looks like it, and two spellings of one real-world name can be two identities or can be one identity and one error.
Three distinct consequences follow, and they are consequences of the same decision rather than three defects:
-
Conversion is the caller's. A non-ASCII name MUST be rejected, not converted:
münchen.deis an error and not a synonym forxn--mnchen-3ya.de. Performing UTS #46 ToASCII inside the identity type would place a Unicode version table on the identity path, where two implementations holding different table versions would map one name to two trust roots. Refusing is a smaller surface than disagreeing. -
An A-label is opaque. A label beginning
xn--is validated as a DNS label and no further: it is not decoded, and its Punycode is not checked.xn--zzzzzzis not valid Punycode and is a valid trust root under this specification. A name converted incorrectly upstream stays incorrect, and nothing downstream will say so. -
Confusables are not detected.
apple.comand the Cyrillic-аhomograph, whose A-label form isxn--pple-43d.com, are two unrelated trust roots. Only the A-label form can ever reach a parser, and at that point the two strings are not similar.
Mitigations:
-
Convert once, at the edge. An implementation that accepts names from people SHOULD apply UTS #46 ToASCII at the point of entry, before the name becomes an identity, and SHOULD carry the A-label form from there on. A name converted twice, or converted in two places by two versions, is the failure this ordering prevents.
-
Authentication does not depend on the name looking right. A verifier fetches keys from the trust root's own well-known endpoint over HTTPS (Section 7.2). A homograph resolves to its own DNS name and its own keys, so it can impersonate a familiar name to a reader but cannot present the imitated root's attestations.
-
Display is where confusability is decided. An implementation that shows a trust root to a person SHOULD apply a confusable-detection policy there — mixed-script restriction, or displaying the A-label form — rather than relying on the parser, which by then has only ASCII to look at.
Residual Risk: A user who is shown a homograph and chooses to trust it has
been deceived before any part of this specification is reached. Nothing in the
URI syntax, the DHT, or attestation detects it, because at every one of those
layers the two names are simply different. Note also that a trailing dot is
rejected rather than stripped (Section 4.1), so a
root-anchored example.com. is not a second spelling of example.com but an
invalid trust root; this removes an equivalence question at the cost of
rejecting a form that DNS accepts.
This specification requests registration of the "agent" URI scheme in the "Uniform Resource Identifier (URI) Schemes" registry.
| Field | Value |
|---|---|
| Scheme name | agent |
| Status | Permanent |
| Applications/protocols | Multi-agent systems, A2A protocol |
| Contact | rrrodzilla@proton.me |
| Change controller | Govcraft |
| Reference | This specification |
This specification requests registration of the following well-known URI:
| Field | Value |
|---|---|
| URI suffix | agent-keys.json |
| Change controller | Govcraft |
| Specification document | This specification, Section 7.2 |
| Related information | None |
-
[RFC 2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997.
-
[RFC 3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax", STD 66, RFC 3986, January 2005.
-
[RFC 5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", STD 68, RFC 5234, January 2008.
-
[RFC 9562] Peabody, B. and K. Davis, "Universally Unique Identifiers (UUIDs)", RFC 9562, May 2024.
-
[PASETO] Arciszewski, S., "Platform-Agnostic Security Tokens", https://paseto.io/, 2018.
-
[TypeID] Jetify, "TypeID Specification", https://github.com/jetify-com/typeid, 2023.
-
[Kademlia] Maymounkov, P. and D. Mazières, "Kademlia: A Peer-to-Peer Information System Based on the XOR Metric", IPTPS 2002.
-
[Saltzer1982] Saltzer, J., "On the Naming and Binding of Network Destinations", Proc. IEEE 70(9), 1982.
-
[FIPA] Foundation for Intelligent Physical Agents, "FIPA Agent Management Specification", SC00023K, 2004.
-
[A2A] Linux Foundation, "Agent-to-Agent (A2A) Protocol Specification", https://a2aprotocol.ai/, 2025.
-
[DIDs] Sporny, M., et al., "Decentralized Identifiers (DIDs) v1.0", W3C Recommendation, 2022.
-
[IDNA] Klensin, J., "Internationalized Domain Names in Applications (IDNA): Protocol", RFC 5891, August 2010.
-
[UTS46] Davis, M. and M. Suignard, "Unicode IDNA Compatibility Processing", Unicode Technical Standard #46, https://www.unicode.org/reports/tr46/.
; Agent URI Scheme - Complete ABNF Grammar
; RFC 5234 compliant
; ==========================================================================
; TOP-LEVEL RULE
; ==========================================================================
agent-uri = scheme "://" trust-root "/" capability-path "/" agent-id
[ "?" query ] [ "#" fragment ]
; Total length MUST NOT exceed 512 characters
scheme = "agent"
; ==========================================================================
; TRUST ROOT
; ==========================================================================
trust-root = host [ ":" port ]
; Maximum 128 characters
host = domain / ip-literal / ipv4-address
domain = label *( "." label )
; Maximum 253 characters (DNS limit)
label = 1*63( ALPHA / DIGIT / "-" )
; Labels cannot start or end with hyphen
ip-literal = "[" ipv6-address "]"
ipv4-address = dec-octet "." dec-octet "." dec-octet "." dec-octet
dec-octet = DIGIT ; 0-9
/ %x31-39 DIGIT ; 10-99
/ "1" 2DIGIT ; 100-199
/ "2" %x30-34 DIGIT ; 200-249
/ "25" %x30-35 ; 250-255
ipv6-address = 6( h16 ":" ) ls32
/ "::" 5( h16 ":" ) ls32
/ [ h16 ] "::" 4( h16 ":" ) ls32
/ [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
/ [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
/ [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
/ [ *4( h16 ":" ) h16 ] "::" ls32
/ [ *5( h16 ":" ) h16 ] "::" h16
/ [ *6( h16 ":" ) h16 ] "::"
h16 = 1*4HEXDIG
ls32 = ( h16 ":" h16 ) / ipv4-address
port = 1*5DIGIT
; 0-65535
; ==========================================================================
; CAPABILITY PATH
; ==========================================================================
capability-path = segment *( "/" segment )
; Maximum 256 characters total
; Maximum 32 segments
segment = 1*64( LOWER / DIGIT / "-" )
; Lowercase only; uppercase input is rejected
; ==========================================================================
; AGENT IDENTIFIER
; ==========================================================================
agent-id = prefix "_" suffix
prefix = LOWER *( LOWER / "_" ) LOWER / LOWER
; Must start and end with ALPHA
; Lowercase only
suffix = first-char 25base32char
; Encodes UUIDv7 (128 bits) in 26 base32 characters
; 3 bits (first char) + 125 bits (25 chars × 5) = 128 bits
first-char = "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7"
; First character limited to 0-7 (3 bits max value)
; Ensures encoded value fits in 128 bits
base32char = DIGIT / "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h"
/ "j" / "k" / "m" / "n" / "p" / "q" / "r" / "s" / "t"
/ "v" / "w" / "x" / "y" / "z"
; Crockford Base32 alphabet (lowercase canonical form)
; Excludes: i, l, o, u (visually ambiguous)
; ==========================================================================
; QUERY AND FRAGMENT
; ==========================================================================
query = *( pchar / "/" / "?" )
fragment = *( pchar / "/" / "?" )
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded = "%" HEXDIG HEXDIG
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
; ==========================================================================
; CORE RULES (RFC 5234 Appendix B)
; ==========================================================================
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
DIGIT = %x30-39 ; 0-9
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
/ "a" / "b" / "c" / "d" / "e" / "f"# Minimal valid URI
Input: agent://a.co/x/llm_01h455vb4pex5vsknk084sn02q
Status: VALID
Trust root: a.co
Capability path: x
Agent ID: llm_01h455vb4pex5vsknk084sn02q
# Typical production URI
Input: agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q
Status: VALID
Trust root: anthropic.com
Capability path: assistant/chat
Agent ID: llm_chat_01h455vb4pex5vsknk084sn02q
# Deep capability path
Input: agent://acme.corp/workflow/approval/invoice/high-value/rule_fsm_01h5fskfsk4fpeqwnsyz5hj55t
Status: VALID
Trust root: acme.corp
Capability path: workflow/approval/invoice/high-value
Agent ID: rule_fsm_01h5fskfsk4fpeqwnsyz5hj55t
# With port
Input: agent://localhost:8472/debug/test/llm_01h455vb4pex5vsknk084sn02q
Status: VALID
Trust root: localhost:8472
Capability path: debug/test
Agent ID: llm_01h455vb4pex5vsknk084sn02q
# IPv4 address
Input: agent://192.168.1.1:8080/internal/agent_01h455vb4pex5vsknk084sn02q
Status: VALID
Trust root: 192.168.1.1:8080
Capability path: internal
Agent ID: agent_01h455vb4pex5vsknk084sn02q
# IPv6 address
Input: agent://[::1]:8472/debug/llm_01h455vb4pex5vsknk084sn02q
Status: VALID
Trust root: [::1]:8472
Capability path: debug
Agent ID: llm_01h455vb4pex5vsknk084sn02q
# With query and fragment (stripped for identity)
Input: agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0#streaming
Status: VALID
Trust root: anthropic.com
Capability path: assistant/chat
Agent ID: llm_01h455vb4pex5vsknk084sn02q
Canonical: agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q
# Wrong scheme
Input: http://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q
Status: INVALID
Reason: Scheme must be "agent"
# Missing agent ID
Input: agent://anthropic.com/assistant/chat
Status: INVALID
Reason: Missing agent identifier
# Empty path segment
Input: agent://anthropic.com//chat/llm_01h455vb4pex5vsknk084sn02q
Status: INVALID
Reason: Empty path segment not allowed
# Invalid agent ID (wrong suffix length)
Input: agent://anthropic.com/chat/llm_01h455vb4pex
Status: INVALID
Reason: Agent ID suffix must be exactly 26 characters
# Invalid agent ID (missing prefix)
Input: agent://anthropic.com/chat/01h455vb4pex5vsknk084sn02q
Status: INVALID
Reason: Agent ID must have prefix followed by underscore
# Trust root too long (>128 chars)
Input: agent://a]
Status: INVALID
Reason: Trust root exceeds 128 character limit
# Uppercase in capability path
Input: agent://anthropic.com/Assistant/Chat/llm_01h455vb4pex5vsknk084sn02q
Status: INVALID
Reason: Capability segments are lowercase by grammar
# Authority normalization
URI A: agent://Anthropic.COM/assistant/chat/llm_01h455vb4pex5vsknk084sn02q
URI B: agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q
Equivalent: YES
Canonical: agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q
# Query string stripped
URI A: agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=1.0
URI B: agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0
Equivalent: YES
Canonical: agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q
# Fragment stripped
URI A: agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#task1
URI B: agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#task2
Equivalent: YES
Canonical: agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q
# Different agents (not equivalent)
URI A: agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q
URI B: agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02r
Equivalent: NO
# Basic derivation
Trust root: anthropic.com
Capability path: assistant/chat
Input: "anthropic.com/assistant/chat"
capability_key = ee7f343128163eec1164fb5afc0a019df215fc73decb14bc58fef1a4966e8262
# Trust-root scoping (different keys)
Input A: "anthropic.com/assistant/chat"
= ee7f343128163eec1164fb5afc0a019df215fc73decb14bc58fef1a4966e8262
Input B: "openai.com/assistant/chat"
= c5a97797f98cc507b8604ebd16a27071e87056b047c8f2625182287d14b31f53
# Prefix key derivation
Trust root: acme.com
Path: workflow/approval/invoice
Keys at depths:
Depth 1: SHA-256("acme.com/workflow")
= 16889f14c0da9c42cae8063d495e33b4fa1b12cabfdd019c1491b217a56c857a
Depth 2: SHA-256("acme.com/workflow/approval")
= b15b22d3c95b3091743a071ed616d9715038a7afd559a7dc28f3d7a1f9eec03e
Depth 3: SHA-256("acme.com/workflow/approval/invoice")
= d9786664a610a9aaa2799a65c6bd3f9baa44a067f7511cb179c63041021f25f2
Every separator below is the ASCII string shown followed by one 0x00 octet.
See Section 6.1.1.
# Identity key (from the canonical URI, so a query or fragment does not move it)
URI: agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q
Input: "agent-uri/dht/identity/v1" 0x00 || that URI
Key: 90a4a81f9c9170054bb24aa43c96d6f228a4af6eb494af2deb9b01fe342f84e0
# Shard descriptor key
Input: "agent-uri/dht/shard-descriptor/v1" 0x00 || "anthropic.com/assistant/chat"
Key: d40a062c6be6ecfdec023bb8b2de9c33ce438b1e1f9a3a1f5810dd692daa6d48
# Pointer page keys
Input: "agent-uri/dht/shard-page/v1" 0x00 || "anthropic.com/assistant/chat"
|| "#" || uint32be(0)
Key: 5f5cb6dbffabc05bcb29216212b86c4b1372881218406df99aeaebd944a81494
Input: ... || uint32be(1)
Key: 53e0301a0584471036c44b3a6fcc5c67d7c0308498a062f83dc1f7e51a29e5ea
# All four kinds of key for one path are distinct
capability_key != identity != descriptor != page (domain separators differ)
# Placement, from the Agent ID alone
Agent ID: llm_01h455vb4pex5vsknk084sn02q
Level 0: page 0 (one page; the mask is zero)
Level 1: page 1
Level 2: page 1
Level 3: page 1
Level 4: page 9
# Growth is backward compatible: raising the level either leaves an agent where
# it was or moves it to page (old + 2^L_old), which a wider reader also visits.
# Here, level 3 -> 4 moves this agent from page 1 to page 1 + 2^3 = 9.
# Exact match
Capabilities: ["workflow/approval"]
Path: workflow/approval
Covered: YES
# Descendant coverage within the URI identity path
Subject path: workflow/approval
Capabilities: ["workflow/approval"]
Requested path: workflow/approval/invoice
Covered: YES
# No coverage (sibling)
Capabilities: ["workflow/approval"]
Path: workflow/review
Covered: NO
# No coverage (partial string)
Capabilities: ["work"]
Path: workflow
Covered: NO
# Multiple capabilities (any covers)
Subject path: workflow/approval
Capabilities: ["workflow/approval/read", "workflow/approval/write"]
Requested path: workflow/approval/write/invoice
Covered: YES (second capability covers)
| Component | Min | Max | Notes |
|---|---|---|---|
| Total URI | 45 | 512 | Minimum assumes shortest valid URI |
| Scheme | 5 | 5 | Fixed: "agent" |
| Trust root | 4 | 128 | Minimum: "a.co" |
| Capability path | 1 | 256 | At least one segment required |
| Path segment | 1 | 64 | Per segment |
| Path segment count | 1 | 32 | Number of segments |
| Agent ID | 28 | 90 | prefix (1-63) + "_" + suffix (26) |
| Agent ID prefix | 1 | 63 | TypeID specification |
| Agent ID suffix | 26 | 26 | Fixed: UUIDv7 in Crockford Base32 |
| Component | Min | Max | Notes |
|---|---|---|---|
| Total token | — | 8192 | PASETO practical limit |
| Payload (decoded) | — | 4096 | Bytes after base64url decode |
| agent_uri | 45 | 512 | Per URI constraints |
| capabilities array | 1 | 64 | Recommended practical item count |
| Each capability | 1 | 256 | Same grammar and limit as URI capability path |
| issuer | 4 | 128 | Matches trust root limit |
| audience | 1 | 128 | Optional |
| Timestamp | — | 30 | ISO 8601 with milliseconds |
| Component | Size | Notes |
|---|---|---|
| DHT key | 256 bits | SHA-256 output, every kind |
| Stored value | Backend-dependent | 16 KiB on libp2p-kad; see §6.2 requirement 6 |
| Registration record | Variable | Depends on endpoint count and attestation size |
| Endpoint | Variable | URL length |
| Pointer | ~74 bytes | At typical URI length; ~494 at the URI ceiling |
| Pointers per 16 KiB page | ~220 | At typical URI length; ~33 at the URI ceiling |
| Shard descriptor | 1 byte | The level; the record is its own key's whole value |
| Shard level | 0 to 16 | 16 RECOMMENDED as the maximum, per §6.1.1 requirement 6 |
The pointer figures are measured against this specification's reference workspace and are informative, not normative. They are what makes the sharded model necessary rather than merely tidy: pointers cut the per-agent cost by roughly a factor of seven, which moves the ceiling but does not remove it. Only sharding removes it, because only sharding adds keys.
| Version | Date | Changes |
|---|---|---|
| 0.8.3 | 2026-07-31 | §4.3 clarified: the type_modifier_modifier pattern stated to be a naming convention rather than syntax, consecutive underscores confirmed well formed as in the TypeID specification, and an implementation exposing the class and modifiers separately required not to report a modifier for the empty run between them |
| 0.8.2 | 2026-07-31 | §7.2 given the shape of a revoked_keys entry, which was named but never defined, and required to carry key material rather than only a name; the verifier-side obligations discovery implies added: refuse a document naming another trust root, do not follow redirects away from the endpoint, bound what is read, and treat an unreachable endpoint as an absence of keys; what a fetched document does and does not establish stated outright |
| 0.8.1 | 2026-07-31 | §7.2 given the verifier-side obligation its published validity periods implied: a key outside its window MUST be refused, judged at verification time rather than against the token's iat, with the rotation overlap that keeps in-flight tokens working stated as a schedule; §7.4 step 3 amended to carry the same check, and the distinction between a published schedule and unplanned revocation made explicit |
| 0.8.0 | 2026-07-31 | jti added to §7.1 as a REQUIRED claim, since §8.2 revocation cannot name a token that has no identifier; §7.4 given an explicit revocation step, placed after signature verification so the presenter cannot choose which list entry is consulted, and a verifier without a revocation list required to reject rather than skip it; §8.2 expanded with the two granularities of revocation and why per-token listing alone cannot bound a key compromise |
| 0.7.1 | 2026-07-31 | Trust root stated to be ASCII, with conversion of internationalized names placed on whatever accepts them from a person and xn-- labels defined as opaque; the resulting confusability and mis-conversion exposure added as §8.11 |
| 0.7.0 | 2026-07-30 | Mutation proofs required to cover the expires_at the write results in, and nodes required to store that value rather than derive one, closing the rewritable expiry described in §6.6; per-key capacity required to be charged to the registering path's own key and not to an ancestor, with the resulting subtree lockout added to §8.4 |
| 0.6.0 | 2026-07-30 | Direct and sharded record models distinguished; sharded key derivation, pointer pages, and shard descriptors defined normatively; prefix lookup no longer claimed to be one exact-key read; §6.2 requirement 6 restated as a protocol ceiling rather than a provisioning matter; pointer injection added as §8.10; placeholder key vectors in B.4 replaced with computed digests |
| 0.5.2 | 2026-07-27 | Four dot-separated all-numeric host labels defined as an ipv4-address rather than a domain; hosts of that shape whose octets are outside 0-255 or carry a leading zero are rejected |
| 0.5.1 | 2026-07-27 | Query parameter percent-decoding defined as UTF-8 octet decoding; values whose decoded octets are not valid UTF-8 are rejected; serializing a query parameter value re-encodes non-unreserved octets as uppercase %XX |
| 0.5.0 | 2026-07-13 | Capability path made constitutive identity material; lowercase path and Agent ID inputs are rejected rather than normalized; URI-scoped capability claims and ancestor-key registration defined |
| 0.4.0 | 2026-01-20 | Initial draft specification |
End of Specification