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_users::UserService;
22
23use crate::context::{AppContext, ConfigPlane, DataPlane, Plugins, Subsystems};
24use crate::error::RuntimeResult;
25use crate::registry::ModuleApiRegistry;
26use core_layer::{CoreLayer, init_core, init_extensions};
27
28/// Assembles an [`AppContext`], owning the bootstrap order described on the
29/// module.
30///
31/// All fields default to a no-op build: extensions are discovered via
32/// inventory, schema installation is off, and the marketplace filter falls
33/// back to the inventory-registered implementation (or an allow-all filter).
34/// Override these with the `with_*` methods before calling
35/// [`build`](Self::build).
36#[derive(Default)]
37pub struct AppContextBuilder {
38    extension_registry: Option<ExtensionRegistry>,
39    show_startup_warnings: bool,
40    marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
41    authz_hook: Option<SharedAuthzHook>,
42    install_schemas: bool,
43    migration_config: MigrationConfig,
44}
45
46impl std::fmt::Debug for AppContextBuilder {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("AppContextBuilder")
49            .field("extension_registry", &self.extension_registry.is_some())
50            .field("show_startup_warnings", &self.show_startup_warnings)
51            .field("marketplace_filter", &self.marketplace_filter.is_some())
52            .field("authz_hook", &self.authz_hook.is_some())
53            .field("install_schemas", &self.install_schemas)
54            .field("migration_config", &self.migration_config)
55            .finish()
56    }
57}
58
59impl AppContextBuilder {
60    #[must_use]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Supplies an explicit extension registry. When unset, `build()`
66    /// discovers extensions via inventory ([`ExtensionRegistry::discover`]).
67    #[must_use]
68    pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
69        self.extension_registry = Some(registry);
70        self
71    }
72
73    #[must_use]
74    pub const fn with_startup_warnings(mut self, show: bool) -> Self {
75        self.show_startup_warnings = show;
76        self
77    }
78
79    /// Supplies an explicit marketplace filter. When unset, `build()` selects
80    /// the highest-priority inventory-registered filter, falling back to an
81    /// allow-all filter when none succeeds.
82    #[must_use]
83    pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
84        self.marketplace_filter = Some(filter);
85        self
86    }
87
88    /// Install / migrate extension schemas as part of `build()`. Off by
89    /// default so admin tools (`db doctor`, repair scripts) can open a
90    /// connection without mutating the schema. `serve` turns this on.
91    #[must_use]
92    pub const fn with_migrations(mut self, install: bool) -> Self {
93        self.install_schemas = install;
94        self
95    }
96
97    /// Supplies an extension-built authz decision hook. The hook is wired
98    /// only when `profile.governance.authz.hook.mode = extension`; pairing
99    /// this call with any other mode is a bootstrap error.
100    #[must_use]
101    pub fn with_authz_hook<H>(mut self, hook: H) -> Self
102    where
103        H: AuthzDecisionHook + 'static,
104    {
105        self.authz_hook = Some(Arc::new(hook));
106        self
107    }
108
109    #[must_use]
110    pub fn with_shared_authz_hook(mut self, hook: SharedAuthzHook) -> Self {
111        self.authz_hook = Some(hook);
112        self
113    }
114
115    #[must_use]
116    pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
117        self.migration_config = config;
118        self
119    }
120
121    pub async fn build(self) -> RuntimeResult<AppContext> {
122        let CoreLayer {
123            config,
124            app_paths,
125            database,
126            authz_hook,
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        let assembly::ContentAnalytics {
139            geoip_reader,
140            content_config,
141            route_classifier,
142            analytics_service,
143            fingerprint_repo,
144        } = assembly::assemble_content_analytics(
145            &config,
146            &app_paths,
147            &database,
148            self.show_startup_warnings,
149        )?;
150
151        // UserService is a mandatory dependency: the system admin cannot be
152        // resolved without it, so a construction failure is fatal here rather
153        // than a warning that re-surfaces as a less specific error downstream.
154        let user_service = Arc::new(UserService::new(&database)?);
155
156        let system_admin =
157            assembly::resolve_and_install_system_admin(&config, &user_service).await?;
158        let mcp_registry = RegistryService::new(system_admin.id().clone());
159
160        let marketplace_filter = self
161            .marketplace_filter
162            .unwrap_or_else(|| assembly::build_marketplace_filter(&database));
163
164        let event_bridge = Arc::new(OnceLock::new());
165
166        Ok(AppContext::from_parts(
167            DataPlane {
168                database,
169                analytics_service,
170                fingerprint_repo,
171                user_service: Some(user_service),
172            },
173            ConfigPlane {
174                config,
175                app_paths,
176                content_config,
177                route_classifier,
178            },
179            Plugins {
180                extension_registry,
181                api_registry,
182                mcp_registry,
183                marketplace_filter,
184            },
185            Subsystems {
186                system_admin,
187                authz_hook,
188                event_bridge,
189                geoip_reader,
190            },
191        ))
192    }
193}