Skip to main content

systemprompt_runtime/context/
mod.rs

1//! [`AppContext`] — the application-wide runtime container.
2//!
3//! Holds shared handles (config, database pool, extension registry,
4//! analytics, route classifier, etc.) cloned cheaply via [`Arc`].
5//! Constructed via [`crate::AppContextBuilder`] or [`AppContext::new`].
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::sync::{Arc, OnceLock};
11
12use tokio::task::JoinHandle;
13
14use systemprompt_agent::repository::A2ARepositories;
15use systemprompt_ai::repository::AiRepositories;
16use systemprompt_analytics::repository::AnalyticsRepositories;
17use systemprompt_analytics::{AnalyticsService, FingerprintRepository, GeoIpReader};
18use systemprompt_content::repository::ContentRepositories;
19use systemprompt_database::{DbPool, ServiceRepository};
20use systemprompt_extension::ExtensionRegistry;
21use systemprompt_files::FileRepository;
22use systemprompt_marketplace::MarketplaceFilter;
23use systemprompt_mcp::repository::McpSessionRepository;
24use systemprompt_mcp::services::registry::RegistryService;
25use systemprompt_models::services::SystemAdmin;
26use systemprompt_models::{AppPaths, Config, ContentConfigRaw, ContentRouting, RouteClassifier};
27use systemprompt_oauth::repository::OAuthRepositories;
28use systemprompt_security::authz::SharedAuthzHook;
29use systemprompt_users::{UserRepository, UserService};
30
31mod context_loaders;
32
33use crate::builder::AppContextBuilder;
34use crate::error::RuntimeResult;
35use crate::registry::ModuleApiRegistry;
36
37/// Database pool and the data-access services layered on it.
38///
39/// `fingerprint_repo` and `user_service` are `None` when the corresponding
40/// resource failed to initialise; callers must degrade gracefully.
41#[derive(Clone)]
42pub struct DataPlane {
43    pub database: DbPool,
44    pub analytics_service: Arc<AnalyticsService>,
45    pub fingerprint_repo: Option<Arc<FingerprintRepository>>,
46    pub user_service: Option<Arc<UserService>>,
47    pub a2a_repositories: Arc<A2ARepositories>,
48    pub content_repositories: Arc<ContentRepositories>,
49    pub oauth_repositories: Arc<OAuthRepositories>,
50    pub user_repository: Arc<UserRepository>,
51    pub service_repository: Arc<ServiceRepository>,
52    pub ai_repositories: Arc<AiRepositories>,
53    pub analytics_repositories: Arc<AnalyticsRepositories>,
54    pub file_repository: Arc<FileRepository>,
55    pub mcp_session_repository: Arc<McpSessionRepository>,
56}
57
58#[derive(Clone)]
59pub struct ConfigPlane {
60    pub config: Arc<Config>,
61    pub app_paths: Arc<AppPaths>,
62    pub content_config: Option<Arc<ContentConfigRaw>>,
63    pub route_classifier: Arc<RouteClassifier>,
64}
65
66#[derive(Clone)]
67pub struct Plugins {
68    pub extension_registry: Arc<ExtensionRegistry>,
69    pub api_registry: Arc<ModuleApiRegistry>,
70    pub mcp_registry: RegistryService,
71    pub marketplace_filter: Arc<dyn MarketplaceFilter>,
72}
73
74#[derive(Clone)]
75pub struct Subsystems {
76    pub system_admin: Arc<SystemAdmin>,
77    pub authz_hook: SharedAuthzHook,
78    pub event_bridge: Arc<OnceLock<JoinHandle<()>>>,
79    pub geoip_reader: Option<GeoIpReader>,
80}
81
82/// Application-wide runtime container shared across the HTTP server, the
83/// scheduler, and CLI commands.
84///
85/// Handles are grouped into four cohesive planes ([`DataPlane`],
86/// [`ConfigPlane`], [`Plugins`], [`Subsystems`]); each field is an [`Arc`] (or
87/// an `Arc`-internal handle such as [`DbPool`]), so `clone` is a
88/// reference-count bump rather than a deep copy. Construct it via
89/// [`AppContext::builder`] (or [`AppContext::new`] for the default build);
90/// [`AppContext::from_parts`] bypasses the bootstrap and is intended for tests
91/// and embedders that assemble the planes themselves. Read individual handles
92/// through the accessor methods.
93#[derive(Clone)]
94pub struct AppContext {
95    pub(crate) data: DataPlane,
96    pub(crate) cfg: ConfigPlane,
97    pub(crate) plugins: Plugins,
98    pub(crate) subsystems: Subsystems,
99}
100
101impl std::fmt::Debug for AppContext {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("AppContext")
104            .field("config", &"Config")
105            .field("database", &"DbPool")
106            .field("api_registry", &"ModuleApiRegistry")
107            .field("extension_registry", &self.plugins.extension_registry)
108            .field("geoip_reader", &self.subsystems.geoip_reader.is_some())
109            .field("content_config", &self.cfg.content_config.is_some())
110            .field("route_classifier", &"RouteClassifier")
111            .field("analytics_service", &"AnalyticsService")
112            .field("fingerprint_repo", &self.data.fingerprint_repo.is_some())
113            .field("user_service", &self.data.user_service.is_some())
114            .field("app_paths", &"AppPaths")
115            .field("marketplace_filter", &self.plugins.marketplace_filter)
116            .field(
117                "event_bridge",
118                &self.subsystems.event_bridge.get().is_some(),
119            )
120            .field("system_admin", &self.subsystems.system_admin.username())
121            .field("mcp_registry", &"RegistryService")
122            .field("authz_hook", &"SharedAuthzHook")
123            .finish()
124    }
125}
126
127impl std::fmt::Debug for DataPlane {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        f.debug_struct("DataPlane")
130            .field("database", &"DbPool")
131            .field("analytics_service", &"AnalyticsService")
132            .field("fingerprint_repo", &self.fingerprint_repo.is_some())
133            .field("user_service", &self.user_service.is_some())
134            .field("a2a_repositories", &"A2ARepositories")
135            .field("content_repositories", &"ContentRepositories")
136            .field("oauth_repositories", &"OAuthRepositories")
137            .field("user_repository", &"UserRepository")
138            .field("service_repository", &"ServiceRepository")
139            .field("ai_repositories", &"AiRepositories")
140            .field("analytics_repositories", &"AnalyticsRepositories")
141            .field("file_repository", &"FileRepository")
142            .field("mcp_session_repository", &"McpSessionRepository")
143            .finish()
144    }
145}
146
147impl std::fmt::Debug for ConfigPlane {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("ConfigPlane")
150            .field("config", &"Config")
151            .field("app_paths", &"AppPaths")
152            .field("content_config", &self.content_config.is_some())
153            .field("route_classifier", &"RouteClassifier")
154            .finish()
155    }
156}
157
158impl std::fmt::Debug for Plugins {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.debug_struct("Plugins")
161            .field("extension_registry", &self.extension_registry)
162            .field("api_registry", &"ModuleApiRegistry")
163            .field("mcp_registry", &"RegistryService")
164            .field("marketplace_filter", &self.marketplace_filter)
165            .finish()
166    }
167}
168
169impl std::fmt::Debug for Subsystems {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("Subsystems")
172            .field("system_admin", &self.system_admin.username())
173            .field("authz_hook", &"SharedAuthzHook")
174            .field("event_bridge", &self.event_bridge.get().is_some())
175            .field("geoip_reader", &self.geoip_reader.is_some())
176            .finish()
177    }
178}
179
180impl AppContext {
181    pub async fn new() -> RuntimeResult<Self> {
182        Self::builder().build().await
183    }
184
185    #[must_use]
186    pub fn builder() -> AppContextBuilder {
187        AppContextBuilder::new()
188    }
189
190    /// Assembles a context directly from pre-built planes, bypassing the
191    /// [`AppContextBuilder`] bootstrap. Intended for tests and embedders that
192    /// own the construction of the individual handles.
193    #[must_use]
194    pub const fn from_parts(
195        data: DataPlane,
196        cfg: ConfigPlane,
197        plugins: Plugins,
198        subsystems: Subsystems,
199    ) -> Self {
200        Self {
201            data,
202            cfg,
203            plugins,
204            subsystems,
205        }
206    }
207
208    pub fn load_geoip_database(
209        config: &Config,
210        show_warnings: bool,
211    ) -> Result<Option<GeoIpReader>, crate::error::RuntimeError> {
212        context_loaders::load_geoip_database(config, show_warnings)
213    }
214
215    pub fn load_content_config(
216        config: &Config,
217        app_paths: &AppPaths,
218    ) -> Option<Arc<ContentConfigRaw>> {
219        context_loaders::load_content_config(config, app_paths)
220    }
221
222    pub fn config(&self) -> &Config {
223        &self.cfg.config
224    }
225
226    pub fn content_config(&self) -> Option<&ContentConfigRaw> {
227        self.cfg.content_config.as_ref().map(AsRef::as_ref)
228    }
229
230    pub fn content_routing(&self) -> Option<Arc<dyn ContentRouting>> {
231        let concrete = Arc::clone(self.cfg.content_config.as_ref()?);
232        let routing: Arc<dyn ContentRouting> = concrete;
233        Some(routing)
234    }
235
236    pub const fn db_pool(&self) -> &DbPool {
237        &self.data.database
238    }
239
240    pub fn api_registry(&self) -> &ModuleApiRegistry {
241        &self.plugins.api_registry
242    }
243
244    pub fn extension_registry(&self) -> &ExtensionRegistry {
245        &self.plugins.extension_registry
246    }
247
248    pub fn server_address(&self) -> String {
249        format!("{}:{}", self.cfg.config.host, self.cfg.config.port)
250    }
251
252    pub const fn geoip_reader(&self) -> Option<&GeoIpReader> {
253        self.subsystems.geoip_reader.as_ref()
254    }
255
256    pub const fn analytics_service(&self) -> &Arc<AnalyticsService> {
257        &self.data.analytics_service
258    }
259
260    /// Session usage counters backed by the analytics session repository, for
261    /// wiring repositories (e.g. the agent `TaskRepository`) that bump
262    /// per-session counters without depending on the analytics crate.
263    #[must_use]
264    pub fn session_usage(&self) -> systemprompt_traits::DynSessionUsageCounters {
265        Arc::new(self.data.analytics_service.session_repo().clone())
266    }
267
268    pub fn context_materializer(&self) -> systemprompt_traits::DynContextMaterializer {
269        Arc::new(systemprompt_agent::services::ContextProviderService::new(
270            self.data.a2a_repositories.contexts.clone(),
271        ))
272    }
273
274    pub const fn a2a_repositories(&self) -> &Arc<A2ARepositories> {
275        &self.data.a2a_repositories
276    }
277
278    pub const fn content_repositories(&self) -> &Arc<ContentRepositories> {
279        &self.data.content_repositories
280    }
281
282    pub const fn oauth_repositories(&self) -> &Arc<OAuthRepositories> {
283        &self.data.oauth_repositories
284    }
285
286    pub const fn user_repository(&self) -> &Arc<UserRepository> {
287        &self.data.user_repository
288    }
289
290    pub const fn service_repository(&self) -> &Arc<ServiceRepository> {
291        &self.data.service_repository
292    }
293
294    pub const fn ai_repositories(&self) -> &Arc<AiRepositories> {
295        &self.data.ai_repositories
296    }
297
298    pub const fn analytics_repositories(&self) -> &Arc<AnalyticsRepositories> {
299        &self.data.analytics_repositories
300    }
301
302    pub const fn file_repository(&self) -> &Arc<FileRepository> {
303        &self.data.file_repository
304    }
305
306    pub const fn mcp_session_repository(&self) -> &Arc<McpSessionRepository> {
307        &self.data.mcp_session_repository
308    }
309
310    pub const fn route_classifier(&self) -> &Arc<RouteClassifier> {
311        &self.cfg.route_classifier
312    }
313
314    pub fn app_paths(&self) -> &AppPaths {
315        &self.cfg.app_paths
316    }
317
318    pub const fn app_paths_arc(&self) -> &Arc<AppPaths> {
319        &self.cfg.app_paths
320    }
321
322    pub fn marketplace_filter(&self) -> &Arc<dyn MarketplaceFilter> {
323        &self.plugins.marketplace_filter
324    }
325
326    pub const fn event_bridge(&self) -> &Arc<OnceLock<JoinHandle<()>>> {
327        &self.subsystems.event_bridge
328    }
329
330    pub fn system_admin(&self) -> &SystemAdmin {
331        &self.subsystems.system_admin
332    }
333
334    pub const fn mcp_registry(&self) -> &RegistryService {
335        &self.plugins.mcp_registry
336    }
337
338    pub const fn authz_hook(&self) -> &SharedAuthzHook {
339        &self.subsystems.authz_hook
340    }
341}