systemprompt-runtime 0.38.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).
//! Subsystem resolution helpers live in [`assembly`].
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

mod assembly;
mod core_layer;

use std::sync::{Arc, OnceLock};

use systemprompt_database::MigrationConfig;
use systemprompt_extension::ExtensionRegistry;
use systemprompt_marketplace::MarketplaceFilter;
use systemprompt_mcp::services::registry::RegistryService;
use systemprompt_security::authz::{AuthzDecisionHook, SharedAuthzHook};
use systemprompt_users::UserService;

use crate::context::{AppContext, ConfigPlane, DataPlane, Plugins, Subsystems};
use crate::error::RuntimeResult;
use crate::registry::ModuleApiRegistry;
use core_layer::{CoreLayer, init_core, init_extensions};

/// Assembles an [`AppContext`], owning the bootstrap order described on the
/// module.
///
/// All fields default to a no-op build: extensions are discovered via
/// inventory, schema installation is off, and the marketplace filter falls
/// back to the inventory-registered implementation (or an allow-all filter).
/// Override these with the `with_*` methods before calling
/// [`build`](Self::build).
#[derive(Default)]
pub struct AppContextBuilder {
    extension_registry: Option<ExtensionRegistry>,
    show_startup_warnings: bool,
    marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
    authz_hook: Option<SharedAuthzHook>,
    install_schemas: bool,
    migration_config: MigrationConfig,
}

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())
            .field("authz_hook", &self.authz_hook.is_some())
            .field("install_schemas", &self.install_schemas)
            .field("migration_config", &self.migration_config)
            .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
    }

    #[must_use]
    pub const fn with_migrations(mut self, install: bool) -> Self {
        self.install_schemas = install;
        self
    }

    #[must_use]
    pub fn with_authz_hook<H>(mut self, hook: H) -> Self
    where
        H: AuthzDecisionHook + 'static,
    {
        self.authz_hook = Some(Arc::new(hook));
        self
    }

    #[must_use]
    pub fn with_shared_authz_hook(mut self, hook: SharedAuthzHook) -> Self {
        self.authz_hook = Some(hook);
        self
    }

    #[must_use]
    pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
        self.migration_config = config;
        self
    }

    pub async fn build(self) -> RuntimeResult<AppContext> {
        let CoreLayer {
            config,
            app_paths,
            database,
            authz_hook,
        } = init_core(self.authz_hook).await?;

        let api_registry = Arc::new(ModuleApiRegistry::new());
        let extension_registry = init_extensions(
            self.extension_registry,
            self.install_schemas,
            self.migration_config,
            &database,
        )
        .await?;

        let assembly::ContentAnalytics {
            geoip_reader,
            content_config,
            route_classifier,
            analytics_service,
            analytics_repositories,
            fingerprint_repo,
        } = assembly::assemble_content_analytics(
            &config,
            &app_paths,
            &database,
            self.show_startup_warnings,
        )?;

        let repositories = build_repositories(&database, analytics_repositories)?;

        let user_service = Arc::new(UserService::new(Arc::clone(&repositories.users)));

        let system_admin =
            assembly::resolve_and_install_system_admin(&config, &user_service).await?;
        let mcp_registry = RegistryService::new(system_admin.id().clone());

        ensure_legacy_context(&repositories, &system_admin).await?;

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

        let event_bridge = Arc::new(OnceLock::new());

        Ok(AppContext::from_parts(
            DataPlane {
                database,
                analytics_service,
                fingerprint_repo,
                user_service: Some(user_service),
                a2a_repositories: repositories.a2a,
                content_repositories: repositories.content,
                oauth_repositories: repositories.oauth,
                user_repository: repositories.users,
                service_repository: repositories.services,
                ai_repositories: repositories.ai,
                analytics_repositories: repositories.analytics,
                file_repository: repositories.files,
                mcp_session_repository: repositories.mcp_sessions,
            },
            ConfigPlane {
                config,
                app_paths,
                content_config,
                route_classifier,
            },
            Plugins {
                extension_registry,
                api_registry,
                mcp_registry,
                marketplace_filter,
            },
            Subsystems {
                system_admin,
                authz_hook,
                event_bridge,
                geoip_reader,
            },
        ))
    }
}

async fn ensure_legacy_context(
    repositories: &RepositoryBundles,
    system_admin: &systemprompt_models::services::SystemAdmin,
) -> RuntimeResult<()> {
    repositories
        .a2a
        .contexts
        .ensure_context(
            &systemprompt_traits::EnsureContextParams {
                context_id: &systemprompt_identifiers::ContextId::legacy(),
                user_id: system_admin.id(),
                session_id: None,
                name: "Legacy (pre-context)",
                kind: systemprompt_models::ContextKind::Legacy.as_str(),
            },
            systemprompt_models::ContextKind::Legacy,
        )
        .await
        .map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))
}

struct RepositoryBundles {
    a2a: Arc<systemprompt_agent::repository::A2ARepositories>,
    content: Arc<systemprompt_content::repository::ContentRepositories>,
    oauth: Arc<systemprompt_oauth::repository::OAuthRepositories>,
    users: Arc<systemprompt_users::UserRepository>,
    services: Arc<systemprompt_database::ServiceRepository>,
    ai: Arc<systemprompt_ai::repository::AiRepositories>,
    analytics: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
    files: Arc<systemprompt_files::FileRepository>,
    mcp_sessions: Arc<systemprompt_mcp::repository::McpSessionRepository>,
}

fn build_repositories(
    database: &systemprompt_database::DbPool,
    analytics: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
) -> RuntimeResult<RepositoryBundles> {
    let session_usage: systemprompt_traits::DynSessionUsageCounters =
        Arc::new(analytics.sessions.clone());
    Ok(RepositoryBundles {
        a2a: Arc::new(systemprompt_agent::repository::A2ARepositories::new(
            database,
            session_usage,
        )?),
        content: Arc::new(systemprompt_content::repository::ContentRepositories::new(
            database,
        )?),
        oauth: Arc::new(systemprompt_oauth::repository::OAuthRepositories::new(
            database,
        )?),
        users: Arc::new(systemprompt_users::UserRepository::new(database)?),
        services: Arc::new(systemprompt_database::ServiceRepository::new(database)?),
        ai: Arc::new(systemprompt_ai::repository::AiRepositories::new(database)?),
        analytics,
        files: Arc::new(systemprompt_files::FileRepository::new(database)?),
        mcp_sessions: Arc::new(systemprompt_mcp::repository::McpSessionRepository::new(
            database,
        )?),
    })
}