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