1use std::sync::{Arc, OnceLock};
8
9use systemprompt_analytics::{AnalyticsService, FingerprintRepository};
10use systemprompt_config::ProfileBootstrap;
11use systemprompt_database::{Database, MigrationConfig, install_extension_schemas_full};
12use systemprompt_extension::ExtensionRegistry;
13use systemprompt_marketplace::{AllowAllFilter, MarketplaceFilter, discover_filters};
14use systemprompt_mcp::services::registry::RegistryService;
15use systemprompt_models::auth::UserRole;
16use systemprompt_models::services::{SystemAdmin, SystemAdminConfig};
17use systemprompt_models::{AppPaths, Config, ContentConfigRaw, ContentRouting};
18use systemprompt_security::authz::{AuthzDecisionHook, SharedAuthzHook};
19use systemprompt_users::UserService;
20
21use crate::context::{AppContext, ConfigPlane, DataPlane, Plugins, Subsystems};
22use crate::error::{RuntimeError, RuntimeResult};
23use crate::registry::ModuleApiRegistry;
24
25#[derive(Default)]
34pub struct AppContextBuilder {
35 extension_registry: Option<ExtensionRegistry>,
36 show_startup_warnings: bool,
37 marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
38 authz_hook: Option<SharedAuthzHook>,
39 install_schemas: bool,
40 migration_config: MigrationConfig,
41}
42
43impl std::fmt::Debug for AppContextBuilder {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("AppContextBuilder")
46 .field("extension_registry", &self.extension_registry.is_some())
47 .field("show_startup_warnings", &self.show_startup_warnings)
48 .field("marketplace_filter", &self.marketplace_filter.is_some())
49 .field("authz_hook", &self.authz_hook.is_some())
50 .field("install_schemas", &self.install_schemas)
51 .field("migration_config", &self.migration_config)
52 .finish()
53 }
54}
55
56impl AppContextBuilder {
57 #[must_use]
58 pub fn new() -> Self {
59 Self::default()
60 }
61
62 #[must_use]
65 pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
66 self.extension_registry = Some(registry);
67 self
68 }
69
70 #[must_use]
71 pub const fn with_startup_warnings(mut self, show: bool) -> Self {
72 self.show_startup_warnings = show;
73 self
74 }
75
76 #[must_use]
80 pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
81 self.marketplace_filter = Some(filter);
82 self
83 }
84
85 #[must_use]
89 pub const fn with_migrations(mut self, install: bool) -> Self {
90 self.install_schemas = install;
91 self
92 }
93
94 #[must_use]
98 pub fn with_authz_hook<H>(mut self, hook: H) -> Self
99 where
100 H: AuthzDecisionHook + 'static,
101 {
102 self.authz_hook = Some(Arc::new(hook));
103 self
104 }
105
106 #[must_use]
112 pub fn with_shared_authz_hook(mut self, hook: SharedAuthzHook) -> Self {
113 self.authz_hook = Some(hook);
114 self
115 }
116
117 #[must_use]
118 pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
119 self.migration_config = config;
120 self
121 }
122
123 pub async fn build(self) -> RuntimeResult<AppContext> {
124 let profile = ProfileBootstrap::get()?;
130 let app_paths = Arc::new(AppPaths::from_profile(&profile.paths)?);
131 systemprompt_files::FilesConfig::init(&app_paths)?;
132 systemprompt_config::try_init_config()
133 .map_err(|err| RuntimeError::Internal(format!("config init: {err}")))?;
134 let config = Arc::new(Config::get()?.clone());
135
136 let database = Arc::new(
137 Database::from_config_with_write(
138 &config.database_type,
139 &config.database_url,
140 config.database_write_url.as_deref(),
141 )
142 .await?,
143 );
144
145 let authz_audit_pool = database.write_pool_arc().ok();
146 let authz_hook = systemprompt_security::authz::build_authz_hook(
147 profile.governance.as_ref(),
148 authz_audit_pool,
149 self.authz_hook,
150 )
151 .map_err(|err| RuntimeError::Internal(format!("authz bootstrap: {err}")))?;
152
153 systemprompt_logging::init_logging(Arc::clone(&database));
154
155 if config.database_write_url.is_some() {
156 tracing::debug!(
157 "Database read/write separation enabled: reads from replica, writes to primary"
158 );
159 }
160
161 let api_registry = Arc::new(ModuleApiRegistry::new());
162
163 let registry = match self.extension_registry {
164 Some(registry) => registry,
165 None => ExtensionRegistry::discover()?,
166 };
167 registry.validate()?;
168
169 if self.install_schemas {
170 install_extension_schemas_full(®istry, database.write(), &[], self.migration_config)
171 .await?;
172 }
173
174 let extension_registry = Arc::new(registry);
175
176 let geoip_reader = AppContext::load_geoip_database(&config, self.show_startup_warnings);
177 let content_config = AppContext::load_content_config(&config, &app_paths);
178 let content_routing = content_routing_from(content_config.as_ref());
179 let route_classifier = Arc::new(systemprompt_models::RouteClassifier::new(
180 content_routing.clone(),
181 ));
182 let analytics_service = Arc::new(AnalyticsService::new(
183 &database,
184 geoip_reader.clone(),
185 content_routing,
186 )?);
187
188 let fingerprint_repo = match FingerprintRepository::new(&database) {
189 Ok(repo) => Some(Arc::new(repo)),
190 Err(e) => {
191 tracing::warn!(error = %e, "Failed to initialize fingerprint repository");
192 None
193 },
194 };
195
196 let user_service = Arc::new(UserService::new(&database)?);
200
201 let system_admin = resolve_and_install_system_admin(&config, &user_service).await?;
202 let mcp_registry = RegistryService::new(system_admin.id().clone());
203
204 let marketplace_filter = self
205 .marketplace_filter
206 .unwrap_or_else(|| build_marketplace_filter(&database));
207
208 let event_bridge = Arc::new(OnceLock::new());
209
210 Ok(AppContext::from_parts(
211 DataPlane {
212 database,
213 analytics_service,
214 fingerprint_repo,
215 user_service: Some(user_service),
216 },
217 ConfigPlane {
218 config,
219 app_paths,
220 content_config,
221 route_classifier,
222 },
223 Plugins {
224 extension_registry,
225 api_registry,
226 mcp_registry,
227 marketplace_filter,
228 },
229 Subsystems {
230 system_admin,
231 authz_hook,
232 event_bridge,
233 geoip_reader,
234 },
235 ))
236 }
237}
238
239async fn resolve_and_install_system_admin(
240 config: &Config,
241 users: &Arc<UserService>,
242) -> RuntimeResult<Arc<SystemAdmin>> {
243 let cfg = SystemAdminConfig {
244 username: config.system_admin_username.clone(),
245 };
246 let resolved = resolve_system_admin(&cfg, users.as_ref()).await?;
247 systemprompt_logging::install_log_attribution(resolved.clone());
248 Ok(Arc::new(resolved))
249}
250
251async fn resolve_system_admin(
252 cfg: &SystemAdminConfig,
253 users: &UserService,
254) -> RuntimeResult<SystemAdmin> {
255 let user = users.find_by_name(&cfg.username).await?.ok_or_else(|| {
256 RuntimeError::SystemAdminNotFound {
257 username: cfg.username.clone(),
258 }
259 })?;
260 if !user.is_active() {
261 return Err(RuntimeError::SystemAdminInactive {
262 username: cfg.username.clone(),
263 });
264 }
265 let admin_role = UserRole::Admin.as_str();
266 if !user.roles.iter().any(|r| r == admin_role) {
267 return Err(RuntimeError::SystemAdminMissingRole {
268 username: cfg.username.clone(),
269 });
270 }
271 Ok(SystemAdmin::new(user.id, user.name))
272}
273
274fn build_marketplace_filter(
275 database: &systemprompt_database::DbPool,
276) -> Arc<dyn MarketplaceFilter> {
277 for reg in discover_filters() {
278 match (reg.factory)(database) {
279 Ok(filter) => {
280 tracing::debug!(
281 priority = reg.priority,
282 "marketplace filter registered via inventory; using highest-priority impl",
283 );
284 return filter;
285 },
286 Err(err) => {
287 tracing::error!(
288 priority = reg.priority,
289 error = %err,
290 "marketplace filter factory failed; trying next candidate",
291 );
292 },
293 }
294 }
295 let fallback: Arc<dyn MarketplaceFilter> = Arc::new(AllowAllFilter);
296 fallback
297}
298
299fn content_routing_from(
300 content_config: Option<&Arc<ContentConfigRaw>>,
301) -> Option<Arc<dyn ContentRouting>> {
302 let concrete = Arc::clone(content_config?);
303 let routing: Arc<dyn ContentRouting> = concrete;
304 Some(routing)
305}