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