Skip to main content

systemprompt_runtime/
builder.rs

1//! Builder that assembles an [`AppContext`] from profile + config state.
2//!
3//! The builder owns the bootstrap order: profile -> paths -> files ->
4//! database -> logging -> extensions -> ancillary services. Failures at
5//! any step propagate as [`RuntimeError`](crate::error::RuntimeError).
6
7use 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}