use std::future::Future;
use std::pin::Pin;
use crate::config::AgentConfig;
use crate::config_provider::ConfigProvider;
use crate::error::ConfigError;
use crate::tenant::TenantContext;
pub struct HttpConfigProvider {
base_url: String,
token: String,
client: reqwest::Client,
}
impl HttpConfigProvider {
pub fn new(base_url: impl Into<String>, token: impl Into<String>) -> Self {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap_or_default();
Self {
base_url: base_url.into().trim_end_matches('/').to_string(),
token: token.into(),
client,
}
}
}
impl ConfigProvider for HttpConfigProvider {
fn agent_config<'a>(
&'a self,
_tenant: &'a TenantContext,
agent_id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<AgentConfig, ConfigError>> + Send + 'a>> {
Box::pin(async move {
let url = format!("{}/api/v1/designer/agents/{agent_id}", self.base_url);
let resp = self
.client
.get(&url)
.bearer_auth(&self.token)
.send()
.await
.map_err(|e| {
ConfigError::Internal(format!("agent registry request failed: {e}"))
})?;
match resp.status().as_u16() {
200 => resp
.json::<AgentConfig>()
.await
.map_err(|e| ConfigError::Misconfigured(format!("agent config decode: {e}"))),
404 => Err(ConfigError::AgentNotFound(agent_id.to_string())),
401 | 403 => Err(ConfigError::Misconfigured(format!(
"agent registry auth rejected (status {})",
resp.status().as_u16()
))),
other => Err(ConfigError::Internal(format!(
"agent registry returned status {other}"
))),
}
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn agent_config_json() -> serde_json::Value {
serde_json::json!({
"agent_id": "bot",
"system_prompt": "be helpful",
"tools": [{ "extension_id": "greentic.tavily", "tool_name": "web_search" }],
"llm": { "provider": "openai", "model": "gpt-4o-mini" },
"limits": {}
})
}
#[tokio::test]
async fn fetches_and_parses_agent_config() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/designer/agents/bot"))
.and(header("authorization", "Bearer gtc_live_x"))
.respond_with(ResponseTemplate::new(200).set_body_json(agent_config_json()))
.mount(&server)
.await;
let provider = HttpConfigProvider::new(server.uri(), "gtc_live_x");
let tenant = TenantContext::new("t", "e");
let cfg = provider.agent_config(&tenant, "bot").await.unwrap();
assert_eq!(cfg.agent_id, "bot");
assert_eq!(cfg.llm.model, "gpt-4o-mini");
assert_eq!(cfg.tools.len(), 1);
}
#[tokio::test]
async fn maps_404_to_agent_not_found() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(404))
.mount(&server)
.await;
let provider = HttpConfigProvider::new(server.uri(), "gtc_live_x");
let tenant = TenantContext::new("t", "e");
let result = provider.agent_config(&tenant, "ghost").await;
assert!(matches!(result, Err(ConfigError::AgentNotFound(_))));
}
#[tokio::test]
async fn maps_5xx_to_internal() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(503))
.mount(&server)
.await;
let provider = HttpConfigProvider::new(server.uri(), "gtc_live_x");
let tenant = TenantContext::new("t", "e");
let result = provider.agent_config(&tenant, "bot").await;
assert!(matches!(result, Err(ConfigError::Internal(_))));
}
#[tokio::test]
async fn maps_malformed_body_to_misconfigured() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(200).set_body_string("{not json"))
.mount(&server)
.await;
let provider = HttpConfigProvider::new(server.uri(), "gtc_live_x");
let tenant = TenantContext::new("t", "e");
let result = provider.agent_config(&tenant, "bot").await;
assert!(matches!(result, Err(ConfigError::Misconfigured(_))));
}
#[tokio::test]
async fn maps_auth_rejection_to_misconfigured() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(401))
.mount(&server)
.await;
let provider = HttpConfigProvider::new(server.uri(), "gtc_live_bad");
let tenant = TenantContext::new("t", "e");
let result = provider.agent_config(&tenant, "bot").await;
assert!(matches!(result, Err(ConfigError::Misconfigured(_))));
}
}