use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Protocol {
#[serde(rename = "A2A")]
A2A,
#[serde(rename = "MCP")]
Mcp,
#[serde(rename = "HTTP-API", alias = "HTTP_API")]
HttpApi,
}
impl fmt::Display for Protocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::A2A => write!(f, "A2A"),
Self::Mcp => write!(f, "MCP"),
Self::HttpApi => write!(f, "HTTP-API"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Transport {
#[serde(rename = "STREAMABLE-HTTP", alias = "STREAMABLE_HTTP")]
StreamableHttp,
#[serde(rename = "SSE")]
Sse,
#[serde(rename = "JSON-RPC", alias = "JSON_RPC")]
JsonRpc,
#[serde(rename = "GRPC")]
Grpc,
#[serde(rename = "REST")]
Rest,
#[serde(rename = "HTTP")]
Http,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AgentFunction {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
impl AgentFunction {
pub fn new(id: impl Into<String>, name: impl Into<String>, tags: Vec<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
tags,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentEndpoint {
pub agent_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta_data_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_url: Option<String>,
pub protocol: Protocol,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub transports: Vec<Transport>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub functions: Vec<AgentFunction>,
}
impl AgentEndpoint {
pub fn new(agent_url: impl Into<String>, protocol: Protocol) -> Self {
Self {
agent_url: agent_url.into(),
meta_data_url: None,
documentation_url: None,
protocol,
transports: Vec::new(),
functions: Vec::new(),
}
}
pub fn with_transports(mut self, transports: Vec<Transport>) -> Self {
self.transports = transports;
self
}
pub fn with_functions(mut self, functions: Vec<AgentFunction>) -> Self {
self.functions = functions;
self
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentRegistrationRequest {
pub agent_display_name: String,
pub agent_host: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_description: Option<String>,
#[serde(rename = "identityCsrPEM")]
pub identity_csr_pem: String,
#[serde(rename = "serverCsrPEM", skip_serializing_if = "Option::is_none")]
pub server_csr_pem: Option<String>,
#[serde(
rename = "serverCertificatePEM",
skip_serializing_if = "Option::is_none"
)]
pub server_certificate_pem: Option<String>,
#[serde(
rename = "serverCertificateChainPEM",
skip_serializing_if = "Option::is_none"
)]
pub server_certificate_chain_pem: Option<String>,
pub endpoints: Vec<AgentEndpoint>,
}
impl AgentRegistrationRequest {
pub fn new(
agent_display_name: impl Into<String>,
agent_host: impl Into<String>,
version: impl Into<String>,
identity_csr_pem: impl Into<String>,
endpoints: Vec<AgentEndpoint>,
) -> Self {
Self {
agent_display_name: agent_display_name.into(),
agent_host: agent_host.into(),
version: version.into(),
agent_description: None,
identity_csr_pem: identity_csr_pem.into(),
server_csr_pem: None,
server_certificate_pem: None,
server_certificate_chain_pem: None,
endpoints,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.agent_description = Some(description.into());
self
}
pub fn with_server_csr_pem(mut self, csr: impl Into<String>) -> Self {
self.server_csr_pem = Some(csr.into());
self
}
pub fn with_server_certificate_pem(mut self, cert: impl Into<String>) -> Self {
self.server_certificate_pem = Some(cert.into());
self
}
pub fn with_server_certificate_chain_pem(mut self, chain: impl Into<String>) -> Self {
self.server_certificate_chain_pem = Some(chain.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum RegistrationStatus {
PendingValidation,
PendingCerts,
PendingDns,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum AgentLifecycleStatus {
PendingValidation,
PendingDns,
Active,
Failed,
Expired,
Revoked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Link {
pub rel: String,
pub href: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DnsRecord {
pub name: String,
#[serde(rename = "type")]
pub record_type: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i32>,
#[serde(default = "default_true")]
pub required: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ChallengeType {
#[serde(rename = "DNS_01")]
Dns01,
#[serde(rename = "HTTP_01")]
Http01,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DnsRecordDetails {
pub name: String,
#[serde(rename = "type")]
pub record_type: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ChallengeInfo {
#[serde(rename = "type")]
pub challenge_type: ChallengeType,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_authorization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub http_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_record: Option<DnsRecordDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum NextStepAction {
ConfigureDns,
ConfigureHttp,
VerifyDns,
ValidateDomain,
Wait,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct NextStep {
pub action: NextStepAction,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub estimated_time_minutes: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RegistrationPending {
pub status: RegistrationStatus,
pub ans_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
pub next_steps: Vec<NextStep>,
#[serde(default)]
pub challenges: Vec<ChallengeInfo>,
#[serde(default)]
pub dns_records: Vec<DnsRecord>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
#[serde(default)]
pub links: Vec<Link>,
}
impl RegistrationPending {
pub fn get_agent_id(&self) -> Option<String> {
if let Some(ref id) = self.agent_id {
return Some(id.clone());
}
self.links
.iter()
.find(|link| link.rel == "self")
.and_then(|link| {
link.href
.trim_end_matches('/')
.rsplit('/')
.next()
.filter(|s| !s.is_empty() && *s != "agents")
.map(String::from)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum RegistrationPhase {
Initialization,
DomainValidation,
CertificateIssuance,
DnsProvisioning,
Completed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentStatus {
pub status: AgentLifecycleStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<RegistrationPhase>,
#[serde(default)]
pub completed_steps: Vec<String>,
#[serde(default)]
pub pending_steps: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentDetails {
pub agent_id: String,
pub agent_display_name: String,
pub agent_host: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_description: Option<String>,
pub ans_name: String,
pub version: String,
pub agent_status: AgentLifecycleStatus,
pub endpoints: Vec<AgentEndpoint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registration_timestamp: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_renewal_timestamp: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registration_pending: Option<RegistrationPending>,
#[serde(default)]
pub links: Vec<Link>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SearchCriteria {
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol: Option<Protocol>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_host: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentSearchResult {
pub ans_name: String,
pub agent_id: String,
pub agent_display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_description: Option<String>,
pub version: String,
pub agent_host: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registration_timestamp: Option<DateTime<Utc>>,
pub endpoints: Vec<AgentEndpoint>,
#[serde(default)]
pub links: Vec<Link>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentSearchResponse {
pub agents: Vec<AgentSearchResult>,
pub total_count: i32,
pub returned_count: i32,
pub limit: i32,
pub offset: i32,
pub has_more: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_criteria: Option<SearchCriteria>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum RevocationReason {
KeyCompromise,
CessationOfOperation,
AffiliationChanged,
Superseded,
CertificateHold,
PrivilegeWithdrawn,
AaCompromise,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct AgentRevocationRequest {
pub reason: RevocationReason,
#[serde(skip_serializing_if = "Option::is_none")]
pub comments: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentRevocationResponse {
pub agent_id: Uuid,
pub ans_name: String,
pub status: AgentLifecycleStatus,
pub revoked_at: DateTime<Utc>,
pub reason: RevocationReason,
pub links: Vec<Link>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CertificateResponse {
pub csr_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_issuer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_serial_number: Option<String>,
pub certificate_valid_from: DateTime<Utc>,
pub certificate_valid_to: DateTime<Utc>,
#[serde(rename = "certificatePEM")]
pub certificate_pem: String,
#[serde(rename = "chainPEM", skip_serializing_if = "Option::is_none")]
pub chain_pem: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_public_key_algorithm: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_signature_algorithm: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CsrSubmissionRequest {
#[serde(rename = "csrPEM")]
pub csr_pem: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CsrSubmissionResponse {
pub csr_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum CsrType {
Server,
Identity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum CsrStatus {
Pending,
Signed,
Rejected,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CsrStatusResponse {
pub csr_id: Uuid,
#[serde(rename = "type")]
pub csr_type: CsrType,
pub status: CsrStatus,
pub submitted_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failure_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentResolutionRequest {
pub agent_host: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AgentResolutionResponse {
pub ans_name: String,
pub links: Vec<Link>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum EventType {
AgentRegistered,
AgentRenewed,
AgentRevoked,
AgentVersionUpdated,
}
impl std::fmt::Display for EventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AgentRegistered => write!(f, "AGENT_REGISTERED"),
Self::AgentRenewed => write!(f, "AGENT_RENEWED"),
Self::AgentRevoked => write!(f, "AGENT_REVOKED"),
Self::AgentVersionUpdated => write!(f, "AGENT_VERSION_UPDATED"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct EventItem {
pub log_id: String,
pub event_type: EventType,
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
pub agent_id: Uuid,
pub ans_name: String,
pub agent_host: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_description: Option<String>,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub endpoints: Vec<AgentEndpoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct EventPageResponse {
pub items: Vec<EventItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_log_id: Option<String>,
}
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#[cfg(test)]
mod tests {
use super::*;
fn make_pending_with_links(
agent_id: Option<&str>,
links: Vec<(&str, &str)>,
) -> RegistrationPending {
RegistrationPending {
status: RegistrationStatus::PendingValidation,
ans_name: "ans://v1.0.0.agent.example.com".to_string(),
agent_id: agent_id.map(String::from),
next_steps: vec![],
challenges: vec![],
dns_records: vec![],
expires_at: None,
links: links
.into_iter()
.map(|(rel, href)| Link {
rel: rel.to_string(),
href: href.to_string(),
})
.collect(),
}
}
#[test]
fn test_get_agent_id_from_field() {
let pending = make_pending_with_links(Some("direct-id"), vec![]);
assert_eq!(pending.get_agent_id(), Some("direct-id".to_string()));
}
#[test]
fn test_get_agent_id_from_self_link_path() {
let pending = make_pending_with_links(None, vec![("self", "/v1/agents/uuid-from-link")]);
assert_eq!(pending.get_agent_id(), Some("uuid-from-link".to_string()));
}
#[test]
fn test_get_agent_id_from_self_link_full_url() {
let pending = make_pending_with_links(
None,
vec![("self", "https://api.example.com/v1/agents/uuid-full-url")],
);
assert_eq!(pending.get_agent_id(), Some("uuid-full-url".to_string()));
}
#[test]
fn test_get_agent_id_from_self_link_trailing_slash() {
let pending = make_pending_with_links(None, vec![("self", "/v1/agents/uuid-trailing/")]);
assert_eq!(pending.get_agent_id(), Some("uuid-trailing".to_string()));
}
#[test]
fn test_get_agent_id_prefers_field_over_link() {
let pending =
make_pending_with_links(Some("field-id"), vec![("self", "/v1/agents/link-id")]);
assert_eq!(pending.get_agent_id(), Some("field-id".to_string()));
}
#[test]
fn test_get_agent_id_no_self_link() {
let pending = make_pending_with_links(None, vec![("other", "/v1/something/else")]);
assert_eq!(pending.get_agent_id(), None);
}
#[test]
fn test_get_agent_id_empty_links() {
let pending = make_pending_with_links(None, vec![]);
assert_eq!(pending.get_agent_id(), None);
}
}