systemprompt-runtime 0.8.0

Application runtime for systemprompt.io AI governance infrastructure. AppContext, lifecycle builder, extension registry, and module wiring for the MCP governance pipeline.
Documentation
//! Builder that assembles an [`AppContext`] from profile + config state.
//!
//! The builder owns the bootstrap order: profile -> paths -> files ->
//! database -> logging -> extensions -> ancillary services. Failures at
//! any step propagate as [`RuntimeError`](crate::error::RuntimeError).

use std::sync::Arc;

use systemprompt_analytics::{AnalyticsService, FingerprintRepository};
use systemprompt_config::ProfileBootstrap;
use systemprompt_database::Database;
use systemprompt_extension::ExtensionRegistry;
use systemprompt_marketplace::{AllowAllFilter, MarketplaceFilter, discover_filters};
use systemprompt_models::{AppPaths, Config, ContentConfigRaw, ContentRouting};
use systemprompt_users::UserService;

use crate::context::{AppContext, AppContextParts};
use crate::error::{RuntimeError, RuntimeResult};
use crate::registry::ModuleApiRegistry;

#[derive(Default)]
pub struct AppContextBuilder {
    extension_registry: Option<ExtensionRegistry>,
    show_startup_warnings: bool,
    marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
}

impl std::fmt::Debug for AppContextBuilder {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AppContextBuilder")
            .field("extension_registry", &self.extension_registry.is_some())
            .field("show_startup_warnings", &self.show_startup_warnings)
            .field("marketplace_filter", &self.marketplace_filter.is_some())
            .finish()
    }
}

impl AppContextBuilder {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
        self.extension_registry = Some(registry);
        self
    }

    #[must_use]
    pub const fn with_startup_warnings(mut self, show: bool) -> Self {
        self.show_startup_warnings = show;
        self
    }

    #[must_use]
    pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
        self.marketplace_filter = Some(filter);
        self
    }

    pub async fn build(self) -> RuntimeResult<AppContext> {
        let profile = ProfileBootstrap::get()?;
        let app_paths = Arc::new(AppPaths::from_profile(&profile.paths)?);
        systemprompt_files::FilesConfig::init(&app_paths)?;
        let config = Arc::new(Config::get()?.clone());

        let database = Arc::new(
            Database::from_config_with_write(
                &config.database_type,
                &config.database_url,
                config.database_write_url.as_deref(),
            )
            .await?,
        );

        let authz_audit_pool = database.write_pool_arc().ok();
        systemprompt_security::authz::install_from_governance_config(
            profile.governance.as_ref(),
            authz_audit_pool,
        )
        .map_err(|err| RuntimeError::Internal(format!("authz bootstrap: {err}")))?;

        systemprompt_logging::init_logging(Arc::clone(&database));

        if config.database_write_url.is_some() {
            tracing::info!(
                "Database read/write separation enabled: reads from replica, writes to primary"
            );
        }

        let api_registry = Arc::new(ModuleApiRegistry::new());

        let registry = self
            .extension_registry
            .unwrap_or_else(ExtensionRegistry::discover);
        registry.validate()?;
        let extension_registry = Arc::new(registry);

        let geoip_reader = AppContext::load_geoip_database(&config, self.show_startup_warnings);
        let content_config = AppContext::load_content_config(&config, &app_paths);
        let content_routing = content_routing_from(content_config.as_ref());
        let route_classifier = Arc::new(systemprompt_models::RouteClassifier::new(
            content_routing.clone(),
        ));
        let analytics_service = Arc::new(AnalyticsService::new(
            &database,
            geoip_reader.clone(),
            content_routing,
        )?);

        let fingerprint_repo = match FingerprintRepository::new(&database) {
            Ok(repo) => Some(Arc::new(repo)),
            Err(e) => {
                tracing::warn!(error = %e, "Failed to initialize fingerprint repository");
                None
            },
        };

        let user_service = match UserService::new(&database) {
            Ok(svc) => Some(Arc::new(svc)),
            Err(e) => {
                tracing::warn!(error = %e, "Failed to initialize user service");
                None
            },
        };

        let marketplace_filter = self
            .marketplace_filter
            .unwrap_or_else(|| build_marketplace_filter(&database));

        Ok(AppContext::from_parts(AppContextParts {
            config,
            database,
            api_registry,
            extension_registry,
            geoip_reader,
            content_config,
            route_classifier,
            analytics_service,
            fingerprint_repo,
            user_service,
            app_paths,
            marketplace_filter,
        }))
    }
}

fn build_marketplace_filter(
    database: &systemprompt_database::DbPool,
) -> Arc<dyn MarketplaceFilter> {
    for reg in discover_filters() {
        match (reg.factory)(database) {
            Ok(filter) => {
                tracing::info!(
                    priority = reg.priority,
                    "marketplace filter registered via inventory; using highest-priority impl",
                );
                return filter;
            },
            Err(err) => {
                tracing::error!(
                    priority = reg.priority,
                    error = %err,
                    "marketplace filter factory failed; trying next candidate",
                );
            },
        }
    }
    let fallback: Arc<dyn MarketplaceFilter> = Arc::new(AllowAllFilter);
    fallback
}

fn content_routing_from(
    content_config: Option<&Arc<ContentConfigRaw>>,
) -> Option<Arc<dyn ContentRouting>> {
    let concrete = Arc::clone(content_config?);
    let routing: Arc<dyn ContentRouting> = concrete;
    Some(routing)
}