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