1#![allow(missing_docs)]
4#![allow(dead_code)]
5#![allow(unused_imports)]
6#![allow(hidden_glob_reexports)]
7#![allow(ambiguous_glob_reexports)]
8#![allow(private_interfaces)]
9#![allow(clippy::useless_conversion)]
10#![allow(clippy::unnecessary_sort_by)]
11#![allow(clippy::empty_line_after_doc_comments)]
12#![allow(clippy::too_many_arguments)]
13
14use std::sync::Arc;
15
16use axum::routing::get;
17use axum::Router;
18use cordis::{Context, CordisError, Plugin, PluginRegistry, Service};
19
20pub mod config;
21pub mod error;
22pub mod pipeline_hook;
23
24pub mod overlay;
25pub mod toon_config;
26
27#[cfg(feature = "postgres")]
28pub mod api;
29#[cfg(feature = "postgres")]
30pub mod auth;
31#[cfg(feature = "postgres")]
32pub mod middleware;
33
34#[cfg(feature = "postgres")]
35pub mod active_runs;
36#[cfg(feature = "postgres")]
37pub mod observability;
38
39#[cfg(feature = "postgres")]
40pub use ares_agent::skills as skill_engine;
41#[cfg(any(feature = "postgres", feature = "skills"))]
42pub use ares_agent::skills;
43#[cfg(feature = "postgres")]
44pub use ares_agent::trigger as trigger_engine;
45#[cfg(feature = "postgres")]
46pub use ares_agent::workflows;
47pub use ares_agent::EmergencyStop;
48
49pub use ares_llm::ConfigBasedLLMFactory;
50pub use ares_rag as rag;
51pub use ares_types::{models, types};
52pub use config::{AuthConfig, ServerConfig};
53pub use error::{app_error_into_response, HttpError};
54pub use overlay::{
55 AresConfig, AresConfigManager, ConfigError, Overlay, OverlayConfig, OverlayPlugin,
56};
57#[cfg(feature = "postgres")]
58pub use pipeline_hook::{PipelineFanout, PipelineFanoutHandle, PipelineOrigin};
59pub use toon_config::DynamicConfigManager;
60
61pub use ares_agent as agents;
63pub use ares_agent::memory;
64#[cfg(feature = "postgres")]
65pub use ares_agent::research;
66pub mod db {
67 pub use ares_store::*;
68}
69pub mod utils {
70 pub use crate::overlay as toml_config;
71 pub use crate::toon_config;
72}
73
74pub type Result<T> = std::result::Result<T, HttpError>;
76
77pub struct Http {
79 pub router: Router,
80}
81
82impl Service for Http {
83 fn name(&self) -> &'static str {
84 "http"
85 }
86}
87
88pub struct HttpPlugin;
91
92impl Plugin for HttpPlugin {
93 type Config = ServerConfig;
94 type Provides = Http;
95
96 fn apply(
97 &self,
98 ctx: &Arc<Context>,
99 _config: Self::Config,
100 ) -> std::result::Result<Arc<Http>, CordisError> {
101 Ok(Arc::new(Http {
102 router: build_router(Arc::clone(ctx)),
103 }))
104 }
105}
106
107pub fn cordis_routes() -> Router<Arc<Context>> {
109 Router::new()
110 .route("/health", get(|| async { "OK" }))
111 .route("/health/context", get(health_context))
112}
113
114async fn health_context(
115 axum::extract::State(ctx): axum::extract::State<Arc<Context>>,
116) -> impl axum::response::IntoResponse {
117 let _events = ctx.get::<cordis::EventsService>();
118 let _registry = ctx.get::<cordis::RegistryService>();
119 let _exec = ctx.get::<ares_agent::Execute>();
120 "OK"
121}
122
123#[cfg(feature = "postgres")]
125pub async fn resolve_model_tier(
126 tenant_id: &str,
127 tier_name: &str,
128 pool: &sqlx::PgPool,
129 config: &AresConfig,
130) -> Option<(String, String)> {
131 let store = ares_store::tenant_model_tiers::TenantModelTierStore::new(pool);
132 if let Ok(Some(tier)) = store.get(tenant_id, tier_name).await {
133 return Some((tier.provider_name, tier.model_name));
134 }
135 config
136 .models
137 .get(tier_name)
138 .map(|mc| (mc.provider.clone(), mc.model.clone()))
139}
140
141pub fn build_router(ctx: Arc<Context>) -> Router {
145 let mut app = cordis_routes();
146 #[cfg(feature = "postgres")]
147 {
148 if let (Some(auth), Some(db)) = (
149 ctx.get::<crate::auth::jwt::AuthService>(),
150 ctx.get::<ares_store::TenantDb>(),
151 ) {
152 app = app.nest("/api", crate::api::routes::create_router(auth, db));
153 }
154 }
155 let _ = ctx.get::<cordis::EventsService>();
156 let _ = ctx.get::<cordis::RegistryService>();
157 let _ = ctx.get::<ares_agent::Execute>();
158 let _ = ctx.get::<ares_tools::Tools>();
159 app = app.layer(axum::middleware::from_fn_with_state(
162 Arc::clone(&ctx),
163 |axum::extract::State(ctx): axum::extract::State<Arc<Context>>,
164 mut req: axum::extract::Request,
165 next: axum::middleware::Next| async move {
166 req.extensions_mut().insert(ctx);
167 next.run(req).await
168 },
169 ));
170 app.with_state(ctx)
171}
172
173fn block_on_plugin<S: Service + 'static>(
174 ctx: &Arc<Context>,
175 svc: S,
176) -> std::result::Result<cordis::FiberId, CordisError> {
177 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(ctx.plugin(svc)))
178}
179
180#[cfg(feature = "postgres")]
181fn factory_auth(
182 ctx: &Arc<Context>,
183 config: &serde_json::Value,
184) -> std::result::Result<cordis::FiberId, CordisError> {
185 use crate::auth::jwt::AuthService;
186 let auth = if let Some(mgr) = ctx.get::<AresConfigManager>() {
187 let cfg = mgr.config();
188 let jwt_secret = cfg.jwt_secret().map_err(|e| {
189 CordisError::Configuration(format!("JWT_SECRET environment variable must be set: {e}"))
190 })?;
191 AuthService::new(
192 jwt_secret,
193 cfg.auth.jwt_access_expiry,
194 cfg.auth.jwt_refresh_expiry,
195 )
196 } else {
197 let auth_cfg: AuthConfig =
198 if config.is_null() || config.as_object().is_some_and(|o| o.is_empty()) {
199 AuthConfig::default()
200 } else {
201 serde_json::from_value(config.clone()).map_err(|e| {
202 CordisError::Configuration(format!("invalid AuthService config: {e}"))
203 })?
204 };
205 let jwt_secret = std::env::var(&auth_cfg.jwt_secret_env).map_err(|_| {
206 CordisError::Configuration(format!(
207 "JWT_SECRET environment variable must be set ({})",
208 auth_cfg.jwt_secret_env
209 ))
210 })?;
211 AuthService::new(
212 jwt_secret,
213 auth_cfg.jwt_access_expiry,
214 auth_cfg.jwt_refresh_expiry,
215 )
216 };
217 tracing::info!("Auth service initialized");
218 block_on_plugin(ctx, auth)
219}
220
221fn factory_http(
222 ctx: &Arc<Context>,
223 config: &serde_json::Value,
224) -> std::result::Result<cordis::FiberId, CordisError> {
225 let server_cfg: ServerConfig =
226 if config.is_null() || config.as_object().is_some_and(|o| o.is_empty()) {
227 if let Some(mgr) = ctx.get::<AresConfigManager>() {
228 mgr.config().server.clone()
229 } else {
230 ServerConfig::default()
231 }
232 } else {
233 serde_json::from_value(config.clone())
234 .map_err(|e| CordisError::Configuration(format!("invalid Http config: {e}")))?
235 };
236 let _ = server_cfg;
237 block_on_plugin(
238 ctx,
239 Http {
240 router: build_router(Arc::clone(ctx)),
241 },
242 )
243}
244
245pub fn register_plugins(reg: &PluginRegistry) {
248 #[cfg(feature = "postgres")]
249 reg.register("AuthService", Arc::new(factory_auth));
250 reg.register("Http", Arc::new(factory_http));
251}
252
253#[cfg(feature = "inventory")]
254inventory::submit! { cordis::CordisInventory { name: "Http" } }
255#[cfg(all(feature = "inventory", feature = "postgres"))]
256inventory::submit! { cordis::CordisInventory { name: "AuthService" } }
257
258#[cfg(feature = "inventory")]
260inventory::submit! {
261 cordis::CordisPluginFactory { name: "Http", make: factory_http }
262}
263#[cfg(all(feature = "inventory", feature = "postgres"))]
264inventory::submit! {
265 cordis::CordisPluginFactory { name: "AuthService", make: factory_auth }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[tokio::test]
273 async fn http_plugin_serves_health() {
274 let ctx = Context::new_root();
275 let http = HttpPlugin
276 .apply(&ctx, ServerConfig::default())
277 .expect("Http::apply");
278 let server = axum_test::TestServer::new(http.router.clone()).expect("test server");
279 let response = server.get("/health").await;
280 response.assert_status_ok();
281 response.assert_text("OK");
282 }
283
284 #[tokio::test]
285 async fn http_plugin_serves_health_context() {
286 let ctx = Context::new_root();
287 let router = build_router(ctx);
288 let server = axum_test::TestServer::new(router).expect("test server");
289 let response = server.get("/health/context").await;
290 response.assert_status_ok();
291 }
292}
293
294#[cfg(all(test, feature = "inventory"))]
295mod inventory_probe_tests {
296 #[test]
297 fn probe_names_in_lib_target() {
298 let mut v: Vec<&'static str> = inventory::iter::<cordis::CordisInventory>
299 .into_iter()
300 .map(|e| e.name)
301 .collect();
302 v.sort();
303 println!("LIBNAMES: {:?}", v);
304 }
305}