Skip to main content

systemprompt_runtime/
context.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
21use crate::builder::AppContextBuilder;
22use crate::context_loaders;
23use crate::error::RuntimeResult;
24use crate::registry::ModuleApiRegistry;
25
26/// Application-wide runtime container shared across the HTTP server, the
27/// scheduler, and CLI commands.
28///
29/// Every field is an [`Arc`] (or an `Arc`-internal handle such as [`DbPool`]),
30/// so `clone` is a reference-count bump rather than a deep copy; the type is
31/// designed to be cloned freely into request handlers, jobs, and spawned
32/// tasks. Construct it via [`AppContext::builder`] (or [`AppContext::new`] for
33/// the default build); [`AppContext::from_parts`] bypasses the bootstrap and
34/// is intended for tests and embedders that assemble the parts themselves.
35///
36/// Some handles are optional: [`geoip_reader`](Self::geoip_reader),
37/// `content_config`, `fingerprint_repo`, and `user_service` are `None` when
38/// the corresponding resource is absent or failed to initialise, and callers
39/// must degrade gracefully rather than assume presence.
40#[derive(Clone)]
41pub struct AppContext {
42    pub(crate) config: Arc<Config>,
43    pub(crate) database: DbPool,
44    pub(crate) api_registry: Arc<ModuleApiRegistry>,
45    pub(crate) extension_registry: Arc<ExtensionRegistry>,
46    pub(crate) geoip_reader: Option<GeoIpReader>,
47    pub(crate) content_config: Option<Arc<ContentConfigRaw>>,
48    pub(crate) route_classifier: Arc<RouteClassifier>,
49    pub(crate) analytics_service: Arc<AnalyticsService>,
50    pub(crate) fingerprint_repo: Option<Arc<FingerprintRepository>>,
51    pub(crate) user_service: Option<Arc<UserService>>,
52    pub(crate) app_paths: Arc<AppPaths>,
53    pub(crate) marketplace_filter: Arc<dyn MarketplaceFilter>,
54    pub(crate) event_bridge: Arc<OnceLock<JoinHandle<()>>>,
55    pub(crate) system_admin: Arc<SystemAdmin>,
56    pub(crate) mcp_registry: RegistryService,
57    pub(crate) authz_hook: SharedAuthzHook,
58}
59
60impl std::fmt::Debug for AppContext {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("AppContext")
63            .field("config", &"Config")
64            .field("database", &"DbPool")
65            .field("api_registry", &"ModuleApiRegistry")
66            .field("extension_registry", &self.extension_registry)
67            .field("geoip_reader", &self.geoip_reader.is_some())
68            .field("content_config", &self.content_config.is_some())
69            .field("route_classifier", &"RouteClassifier")
70            .field("analytics_service", &"AnalyticsService")
71            .field("fingerprint_repo", &self.fingerprint_repo.is_some())
72            .field("user_service", &self.user_service.is_some())
73            .field("app_paths", &"AppPaths")
74            .field("marketplace_filter", &self.marketplace_filter)
75            .field("event_bridge", &self.event_bridge.get().is_some())
76            .field("system_admin", &self.system_admin.username())
77            .field("mcp_registry", &"RegistryService")
78            .field("authz_hook", &"SharedAuthzHook")
79            .finish()
80    }
81}
82
83/// Owned constructor inputs for [`AppContext::from_parts`].
84///
85/// Exposes every field of [`AppContext`] as a public, movable value so an
86/// embedder or test can assemble a context without running the full
87/// [`AppContextBuilder`] bootstrap.
88#[derive(Debug)]
89pub struct AppContextParts {
90    pub config: Arc<Config>,
91    pub database: DbPool,
92    pub api_registry: Arc<ModuleApiRegistry>,
93    pub extension_registry: Arc<ExtensionRegistry>,
94    pub geoip_reader: Option<GeoIpReader>,
95    pub content_config: Option<Arc<ContentConfigRaw>>,
96    pub route_classifier: Arc<RouteClassifier>,
97    pub analytics_service: Arc<AnalyticsService>,
98    pub fingerprint_repo: Option<Arc<FingerprintRepository>>,
99    pub user_service: Option<Arc<UserService>>,
100    pub app_paths: Arc<AppPaths>,
101    pub marketplace_filter: Arc<dyn MarketplaceFilter>,
102    pub event_bridge: Arc<OnceLock<JoinHandle<()>>>,
103    pub system_admin: Arc<SystemAdmin>,
104    pub mcp_registry: RegistryService,
105    pub authz_hook: SharedAuthzHook,
106}
107
108impl AppContext {
109    /// Builds a context with default settings: schema installation off,
110    /// extensions discovered via inventory, and the inventory-registered
111    /// marketplace filter. Equivalent to `Self::builder().build().await`.
112    pub async fn new() -> RuntimeResult<Self> {
113        Self::builder().build().await
114    }
115
116    #[must_use]
117    pub fn builder() -> AppContextBuilder {
118        AppContextBuilder::new()
119    }
120
121    /// Assembles a context directly from pre-built parts, bypassing the
122    /// [`AppContextBuilder`] bootstrap. Intended for tests and embedders that
123    /// own the construction of the individual handles.
124    pub fn from_parts(parts: AppContextParts) -> Self {
125        Self {
126            config: parts.config,
127            database: parts.database,
128            api_registry: parts.api_registry,
129            extension_registry: parts.extension_registry,
130            geoip_reader: parts.geoip_reader,
131            content_config: parts.content_config,
132            route_classifier: parts.route_classifier,
133            analytics_service: parts.analytics_service,
134            fingerprint_repo: parts.fingerprint_repo,
135            user_service: parts.user_service,
136            app_paths: parts.app_paths,
137            marketplace_filter: parts.marketplace_filter,
138            event_bridge: parts.event_bridge,
139            system_admin: parts.system_admin,
140            mcp_registry: parts.mcp_registry,
141            authz_hook: parts.authz_hook,
142        }
143    }
144
145    pub fn load_geoip_database(config: &Config, show_warnings: bool) -> Option<GeoIpReader> {
146        context_loaders::load_geoip_database(config, show_warnings)
147    }
148
149    pub fn load_content_config(
150        config: &Config,
151        app_paths: &AppPaths,
152    ) -> Option<Arc<ContentConfigRaw>> {
153        context_loaders::load_content_config(config, app_paths)
154    }
155
156    pub fn config(&self) -> &Config {
157        &self.config
158    }
159
160    pub fn content_config(&self) -> Option<&ContentConfigRaw> {
161        self.content_config.as_ref().map(AsRef::as_ref)
162    }
163
164    pub fn content_routing(&self) -> Option<Arc<dyn ContentRouting>> {
165        let concrete = Arc::clone(self.content_config.as_ref()?);
166        let routing: Arc<dyn ContentRouting> = concrete;
167        Some(routing)
168    }
169
170    pub const fn db_pool(&self) -> &DbPool {
171        &self.database
172    }
173
174    pub const fn database(&self) -> &DbPool {
175        &self.database
176    }
177
178    pub fn api_registry(&self) -> &ModuleApiRegistry {
179        &self.api_registry
180    }
181
182    pub fn extension_registry(&self) -> &ExtensionRegistry {
183        &self.extension_registry
184    }
185
186    pub fn server_address(&self) -> String {
187        format!("{}:{}", self.config.host, self.config.port)
188    }
189
190    pub const fn geoip_reader(&self) -> Option<&GeoIpReader> {
191        self.geoip_reader.as_ref()
192    }
193
194    pub const fn analytics_service(&self) -> &Arc<AnalyticsService> {
195        &self.analytics_service
196    }
197
198    pub const fn route_classifier(&self) -> &Arc<RouteClassifier> {
199        &self.route_classifier
200    }
201
202    pub fn app_paths(&self) -> &AppPaths {
203        &self.app_paths
204    }
205
206    pub const fn app_paths_arc(&self) -> &Arc<AppPaths> {
207        &self.app_paths
208    }
209
210    pub fn marketplace_filter(&self) -> &Arc<dyn MarketplaceFilter> {
211        &self.marketplace_filter
212    }
213
214    pub const fn event_bridge(&self) -> &Arc<OnceLock<JoinHandle<()>>> {
215        &self.event_bridge
216    }
217
218    pub fn system_admin(&self) -> &SystemAdmin {
219        &self.system_admin
220    }
221
222    pub const fn mcp_registry(&self) -> &RegistryService {
223        &self.mcp_registry
224    }
225
226    pub const fn authz_hook(&self) -> &SharedAuthzHook {
227        &self.authz_hook
228    }
229}