agent_uri/lib.rs
1//! Parser and validator for the `agent://` URI scheme.
2//!
3//! This crate implements parsing, validation, and serialization of agent URIs
4//! as defined in the Agent Identity URI Scheme specification.
5//!
6//! # Overview
7//!
8//! Agent URIs provide topology-independent identity for agents with
9//! capability-based discovery. They have the structure:
10//!
11//! ```text
12//! agent://<trust-root>/<capability-path>/<agent-id>[?query][#fragment]
13//! ```
14//!
15//! # Quick Start
16//!
17//! ```rust
18//! use agent_uri::AgentUri;
19//!
20//! // Parse an agent URI
21//! let uri = AgentUri::parse(
22//! "agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q"
23//! ).unwrap();
24//!
25//! // Access components
26//! assert_eq!(uri.trust_root().host_str(), "anthropic.com");
27//! assert_eq!(uri.capability_path().as_str(), "assistant/chat");
28//! assert_eq!(uri.agent_id().prefix().as_str(), "llm_chat");
29//!
30//! // Create new agent IDs
31//! use agent_uri::AgentId;
32//! let id = AgentId::new("llm_chat");
33//! println!("New agent: {}", id);
34//!
35//! // `new` sanitizes a prefix the grammar refuses. Where the prefix did not
36//! // come from your own source, use `try_new`, which reports it instead.
37//! assert_eq!(AgentId::new("LLM Chat!").prefix().as_str(), "llmchat");
38//! assert!(AgentId::try_new("LLM Chat!").is_err());
39//! ```
40//!
41//! # Builder Pattern
42//!
43//! Use the typestate builder for compile-time enforced construction:
44//!
45//! ```rust
46//! use agent_uri::{AgentUriBuilder, TrustRoot, CapabilityPath, AgentId};
47//!
48//! let uri = AgentUriBuilder::new()
49//! .trust_root(TrustRoot::parse("anthropic.com").unwrap())
50//! .capability_path(CapabilityPath::parse("assistant/chat").unwrap())
51//! .agent_id(AgentId::new("llm_chat"))
52//! .build()
53//! .unwrap();
54//!
55//! assert_eq!(uri.trust_root().host_str(), "anthropic.com");
56//! ```
57//!
58//! # Length Constraints
59//!
60//! | Component | Max Length |
61//! |-----------|------------|
62//! | Total URI | 512 chars |
63//! | Trust root | 128 chars |
64//! | Capability path | 256 chars |
65//! | Path segments | 32 max count |
66//! | Each segment | 64 chars |
67//! | Agent ID prefix | 63 chars |
68//! | Agent ID suffix | 26 chars (fixed) |
69//!
70//! # Grammar Specification
71//!
72//! This crate implements the ABNF grammar defined in `grammar.abnf` at the crate root.
73//! The grammar follows RFC 5234 (ABNF) and specifies:
74//!
75//! - **URI structure**: `agent://<trust-root>/<capability-path>/<agent-id>[?query][#fragment]`
76//! - **Trust root**: Domain names, IPv4, and IPv6 addresses with optional ports
77//! - **Capability path**: Hierarchical path with 1-32 segments
78//! - **Agent ID**: `TypeID` format with semantic prefix and `UUIDv7` suffix
79//!
80//! See `grammar.abnf` for the complete formal specification.
81//!
82//! # Breaking changes in 0.6.0
83//!
84//! Two changes, neither of which affects a caller who only propagates errors.
85//!
86//! ## Error enums are `#[non_exhaustive]`
87//!
88//! An exhaustive `match` on [`ParseErrorKind`], [`TrustRootError`],
89//! [`CapabilityPathError`], [`PathSegmentError`], [`AgentIdError`],
90//! [`AgentPrefixError`], [`QueryError`], [`FragmentError`], [`BuilderError`],
91//! or [`TypeClassError`] now needs a `_` arm.
92//!
93//! A parser learns to be more specific over time: a rejection reported today as
94//! a generic bad character turns out to deserve a variant of its own, and
95//! naming it is an improvement a caller wants. On an exhaustive enum that
96//! improvement is a major version bump forever, so past 1.0 it stops happening
97//! and the errors stay vaguer than they need to be.
98//!
99//! The wildcard arm should say what to do about a variant this version does not
100//! recognise, rather than treat its absence as impossible:
101//!
102//! ```
103//! use agent_uri::{AgentUri, ParseErrorKind};
104//!
105//! let error = AgentUri::parse("").unwrap_err();
106//! let advice = match error.kind {
107//! ParseErrorKind::Empty => "supply a URI",
108//! ParseErrorKind::TooLong { .. } => "shorten it",
109//! _ => "check the URI against the specification",
110//! };
111//! assert_eq!(advice, "supply a URI");
112//! ```
113//!
114//! Matching on a specific variant, which is the common case, is unaffected:
115//! `matches!`, `if let`, and `Result::is_err` all read the same as before.
116//!
117//! ## `TypeClass` parsing reports a real error
118//!
119//! [`ExtensionClass::new`] and `<TypeClass as FromStr>::Err` return
120//! [`TypeClassError`] instead of `&'static str`, so a type-class rejection is a
121//! [`std::error::Error`] that composes with `?` and names the offending
122//! character and its position. [`AgentPrefixError::InvalidTypeClass`]'s
123//! `reason` field carries it too.
124
125#![deny(missing_docs)]
126#![deny(clippy::all)]
127#![deny(clippy::pedantic)]
128#![allow(clippy::module_name_repetitions)]
129
130mod agent_id;
131mod agent_prefix;
132mod builder;
133mod capability_path;
134mod constants;
135mod error;
136mod fragment;
137#[cfg(kani)]
138mod kani_impls;
139mod path_segment;
140pub mod prelude;
141mod query;
142mod trust_root;
143mod type_class;
144mod uri;
145
146pub use agent_id::AgentId;
147pub use agent_prefix::AgentPrefix;
148pub use builder::{AgentUriBuilder, Empty, HasCapabilityPath, HasTrustRoot, Ready};
149pub use capability_path::CapabilityPath;
150pub use constants::{
151 AGENT_SUFFIX_LENGTH, MAX_AGENT_ID_LENGTH, MAX_AGENT_PREFIX_LENGTH, MAX_CAPABILITY_PATH_LENGTH,
152 MAX_DNS_DOMAIN_LENGTH, MAX_DNS_LABEL_LENGTH, MAX_PATH_SEGMENT_LENGTH, MAX_PATH_SEGMENTS,
153 MAX_TRUST_ROOT_LENGTH, MAX_URI_LENGTH, SCHEME,
154};
155pub use error::{
156 AgentIdError, AgentPrefixError, BuilderError, CapabilityPathError, FragmentError, ParseError,
157 ParseErrorKind, PathSegmentError, QueryError, TrustRootError, TypeClassError,
158};
159pub use fragment::Fragment;
160pub use path_segment::PathSegment;
161pub use query::QueryParams;
162pub use trust_root::{Host, TrustRoot};
163pub use type_class::{ExtensionClass, TypeClass};
164pub use uri::AgentUri;