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
7use std::sync::{Arc, OnceLock};
8
9use tokio::task::JoinHandle;
10
11use systemprompt_analytics::{AnalyticsService, FingerprintRepository, GeoIpReader};
12use systemprompt_database::DbPool;
13use systemprompt_extension::ExtensionRegistry;
14use systemprompt_marketplace::MarketplaceFilter;
15use systemprompt_mcp::services::registry::RegistryService;
16use systemprompt_models::services::SystemAdmin;
17use systemprompt_models::{AppPaths, Config, ContentConfigRaw, ContentRouting, RouteClassifier};
18use systemprompt_security::authz::SharedAuthzHook;
19use systemprompt_users::UserService;
20
21mod context_loaders;
22
23use crate::builder::AppContextBuilder;
24use crate::error::RuntimeResult;
25use crate::registry::ModuleApiRegistry;
26
27/// Database pool and the data-access services layered on it.
28///
29/// `fingerprint_repo` and `user_service` are `None` when the corresponding
30/// resource failed to initialise; callers must degrade gracefully.
31#[derive(Clone)]
32pub struct DataPlane {
33    pub database: DbPool,
34    pub analytics_service: Arc<AnalyticsService>,
35    pub fingerprint_repo: Option<Arc<FingerprintRepository>>,
36    pub user_service: Option<Arc<UserService>>,
37}
38
39#[derive(Clone)]
40pub struct ConfigPlane {
41    pub config: Arc<Config>,
42    pub app_paths: Arc<AppPaths>,
43    pub content_config: Option<Arc<ContentConfigRaw>>,
44    pub route_classifier: Arc<RouteClassifier>,
45}
46
47#[derive(Clone)]
48pub struct Plugins {
49    pub extension_registry: Arc<ExtensionRegistry>,
50    pub api_registry: Arc<ModuleApiRegistry>,
51    pub mcp_registry: RegistryService,
52    pub marketplace_filter: Arc<dyn MarketplaceFilter>,
53}
54
55#[derive(Clone)]
56pub struct Subsystems {
57    pub system_admin: Arc<SystemAdmin>,
58    pub authz_hook: SharedAuthzHook,
59    pub event_bridge: Arc<OnceLock<JoinHandle<()>>>,
60    pub geoip_reader: Option<GeoIpReader>,
61}
62
63/// Application-wide runtime container shared across the HTTP server, the
64/// scheduler, and CLI commands.
65///
66/// Handles are grouped into four cohesive planes ([`DataPlane`],
67/// [`ConfigPlane`], [`Plugins`], [`Subsystems`]); each field is an [`Arc`] (or
68/// an `Arc`-internal handle such as [`DbPool`]), so `clone` is a
69/// reference-count bump rather than a deep copy. Construct it via
70/// [`AppContext::builder`] (or [`AppContext::new`] for the default build);
71/// [`AppContext::from_parts`] bypasses the bootstrap and is intended for tests
72/// and embedders that assemble the planes themselves. Read individual handles
73/// through the accessor methods.
74#[derive(Clone)]
75pub struct AppContext {
76    pub(crate) data: DataPlane,
77    pub(crate) cfg: ConfigPlane,
78    pub(crate) plugins: Plugins,
79    pub(crate) subsystems: Subsystems,
80}
81
82impl std::fmt::Debug for AppContext {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.debug_struct("AppContext")
85            .field("config", &"Config")
86            .field("database", &"DbPool")
87            .field("api_registry", &"ModuleApiRegistry")
88            .field("extension_registry", &self.plugins.extension_registry)
89            .field("geoip_reader", &self.subsystems.geoip_reader.is_some())
90            .field("content_config", &self.cfg.content_config.is_some())
91            .field("route_classifier", &"RouteClassifier")
92            .field("analytics_service", &"AnalyticsService")
93            .field("fingerprint_repo", &self.data.fingerprint_repo.is_some())
94            .field("user_service", &self.data.user_service.is_some())
95            .field("app_paths", &"AppPaths")
96            .field("marketplace_filter", &self.plugins.marketplace_filter)
97            .field(
98                "event_bridge",
99                &self.subsystems.event_bridge.get().is_some(),
100            )
101            .field("system_admin", &self.subsystems.system_admin.username())
102            .field("mcp_registry", &"RegistryService")
103            .field("authz_hook", &"SharedAuthzHook")
104            .finish()
105    }
106}
107
108impl std::fmt::Debug for DataPlane {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("DataPlane")
111            .field("database", &"DbPool")
112            .field("analytics_service", &"AnalyticsService")
113            .field("fingerprint_repo", &self.fingerprint_repo.is_some())
114            .field("user_service", &self.user_service.is_some())
115            .finish()
116    }
117}
118
119impl std::fmt::Debug for ConfigPlane {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.debug_struct("ConfigPlane")
122            .field("config", &"Config")
123            .field("app_paths", &"AppPaths")
124            .field("content_config", &self.content_config.is_some())
125            .field("route_classifier", &"RouteClassifier")
126            .finish()
127    }
128}
129
130impl std::fmt::Debug for Plugins {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("Plugins")
133            .field("extension_registry", &self.extension_registry)
134            .field("api_registry", &"ModuleApiRegistry")
135            .field("mcp_registry", &"RegistryService")
136            .field("marketplace_filter", &self.marketplace_filter)
137            .finish()
138    }
139}
140
141impl std::fmt::Debug for Subsystems {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.debug_struct("Subsystems")
144            .field("system_admin", &self.system_admin.username())
145            .field("authz_hook", &"SharedAuthzHook")
146            .field("event_bridge", &self.event_bridge.get().is_some())
147            .field("geoip_reader", &self.geoip_reader.is_some())
148            .finish()
149    }
150}
151
152impl AppContext {
153    /// Builds a context with default settings: schema installation off,
154    /// extensions discovered via inventory, and the inventory-registered
155    /// marketplace filter. Equivalent to `Self::builder().build().await`.
156    pub async fn new() -> RuntimeResult<Self> {
157        Self::builder().build().await
158    }
159
160    #[must_use]
161    pub fn builder() -> AppContextBuilder {
162        AppContextBuilder::new()
163    }
164
165    /// Assembles a context directly from pre-built planes, bypassing the
166    /// [`AppContextBuilder`] bootstrap. Intended for tests and embedders that
167    /// own the construction of the individual handles.
168    #[must_use]
169    pub const fn from_parts(
170        data: DataPlane,
171        cfg: ConfigPlane,
172        plugins: Plugins,
173        subsystems: Subsystems,
174    ) -> Self {
175        Self {
176            data,
177            cfg,
178            plugins,
179            subsystems,
180        }
181    }
182
183    pub fn load_geoip_database(config: &Config, show_warnings: bool) -> Option<GeoIpReader> {
184        context_loaders::load_geoip_database(config, show_warnings)
185    }
186
187    pub fn load_content_config(
188        config: &Config,
189        app_paths: &AppPaths,
190    ) -> Option<Arc<ContentConfigRaw>> {
191        context_loaders::load_content_config(config, app_paths)
192    }
193
194    pub fn config(&self) -> &Config {
195        &self.cfg.config
196    }
197
198    pub fn content_config(&self) -> Option<&ContentConfigRaw> {
199        self.cfg.content_config.as_ref().map(AsRef::as_ref)
200    }
201
202    pub fn content_routing(&self) -> Option<Arc<dyn ContentRouting>> {
203        let concrete = Arc::clone(self.cfg.content_config.as_ref()?);
204        let routing: Arc<dyn ContentRouting> = concrete;
205        Some(routing)
206    }
207
208    pub const fn db_pool(&self) -> &DbPool {
209        &self.data.database
210    }
211
212    pub fn api_registry(&self) -> &ModuleApiRegistry {
213        &self.plugins.api_registry
214    }
215
216    pub fn extension_registry(&self) -> &ExtensionRegistry {
217        &self.plugins.extension_registry
218    }
219
220    pub fn server_address(&self) -> String {
221        format!("{}:{}", self.cfg.config.host, self.cfg.config.port)
222    }
223
224    pub const fn geoip_reader(&self) -> Option<&GeoIpReader> {
225        self.subsystems.geoip_reader.as_ref()
226    }
227
228    pub const fn analytics_service(&self) -> &Arc<AnalyticsService> {
229        &self.data.analytics_service
230    }
231
232    pub const fn route_classifier(&self) -> &Arc<RouteClassifier> {
233        &self.cfg.route_classifier
234    }
235
236    pub fn app_paths(&self) -> &AppPaths {
237        &self.cfg.app_paths
238    }
239
240    pub const fn app_paths_arc(&self) -> &Arc<AppPaths> {
241        &self.cfg.app_paths
242    }
243
244    pub fn marketplace_filter(&self) -> &Arc<dyn MarketplaceFilter> {
245        &self.plugins.marketplace_filter
246    }
247
248    pub const fn event_bridge(&self) -> &Arc<OnceLock<JoinHandle<()>>> {
249        &self.subsystems.event_bridge
250    }
251
252    pub fn system_admin(&self) -> &SystemAdmin {
253        &self.subsystems.system_admin
254    }
255
256    pub const fn mcp_registry(&self) -> &RegistryService {
257        &self.plugins.mcp_registry
258    }
259
260    pub const fn authz_hook(&self) -> &SharedAuthzHook {
261        &self.subsystems.authz_hook
262    }
263}