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