use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::sync::atomic::AtomicU64;
use crate::a2a::A2A;
use crate::actions::ActionsAPI;
use crate::agentcards::AgentCards;
use crate::agents::AgentsAPI;
use crate::approvals::ApprovalsAPI;
use crate::delegations::Delegations;
use crate::error::{error_from_status, AgentTrustError, Result};
use crate::federation::Federation;
use crate::mcp::Mcp;
use crate::models::{ErrorResponse, HealthResponse};
use crate::sessions::SessionsAPI;
use crate::streaming::Streaming;
use crate::telemetry::TelemetryAPI;
use crate::tokens::TokensAPI;
use crate::wimse::Wimse;
pub const DEFAULT_BASE_URL: &str = "http://localhost:8080";
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub struct AgentTrustClient {
pub(crate) http: reqwest::blocking::Client,
pub(crate) base_url: String,
pub(crate) api_key: Option<String>,
}
impl AgentTrustClient {
pub fn builder() -> AgentTrustClientBuilder {
AgentTrustClientBuilder::default()
}
pub fn from_env() -> Result<Self> {
Self::from_env_with(|key| std::env::var(key).ok())
}
pub(crate) fn from_env_with<F>(reader: F) -> Result<Self>
where
F: Fn(&str) -> Option<String>,
{
let base_url = reader("AGENTTRUST_URL")
.filter(|v| !v.is_empty())
.or_else(|| reader("AGENTTRUST_BASE_URL").filter(|v| !v.is_empty()))
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
let api_key = reader("AGENTTRUST_API_KEY").filter(|v| !v.is_empty());
let mut builder = AgentTrustClientBuilder::default();
builder.base_url = base_url;
builder.api_key = api_key;
builder.build()
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn health(&self) -> Result<HealthResponse> {
self.request::<HealthResponse>("GET", "/health", None::<&()>)
}
pub fn agents(&self) -> AgentsAPI<'_> {
AgentsAPI { client: self }
}
pub fn tokens(&self) -> TokensAPI<'_> {
TokensAPI { client: self }
}
pub fn actions(&self) -> ActionsAPI<'_> {
ActionsAPI { client: self }
}
pub fn telemetry(&self) -> TelemetryAPI<'_> {
TelemetryAPI { client: self }
}
pub fn sessions(&self) -> SessionsAPI<'_> {
SessionsAPI { client: self }
}
pub fn approvals(&self) -> ApprovalsAPI<'_> {
ApprovalsAPI { client: self }
}
pub fn agentcards(&self) -> AgentCards<'_> {
AgentCards { client: self }
}
pub fn a2a(&self) -> A2A<'_> {
A2A {
client: self,
request_id: AtomicU64::new(0),
}
}
pub fn mcp(&self) -> Mcp<'_> {
Mcp { client: self }
}
pub fn delegations(&self) -> Delegations<'_> {
Delegations { client: self }
}
pub fn federation(&self) -> Federation<'_> {
Federation { client: self }
}
pub fn streaming(&self) -> Streaming<'_> {
Streaming { client: self }
}
pub fn wimse(&self) -> Wimse<'_> {
Wimse { client: self }
}
pub(crate) fn request<T: DeserializeOwned>(
&self,
method: &str,
path: &str,
body: Option<impl Serialize>,
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let mut req = match method {
"GET" => self.http.get(&url),
"POST" => self.http.post(&url),
"PUT" => self.http.put(&url),
"DELETE" => self.http.delete(&url),
"PATCH" => self.http.patch(&url),
_ => self.http.get(&url),
};
req = req.header(CONTENT_TYPE, "application/json");
if let Some(key) = &self.api_key {
req = req.header("X-API-Key", key);
}
if let Some(b) = body {
req = req.json(&b);
}
let resp = req.send()?;
let status = resp.status().as_u16();
if status < 200 || status >= 300 {
let body_text = resp.text().unwrap_or_default();
let message = serde_json::from_str::<ErrorResponse>(&body_text)
.map(|e| e.message)
.unwrap_or_else(|_| format!("HTTP {} error", status));
return Err(error_from_status(status, message));
}
let body_text = resp.text().unwrap_or_default();
if body_text.is_empty() {
let result: T = serde_json::from_str("{}")?;
return Ok(result);
}
let result: T = serde_json::from_str(&body_text)?;
Ok(result)
}
pub(crate) fn request_no_response(
&self,
method: &str,
path: &str,
body: Option<impl Serialize>,
) -> Result<()> {
let url = format!("{}{}", self.base_url, path);
let mut req = match method {
"GET" => self.http.get(&url),
"POST" => self.http.post(&url),
"PUT" => self.http.put(&url),
"DELETE" => self.http.delete(&url),
"PATCH" => self.http.patch(&url),
_ => self.http.get(&url),
};
req = req.header(CONTENT_TYPE, "application/json");
if let Some(key) = &self.api_key {
req = req.header("X-API-Key", key);
}
if let Some(b) = body {
req = req.json(&b);
}
let resp = req.send()?;
let status = resp.status().as_u16();
if status < 200 || status >= 300 {
let body_text = resp.text().unwrap_or_default();
let message = serde_json::from_str::<ErrorResponse>(&body_text)
.map(|e| e.message)
.unwrap_or_else(|_| format!("HTTP {} error", status));
return Err(error_from_status(status, message));
}
Ok(())
}
}
pub struct AgentTrustClientBuilder {
base_url: String,
api_key: Option<String>,
timeout: Duration,
default_headers: HeaderMap,
}
impl Default for AgentTrustClientBuilder {
fn default() -> Self {
Self {
base_url: DEFAULT_BASE_URL.to_string(),
api_key: None,
timeout: DEFAULT_TIMEOUT,
default_headers: HeaderMap::new(),
}
}
}
impl AgentTrustClientBuilder {
pub fn base_url(mut self, url: &str) -> Self {
self.base_url = url.trim_end_matches('/').to_string();
self
}
pub fn api_key(mut self, key: &str) -> Self {
self.api_key = Some(key.to_string());
self
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = duration;
self
}
pub fn default_header(mut self, name: &str, value: &str) -> Self {
if let Ok(v) = HeaderValue::from_str(value) {
if let Ok(n) = reqwest::header::HeaderName::from_bytes(name.as_bytes()) {
self.default_headers.insert(n, v);
}
}
self
}
pub fn build(self) -> Result<AgentTrustClient> {
let http = reqwest::blocking::ClientBuilder::new()
.timeout(self.timeout)
.default_headers(self.default_headers)
.build()
.map_err(AgentTrustError::Network)?;
Ok(AgentTrustClient {
http,
base_url: self.base_url.trim_end_matches('/').to_string(),
api_key: self.api_key,
})
}
}