greentic_aw_runtime/
http_provider.rs1use std::future::Future;
7use std::pin::Pin;
8
9use crate::config::AgentConfig;
10use crate::config_provider::ConfigProvider;
11use crate::error::ConfigError;
12use crate::tenant::TenantContext;
13
14pub struct HttpConfigProvider {
16 base_url: String,
17 token: String,
18 client: reqwest::Client,
19}
20
21impl HttpConfigProvider {
22 pub fn new(base_url: impl Into<String>, token: impl Into<String>) -> Self {
25 let client = reqwest::Client::builder()
30 .timeout(std::time::Duration::from_secs(10))
31 .build()
32 .unwrap_or_default();
33 Self {
34 base_url: base_url.into().trim_end_matches('/').to_string(),
35 token: token.into(),
36 client,
37 }
38 }
39}
40
41impl ConfigProvider for HttpConfigProvider {
42 fn agent_config<'a>(
43 &'a self,
44 _tenant: &'a TenantContext,
45 agent_id: &'a str,
46 ) -> Pin<Box<dyn Future<Output = Result<AgentConfig, ConfigError>> + Send + 'a>> {
47 Box::pin(async move {
48 let url = format!("{}/api/v1/designer/agents/{agent_id}", self.base_url);
49 let resp = self
50 .client
51 .get(&url)
52 .bearer_auth(&self.token)
53 .send()
54 .await
55 .map_err(|e| {
56 ConfigError::Internal(format!("agent registry request failed: {e}"))
57 })?;
58
59 match resp.status().as_u16() {
60 200 => resp
61 .json::<AgentConfig>()
62 .await
63 .map_err(|e| ConfigError::Misconfigured(format!("agent config decode: {e}"))),
64 404 => Err(ConfigError::AgentNotFound(agent_id.to_string())),
65 401 | 403 => Err(ConfigError::Misconfigured(format!(
70 "agent registry auth rejected (status {})",
71 resp.status().as_u16()
72 ))),
73 other => Err(ConfigError::Internal(format!(
74 "agent registry returned status {other}"
75 ))),
76 }
77 })
78 }
79}
80
81#[cfg(test)]
82#[allow(clippy::unwrap_used, clippy::expect_used)]
83mod tests {
84 use super::*;
85 use wiremock::matchers::{header, method, path};
86 use wiremock::{Mock, MockServer, ResponseTemplate};
87
88 fn agent_config_json() -> serde_json::Value {
89 serde_json::json!({
90 "agent_id": "bot",
91 "system_prompt": "be helpful",
92 "tools": [{ "extension_id": "greentic.tavily", "tool_name": "web_search" }],
93 "llm": { "provider": "openai", "model": "gpt-4o-mini" },
94 "limits": {}
95 })
96 }
97
98 #[tokio::test]
99 async fn fetches_and_parses_agent_config() {
100 let server = MockServer::start().await;
101 Mock::given(method("GET"))
102 .and(path("/api/v1/designer/agents/bot"))
103 .and(header("authorization", "Bearer gtc_live_x"))
104 .respond_with(ResponseTemplate::new(200).set_body_json(agent_config_json()))
105 .mount(&server)
106 .await;
107
108 let provider = HttpConfigProvider::new(server.uri(), "gtc_live_x");
109 let tenant = TenantContext::new("t", "e");
110 let cfg = provider.agent_config(&tenant, "bot").await.unwrap();
111 assert_eq!(cfg.agent_id, "bot");
112 assert_eq!(cfg.llm.model, "gpt-4o-mini");
113 assert_eq!(cfg.tools.len(), 1);
114 }
115
116 #[tokio::test]
117 async fn maps_404_to_agent_not_found() {
118 let server = MockServer::start().await;
119 Mock::given(method("GET"))
120 .respond_with(ResponseTemplate::new(404))
121 .mount(&server)
122 .await;
123 let provider = HttpConfigProvider::new(server.uri(), "gtc_live_x");
124 let tenant = TenantContext::new("t", "e");
125 let result = provider.agent_config(&tenant, "ghost").await;
126 assert!(matches!(result, Err(ConfigError::AgentNotFound(_))));
127 }
128
129 #[tokio::test]
130 async fn maps_5xx_to_internal() {
131 let server = MockServer::start().await;
132 Mock::given(method("GET"))
133 .respond_with(ResponseTemplate::new(503))
134 .mount(&server)
135 .await;
136 let provider = HttpConfigProvider::new(server.uri(), "gtc_live_x");
137 let tenant = TenantContext::new("t", "e");
138 let result = provider.agent_config(&tenant, "bot").await;
139 assert!(matches!(result, Err(ConfigError::Internal(_))));
140 }
141
142 #[tokio::test]
143 async fn maps_malformed_body_to_misconfigured() {
144 let server = MockServer::start().await;
145 Mock::given(method("GET"))
146 .respond_with(ResponseTemplate::new(200).set_body_string("{not json"))
147 .mount(&server)
148 .await;
149 let provider = HttpConfigProvider::new(server.uri(), "gtc_live_x");
150 let tenant = TenantContext::new("t", "e");
151 let result = provider.agent_config(&tenant, "bot").await;
152 assert!(matches!(result, Err(ConfigError::Misconfigured(_))));
153 }
154
155 #[tokio::test]
156 async fn maps_auth_rejection_to_misconfigured() {
157 let server = MockServer::start().await;
158 Mock::given(method("GET"))
159 .respond_with(ResponseTemplate::new(401))
160 .mount(&server)
161 .await;
162 let provider = HttpConfigProvider::new(server.uri(), "gtc_live_bad");
163 let tenant = TenantContext::new("t", "e");
164 let result = provider.agent_config(&tenant, "bot").await;
165 assert!(matches!(result, Err(ConfigError::Misconfigured(_))));
166 }
167}