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