Skip to main content

systemprompt_runtime/builder/
mod.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//! Subsystem resolution helpers live in [`assembly`].
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11mod assembly;
12mod composition;
13mod core_layer;
14
15use composition::{build_data_plane, build_repositories, ensure_legacy_context};
16
17use std::sync::{Arc, OnceLock};
18
19use systemprompt_database::MigrationConfig;
20use systemprompt_extension::ExtensionRegistry;
21use systemprompt_marketplace::{MarketplaceCache, MarketplaceFilter};
22use systemprompt_mcp::services::registry::RegistryService;
23use systemprompt_security::authz::{AuthzDecisionHook, SharedAuthzHook};
24use systemprompt_users::UserService;
25
26use crate::context::{AppContext, ConfigPlane, DataPlane, Plugins, ShutdownRequest, Subsystems};
27use crate::error::RuntimeResult;
28use crate::registry::ModuleApiRegistry;
29pub use core_layer::discover_vertex_models as discover_models;
30use core_layer::{CoreLayer, init_core, init_extensions};
31
32/// Assembles an [`AppContext`], owning the bootstrap order described on the
33/// module.
34///
35/// All fields default to a no-op build: extensions are discovered via
36/// inventory, schema installation is off, and the marketplace filter falls
37/// back to the inventory-registered implementation (or an allow-all filter).
38/// Override these with the `with_*` methods before calling
39/// [`build`](Self::build).
40#[derive(Default)]
41pub struct AppContextBuilder {
42    extension_registry: Option<ExtensionRegistry>,
43    show_startup_warnings: bool,
44    marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
45    authz_hook: Option<SharedAuthzHook>,
46    install_schemas: bool,
47    migration_config: MigrationConfig,
48    shutdown: Option<ShutdownRequest>,
49}
50
51impl std::fmt::Debug for AppContextBuilder {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("AppContextBuilder")
54            .field("extension_registry", &self.extension_registry.is_some())
55            .field("show_startup_warnings", &self.show_startup_warnings)
56            .field("marketplace_filter", &self.marketplace_filter.is_some())
57            .field("authz_hook", &self.authz_hook.is_some())
58            .field("install_schemas", &self.install_schemas)
59            .field("migration_config", &self.migration_config)
60            .field("shutdown", &self.shutdown.is_some())
61            .finish()
62    }
63}
64
65impl AppContextBuilder {
66    #[must_use]
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    #[must_use]
72    pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
73        self.extension_registry = Some(registry);
74        self
75    }
76
77    #[must_use]
78    pub const fn with_startup_warnings(mut self, show: bool) -> Self {
79        self.show_startup_warnings = show;
80        self
81    }
82
83    #[must_use]
84    pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
85        self.marketplace_filter = Some(filter);
86        self
87    }
88
89    #[must_use]
90    pub const fn with_migrations(mut self, install: bool) -> Self {
91        self.install_schemas = install;
92        self
93    }
94
95    #[must_use]
96    pub fn with_authz_hook<H>(mut self, hook: H) -> Self
97    where
98        H: AuthzDecisionHook + 'static,
99    {
100        self.authz_hook = Some(Arc::new(hook));
101        self
102    }
103
104    #[must_use]
105    pub fn with_shared_authz_hook(mut self, hook: SharedAuthzHook) -> Self {
106        self.authz_hook = Some(hook);
107        self
108    }
109
110    #[must_use]
111    pub fn with_shutdown(mut self, shutdown: ShutdownRequest) -> Self {
112        self.shutdown = Some(shutdown);
113        self
114    }
115
116    #[must_use]
117    pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
118        self.migration_config = config;
119        self
120    }
121
122    pub async fn build(self) -> RuntimeResult<AppContext> {
123        let shutdown = self.shutdown.unwrap_or_default();
124        let CoreLayer {
125            config,
126            app_paths,
127            database,
128            authz_hook,
129            governance,
130            file_storage,
131        } = init_core(self.authz_hook).await?;
132
133        let api_registry = Arc::new(ModuleApiRegistry::new());
134        let (extension_registry, schema_install) = init_extensions(
135            self.extension_registry,
136            self.install_schemas,
137            self.migration_config,
138            &database,
139        )
140        .await?;
141
142        crate::reporting::initialize(&database).await?;
143
144        let assembly::ContentAnalytics {
145            geoip_reader,
146            content_config,
147            route_classifier,
148            analytics_service,
149            analytics_repositories,
150            fingerprint_repo,
151        } = assembly::assemble_content_analytics(
152            &config,
153            &app_paths,
154            &database,
155            self.show_startup_warnings,
156        )?;
157
158        let (repositories, user_service, system_admin, mcp_registry) =
159            build_domain_layer(&config, &database, analytics_repositories).await?;
160
161        let marketplace_filter = self
162            .marketplace_filter
163            .unwrap_or_else(|| assembly::build_marketplace_filter(&database));
164
165        let subsystems = Subsystems {
166            system_admin,
167            authz_hook,
168            governance,
169            schema_install: Arc::new(schema_install),
170            event_bridge: Arc::new(OnceLock::new()),
171            geoip_reader,
172            file_storage,
173            shutdown,
174            publish_guard: Arc::new(tokio::sync::Mutex::new(
175                systemprompt_marketplace::inventory::PublishGuard::default(),
176            )),
177            snapshot_wakeup: Arc::new(crate::reporting::SnapshotWakeup::default()),
178        };
179
180        Ok(AppContext::from_parts(
181            build_data_plane(
182                database,
183                analytics_service,
184                fingerprint_repo,
185                user_service,
186                repositories,
187            ),
188            ConfigPlane {
189                config,
190                app_paths,
191                content_config,
192                route_classifier,
193            },
194            Plugins {
195                extension_registry,
196                api_registry,
197                mcp_registry,
198                marketplace_filter,
199                marketplace_cache: Arc::new(MarketplaceCache::default()),
200            },
201            subsystems,
202        ))
203    }
204}
205
206async fn build_domain_layer(
207    config: &systemprompt_models::Config,
208    database: &systemprompt_database::DbPool,
209    analytics_repositories: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
210) -> RuntimeResult<(
211    composition::RepositoryBundles,
212    Arc<UserService>,
213    Arc<systemprompt_models::SystemAdmin>,
214    RegistryService,
215)> {
216    let mut repositories = build_repositories(
217        database,
218        analytics_repositories,
219        systemprompt_identifiers::InstanceId::new(&config.instance_id),
220    )?;
221    let user_service = Arc::new(UserService::new(Arc::clone(&repositories.users)));
222    let system_admin = assembly::resolve_and_install_system_admin(config, &user_service).await?;
223    repositories.install_organization_resolver(system_admin.id());
224    let mcp_registry = RegistryService::new(system_admin.id().clone());
225    ensure_legacy_context(&repositories, &system_admin).await?;
226    Ok((repositories, user_service, system_admin, mcp_registry))
227}