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