Skip to main content

agent_uri_dht/
lib.rs

1//! Capability-based DHT discovery for the agent:// URI scheme.
2//!
3//! This crate provides DHT (Distributed Hash Table) infrastructure for
4//! discovering agents by their capabilities. It includes:
5//!
6//! - **Key derivation**: [`DhtKey`] for Kademlia-style routing
7//! - **Registration records**: [`Registration`] with endpoints and attestations
8//! - **Trait interface**: [`Dht`], an async trait abstracting DHT backends
9//! - **In-memory simulation**: [`SimulatedDht`] for evaluation and testing
10//! - **Prefix matching**: [`PathTrie`], a standalone helper for callers that
11//!   want a local hierarchical index. Discovery itself does not use it: in this
12//!   crate a prefix lookup reads one materialized ancestor key. A networked
13//!   backend reads pointers from that key instead, which is one of several
14//!   differences the Scope section below sets out.
15//!
16//! # Overview
17//!
18//! [`SimulatedDht`] implements the **direct** record model of SPECIFICATION.md
19//! ยง6.2: agents register at keys for their exact capability path and every
20//! ancestor, and a prefix lookup is one exact-key read.
21//!
22//! ```text
23//! key = SHA256(trust_root || "/" || capability_path)
24//! ```
25//!
26//! That model holds where one store owns the namespace and a stored value has
27//! no practical size bound, which is what an in-process index is. A Kademlia
28//! overlay is not, and is required to use the sharded model instead; see
29//! [`agent-uri-dht-libp2p`](https://crates.io/crates/agent-uri-dht-libp2p). The
30//! [`Dht`] trait is the same either way.
31//!
32//! This enables capability-based discovery: "find me an agent at
33//! `anthropic.com` that can do `assistant/chat`".
34//!
35//! # Scope
36//!
37//! [`SimulatedDht`] is the reference implementation and the test double: one
38//! in-process index holding one authoritative copy, which is what you want to
39//! develop, test, and benchmark against. It is not a deployment, and two things
40//! follow from that.
41//!
42//! **Its limits are its own.** [`SimulationConfig`] bounds a key at 1000
43//! registrations by default and bounds a stored value not at all. A Kademlia
44//! overlay bounds a record at the wire size a peer will accept, which for
45//! `libp2p-kad` is 1 to 27 registrations, so it stores pointers at ancestor keys
46//! and sharded pages behind them. Code that asks whether a write succeeded and
47//! pages through what a lookup returned ports unchanged. Configuration tuned
48//! against these numbers does not.
49//!
50//! **A measurement taken against it is a measurement of indexing.** Nothing here
51//! partitions, replicates, retries, or loses a peer, so a lookup returns exactly
52//! what was written to it. That makes it a clean way to ask whether this crate
53//! indexes and pages what it was given correctly, and no way at all to ask what
54//! a distributed store returns under load or partition. The discovery precision
55//! and recall figures in `agent-uri-eval` were measured against this crate and
56//! are bounded by both facts; see its crate docs before quoting them.
57//!
58//! # Quick Start
59//!
60//! ```rust
61//! use agent_uri::{AgentUri, CapabilityPath, TrustRoot};
62//! use agent_uri_attestation::SigningKey;
63//! use agent_uri_dht::{
64//!     Dht, Endpoint, MutationProof, Query, ReadOptions, Registration, SimulatedDht,
65//!     SimulationConfig, WriteOptions,
66//! };
67//! use futures::executor::block_on;
68//!
69//! // Disable verification only for this isolated indexing example. Production
70//! // registration uses the secure default and a configured Verifier.
71//! let dht = SimulatedDht::new(
72//!     SimulationConfig::default().with_verify_attestations(false)
73//! );
74//!
75//! // Register an agent. The key it names is the only one that will be able to
76//! // change or remove the record later.
77//! let uri = AgentUri::parse(
78//!     "agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q"
79//! ).unwrap();
80//! let endpoint = Endpoint::https("agent.anthropic.com:443");
81//! let agent_key = SigningKey::generate();
82//! let registration = Registration::new(uri, agent_key.verifying_key(), vec![endpoint]);
83//! let proof = MutationProof::sign_registration(&agent_key, &registration);
84//!
85//! // Every operation is async, because a real backend crosses the network.
86//! block_on(dht.register(registration, &proof, WriteOptions::default())).unwrap();
87//!
88//! // Discover agents by capability. Results are paged.
89//! let query = Query::prefix(
90//!     TrustRoot::parse("anthropic.com").unwrap(),
91//!     CapabilityPath::parse("assistant").unwrap(),
92//! );
93//! let page = block_on(dht.lookup(&query, &ReadOptions::default())).unwrap();
94//!
95//! assert_eq!(page.len(), 1);
96//! assert!(!page.has_more());
97//! ```
98//!
99//! # Operating Against a Network
100//!
101//! [`Dht`] describes a distributed store, not a map, and its shape reflects
102//! that even when the backend is [`SimulatedDht`]:
103//!
104//! - **Every operation is async and takes a deadline.** [`WriteOptions`] and
105//!   [`ReadOptions`] carry a timeout; a call that does not finish returns
106//!   [`DhtError::Timeout`], which does not mean the write failed to apply.
107//! - **Writes name a [`Quorum`].** How many replicas must acknowledge is the
108//!   caller's latency-versus-durability trade, and [`WriteReceipt`] reports
109//!   what was actually reached.
110//! - **Lookups are paged.** [`Dht::lookup`] returns a [`Page`]; follow
111//!   [`Page::next_cursor`] until it is `None` or you are sampling rather than
112//!   enumerating.
113//! - **Failures are distinguishable.** [`DhtError::is_transient`] separates a
114//!   slow or partitioned network from a rejected request. Collapsing the two
115//!   turns a partition into an apparently empty namespace.
116//! - **Writes are authorized, not merely addressed.** An agent URI is public,
117//!   and so is the attestation token stored beside it, so every write carries a
118//!   [`MutationProof`] signed by the agent's own key. Without this, reaching a
119//!   replica would be enough to repoint an agent.
120//!
121//! `SimulatedDht` honors all of this against one in-process copy, so code
122//! written against it does not acquire habits a real backend will punish.
123//!
124//! # Key Derivation
125//!
126//! DHT keys are derived deterministically from trust root and capability path
127//! using SHA-256. This enables:
128//!
129//! - **Exact lookup**: find agents at a specific capability path
130//! - **Prefix lookup**: find agents at a path and every path beneath it, from
131//!   the single key that path derives
132//!
133//! There is deliberately no cross-trust-root lookup. Every key is derived from
134//! one trust root, so a query is answered by one authority, and a compromised
135//! trust-root key reaches no further than the namespace it already owned.
136//!
137//! ```rust
138//! use agent_uri::{TrustRoot, CapabilityPath};
139//! use agent_uri_dht::DhtKey;
140//!
141//! let trust_root = TrustRoot::parse("anthropic.com").unwrap();
142//! let path = CapabilityPath::parse("assistant/chat").unwrap();
143//!
144//! // Derive a key for this capability
145//! let key = DhtKey::derive(&trust_root, &path);
146//!
147//! // Keys are deterministic
148//! let key2 = DhtKey::derive(&trust_root, &path);
149//! assert_eq!(key, key2);
150//!
151//! // Partial depth for hierarchical queries
152//! let parent_key = DhtKey::derive_at_depth(&trust_root, &path, 1).unwrap();
153//! ```
154//!
155//! # Agent Migration
156//!
157//! The identity stability of agent:// URIs means agents can migrate
158//! between network locations without changing their identity:
159//!
160//! ```rust
161//! use agent_uri::AgentUri;
162//! use agent_uri_attestation::SigningKey;
163//! use agent_uri_dht::{
164//!     Dht, Endpoint, Mutation, MutationProof, Registration, SimulatedDht, SimulationConfig,
165//!     WriteOptions,
166//! };
167//! use futures::executor::block_on;
168//!
169//! let dht = SimulatedDht::new(
170//!     SimulationConfig::default().with_verify_attestations(false)
171//! );
172//!
173//! let uri = AgentUri::parse(
174//!     "agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q"
175//! ).unwrap();
176//! let agent_key = SigningKey::generate();
177//!
178//! // Register at initial location
179//! let registration = Registration::new(
180//!     uri.clone(),
181//!     agent_key.verifying_key(),
182//!     vec![Endpoint::https("us-east-1.agent.anthropic.com")]
183//! );
184//! let registration_proof = MutationProof::sign_registration(&agent_key, &registration);
185//! block_on(dht.register(registration.clone(), &registration_proof, WriteOptions::default()))
186//!     .unwrap();
187//!
188//! // Migrate to new location (same identity). The proof is signed against the
189//! // record as it stands, so an intercepted migration cannot be re-aimed at
190//! // other endpoints, applied twice, or replayed after the agent re-registers.
191//! let endpoints = vec![Endpoint::https("eu-west-1.agent.anthropic.com")];
192//! let mutation = Mutation::UpdateEndpoint {
193//!     endpoints: &endpoints,
194//!     expires_at: registration.expires_at(),
195//! };
196//! let proof = MutationProof::sign_next(&agent_key, &registration, &mutation);
197//!
198//! block_on(dht.update_endpoint(
199//!     &uri,
200//!     endpoints.clone(),
201//!     &proof,
202//!     WriteOptions::default(),
203//! )).unwrap();
204//! ```
205
206#![deny(missing_docs)]
207#![deny(clippy::all)]
208#![deny(clippy::pedantic)]
209#![allow(clippy::module_name_repetitions)]
210
211mod config;
212mod endpoint;
213mod error;
214mod expiration;
215mod key;
216mod node;
217mod options;
218mod page;
219mod proof;
220mod query;
221mod registration;
222mod simulation;
223mod stats;
224mod traits;
225mod trie;
226
227pub use config::SimulationConfig;
228pub use endpoint::Endpoint;
229pub use error::DhtError;
230pub use key::DhtKey;
231pub use node::{NodeId, PeerAddr, WriteReceipt};
232pub use options::{Quorum, ReadOptions, WriteOptions};
233pub use page::{Cursor, Page};
234pub use proof::{Mutation, MutationKind, MutationProof, RecordVersion, signing_payload};
235pub use query::{MatchMode, Query};
236pub use registration::Registration;
237pub use simulation::SimulatedDht;
238pub use stats::{DhtStats, MigrationResult};
239pub use traits::Dht;
240pub use trie::PathTrie;