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, 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}
46
47impl std::fmt::Debug for AppContextBuilder {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("AppContextBuilder")
50 .field("extension_registry", &self.extension_registry.is_some())
51 .field("show_startup_warnings", &self.show_startup_warnings)
52 .field("marketplace_filter", &self.marketplace_filter.is_some())
53 .field("authz_hook", &self.authz_hook.is_some())
54 .field("install_schemas", &self.install_schemas)
55 .field("migration_config", &self.migration_config)
56 .finish()
57 }
58}
59
60impl AppContextBuilder {
61 #[must_use]
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 #[must_use]
67 pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
68 self.extension_registry = Some(registry);
69 self
70 }
71
72 #[must_use]
73 pub const fn with_startup_warnings(mut self, show: bool) -> Self {
74 self.show_startup_warnings = show;
75 self
76 }
77
78 #[must_use]
79 pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
80 self.marketplace_filter = Some(filter);
81 self
82 }
83
84 #[must_use]
85 pub const fn with_migrations(mut self, install: bool) -> Self {
86 self.install_schemas = install;
87 self
88 }
89
90 #[must_use]
91 pub fn with_authz_hook<H>(mut self, hook: H) -> Self
92 where
93 H: AuthzDecisionHook + 'static,
94 {
95 self.authz_hook = Some(Arc::new(hook));
96 self
97 }
98
99 #[must_use]
100 pub fn with_shared_authz_hook(mut self, hook: SharedAuthzHook) -> Self {
101 self.authz_hook = Some(hook);
102 self
103 }
104
105 #[must_use]
106 pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
107 self.migration_config = config;
108 self
109 }
110
111 pub async fn build(self) -> RuntimeResult<AppContext> {
112 let CoreLayer {
113 config,
114 app_paths,
115 database,
116 authz_hook,
117 file_storage,
118 } = init_core(self.authz_hook).await?;
119
120 let api_registry = Arc::new(ModuleApiRegistry::new());
121 let extension_registry = init_extensions(
122 self.extension_registry,
123 self.install_schemas,
124 self.migration_config,
125 &database,
126 )
127 .await?;
128
129 GovernanceEngine::global()?;
130
131 let assembly::ContentAnalytics {
132 geoip_reader,
133 content_config,
134 route_classifier,
135 analytics_service,
136 analytics_repositories,
137 fingerprint_repo,
138 } = assembly::assemble_content_analytics(
139 &config,
140 &app_paths,
141 &database,
142 self.show_startup_warnings,
143 )?;
144
145 let instance_id = systemprompt_identifiers::InstanceId::new(&config.instance_id);
146 let repositories = build_repositories(&database, analytics_repositories, instance_id)?;
147
148 let user_service = Arc::new(UserService::new(Arc::clone(&repositories.users)));
149
150 let system_admin =
151 assembly::resolve_and_install_system_admin(&config, &user_service).await?;
152 let mcp_registry = RegistryService::new(system_admin.id().clone());
153
154 ensure_legacy_context(&repositories, &system_admin).await?;
155
156 let marketplace_filter = self
157 .marketplace_filter
158 .unwrap_or_else(|| assembly::build_marketplace_filter(&database));
159
160 Ok(AppContext::from_parts(
161 DataPlane {
162 database,
163 analytics_service,
164 fingerprint_repo,
165 user_service: Some(user_service),
166 a2a_repositories: repositories.a2a,
167 content_repositories: repositories.content,
168 oauth_repositories: repositories.oauth,
169 user_repository: repositories.users,
170 service_repository: repositories.services,
171 ai_repositories: repositories.ai,
172 analytics_repositories: repositories.analytics,
173 file_repository: repositories.files,
174 mcp_session_repository: repositories.mcp_sessions,
175 },
176 ConfigPlane {
177 config,
178 app_paths,
179 content_config,
180 route_classifier,
181 },
182 Plugins {
183 extension_registry,
184 api_registry,
185 mcp_registry,
186 marketplace_filter,
187 },
188 build_subsystems(system_admin, authz_hook, geoip_reader, file_storage),
189 ))
190 }
191}
192
193fn build_subsystems(
194 system_admin: Arc<systemprompt_models::services::SystemAdmin>,
195 authz_hook: SharedAuthzHook,
196 geoip_reader: Option<systemprompt_analytics::GeoIpReader>,
197 file_storage: Arc<dyn systemprompt_traits::FileStorage>,
198) -> Subsystems {
199 Subsystems {
200 system_admin,
201 authz_hook,
202 event_bridge: Arc::new(OnceLock::new()),
203 geoip_reader,
204 file_storage,
205 }
206}
207
208async fn ensure_legacy_context(
209 repositories: &RepositoryBundles,
210 system_admin: &systemprompt_models::services::SystemAdmin,
211) -> RuntimeResult<()> {
212 repositories
213 .a2a
214 .contexts
215 .ensure_context(
216 &systemprompt_traits::EnsureContextParams {
217 context_id: &systemprompt_identifiers::ContextId::legacy(),
218 user_id: system_admin.id(),
219 session_id: None,
220 name: "Legacy (pre-context)",
221 kind: systemprompt_models::ContextKind::Legacy.as_str(),
222 },
223 systemprompt_models::ContextKind::Legacy,
224 )
225 .await
226 .map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))
227}
228
229struct RepositoryBundles {
230 a2a: Arc<systemprompt_agent::repository::A2ARepositories>,
231 content: Arc<systemprompt_content::repository::ContentRepositories>,
232 oauth: Arc<systemprompt_oauth::repository::OAuthRepositories>,
233 users: Arc<systemprompt_users::UserRepository>,
234 services: Arc<systemprompt_database::ServiceRepository>,
235 ai: Arc<systemprompt_ai::repository::AiRepositories>,
236 analytics: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
237 files: Arc<systemprompt_files::FileRepository>,
238 mcp_sessions: Arc<systemprompt_mcp::repository::McpSessionRepository>,
239}
240
241fn build_repositories(
242 database: &systemprompt_database::DbPool,
243 analytics: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
244 instance_id: systemprompt_identifiers::InstanceId,
245) -> RuntimeResult<RepositoryBundles> {
246 let session_usage: systemprompt_traits::DynSessionUsageCounters =
247 Arc::new(analytics.sessions.clone());
248 Ok(RepositoryBundles {
249 a2a: Arc::new(systemprompt_agent::repository::A2ARepositories::new(
250 database,
251 session_usage,
252 instance_id.clone(),
253 )?),
254 content: Arc::new(systemprompt_content::repository::ContentRepositories::new(
255 database,
256 )?),
257 oauth: Arc::new(systemprompt_oauth::repository::OAuthRepositories::new(
258 database,
259 )?),
260 users: Arc::new(systemprompt_users::UserRepository::new(database)?),
261 services: Arc::new(systemprompt_database::ServiceRepository::new(
262 database,
263 instance_id,
264 )?),
265 ai: Arc::new(systemprompt_ai::repository::AiRepositories::new(database)?),
266 analytics,
267 files: Arc::new(systemprompt_files::FileRepository::new(database)?),
268 mcp_sessions: Arc::new(systemprompt_mcp::repository::McpSessionRepository::new(
269 database,
270 )?),
271 })
272}