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