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