mod assembly;
mod core_layer;
use std::sync::{Arc, OnceLock};
use systemprompt_database::MigrationConfig;
use systemprompt_extension::ExtensionRegistry;
use systemprompt_marketplace::MarketplaceFilter;
use systemprompt_mcp::services::registry::RegistryService;
use systemprompt_security::authz::{AuthzDecisionHook, SharedAuthzHook};
use systemprompt_users::UserService;
use crate::context::{AppContext, ConfigPlane, DataPlane, Plugins, Subsystems};
use crate::error::RuntimeResult;
use crate::registry::ModuleApiRegistry;
use core_layer::{CoreLayer, init_core, init_extensions};
#[derive(Default)]
pub struct AppContextBuilder {
extension_registry: Option<ExtensionRegistry>,
show_startup_warnings: bool,
marketplace_filter: Option<Arc<dyn MarketplaceFilter>>,
authz_hook: Option<SharedAuthzHook>,
install_schemas: bool,
migration_config: MigrationConfig,
}
impl std::fmt::Debug for AppContextBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppContextBuilder")
.field("extension_registry", &self.extension_registry.is_some())
.field("show_startup_warnings", &self.show_startup_warnings)
.field("marketplace_filter", &self.marketplace_filter.is_some())
.field("authz_hook", &self.authz_hook.is_some())
.field("install_schemas", &self.install_schemas)
.field("migration_config", &self.migration_config)
.finish()
}
}
impl AppContextBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_extensions(mut self, registry: ExtensionRegistry) -> Self {
self.extension_registry = Some(registry);
self
}
#[must_use]
pub const fn with_startup_warnings(mut self, show: bool) -> Self {
self.show_startup_warnings = show;
self
}
#[must_use]
pub fn with_marketplace_filter(mut self, filter: Arc<dyn MarketplaceFilter>) -> Self {
self.marketplace_filter = Some(filter);
self
}
#[must_use]
pub const fn with_migrations(mut self, install: bool) -> Self {
self.install_schemas = install;
self
}
#[must_use]
pub fn with_authz_hook<H>(mut self, hook: H) -> Self
where
H: AuthzDecisionHook + 'static,
{
self.authz_hook = Some(Arc::new(hook));
self
}
#[must_use]
pub fn with_shared_authz_hook(mut self, hook: SharedAuthzHook) -> Self {
self.authz_hook = Some(hook);
self
}
#[must_use]
pub const fn with_migration_config(mut self, config: MigrationConfig) -> Self {
self.migration_config = config;
self
}
pub async fn build(self) -> RuntimeResult<AppContext> {
let CoreLayer {
config,
app_paths,
database,
authz_hook,
} = init_core(self.authz_hook).await?;
let api_registry = Arc::new(ModuleApiRegistry::new());
let extension_registry = init_extensions(
self.extension_registry,
self.install_schemas,
self.migration_config,
&database,
)
.await?;
let assembly::ContentAnalytics {
geoip_reader,
content_config,
route_classifier,
analytics_service,
analytics_repositories,
fingerprint_repo,
} = assembly::assemble_content_analytics(
&config,
&app_paths,
&database,
self.show_startup_warnings,
)?;
let repositories = build_repositories(&database, analytics_repositories)?;
let user_service = Arc::new(UserService::new(Arc::clone(&repositories.users)));
let system_admin =
assembly::resolve_and_install_system_admin(&config, &user_service).await?;
let mcp_registry = RegistryService::new(system_admin.id().clone());
ensure_legacy_context(&repositories, &system_admin).await?;
let marketplace_filter = self
.marketplace_filter
.unwrap_or_else(|| assembly::build_marketplace_filter(&database));
let event_bridge = Arc::new(OnceLock::new());
Ok(AppContext::from_parts(
DataPlane {
database,
analytics_service,
fingerprint_repo,
user_service: Some(user_service),
a2a_repositories: repositories.a2a,
content_repositories: repositories.content,
oauth_repositories: repositories.oauth,
user_repository: repositories.users,
service_repository: repositories.services,
ai_repositories: repositories.ai,
analytics_repositories: repositories.analytics,
file_repository: repositories.files,
mcp_session_repository: repositories.mcp_sessions,
},
ConfigPlane {
config,
app_paths,
content_config,
route_classifier,
},
Plugins {
extension_registry,
api_registry,
mcp_registry,
marketplace_filter,
},
Subsystems {
system_admin,
authz_hook,
event_bridge,
geoip_reader,
},
))
}
}
async fn ensure_legacy_context(
repositories: &RepositoryBundles,
system_admin: &systemprompt_models::services::SystemAdmin,
) -> RuntimeResult<()> {
repositories
.a2a
.contexts
.ensure_context(
&systemprompt_traits::EnsureContextParams {
context_id: &systemprompt_identifiers::ContextId::legacy(),
user_id: system_admin.id(),
session_id: None,
name: "Legacy (pre-context)",
kind: systemprompt_models::ContextKind::Legacy.as_str(),
},
systemprompt_models::ContextKind::Legacy,
)
.await
.map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))
}
struct RepositoryBundles {
a2a: Arc<systemprompt_agent::repository::A2ARepositories>,
content: Arc<systemprompt_content::repository::ContentRepositories>,
oauth: Arc<systemprompt_oauth::repository::OAuthRepositories>,
users: Arc<systemprompt_users::UserRepository>,
services: Arc<systemprompt_database::ServiceRepository>,
ai: Arc<systemprompt_ai::repository::AiRepositories>,
analytics: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
files: Arc<systemprompt_files::FileRepository>,
mcp_sessions: Arc<systemprompt_mcp::repository::McpSessionRepository>,
}
fn build_repositories(
database: &systemprompt_database::DbPool,
analytics: Arc<systemprompt_analytics::repository::AnalyticsRepositories>,
) -> RuntimeResult<RepositoryBundles> {
let session_usage: systemprompt_traits::DynSessionUsageCounters =
Arc::new(analytics.sessions.clone());
Ok(RepositoryBundles {
a2a: Arc::new(systemprompt_agent::repository::A2ARepositories::new(
database,
session_usage,
)?),
content: Arc::new(systemprompt_content::repository::ContentRepositories::new(
database,
)?),
oauth: Arc::new(systemprompt_oauth::repository::OAuthRepositories::new(
database,
)?),
users: Arc::new(systemprompt_users::UserRepository::new(database)?),
services: Arc::new(systemprompt_database::ServiceRepository::new(database)?),
ai: Arc::new(systemprompt_ai::repository::AiRepositories::new(database)?),
analytics,
files: Arc::new(systemprompt_files::FileRepository::new(database)?),
mcp_sessions: Arc::new(systemprompt_mcp::repository::McpSessionRepository::new(
database,
)?),
})
}