use std::sync::Arc;
use crate::core::Egress;
use super::card::{AgentCard, CardInterface, WELL_KNOWN_PATH};
use super::card_sig::{CardSignatureError, CardVerifier};
pub const JSONRPC: &str = "JSONRPC";
#[derive(Debug, thiserror::Error)]
pub enum DiscoveryError {
#[error("this client may not connect to '{0}'")]
Refused(String),
#[error("the card could not be fetched: {0}")]
Unreachable(String),
#[error("the card at '{url}' is not a valid Agent Card: {detail}")]
Malformed { url: String, detail: String },
#[error("the card's signature was not acceptable: {0}")]
Signature(#[from] CardSignatureError),
#[error(
"the agent at '{url}' offers no {binding} interface speaking A2A {version} — \
it advertises: {offered}"
)]
NoUsableInterface {
url: String,
binding: String,
version: String,
offered: String,
},
}
#[derive(Debug, Clone, Default)]
pub struct CardClient {
egress: Option<Egress>,
verifier: Option<Arc<dyn CardVerifier>>,
}
impl CardClient {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn egress(mut self, egress: Egress) -> Self {
self.egress = Some(egress);
self
}
#[must_use]
pub fn verifying_with(mut self, verifier: Arc<dyn CardVerifier>) -> Self {
self.verifier = Some(verifier);
self
}
pub async fn discover(&self, origin: &str) -> Result<AgentCard, DiscoveryError> {
let url = format!("{}{WELL_KNOWN_PATH}", origin.trim_end_matches('/'));
self.fetch(&url).await
}
pub async fn fetch(&self, url: &str) -> Result<AgentCard, DiscoveryError> {
if let Some(egress) = &self.egress {
let host = reqwest::Url::parse(url)
.ok()
.and_then(|u| u.host_str().map(ToOwned::to_owned));
if let Err(e) = egress.permits(host.as_deref()) {
return Err(DiscoveryError::Refused(e.to_string()));
}
}
let response = reqwest::Client::new()
.get(url)
.header("Accept", "application/json")
.send()
.await
.map_err(|e| DiscoveryError::Unreachable(e.to_string()))?;
if !response.status().is_success() {
return Err(DiscoveryError::Unreachable(format!(
"{url} answered {}",
response.status()
)));
}
let card: AgentCard = response
.json()
.await
.map_err(|e| DiscoveryError::Malformed {
url: url.to_owned(),
detail: e.to_string(),
})?;
if let Some(verifier) = &self.verifier {
card.verify(verifier.as_ref())?;
}
Ok(card)
}
}
impl AgentCard {
#[must_use]
pub fn select_interface(&self, binding: &str, version: &str) -> Option<&CardInterface> {
let want = super::protocol_major_minor(version)?;
self.supported_interfaces.iter().find(|i| {
i.protocol_binding == binding
&& super::protocol_major_minor(&i.protocol_version) == Some(want)
})
}
#[cfg(feature = "a2a")]
pub fn endpoint(&self) -> Result<super::a2a::Endpoint, DiscoveryError> {
let version = super::PROTOCOL_VERSION;
let iface = self.select_interface(JSONRPC, version).ok_or_else(|| {
DiscoveryError::NoUsableInterface {
url: self.name.clone(),
binding: JSONRPC.to_owned(),
version: version.to_owned(),
offered: self
.supported_interfaces
.iter()
.map(|i| format!("{} {}", i.protocol_binding, i.protocol_version))
.collect::<Vec<_>>()
.join(", "),
}
})?;
let endpoint = super::a2a::Endpoint::new(iface.url.clone());
Ok(match &iface.tenant {
Some(t) => endpoint.for_tenant(t.clone()),
None => endpoint,
})
}
}