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, AppContextParts};
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()?;
125 let app_paths = Arc::new(AppPaths::from_profile(&profile.paths)?);
126 systemprompt_files::FilesConfig::init(&app_paths)?;
127 let config = Arc::new(Config::get()?.clone());
128
129 let database = Arc::new(
130 Database::from_config_with_write(
131 &config.database_type,
132 &config.database_url,
133 config.database_write_url.as_deref(),
134 )
135 .await?,
136 );
137
138 let authz_audit_pool = database.write_pool_arc().ok();
139 let authz_hook = systemprompt_security::authz::build_authz_hook(
140 profile.governance.as_ref(),
141 authz_audit_pool,
142 self.authz_hook,
143 )
144 .map_err(|err| RuntimeError::Internal(format!("authz bootstrap: {err}")))?;
145
146 systemprompt_logging::init_logging(Arc::clone(&database));
147
148 if config.database_write_url.is_some() {
149 tracing::debug!(
150 "Database read/write separation enabled: reads from replica, writes to primary"
151 );
152 }
153
154 let api_registry = Arc::new(ModuleApiRegistry::new());
155
156 let registry = match self.extension_registry {
157 Some(registry) => registry,
158 None => ExtensionRegistry::discover()?,
159 };
160 registry.validate()?;
161
162 if self.install_schemas {
163 install_extension_schemas_full(®istry, database.write(), &[], self.migration_config)
164 .await?;
165 }
166
167 let extension_registry = Arc::new(registry);
168
169 let geoip_reader = AppContext::load_geoip_database(&config, self.show_startup_warnings);
170 let content_config = AppContext::load_content_config(&config, &app_paths);
171 let content_routing = content_routing_from(content_config.as_ref());
172 let route_classifier = Arc::new(systemprompt_models::RouteClassifier::new(
173 content_routing.clone(),
174 ));
175 let analytics_service = Arc::new(AnalyticsService::new(
176 &database,
177 geoip_reader.clone(),
178 content_routing,
179 )?);
180
181 let fingerprint_repo = match FingerprintRepository::new(&database) {
182 Ok(repo) => Some(Arc::new(repo)),
183 Err(e) => {
184 tracing::warn!(error = %e, "Failed to initialize fingerprint repository");
185 None
186 },
187 };
188
189 let user_service = match UserService::new(&database) {
190 Ok(svc) => Some(Arc::new(svc)),
191 Err(e) => {
192 tracing::warn!(error = %e, "Failed to initialize user service");
193 None
194 },
195 };
196
197 let system_admin = resolve_and_install_system_admin(&config, user_service.as_ref()).await?;
198 let mcp_registry = RegistryService::new(system_admin.id().clone());
199
200 let marketplace_filter = self
201 .marketplace_filter
202 .unwrap_or_else(|| build_marketplace_filter(&database));
203
204 let event_bridge = Arc::new(OnceLock::new());
205
206 Ok(AppContext::from_parts(AppContextParts {
207 config,
208 database,
209 api_registry,
210 extension_registry,
211 geoip_reader,
212 content_config,
213 route_classifier,
214 analytics_service,
215 fingerprint_repo,
216 user_service,
217 app_paths,
218 marketplace_filter,
219 event_bridge,
220 system_admin,
221 mcp_registry,
222 authz_hook,
223 }))
224 }
225}
226
227async fn resolve_and_install_system_admin(
228 config: &Config,
229 user_service: Option<&Arc<UserService>>,
230) -> RuntimeResult<Arc<SystemAdmin>> {
231 let users = user_service.ok_or(RuntimeError::SystemAdminUserServiceUnavailable)?;
232 let cfg = SystemAdminConfig {
233 username: config.system_admin_username.clone(),
234 };
235 let resolved = resolve_system_admin(&cfg, users.as_ref()).await?;
236 systemprompt_logging::install_log_attribution(resolved.clone());
237 Ok(Arc::new(resolved))
238}
239
240async fn resolve_system_admin(
241 cfg: &SystemAdminConfig,
242 users: &UserService,
243) -> RuntimeResult<SystemAdmin> {
244 let user = users.find_by_name(&cfg.username).await?.ok_or_else(|| {
245 RuntimeError::SystemAdminNotFound {
246 username: cfg.username.clone(),
247 }
248 })?;
249 if !user.is_active() {
250 return Err(RuntimeError::SystemAdminInactive {
251 username: cfg.username.clone(),
252 });
253 }
254 let admin_role = UserRole::Admin.as_str();
255 if !user.roles.iter().any(|r| r == admin_role) {
256 return Err(RuntimeError::SystemAdminMissingRole {
257 username: cfg.username.clone(),
258 });
259 }
260 Ok(SystemAdmin::new(user.id, user.name))
261}
262
263fn build_marketplace_filter(
264 database: &systemprompt_database::DbPool,
265) -> Arc<dyn MarketplaceFilter> {
266 for reg in discover_filters() {
267 match (reg.factory)(database) {
268 Ok(filter) => {
269 tracing::debug!(
270 priority = reg.priority,
271 "marketplace filter registered via inventory; using highest-priority impl",
272 );
273 return filter;
274 },
275 Err(err) => {
276 tracing::error!(
277 priority = reg.priority,
278 error = %err,
279 "marketplace filter factory failed; trying next candidate",
280 );
281 },
282 }
283 }
284 let fallback: Arc<dyn MarketplaceFilter> = Arc::new(AllowAllFilter);
285 fallback
286}
287
288fn content_routing_from(
289 content_config: Option<&Arc<ContentConfigRaw>>,
290) -> Option<Arc<dyn ContentRouting>> {
291 let concrete = Arc::clone(content_config?);
292 let routing: Arc<dyn ContentRouting> = concrete;
293 Some(routing)
294}