systemprompt_runtime/
builder.rs1use 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::services::{SystemAdmin, SystemAdminConfig};
16use systemprompt_models::{AppPaths, Config, ContentConfigRaw, ContentRouting};
17use systemprompt_users::UserService;
18
19use crate::context::{AppContext, AppContextParts};
20use crate::context_loaders;
21use crate::error::{RuntimeError, RuntimeResult};
22use crate::registry::ModuleApiRegistry;
23
24#[derive(Default)]
33pub struct AppContextBuilder {
34 extension_registry: Option<ExtensionRegistry>,
35 show_startup_warnings: bool,
36 marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
37 install_schemas: bool,
38 migration_config: MigrationConfig,
39}
40
41impl std::fmt::Debug for AppContextBuilder {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct("AppContextBuilder")
44 .field("extension_registry", &self.extension_registry.is_some())
45 .field("show_startup_warnings", &self.show_startup_warnings)
46 .field("marketplace_filter", &self.marketplace_filter.is_some())
47 .field("install_schemas", &self.install_schemas)
48 .field("migration_config", &self.migration_config)
49 .finish()
50 }
51}
52
53impl AppContextBuilder {
54 #[must_use]
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 #[must_use]
62 pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
63 self.extension_registry = Some(registry);
64 self
65 }
66
67 #[must_use]
68 pub const fn with_startup_warnings(mut self, show: bool) -> Self {
69 self.show_startup_warnings = show;
70 self
71 }
72
73 #[must_use]
77 pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
78 self.marketplace_filter = Some(filter);
79 self
80 }
81
82 #[must_use]
86 pub const fn with_migrations(mut self, install: bool) -> Self {
87 self.install_schemas = install;
88 self
89 }
90
91 #[must_use]
92 pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
93 self.migration_config = config;
94 self
95 }
96
97 pub async fn build(self) -> RuntimeResult<AppContext> {
98 let profile = ProfileBootstrap::get()?;
99 let app_paths = Arc::new(AppPaths::from_profile(&profile.paths)?);
100 systemprompt_files::FilesConfig::init(&app_paths)?;
101 let config = Arc::new(Config::get()?.clone());
102
103 let database = Arc::new(
104 Database::from_config_with_write(
105 &config.database_type,
106 &config.database_url,
107 config.database_write_url.as_deref(),
108 )
109 .await?,
110 );
111
112 let authz_audit_pool = database.write_pool_arc().ok();
113 let authz_hook = systemprompt_security::authz::build_authz_hook(
114 profile.governance.as_ref(),
115 authz_audit_pool,
116 )
117 .map_err(|err| RuntimeError::Internal(format!("authz bootstrap: {err}")))?;
118
119 systemprompt_logging::init_logging(Arc::clone(&database));
120
121 if config.database_write_url.is_some() {
122 tracing::debug!(
123 "Database read/write separation enabled: reads from replica, writes to primary"
124 );
125 }
126
127 let api_registry = Arc::new(ModuleApiRegistry::new());
128
129 let registry = match self.extension_registry {
130 Some(registry) => registry,
131 None => ExtensionRegistry::discover()?,
132 };
133 registry.validate()?;
134
135 if self.install_schemas {
136 install_extension_schemas_full(
137 ®istry,
138 database.write_provider(),
139 &[],
140 self.migration_config,
141 )
142 .await?;
143 }
144
145 let extension_registry = Arc::new(registry);
146
147 let geoip_reader = AppContext::load_geoip_database(&config, self.show_startup_warnings);
148 let content_config = AppContext::load_content_config(&config, &app_paths);
149 let content_routing = content_routing_from(content_config.as_ref());
150 let route_classifier = Arc::new(systemprompt_models::RouteClassifier::new(
151 content_routing.clone(),
152 ));
153 let analytics_service = Arc::new(AnalyticsService::new(
154 &database,
155 geoip_reader.clone(),
156 content_routing,
157 )?);
158
159 let fingerprint_repo = match FingerprintRepository::new(&database) {
160 Ok(repo) => Some(Arc::new(repo)),
161 Err(e) => {
162 tracing::warn!(error = %e, "Failed to initialize fingerprint repository");
163 None
164 },
165 };
166
167 let user_service = match UserService::new(&database) {
168 Ok(svc) => Some(Arc::new(svc)),
169 Err(e) => {
170 tracing::warn!(error = %e, "Failed to initialize user service");
171 None
172 },
173 };
174
175 let system_admin = resolve_and_install_system_admin(&config, user_service.as_ref()).await?;
176 let mcp_registry = RegistryService::new(system_admin.id().clone());
177
178 let marketplace_filter = self
179 .marketplace_filter
180 .unwrap_or_else(|| build_marketplace_filter(&database));
181
182 let event_bridge = Arc::new(OnceLock::new());
183
184 Ok(AppContext::from_parts(AppContextParts {
185 config,
186 database,
187 api_registry,
188 extension_registry,
189 geoip_reader,
190 content_config,
191 route_classifier,
192 analytics_service,
193 fingerprint_repo,
194 user_service,
195 app_paths,
196 marketplace_filter,
197 event_bridge,
198 system_admin,
199 mcp_registry,
200 authz_hook,
201 }))
202 }
203}
204
205async fn resolve_and_install_system_admin(
206 config: &Config,
207 user_service: Option<&Arc<UserService>>,
208) -> RuntimeResult<Arc<SystemAdmin>> {
209 let users = user_service.ok_or(RuntimeError::SystemAdminUserServiceUnavailable)?;
210 let cfg = SystemAdminConfig {
211 username: config.system_admin_username.clone(),
212 };
213 let resolved = context_loaders::resolve_system_admin(&cfg, users.as_ref()).await?;
214 systemprompt_logging::install_log_attribution(resolved.clone());
215 Ok(Arc::new(resolved))
216}
217
218fn build_marketplace_filter(
219 database: &systemprompt_database::DbPool,
220) -> Arc<dyn MarketplaceFilter> {
221 for reg in discover_filters() {
222 match (reg.factory)(database) {
223 Ok(filter) => {
224 tracing::debug!(
225 priority = reg.priority,
226 "marketplace filter registered via inventory; using highest-priority impl",
227 );
228 return filter;
229 },
230 Err(err) => {
231 tracing::error!(
232 priority = reg.priority,
233 error = %err,
234 "marketplace filter factory failed; trying next candidate",
235 );
236 },
237 }
238 }
239 let fallback: Arc<dyn MarketplaceFilter> = Arc::new(AllowAllFilter);
240 fallback
241}
242
243fn content_routing_from(
244 content_config: Option<&Arc<ContentConfigRaw>>,
245) -> Option<Arc<dyn ContentRouting>> {
246 let concrete = Arc::clone(content_config?);
247 let routing: Arc<dyn ContentRouting> = concrete;
248 Some(routing)
249}