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;
27pub use core_layer::discover_vertex_models as discover_models;
28use core_layer::{CoreLayer, init_core, init_extensions};
29
30#[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}