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