1use std::time::{Duration, SystemTime};
4
5use agent_uri::AgentUri;
6use agent_uri_attestation::VerifyingKey;
7
8use crate::Endpoint;
9
10#[derive(Debug, Clone)]
38pub struct Registration {
39 agent_uri: AgentUri,
41 agent_key: VerifyingKey,
43 endpoints: Vec<Endpoint>,
45 attestation: Option<String>,
47 expires_at: SystemTime,
49 registered_at: SystemTime,
51 sequence: u64,
53}
54
55impl Registration {
56 pub const DEFAULT_TTL: Duration = Duration::from_hours(1);
58
59 #[must_use]
67 pub fn new(agent_uri: AgentUri, agent_key: VerifyingKey, endpoints: Vec<Endpoint>) -> Self {
68 let now = SystemTime::now();
69 Self {
70 agent_uri,
71 agent_key,
72 endpoints,
73 attestation: None,
74 expires_at: now + Self::DEFAULT_TTL,
75 registered_at: now,
76 sequence: 0,
77 }
78 }
79
80 #[must_use]
82 pub fn with_ttl(mut self, ttl: Duration) -> Self {
83 self.expires_at = self.registered_at + ttl;
84 self
85 }
86
87 #[must_use]
89 pub fn with_attestation(mut self, attestation: impl Into<String>) -> Self {
90 self.attestation = Some(attestation.into());
91 self
92 }
93
94 #[must_use]
96 pub fn with_expires_at(mut self, expires_at: SystemTime) -> Self {
97 self.expires_at = expires_at;
98 self
99 }
100
101 #[must_use]
103 pub fn with_registered_at(mut self, registered_at: SystemTime) -> Self {
104 self.registered_at = registered_at;
105 self
106 }
107
108 #[must_use]
116 pub const fn with_sequence(mut self, sequence: u64) -> Self {
117 self.sequence = sequence;
118 self
119 }
120
121 #[must_use]
123 pub fn agent_uri(&self) -> &AgentUri {
124 &self.agent_uri
125 }
126
127 #[must_use]
129 pub fn agent_key(&self) -> &VerifyingKey {
130 &self.agent_key
131 }
132
133 #[must_use]
138 pub const fn sequence(&self) -> u64 {
139 self.sequence
140 }
141
142 #[must_use]
144 pub fn endpoints(&self) -> &[Endpoint] {
145 &self.endpoints
146 }
147
148 #[must_use]
150 pub fn attestation(&self) -> Option<&str> {
151 self.attestation.as_deref()
152 }
153
154 #[must_use]
156 pub fn expires_at(&self) -> SystemTime {
157 self.expires_at
158 }
159
160 #[must_use]
162 pub fn registered_at(&self) -> SystemTime {
163 self.registered_at
164 }
165
166 #[must_use]
168 pub fn is_expired(&self) -> bool {
169 SystemTime::now() >= self.expires_at
170 }
171
172 #[must_use]
174 pub fn remaining_ttl(&self) -> Option<Duration> {
175 self.expires_at.duration_since(SystemTime::now()).ok()
176 }
177
178 pub fn update_endpoints(&mut self, endpoints: Vec<Endpoint>) {
180 self.endpoints = endpoints;
181 }
182
183 pub const fn set_expires_at(&mut self, expires_at: SystemTime) {
197 self.expires_at = expires_at;
198 }
199
200 pub const fn set_sequence(&mut self, sequence: u64) {
207 self.sequence = sequence;
208 }
209}
210
211impl PartialEq for Registration {
212 fn eq(&self, other: &Self) -> bool {
213 self.agent_uri == other.agent_uri
215 }
216}
217
218impl Eq for Registration {}
219
220impl std::hash::Hash for Registration {
221 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
222 self.agent_uri.as_str().hash(state);
223 }
224}
225
226pub(crate) fn system_time_to_millis(time: SystemTime) -> u64 {
233 time.duration_since(SystemTime::UNIX_EPOCH)
234 .map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
235}
236
237#[cfg(feature = "serde")]
238fn millis_to_system_time(millis: u64) -> SystemTime {
239 SystemTime::UNIX_EPOCH + Duration::from_millis(millis)
240}
241
242#[cfg(feature = "serde")]
243impl serde::Serialize for Registration {
244 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
245 where
246 S: serde::Serializer,
247 {
248 use serde::ser::SerializeStruct;
249 let mut state = serializer.serialize_struct("Registration", 7)?;
250 state.serialize_field("agent_uri", self.agent_uri.as_str())?;
251 state.serialize_field("agent_key", &self.agent_key.to_bytes())?;
252 state.serialize_field("endpoints", &self.endpoints)?;
253 state.serialize_field("attestation", &self.attestation)?;
254 state.serialize_field("expires_at", &system_time_to_millis(self.expires_at))?;
255 state.serialize_field("registered_at", &system_time_to_millis(self.registered_at))?;
256 state.serialize_field("sequence", &self.sequence)?;
257 state.end()
258 }
259}
260
261#[cfg(feature = "serde")]
262impl<'de> serde::Deserialize<'de> for Registration {
263 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
264 where
265 D: serde::Deserializer<'de>,
266 {
267 #[derive(serde::Deserialize)]
268 struct RegistrationData {
269 agent_uri: String,
270 agent_key: [u8; 32],
271 endpoints: Vec<Endpoint>,
272 attestation: Option<String>,
273 expires_at: u64,
274 registered_at: u64,
275 sequence: u64,
276 }
277
278 let data = RegistrationData::deserialize(deserializer)?;
279 let agent_uri = AgentUri::parse(&data.agent_uri).map_err(serde::de::Error::custom)?;
280 let agent_key =
283 VerifyingKey::from_bytes(&data.agent_key).map_err(serde::de::Error::custom)?;
284
285 Ok(Self {
286 agent_uri,
287 agent_key,
288 endpoints: data.endpoints,
289 attestation: data.attestation,
290 expires_at: millis_to_system_time(data.expires_at),
291 registered_at: millis_to_system_time(data.registered_at),
292 sequence: data.sequence,
293 })
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn test_uri() -> AgentUri {
302 AgentUri::parse("agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q")
303 .unwrap()
304 }
305
306 fn test_endpoint() -> Endpoint {
307 Endpoint::https("agent.anthropic.com:443")
308 }
309
310 fn test_key() -> agent_uri_attestation::SigningKey {
311 agent_uri_attestation::SigningKey::generate()
312 }
313
314 #[test]
315 fn new_registration_has_default_ttl() {
316 let registration = Registration::new(
317 test_uri(),
318 test_key().verifying_key(),
319 vec![test_endpoint()],
320 );
321 assert!(!registration.is_expired());
322 assert!(registration.remaining_ttl().is_some());
323 }
324
325 #[test]
326 fn with_ttl_sets_expiration() {
327 let registration = Registration::new(
328 test_uri(),
329 test_key().verifying_key(),
330 vec![test_endpoint()],
331 )
332 .with_ttl(Duration::from_mins(1));
333 let remaining = registration.remaining_ttl().unwrap();
334 assert!(remaining.as_secs() <= 60);
336 assert!(remaining.as_secs() >= 59);
337 }
338
339 #[test]
340 fn with_attestation_sets_attestation() {
341 let registration = Registration::new(
342 test_uri(),
343 test_key().verifying_key(),
344 vec![test_endpoint()],
345 )
346 .with_attestation("token123");
347 assert_eq!(registration.attestation(), Some("token123"));
348 }
349
350 #[test]
351 fn expired_registration() {
352 let past = SystemTime::now() - Duration::from_secs(10);
353 let registration = Registration::new(
354 test_uri(),
355 test_key().verifying_key(),
356 vec![test_endpoint()],
357 )
358 .with_expires_at(past);
359 assert!(registration.is_expired());
360 assert!(registration.remaining_ttl().is_none());
361 }
362
363 #[test]
364 fn update_endpoints() {
365 let mut registration = Registration::new(
366 test_uri(),
367 test_key().verifying_key(),
368 vec![test_endpoint()],
369 );
370 let new_endpoint = Endpoint::grpc("agent.anthropic.com:50051");
371 registration.update_endpoints(vec![new_endpoint.clone()]);
372 assert_eq!(registration.endpoints(), &[new_endpoint]);
373 }
374
375 #[test]
376 fn setting_the_expiry_revives_a_lapsed_registration() {
377 let past = SystemTime::now() - Duration::from_hours(1);
378 let mut registration = Registration::new(
379 test_uri(),
380 test_key().verifying_key(),
381 vec![test_endpoint()],
382 )
383 .with_registered_at(past)
384 .with_expires_at(past);
385
386 assert!(registration.is_expired());
387
388 registration.set_expires_at(SystemTime::now() + Duration::from_mins(1));
389
390 assert!(!registration.is_expired());
391 assert!(registration.remaining_ttl().is_some());
392 }
393
394 #[test]
395 fn setting_the_expiry_leaves_the_record_instance_alone() {
396 let mut registration = Registration::new(
400 test_uri(),
401 test_key().verifying_key(),
402 vec![test_endpoint()],
403 );
404 let instance = registration.registered_at();
405
406 registration.set_expires_at(SystemTime::now() + Duration::from_hours(9));
407
408 assert_eq!(registration.registered_at(), instance);
409 }
410
411 #[test]
412 fn equality_based_on_uri() {
413 let registration1 = Registration::new(
414 test_uri(),
415 test_key().verifying_key(),
416 vec![test_endpoint()],
417 );
418 let registration2 = Registration::new(
419 test_uri(),
420 test_key().verifying_key(),
421 vec![Endpoint::grpc("other.com:50051")],
422 );
423 assert_eq!(registration1, registration2);
424 }
425
426 #[test]
427 fn different_uris_not_equal() {
428 let uri1 =
429 AgentUri::parse("agent://anthropic.com/assistant/chat/llm_01h455vb4pex5vsknk084sn02q")
430 .unwrap();
431 let uri2 =
432 AgentUri::parse("agent://anthropic.com/assistant/code/llm_01h455vb4pex5vsknk084sn02r")
433 .unwrap();
434 let registration1 =
435 Registration::new(uri1, test_key().verifying_key(), vec![test_endpoint()]);
436 let registration2 =
437 Registration::new(uri2, test_key().verifying_key(), vec![test_endpoint()]);
438 assert_ne!(registration1, registration2);
439 }
440
441 #[cfg(feature = "serde")]
442 #[test]
443 fn pre_epoch_time_serializes_to_zero_millis() {
444 let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
445 assert_eq!(system_time_to_millis(time), 0);
446 }
447
448 #[cfg(feature = "serde")]
449 #[test]
450 fn far_future_time_saturates_instead_of_wrapping() {
451 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(u64::MAX / 1000 + 1);
452 assert_eq!(system_time_to_millis(time), u64::MAX);
453 }
454
455 #[cfg(feature = "serde")]
456 #[test]
457 fn millis_round_trip_preserves_time() {
458 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
459 assert_eq!(millis_to_system_time(system_time_to_millis(time)), time);
460 }
461}