Skip to main content

agent_uri_dht_libp2p/
lib.rs

1//! A Kademlia backend for `agent://` discovery, over `libp2p-kad`.
2//!
3//! [`agent-uri-dht`](agent_uri_dht) defines what discovery means and ships an
4//! in-process index that implements it. This crate implements the same
5//! [`Dht`](agent_uri_dht::Dht) trait against a real overlay: records live on
6//! other people's machines, reads cross the network, and every node that stores
7//! a record decides for itself whether it should.
8//!
9//! It is a separate crate because `libp2p` is a large dependency and the core
10//! crate is useful without it. A caller that only derives DHT keys should not
11//! inherit a networking stack.
12//!
13//! # Starting a node
14//!
15//! ```no_run
16//! use std::time::Duration;
17//! use agent_uri::AgentUri;
18//! use agent_uri_attestation::{SigningKey, Verifier};
19//! use agent_uri_dht::{
20//!     Dht, Endpoint, MutationProof, Query, ReadOptions, Registration, WriteOptions,
21//! };
22//! use agent_uri_dht_libp2p::Libp2pConfig;
23//! use libp2p::identity;
24//!
25//! # async fn example(trust_root_key: agent_uri_attestation::VerifyingKey, token: String)
26//! # -> Result<(), Box<dyn std::error::Error>> {
27//! // A node verifies attestations against the trust roots it is given, and
28//! // must also say what it does about revocation. `AcceptAll` states that this
29//! // node does not revoke; a `Denylist` is what checks a real list.
30//! let mut verifier = Verifier::new().with_revocation(agent_uri_attestation::AcceptAll);
31//! verifier.add_trusted_root("anthropic.com", trust_root_key);
32//!
33//! let node = agent_uri_dht_libp2p::start(
34//!     Libp2pConfig::default().listening_on("/ip4/0.0.0.0/tcp/0".parse()?),
35//!     verifier,
36//!     identity::Keypair::generate_ed25519(),
37//! )
38//! .await?;
39//!
40//! // Join an existing overlay. Peer addresses carry a peer identity, because
41//! // Kademlia routes to identities rather than to addresses.
42//! node.bootstrap(&[agent_uri_dht::PeerAddr::new(
43//!     "/ip4/198.51.100.7/tcp/4001/p2p/12D3KooWExample",
44//! )])
45//! .await?;
46//!
47//! // Publishing is a signed write, like every other write to a record.
48//! let uri = AgentUri::parse(
49//!     "agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q",
50//! )?;
51//! let agent_key = SigningKey::generate();
52//! let registration = Registration::new(
53//!     uri.clone(),
54//!     agent_key.verifying_key(),
55//!     vec![Endpoint::https("agent.anthropic.com:443")],
56//! )
57//! .with_attestation(token);
58//! let proof = MutationProof::sign_registration(&agent_key, &registration);
59//! node.register(registration, &proof, WriteOptions::default()).await?;
60//!
61//! // A registration expires unless the agent renews it, and renewing needs
62//! // the agent's key, so it cannot be the node's job.
63//! let _renewal = agent_uri_dht_libp2p::keep_alive(
64//!     node.clone(),
65//!     uri,
66//!     agent_key,
67//!     Duration::from_hours(1),
68//! );
69//!
70//! // Discovery by capability, paged.
71//! let query = Query::prefix(
72//!     agent_uri::TrustRoot::parse("anthropic.com")?,
73//!     agent_uri::CapabilityPath::parse("assistant")?,
74//! );
75//! let page = node.lookup(&query, &ReadOptions::default()).await?;
76//! # Ok(())
77//! # }
78//! ```
79//!
80//! # How a registration is stored
81//!
82//! This crate implements the **sharded** record model of SPECIFICATION.md §6.2,
83//! which a Kademlia overlay is required to use. The alternative, materializing
84//! the whole registration at the exact path and every ancestor, does not
85//! survive contact with a real overlay: `libp2p-kad`'s wire limit is 16 KiB,
86//! which the spike in issue #72 measured at 1 to 27 registrations, against a
87//! broad ancestor key meant to hold an entire subtree.
88//!
89//! The sharded model splits the two jobs one record was doing:
90//!
91//! | key | holds | written by |
92//! |---|---|---|
93//! | identity | the agent's signed registration | the agent, once per write |
94//! | page | pointers to agents beneath a path | every agent, at every ancestor |
95//! | descriptor | how many pages a path is spread over | whoever widens it |
96//!
97//! A pointer is about 74 bytes against a registration's 600, so a page holds
98//! roughly 220 agents rather than 27, and pages shard when that is not enough.
99//! Lookup costs a descriptor read, a page read per shard, and one read per agent
100//! found, which is what §6.4 costs the model at; each is still `O(log N)` hops.
101//! See [`keys`] for the derivations, which §6.1.1 makes normative.
102//!
103//! # What makes this safe under replication
104//!
105//! Kademlia copies records between nodes verbatim, on its own schedule, in
106//! whichever direction fires first. A backend whose nodes edited what they
107//! stored, or preferred whatever arrived last, would have replication undoing
108//! writes.
109//!
110//! Every value here is instead reconciled by a rule that does not depend on
111//! arrival order, and no node ever rewrites a value it accepts:
112//!
113//! - identity records are immutable and signed, and the later
114//!   `(registered_at, sequence)` wins;
115//! - pointer pages union, keeping the later expiry per URI;
116//! - shard descriptors take the greater level.
117//!
118//! All three are commutative, associative, and idempotent. Replication becomes
119//! anti-entropy rather than a race. See [`merge`] for the rules and
120//! [`validate`] for what a node checks before applying them.
121//!
122//! # What a node refuses to store
123//!
124//! §6.2 requirement 4 says DHT nodes MUST verify attestations before storing
125//! records. Every value entering a node's store passes [`validate`], via
126//! Kademlia's `StoreInserts::FilterBoth`, including values this node publishes
127//! itself. A node will not store a record whose signature does not cover it,
128//! whose key does not match its URI, or whose attestation names a different key
129//! than the record does.
130//!
131//! The one place that requirement cannot be met literally is a trust root the
132//! node has no key for, and a node holds records for keys near it in the
133//! overlay, which is every trust root. [`AttestationPolicy`] is where that is
134//! decided rather than assumed.
135//!
136//! # Reads verify too
137//!
138//! Being handed a record by the overlay makes it neither true nor current. A
139//! lookup runs the same validation on every copy returned and then takes the
140//! greatest version, so it is as fresh as the best-informed peer that replied
141//! and no more trusting than a storing node.
142//!
143//! # Known limits
144//!
145//! - **A node with no peers cannot publish.** Kademlia counts remote
146//!   acknowledgements, and a local store write is not one. Bootstrap first.
147//! - **Pointer pages are unauthenticated.** Anyone can put any URI on any page.
148//!   A lookup dereferences each pointer and checks the agent's own URI against
149//!   the path queried, so an injected pointer costs a wasted read and nothing
150//!   more.
151//! - **Shard growth is opportunistic.** A page over its high-water mark is
152//!   widened by whoever notices; until then it keeps accepting pointers.
153
154#![deny(missing_docs)]
155#![deny(clippy::all)]
156#![deny(clippy::pedantic)]
157
158pub mod config;
159mod dht;
160pub mod keys;
161pub mod merge;
162mod node;
163pub mod record;
164mod republish;
165pub mod validate;
166mod worker;
167
168pub use config::{AttestationPolicy, Libp2pConfig};
169pub use dht::Libp2pDht;
170pub use node::start;
171pub use republish::{Renewal, keep_alive, renew};