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