systemprompt_runtime/builder/
mod.rs1mod 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#[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}