1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[non_exhaustive]
13pub enum Protocol {
14 #[serde(rename = "A2A")]
16 A2A,
17 #[serde(rename = "MCP")]
19 Mcp,
20 #[serde(rename = "HTTP-API", alias = "HTTP_API")]
22 HttpApi,
23}
24
25impl fmt::Display for Protocol {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::A2A => write!(f, "A2A"),
29 Self::Mcp => write!(f, "MCP"),
30 Self::HttpApi => write!(f, "HTTP-API"),
31 }
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[non_exhaustive]
38pub enum Transport {
39 #[serde(rename = "STREAMABLE-HTTP", alias = "STREAMABLE_HTTP")]
41 StreamableHttp,
42 #[serde(rename = "SSE")]
44 Sse,
45 #[serde(rename = "JSON-RPC", alias = "JSON_RPC")]
47 JsonRpc,
48 #[serde(rename = "GRPC")]
50 Grpc,
51 #[serde(rename = "REST")]
53 Rest,
54 #[serde(rename = "HTTP")]
56 Http,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct AgentFunction {
63 pub id: String,
65 pub name: String,
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub tags: Vec<String>,
70}
71
72impl AgentFunction {
73 pub fn new(id: impl Into<String>, name: impl Into<String>, tags: Vec<String>) -> Self {
75 Self {
76 id: id.into(),
77 name: name.into(),
78 tags,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86#[non_exhaustive]
87pub struct AgentEndpoint {
88 pub agent_url: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub meta_data_url: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub documentation_url: Option<String>,
96 pub protocol: Protocol,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
100 pub transports: Vec<Transport>,
101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
103 pub functions: Vec<AgentFunction>,
104}
105
106impl AgentEndpoint {
107 pub fn new(agent_url: impl Into<String>, protocol: Protocol) -> Self {
109 Self {
110 agent_url: agent_url.into(),
111 meta_data_url: None,
112 documentation_url: None,
113 protocol,
114 transports: Vec::new(),
115 functions: Vec::new(),
116 }
117 }
118
119 pub fn with_transports(mut self, transports: Vec<Transport>) -> Self {
121 self.transports = transports;
122 self
123 }
124
125 pub fn with_functions(mut self, functions: Vec<AgentFunction>) -> Self {
127 self.functions = functions;
128 self
129 }
130}
131
132#[derive(Debug, Clone, Serialize)]
134#[serde(rename_all = "camelCase")]
135#[non_exhaustive]
136pub struct AgentRegistrationRequest {
137 pub agent_display_name: String,
139 pub agent_host: String,
141 pub version: String,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub agent_description: Option<String>,
146 #[serde(rename = "identityCsrPEM")]
148 pub identity_csr_pem: String,
149 #[serde(rename = "serverCsrPEM", skip_serializing_if = "Option::is_none")]
151 pub server_csr_pem: Option<String>,
152 #[serde(
154 rename = "serverCertificatePEM",
155 skip_serializing_if = "Option::is_none"
156 )]
157 pub server_certificate_pem: Option<String>,
158 #[serde(
160 rename = "serverCertificateChainPEM",
161 skip_serializing_if = "Option::is_none"
162 )]
163 pub server_certificate_chain_pem: Option<String>,
164 pub endpoints: Vec<AgentEndpoint>,
166}
167
168impl AgentRegistrationRequest {
169 pub fn new(
171 agent_display_name: impl Into<String>,
172 agent_host: impl Into<String>,
173 version: impl Into<String>,
174 identity_csr_pem: impl Into<String>,
175 endpoints: Vec<AgentEndpoint>,
176 ) -> Self {
177 Self {
178 agent_display_name: agent_display_name.into(),
179 agent_host: agent_host.into(),
180 version: version.into(),
181 agent_description: None,
182 identity_csr_pem: identity_csr_pem.into(),
183 server_csr_pem: None,
184 server_certificate_pem: None,
185 server_certificate_chain_pem: None,
186 endpoints,
187 }
188 }
189
190 pub fn with_description(mut self, description: impl Into<String>) -> Self {
192 self.agent_description = Some(description.into());
193 self
194 }
195
196 pub fn with_server_csr_pem(mut self, csr: impl Into<String>) -> Self {
198 self.server_csr_pem = Some(csr.into());
199 self
200 }
201
202 pub fn with_server_certificate_pem(mut self, cert: impl Into<String>) -> Self {
204 self.server_certificate_pem = Some(cert.into());
205 self
206 }
207
208 pub fn with_server_certificate_chain_pem(mut self, chain: impl Into<String>) -> Self {
210 self.server_certificate_chain_pem = Some(chain.into());
211 self
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
218#[non_exhaustive]
219pub enum RegistrationStatus {
220 PendingValidation,
222 PendingCerts,
224 PendingDns,
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
231#[non_exhaustive]
232pub enum AgentLifecycleStatus {
233 PendingValidation,
235 PendingDns,
237 Active,
239 Failed,
241 Expired,
243 Revoked,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[non_exhaustive]
250pub struct Link {
251 pub rel: String,
253 pub href: String,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259#[non_exhaustive]
260pub struct DnsRecord {
261 pub name: String,
263 #[serde(rename = "type")]
265 pub record_type: String,
266 pub value: String,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub purpose: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub ttl: Option<i32>,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub priority: Option<i32>,
277 #[serde(default = "default_true")]
279 pub required: bool,
280}
281
282fn default_true() -> bool {
283 true
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[non_exhaustive]
289pub enum ChallengeType {
290 #[serde(rename = "DNS_01")]
292 Dns01,
293 #[serde(rename = "HTTP_01")]
295 Http01,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300#[non_exhaustive]
301pub struct DnsRecordDetails {
302 pub name: String,
304 #[serde(rename = "type")]
306 pub record_type: String,
307 pub value: String,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314#[non_exhaustive]
315pub struct ChallengeInfo {
316 #[serde(rename = "type")]
318 pub challenge_type: ChallengeType,
319 #[serde(skip_serializing_if = "Option::is_none")]
321 pub token: Option<String>,
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub key_authorization: Option<String>,
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub http_path: Option<String>,
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub dns_record: Option<DnsRecordDetails>,
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub expires_at: Option<DateTime<Utc>>,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
338#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
339#[non_exhaustive]
340pub enum NextStepAction {
341 ConfigureDns,
343 ConfigureHttp,
345 VerifyDns,
347 ValidateDomain,
349 Wait,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356#[non_exhaustive]
357pub struct NextStep {
358 pub action: NextStepAction,
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub description: Option<String>,
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub endpoint: Option<String>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub estimated_time_minutes: Option<i32>,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373#[serde(rename_all = "camelCase")]
374#[non_exhaustive]
375pub struct RegistrationPending {
376 pub status: RegistrationStatus,
378 pub ans_name: String,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub agent_id: Option<String>,
383 pub next_steps: Vec<NextStep>,
385 #[serde(default)]
387 pub challenges: Vec<ChallengeInfo>,
388 #[serde(default)]
390 pub dns_records: Vec<DnsRecord>,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub expires_at: Option<DateTime<Utc>>,
394 #[serde(default)]
396 pub links: Vec<Link>,
397}
398
399impl RegistrationPending {
400 pub fn get_agent_id(&self) -> Option<String> {
405 if let Some(ref id) = self.agent_id {
407 return Some(id.clone());
408 }
409
410 self.links
412 .iter()
413 .find(|link| link.rel == "self")
414 .and_then(|link| {
415 link.href
417 .trim_end_matches('/')
418 .rsplit('/')
419 .next()
420 .filter(|s| !s.is_empty() && *s != "agents")
421 .map(String::from)
422 })
423 }
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
429#[non_exhaustive]
430pub enum RegistrationPhase {
431 Initialization,
433 DomainValidation,
435 CertificateIssuance,
437 DnsProvisioning,
439 Completed,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446#[non_exhaustive]
447pub struct AgentStatus {
448 pub status: AgentLifecycleStatus,
450 #[serde(skip_serializing_if = "Option::is_none")]
452 pub phase: Option<RegistrationPhase>,
453 #[serde(default)]
455 pub completed_steps: Vec<String>,
456 #[serde(default)]
458 pub pending_steps: Vec<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
461 pub created_at: Option<DateTime<Utc>>,
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub updated_at: Option<DateTime<Utc>>,
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub expires_at: Option<DateTime<Utc>>,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473#[non_exhaustive]
474pub struct AgentDetails {
475 pub agent_id: String,
477 pub agent_display_name: String,
479 pub agent_host: String,
481 #[serde(skip_serializing_if = "Option::is_none")]
483 pub agent_description: Option<String>,
484 pub ans_name: String,
486 pub version: String,
488 pub agent_status: AgentLifecycleStatus,
490 pub endpoints: Vec<AgentEndpoint>,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub registration_timestamp: Option<DateTime<Utc>>,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub last_renewal_timestamp: Option<DateTime<Utc>>,
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub registration_pending: Option<RegistrationPending>,
501 #[serde(default)]
503 pub links: Vec<Link>,
504}
505
506#[derive(Debug, Clone, Default, Serialize, Deserialize)]
508#[serde(rename_all = "camelCase")]
509#[non_exhaustive]
510pub struct SearchCriteria {
511 #[serde(skip_serializing_if = "Option::is_none")]
513 pub protocol: Option<Protocol>,
514 #[serde(skip_serializing_if = "Option::is_none")]
516 pub agent_display_name: Option<String>,
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub version: Option<String>,
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub agent_host: Option<String>,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
527#[serde(rename_all = "camelCase")]
528#[non_exhaustive]
529pub struct AgentSearchResult {
530 pub ans_name: String,
532 pub agent_id: String,
534 pub agent_display_name: String,
536 #[serde(skip_serializing_if = "Option::is_none")]
538 pub agent_description: Option<String>,
539 pub version: String,
541 pub agent_host: String,
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub ttl: Option<i32>,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub registration_timestamp: Option<DateTime<Utc>>,
549 pub endpoints: Vec<AgentEndpoint>,
551 #[serde(default)]
553 pub links: Vec<Link>,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize)]
558#[serde(rename_all = "camelCase")]
559#[non_exhaustive]
560pub struct AgentSearchResponse {
561 pub agents: Vec<AgentSearchResult>,
563 pub total_count: i32,
565 pub returned_count: i32,
567 pub limit: i32,
569 pub offset: i32,
571 pub has_more: bool,
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub search_criteria: Option<SearchCriteria>,
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
580#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
581#[non_exhaustive]
582pub enum RevocationReason {
583 KeyCompromise,
585 CessationOfOperation,
587 AffiliationChanged,
589 Superseded,
591 CertificateHold,
593 PrivilegeWithdrawn,
595 AaCompromise,
597}
598
599#[derive(Debug, Clone, Serialize)]
601#[non_exhaustive]
602pub struct AgentRevocationRequest {
603 pub reason: RevocationReason,
605 #[serde(skip_serializing_if = "Option::is_none")]
607 pub comments: Option<String>,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
612#[serde(rename_all = "camelCase")]
613#[non_exhaustive]
614pub struct AgentRevocationResponse {
615 pub agent_id: Uuid,
617 pub ans_name: String,
619 pub status: AgentLifecycleStatus,
621 pub revoked_at: DateTime<Utc>,
623 pub reason: RevocationReason,
625 pub links: Vec<Link>,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632#[non_exhaustive]
633pub struct CertificateResponse {
634 pub csr_id: Uuid,
636 #[serde(skip_serializing_if = "Option::is_none")]
638 pub certificate_subject: Option<String>,
639 #[serde(skip_serializing_if = "Option::is_none")]
641 pub certificate_issuer: Option<String>,
642 #[serde(skip_serializing_if = "Option::is_none")]
644 pub certificate_serial_number: Option<String>,
645 pub certificate_valid_from: DateTime<Utc>,
647 pub certificate_valid_to: DateTime<Utc>,
649 #[serde(rename = "certificatePEM")]
651 pub certificate_pem: String,
652 #[serde(rename = "chainPEM", skip_serializing_if = "Option::is_none")]
654 pub chain_pem: Option<String>,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub certificate_public_key_algorithm: Option<String>,
658 #[serde(skip_serializing_if = "Option::is_none")]
660 pub certificate_signature_algorithm: Option<String>,
661}
662
663#[derive(Debug, Clone, Serialize)]
665#[serde(rename_all = "camelCase")]
666#[non_exhaustive]
667pub struct CsrSubmissionRequest {
668 #[serde(rename = "csrPEM")]
670 pub csr_pem: String,
671}
672
673#[derive(Debug, Clone, Deserialize)]
675#[serde(rename_all = "camelCase")]
676#[non_exhaustive]
677pub struct CsrSubmissionResponse {
678 pub csr_id: Uuid,
680 #[serde(skip_serializing_if = "Option::is_none")]
682 pub message: Option<String>,
683}
684
685#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
687#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
688#[non_exhaustive]
689pub enum CsrType {
690 Server,
692 Identity,
694}
695
696#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
699#[non_exhaustive]
700pub enum CsrStatus {
701 Pending,
703 Signed,
705 Rejected,
707}
708
709#[derive(Debug, Clone, Deserialize)]
711#[serde(rename_all = "camelCase")]
712#[non_exhaustive]
713pub struct CsrStatusResponse {
714 pub csr_id: Uuid,
716 #[serde(rename = "type")]
718 pub csr_type: CsrType,
719 pub status: CsrStatus,
721 pub submitted_at: DateTime<Utc>,
723 pub updated_at: DateTime<Utc>,
725 #[serde(skip_serializing_if = "Option::is_none")]
727 pub failure_reason: Option<String>,
728}
729
730#[derive(Debug, Clone, Serialize)]
732#[serde(rename_all = "camelCase")]
733#[non_exhaustive]
734pub struct AgentResolutionRequest {
735 pub agent_host: String,
737 pub version: String,
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize)]
743#[serde(rename_all = "camelCase")]
744#[non_exhaustive]
745pub struct AgentResolutionResponse {
746 pub ans_name: String,
748 pub links: Vec<Link>,
750}
751
752#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
759#[non_exhaustive]
760pub enum EventType {
761 AgentRegistered,
763 AgentRenewed,
765 AgentRevoked,
767 AgentVersionUpdated,
769}
770
771impl std::fmt::Display for EventType {
772 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773 match self {
774 Self::AgentRegistered => write!(f, "AGENT_REGISTERED"),
775 Self::AgentRenewed => write!(f, "AGENT_RENEWED"),
776 Self::AgentRevoked => write!(f, "AGENT_REVOKED"),
777 Self::AgentVersionUpdated => write!(f, "AGENT_VERSION_UPDATED"),
778 }
779 }
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize)]
784#[serde(rename_all = "camelCase")]
785#[non_exhaustive]
786pub struct EventItem {
787 pub log_id: String,
789 pub event_type: EventType,
791 pub created_at: DateTime<Utc>,
793 #[serde(skip_serializing_if = "Option::is_none")]
795 pub expires_at: Option<DateTime<Utc>>,
796 pub agent_id: Uuid,
798 pub ans_name: String,
800 pub agent_host: String,
802 #[serde(skip_serializing_if = "Option::is_none")]
804 pub agent_display_name: Option<String>,
805 #[serde(skip_serializing_if = "Option::is_none")]
807 pub agent_description: Option<String>,
808 pub version: String,
810 #[serde(skip_serializing_if = "Option::is_none")]
812 pub provider_id: Option<String>,
813 #[serde(default, skip_serializing_if = "Vec::is_empty")]
815 pub endpoints: Vec<AgentEndpoint>,
816}
817
818#[derive(Debug, Clone, Serialize, Deserialize)]
820#[serde(rename_all = "camelCase")]
821#[non_exhaustive]
822pub struct EventPageResponse {
823 pub items: Vec<EventItem>,
825 #[serde(skip_serializing_if = "Option::is_none")]
828 pub last_log_id: Option<String>,
829}
830
831#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
832#[cfg(test)]
833mod tests {
834 use super::*;
835
836 fn make_pending_with_links(
837 agent_id: Option<&str>,
838 links: Vec<(&str, &str)>,
839 ) -> RegistrationPending {
840 RegistrationPending {
841 status: RegistrationStatus::PendingValidation,
842 ans_name: "ans://v1.0.0.agent.example.com".to_string(),
843 agent_id: agent_id.map(String::from),
844 next_steps: vec![],
845 challenges: vec![],
846 dns_records: vec![],
847 expires_at: None,
848 links: links
849 .into_iter()
850 .map(|(rel, href)| Link {
851 rel: rel.to_string(),
852 href: href.to_string(),
853 })
854 .collect(),
855 }
856 }
857
858 #[test]
859 fn test_get_agent_id_from_field() {
860 let pending = make_pending_with_links(Some("direct-id"), vec![]);
861 assert_eq!(pending.get_agent_id(), Some("direct-id".to_string()));
862 }
863
864 #[test]
865 fn test_get_agent_id_from_self_link_path() {
866 let pending = make_pending_with_links(None, vec![("self", "/v1/agents/uuid-from-link")]);
867 assert_eq!(pending.get_agent_id(), Some("uuid-from-link".to_string()));
868 }
869
870 #[test]
871 fn test_get_agent_id_from_self_link_full_url() {
872 let pending = make_pending_with_links(
873 None,
874 vec![("self", "https://api.example.com/v1/agents/uuid-full-url")],
875 );
876 assert_eq!(pending.get_agent_id(), Some("uuid-full-url".to_string()));
877 }
878
879 #[test]
880 fn test_get_agent_id_from_self_link_trailing_slash() {
881 let pending = make_pending_with_links(None, vec![("self", "/v1/agents/uuid-trailing/")]);
882 assert_eq!(pending.get_agent_id(), Some("uuid-trailing".to_string()));
883 }
884
885 #[test]
886 fn test_get_agent_id_prefers_field_over_link() {
887 let pending =
888 make_pending_with_links(Some("field-id"), vec![("self", "/v1/agents/link-id")]);
889 assert_eq!(pending.get_agent_id(), Some("field-id".to_string()));
890 }
891
892 #[test]
893 fn test_get_agent_id_no_self_link() {
894 let pending = make_pending_with_links(None, vec![("other", "/v1/something/else")]);
895 assert_eq!(pending.get_agent_id(), None);
896 }
897
898 #[test]
899 fn test_get_agent_id_empty_links() {
900 let pending = make_pending_with_links(None, vec![]);
901 assert_eq!(pending.get_agent_id(), None);
902 }
903}