systemprompt_runtime/builder/
mod.rs1mod assembly;
9
10use std::sync::{Arc, OnceLock};
11
12use systemprompt_analytics::{AnalyticsService, FingerprintRepository};
13use systemprompt_config::ProfileBootstrap;
14use systemprompt_database::{Database, MigrationConfig, install_extension_schemas_full};
15use systemprompt_extension::ExtensionRegistry;
16use systemprompt_marketplace::MarketplaceFilter;
17use systemprompt_mcp::services::registry::RegistryService;
18use systemprompt_models::{AppPaths, Config};
19use systemprompt_security::authz::{AuthzDecisionHook, SharedAuthzHook};
20use systemprompt_users::UserService;
21
22use crate::context::{AppContext, ConfigPlane, DataPlane, Plugins, Subsystems};
23use crate::error::{RuntimeError, RuntimeResult};
24use crate::registry::ModuleApiRegistry;
25
26#[derive(Default)]
35pub struct AppContextBuilder {
36 extension_registry: Option<ExtensionRegistry>,
37 show_startup_warnings: bool,
38 marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
39 authz_hook: Option<SharedAuthzHook>,
40 install_schemas: bool,
41 migration_config: MigrationConfig,
42}
43
44impl std::fmt::Debug for AppContextBuilder {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("AppContextBuilder")
47 .field("extension_registry", &self.extension_registry.is_some())
48 .field("show_startup_warnings", &self.show_startup_warnings)
49 .field("marketplace_filter", &self.marketplace_filter.is_some())
50 .field("authz_hook", &self.authz_hook.is_some())
51 .field("install_schemas", &self.install_schemas)
52 .field("migration_config", &self.migration_config)
53 .finish()
54 }
55}
56
57struct CoreLayer {
58 config: Arc<Config>,
59 app_paths: Arc<AppPaths>,
60 database: Arc<Database>,
61 authz_hook: SharedAuthzHook,
62}
63
64impl AppContextBuilder {
65 #[must_use]
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 #[must_use]
73 pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
74 self.extension_registry = Some(registry);
75 self
76 }
77
78 #[must_use]
79 pub const fn with_startup_warnings(mut self, show: bool) -> Self {
80 self.show_startup_warnings = show;
81 self
82 }
83
84 #[must_use]
88 pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
89 self.marketplace_filter = Some(filter);
90 self
91 }
92
93 #[must_use]
97 pub const fn with_migrations(mut self, install: bool) -> Self {
98 self.install_schemas = install;
99 self
100 }
101
102 #[must_use]
106 pub fn with_authz_hook<H>(mut self, hook: H) -> Self
107 where
108 H: AuthzDecisionHook + 'static,
109 {
110 self.authz_hook = Some(Arc::new(hook));
111 self
112 }
113
114 #[must_use]
120 pub fn with_shared_authz_hook(mut self, hook: SharedAuthzHook) -> Self {
121 self.authz_hook = Some(hook);
122 self
123 }
124
125 #[must_use]
126 pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
127 self.migration_config = config;
128 self
129 }
130
131 pub async fn build(self) -> RuntimeResult<AppContext> {
132 let CoreLayer {
133 config,
134 app_paths,
135 database,
136 authz_hook,
137 } = init_core(self.authz_hook).await?;
138
139 let api_registry = Arc::new(ModuleApiRegistry::new());
140 let extension_registry = init_extensions(
141 self.extension_registry,
142 self.install_schemas,
143 self.migration_config,
144 &database,
145 )
146 .await?;
147
148 let geoip_reader = AppContext::load_geoip_database(&config, self.show_startup_warnings);
149 let content_config = AppContext::load_content_config(&config, &app_paths);
150 let content_routing = assembly::content_routing_from(content_config.as_ref());
151 let route_classifier = Arc::new(systemprompt_models::RouteClassifier::new(
152 content_routing.clone(),
153 ));
154 let analytics_service = Arc::new(AnalyticsService::new(
155 &database,
156 geoip_reader.clone(),
157 content_routing,
158 )?);
159
160 let fingerprint_repo = match FingerprintRepository::new(&database) {
161 Ok(repo) => Some(Arc::new(repo)),
162 Err(e) => {
163 tracing::warn!(error = %e, "Failed to initialize fingerprint repository");
164 None
165 },
166 };
167
168 let user_service = Arc::new(UserService::new(&database)?);
172
173 let system_admin =
174 assembly::resolve_and_install_system_admin(&config, &user_service).await?;
175 let mcp_registry = RegistryService::new(system_admin.id().clone());
176
177 let marketplace_filter = self
178 .marketplace_filter
179 .unwrap_or_else(|| assembly::build_marketplace_filter(&database));
180
181 let event_bridge = Arc::new(OnceLock::new());
182
183 Ok(AppContext::from_parts(
184 DataPlane {
185 database,
186 analytics_service,
187 fingerprint_repo,
188 user_service: Some(user_service),
189 },
190 ConfigPlane {
191 config,
192 app_paths,
193 content_config,
194 route_classifier,
195 },
196 Plugins {
197 extension_registry,
198 api_registry,
199 mcp_registry,
200 marketplace_filter,
201 },
202 Subsystems {
203 system_admin,
204 authz_hook,
205 event_bridge,
206 geoip_reader,
207 },
208 ))
209 }
210}
211
212async fn init_core(authz_hook_override: Option<SharedAuthzHook>) -> RuntimeResult<CoreLayer> {
216 let profile = ProfileBootstrap::get()?;
217 let app_paths = Arc::new(AppPaths::from_profile(&profile.paths)?);
218 systemprompt_files::FilesConfig::init(&app_paths)?;
219 systemprompt_config::try_init_config()
220 .map_err(|err| RuntimeError::Internal(format!("config init: {err}")))?;
221 let config = Arc::new(Config::get()?.clone());
222
223 systemprompt_security::keys::authority::init()
224 .map_err(|err| RuntimeError::Internal(format!("signing key init: {err}")))?;
225
226 let database = Arc::new(
227 Database::from_config_with_write(
228 &config.database_type,
229 &config.database_url,
230 config.database_write_url.as_deref(),
231 )
232 .await?,
233 );
234
235 let authz_audit_pool = database.write_pool_arc().ok();
236 let authz_hook = systemprompt_security::authz::build_authz_hook(
237 profile.governance.as_ref(),
238 authz_audit_pool,
239 authz_hook_override,
240 )
241 .map_err(|err| RuntimeError::Internal(format!("authz bootstrap: {err}")))?;
242
243 systemprompt_logging::init_logging(Arc::clone(&database));
244
245 if config.database_write_url.is_some() {
246 tracing::debug!(
247 "Database read/write separation enabled: reads from replica, writes to primary"
248 );
249 }
250
251 Ok(CoreLayer {
252 config,
253 app_paths,
254 database,
255 authz_hook,
256 })
257}
258
259async fn init_extensions(
260 extension_registry: Option<ExtensionRegistry>,
261 install_schemas: bool,
262 migration_config: MigrationConfig,
263 database: &Arc<Database>,
264) -> RuntimeResult<Arc<ExtensionRegistry>> {
265 let registry = match extension_registry {
266 Some(registry) => registry,
267 None => ExtensionRegistry::discover()?,
268 };
269 registry.validate()?;
270
271 if install_schemas {
272 install_extension_schemas_full(®istry, database.write(), &[], migration_config).await?;
273 }
274
275 Ok(Arc::new(registry))
276}