use std::time::{Duration, SystemTime};
use agent_uri_attestation::Signature;
use agent_uri_dht::{MutationKind, Registration};
use serde::{Deserialize, Serialize};
const ENCODING_VERSION: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shape {
Identity = 1,
Descriptor = 2,
Page = 3,
}
pub fn classify(bytes: &[u8]) -> Result<Shape, CodecError> {
let (&version, rest) = bytes.split_first().ok_or(CodecError::Empty)?;
if version != ENCODING_VERSION {
return Err(CodecError::UnknownVersion(version));
}
match rest.first() {
Some(1) => Ok(Shape::Identity),
Some(2) => Ok(Shape::Descriptor),
Some(3) => Ok(Shape::Page),
_ => Err(CodecError::ShapeMismatch),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CodecError {
Empty,
UnknownVersion(u8),
ShapeMismatch,
Malformed,
}
impl std::fmt::Display for CodecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => f.write_str("record value is empty"),
Self::UnknownVersion(v) => {
write!(
f,
"record announces encoding version {v}, which this node does not speak"
)
}
Self::ShapeMismatch => {
f.write_str("record decoded as a different shape than its key holds")
}
Self::Malformed => f.write_str("record value is not a well-formed record"),
}
}
}
impl std::error::Error for CodecError {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IdentityRecord {
pub state: Registration,
pub kind: WireKind,
pub refresh_ttl_millis: Option<u64>,
pub signature: WireSignature,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WireSignature([u8; 64]);
impl WireSignature {
#[must_use]
pub const fn new(bytes: [u8; 64]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn to_bytes(self) -> [u8; 64] {
self.0
}
}
impl From<Signature> for WireSignature {
fn from(signature: Signature) -> Self {
Self(signature.to_bytes())
}
}
impl From<WireSignature> for Signature {
fn from(signature: WireSignature) -> Self {
Self::from_bytes(signature.0)
}
}
impl Serialize for WireSignature {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(&self.0)
}
}
impl<'de> Deserialize<'de> for WireSignature {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let bytes = <&[u8]>::deserialize(deserializer)?;
let bytes: [u8; 64] = bytes
.try_into()
.map_err(|_| serde::de::Error::custom("signature must be 64 bytes"))?;
Ok(Self(bytes))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum WireKind {
Register = 1,
UpdateEndpoint = 2,
Refresh = 3,
Deregister = 4,
}
impl From<WireKind> for MutationKind {
fn from(kind: WireKind) -> Self {
match kind {
WireKind::Register => Self::Register,
WireKind::UpdateEndpoint => Self::UpdateEndpoint,
WireKind::Refresh => Self::Refresh,
WireKind::Deregister => Self::Deregister,
}
}
}
impl From<MutationKind> for WireKind {
fn from(kind: MutationKind) -> Self {
match kind {
MutationKind::Register => Self::Register,
MutationKind::UpdateEndpoint => Self::UpdateEndpoint,
MutationKind::Refresh => Self::Refresh,
MutationKind::Deregister => Self::Deregister,
}
}
}
impl IdentityRecord {
#[must_use]
pub const fn is_tombstone(&self) -> bool {
matches!(self.kind, WireKind::Deregister)
}
#[must_use]
pub fn refresh_ttl(&self) -> Option<Duration> {
self.refresh_ttl_millis.map(Duration::from_millis)
}
#[must_use]
pub fn version(&self) -> (u64, u64) {
(millis(self.state.registered_at()), self.state.sequence())
}
pub fn encode(&self) -> Result<Vec<u8>, CodecError> {
encode(Shape::Identity, self)
}
pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
decode(Shape::Identity, bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pointer {
pub agent_uri: String,
pub expires_at_millis: u64,
}
impl Pointer {
#[must_use]
pub fn new(agent_uri: impl Into<String>, expires_at: SystemTime) -> Self {
Self {
agent_uri: agent_uri.into(),
expires_at_millis: millis(expires_at),
}
}
#[must_use]
pub fn is_expired(&self, now: SystemTime) -> bool {
millis(now) >= self.expires_at_millis
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PointerPage {
pub pointers: Vec<Pointer>,
}
impl PointerPage {
#[must_use]
pub const fn new(pointers: Vec<Pointer>) -> Self {
Self { pointers }
}
pub fn encode(&self) -> Result<Vec<u8>, CodecError> {
encode(Shape::Page, self)
}
pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
decode(Shape::Page, bytes)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShardDescriptor {
pub level: u8,
}
impl ShardDescriptor {
#[must_use]
pub const fn at_level(level: u8) -> Self {
Self { level }
}
pub fn encode(&self) -> Result<Vec<u8>, CodecError> {
encode(Shape::Descriptor, self)
}
pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
decode(Shape::Descriptor, bytes)
}
}
pub(crate) fn millis(time: SystemTime) -> u64 {
time.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
}
fn encode<T: Serialize>(shape: Shape, value: &T) -> Result<Vec<u8>, CodecError> {
let mut bytes = vec![ENCODING_VERSION, shape as u8];
let body = postcard::to_stdvec(value).map_err(|_| CodecError::Malformed)?;
bytes.extend_from_slice(&body);
Ok(bytes)
}
fn decode<T: for<'de> Deserialize<'de>>(shape: Shape, bytes: &[u8]) -> Result<T, CodecError> {
let (&version, rest) = bytes.split_first().ok_or(CodecError::Empty)?;
if version != ENCODING_VERSION {
return Err(CodecError::UnknownVersion(version));
}
let (&tag, body) = rest.split_first().ok_or(CodecError::Malformed)?;
if tag != shape as u8 {
return Err(CodecError::ShapeMismatch);
}
postcard::from_bytes(body).map_err(|_| CodecError::Malformed)
}
#[cfg(test)]
mod tests {
use super::*;
use agent_uri::AgentUri;
use agent_uri_attestation::SigningKey;
use agent_uri_dht::Endpoint;
fn registration() -> Registration {
let uri =
AgentUri::parse("agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q")
.unwrap();
Registration::new(
uri,
SigningKey::generate().verifying_key(),
vec![Endpoint::https("agent.anthropic.com:443")],
)
}
fn identity() -> IdentityRecord {
IdentityRecord {
state: registration(),
kind: WireKind::Register,
refresh_ttl_millis: None,
signature: WireSignature::new([7u8; 64]),
}
}
#[test]
fn identity_record_round_trips() {
let record = identity();
let decoded = IdentityRecord::decode(&record.encode().unwrap()).unwrap();
assert_eq!(decoded.state.agent_uri(), record.state.agent_uri());
assert_eq!(decoded.signature, record.signature);
assert_eq!(decoded.kind, WireKind::Register);
assert_eq!(decoded.state.endpoints(), record.state.endpoints());
assert_eq!(decoded.state.agent_key(), record.state.agent_key());
}
#[test]
fn pointer_page_round_trips() {
let page = PointerPage::new(vec![Pointer::new("agent://a.com/b/c_1", SystemTime::now())]);
assert_eq!(PointerPage::decode(&page.encode().unwrap()).unwrap(), page);
}
#[test]
fn descriptor_round_trips() {
let descriptor = ShardDescriptor::at_level(5);
assert_eq!(
ShardDescriptor::decode(&descriptor.encode().unwrap()).unwrap(),
descriptor
);
}
#[test]
fn classify_reads_the_shape_without_the_body() {
assert_eq!(
classify(&identity().encode().unwrap()).unwrap(),
Shape::Identity
);
assert_eq!(
classify(&PointerPage::default().encode().unwrap()).unwrap(),
Shape::Page
);
assert_eq!(
classify(&ShardDescriptor::default().encode().unwrap()).unwrap(),
Shape::Descriptor
);
assert_eq!(classify(&[]).unwrap_err(), CodecError::Empty);
assert_eq!(classify(&[1, 99]).unwrap_err(), CodecError::ShapeMismatch);
assert_eq!(
classify(&[9, 1]).unwrap_err(),
CodecError::UnknownVersion(9)
);
}
#[test]
fn a_page_does_not_decode_as_an_identity_record() {
let page = PointerPage::new(vec![]).encode().unwrap();
assert_eq!(
IdentityRecord::decode(&page).unwrap_err(),
CodecError::ShapeMismatch
);
}
#[test]
fn an_unknown_version_is_refused_not_guessed_at() {
let mut bytes = identity().encode().unwrap();
bytes[0] = 99;
assert_eq!(
IdentityRecord::decode(&bytes).unwrap_err(),
CodecError::UnknownVersion(99)
);
}
#[test]
fn an_empty_value_is_refused() {
assert_eq!(IdentityRecord::decode(&[]).unwrap_err(), CodecError::Empty);
}
#[test]
fn truncated_bytes_are_refused() {
let bytes = identity().encode().unwrap();
assert!(IdentityRecord::decode(&bytes[..bytes.len() / 2]).is_err());
}
#[test]
fn version_orders_by_instance_then_sequence() {
let mut a = identity();
a.state = a.state.clone().with_sequence(9);
let mut b = identity();
b.state = b
.state
.clone()
.with_registered_at(a.state.registered_at() + Duration::from_secs(1))
.with_sequence(0);
assert!(b.version() > a.version());
}
#[test]
fn a_deregistration_is_a_tombstone() {
let record = IdentityRecord {
kind: WireKind::Deregister,
..identity()
};
assert!(record.is_tombstone());
assert!(!identity().is_tombstone());
}
#[test]
fn wire_kinds_map_onto_mutation_kinds_both_ways() {
for kind in [
WireKind::Register,
WireKind::UpdateEndpoint,
WireKind::Refresh,
WireKind::Deregister,
] {
assert_eq!(WireKind::from(MutationKind::from(kind)), kind);
}
}
#[test]
fn a_pointer_expires() {
let now = SystemTime::now();
let stale = Pointer::new("agent://a.com/b/c_1", now - Duration::from_secs(1));
let fresh = Pointer::new("agent://a.com/b/c_1", now + Duration::from_mins(1));
assert!(stale.is_expired(now));
assert!(!fresh.is_expired(now));
}
#[test]
fn a_registration_and_its_attestation_fit_the_wire_limit() {
let record = IdentityRecord {
state: registration().with_attestation("v4.public.".to_string() + &"x".repeat(8000)),
..identity()
};
let encoded = record.encode().unwrap();
assert!(
encoded.len() < 16 * 1024,
"one registration at the 8 KiB token cap encoded to {} bytes",
encoded.len()
);
}
#[test]
fn a_page_holds_the_pointer_count_the_spec_publishes() {
let uri = "agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q";
let now = SystemTime::now();
let page = PointerPage::new((0..220).map(|_| Pointer::new(uri, now)).collect());
let encoded = page.encode().unwrap().len();
assert!(
encoded <= 16 * 1024,
"220 pointers encoded to {encoded} bytes, over the 16 KiB wire limit"
);
let long = format!(
"agent://{}/{}/{}_01h455vb4pex5vsknk084sn02q",
"a".repeat(115) + ".example.com",
vec!["s".repeat(51); 5].join("/"),
"p".repeat(63)
);
let page = PointerPage::new((0..33).map(|_| Pointer::new(long.clone(), now)).collect());
assert!(page.encode().unwrap().len() <= 16 * 1024);
}
}