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 core_layer;
13
14use std::sync::{Arc, OnceLock};
15
16use systemprompt_database::MigrationConfig;
17use systemprompt_extension::ExtensionRegistry;
18use systemprompt_marketplace::MarketplaceFilter;
19use systemprompt_mcp::services::registry::RegistryService;
20use systemprompt_security::authz::{AuthzDecisionHook, SharedAuthzHook};
21use systemprompt_security::policy::GovernanceEngine;
22use systemprompt_users::UserService;
23
24use crate::context::{AppContext, ConfigPlane, DataPlane, Plugins, ShutdownRequest, Subsystems};
25use crate::error::RuntimeResult;
26use crate::registry::ModuleApiRegistry;
27use core_layer::{CoreLayer, init_core, init_extensions};
28
29/// Assembles an [`AppContext`], owning the bootstrap order described on the
30/// module.
31///
32/// All fields default to a no-op build: extensions are discovered via
33/// inventory, schema installation is off, and the marketplace filter falls
34/// back to the inventory-registered implementation (or an allow-all filter).
35/// Override these with the `with_*` methods before calling
36/// [`build`](Self::build).
37#[derive(Default)]
38pub struct AppContextBuilder {
39    extension_registry: Option<ExtensionRegistry>,
40    show_startup_warnings: bool,
41    marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
42    authz_hook: Option<SharedAuthzHook>,
43    install_schemas: bool,
44    migration_config: MigrationConfig,
45    shutdown: Option<ShutdownRequest>,
46}
47
48impl std::fmt::Debug for AppContextBuilder {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("AppContextBuilder")
51            .field("extension_registry", &self.extension_registry.is_some())
52            .field("show_startup_warnings", &self.show_startup_warnings)
53            .field("marketplace_filter", &self.marketplace_filter.is_some())
54            .field("authz_hook", &self.authz_hook.is_some())
55            .field("install_schemas", &self.install_schemas)
56            .field("migration_config", &self.migration_config)
57            .field("shutdown", &self.shutdown.is_some())
58            .finish()
59    }
60}
61
62impl AppContextBuilder {
63    #[must_use]
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    #[must_use]
69    pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
70        self.extension_registry = Some(registry);
71        self
72    }
73
74    #[must_use]
75    pub const fn with_startup_warnings(mut self, show: bool) -> Self {
76        self.show_startup_warnings = show;
77        self
78    }
79
80    #[must_use]
81    pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
82        self.marketplace_filter = Some(filter);
83        self
84    }
85
86    #[must_use]
87    pub const fn with_migrations(mut self, install: bool) -> Self {
88        self.install_schemas = install;
89        self
90    }
91
92    #[must_use]
93    pub fn with_authz_hook<H>(mut self, hook: H) -> Self
94    where
95        H: AuthzDecisionHook + 'static,
96    {
97        self.authz_hook = Some(Arc::new(hook));
98        self
99    }
100
101    #[must_use]
102    pub fn with_shared_authz_hook(mut self, hook: SharedAuthzHook) -> Self {
103        self.authz_hook = Some(hook);
104        self
105    }
106
107    #[must_use]
108    pub fn with_shutdown(mut self, shutdown: ShutdownRequest) -> Self {
109        self.shutdown = Some(shutdown);
110        self
111    }
112
113    #[must_use]
114    pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
115        self.migration_config = config;
116        self
117    }
118
119    pub async fn build(self) -> RuntimeResult<AppContext> {
120        let shutdown = self.shutdown.unwrap_or_default();
121        let CoreLayer {
122            config,
123            app_paths,
124            database,
125            authz_hook,
126            file_storage,
127        } = init_core(self.authz_hook).await?;
128
129        let api_registry = Arc::new(ModuleApiRegistry::new());
130        let extension_registry = init_extensions(
131            self.extension_registry,
132            self.install_schemas,
133            self.migration_config,
134            &database,
135        )
136        .await?;
137
138        GovernanceEngine::global()?;
139
140        let assembly::ContentAnalytics {
141            geoip_reader,
142            content_config,
143            route_classifier,
144            analytics_service,
145            analytics_repositories,
146            fingerprint_repo,
147        } = assembly::assemble_content_analytics(
148            &config,
149            &app_paths,
150            &database,
151            self.show_startup_warnings,
152        )?;
153
154        let instance_id = systemprompt_identifiers::InstanceId::new(&config.instance_id);
155        let repositories = build_repositories(&database, analytics_repositories, instance_id)?;
156
157        let user_service = Arc::new(UserService::new(Arc::clone(&repositories.users)));
158
159        let system_admin =
160            assembly::resolve_and_install_system_admin(&config, &user_service).await?;
161        let mcp_registry = RegistryService::new(system_admin.id().clone());
162
163        ensure_legacy_context(&repositories, &system_admin).await?;
164
165        let marketplace_filter = self
166            .marketplace_filter
167            .unwrap_or_else(|| assembly::build_marketplace_filter(&database));
168
169        let subsystems = build_subsystems(
170            system_admin,
171            authz_hook,
172            geoip_reader,
173            file_storage,
174            shutdown,
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,
194                mcp_registry,
195                marketplace_filter,
196            },
197            subsystems,
198        ))
199    }
200}
201
202fn build_data_plane(
203    database: Arc<systemprompt_database::Database>,
204    analytics_service: Arc<systemprompt_analytics::AnalyticsService>,
205    fingerprint_repo: Option<Arc<systemprompt_analytics::FingerprintRepository>>,
206    user_service: Arc<UserService>,
207    repositories: RepositoryBundles,
208) -> DataPlane {
209    DataPlane {
210        database,
211        analytics_service,
212        fingerprint_repo,
213        user_service: Some(user_service),
214        a2a_repositories: repositories.a2a,
215        content_repositories: repositories.content,
216        oauth_repositories: repositories.oauth,
217        user_repository: repositories.users,
218        service_repository: repositories.services,
219        ai_repositories: repositories.ai,
220        analytics_repositories: repositories.analytics,
221        file_repository: repositories.files,
222        mcp_session_repository: repositories.mcp_sessions,
223    }
224}
225
226fn build_subsystems(
227    system_admin: Arc<systemprompt_models::services::SystemAdmin>,
228    authz_hook: SharedAuthzHook,
229    geoip_reader: Option<systemprompt_analytics::GeoIpReader>,
230    file_storage: Arc<dyn systemprompt_traits::FileStorage>,
231    shutdown: ShutdownRequest,
232) -> Subsystems {
233    Subsystems {
234        system_admin,
235        authz_hook,
236        event_bridge: Arc::new(OnceLock::new()),
237        geoip_reader,
238        file_storage,
239        shutdown,
240    }
241}
242
243async fn ensure_legacy_context(
244    repositories: &RepositoryBundles,
245    system_admin: &systemprompt_models::services::SystemAdmin,
246) -> RuntimeResult<()> {
247    repositories
248        .a2a
249        .contexts
250        .ensure_context(
251            &systemprompt_traits::EnsureContextParams {
252                context_id: &systemprompt_identifiers::ContextId::legacy(),
253                user_id: system_admin.id(),
254                session_id: None,
255                name: "Legacy (pre-context)",
256                kind: systemprompt_models::ContextKind::Legacy.as_str(),
257            },
258            systemprompt_models::ContextKind::Legacy,
259        )
260        .await
261        .map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))
262}
263
264struct RepositoryBundles {
265    a2a: Arc<systemprompt_agent::repository::A2ARepositories>,
266    content: Arc<systemprompt_content::repository::ContentRepositories>,
267    oauth: Arc<systemprompt_oauth::repository::OAuthRepositories>,
268    users: Arc<systemprompt_users::UserRepository>,
269    services: Arc<systemprompt_database::ServiceRepository>,
270    ai: Arc<systemprompt_ai::repository::AiRepositories>,
271    analytics: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
272    files: Arc<systemprompt_files::FileRepository>,
273    mcp_sessions: Arc<systemprompt_mcp::repository::McpSessionRepository>,
274}
275
276fn build_repositories(
277    database: &systemprompt_database::DbPool,
278    analytics: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
279    instance_id: systemprompt_identifiers::InstanceId,
280) -> RuntimeResult<RepositoryBundles> {
281    let session_usage: systemprompt_traits::DynSessionUsageCounters =
282        Arc::new(analytics.sessions.clone());
283    Ok(RepositoryBundles {
284        a2a: Arc::new(systemprompt_agent::repository::A2ARepositories::new(
285            database,
286            session_usage,
287            instance_id.clone(),
288        )?),
289        content: Arc::new(systemprompt_content::repository::ContentRepositories::new(
290            database,
291        )?),
292        oauth: Arc::new(systemprompt_oauth::repository::OAuthRepositories::new(
293            database,
294        )?),
295        users: Arc::new(systemprompt_users::UserRepository::new(database)?),
296        services: Arc::new(systemprompt_database::ServiceRepository::new(
297            database,
298            instance_id,
299        )?),
300        ai: Arc::new(systemprompt_ai::repository::AiRepositories::new(database)?),
301        analytics,
302        files: Arc::new(systemprompt_files::FileRepository::new(database)?),
303        mcp_sessions: Arc::new(systemprompt_mcp::repository::McpSessionRepository::new(
304            database,
305        )?),
306    })
307}