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