Skip to main content

systemprompt_loader/
services_bootstrap.rs

1//! Process-wide services-config bootstrap.
2//!
3//! Mirrors `systemprompt_config::ProfileBootstrap` for the services tree: the
4//! merged, validated [`ServicesConfig`] — provider catalog, resolved gateway,
5//! agents, MCP servers — is loaded once, right after the profile, and read
6//! through `&'static` accessors for the life of the process. A load failure is
7//! a boot failure: nothing downstream may run against a catalog that did not
8//! parse.
9//!
10//! Provider discovery ([`ServicesBootstrap::try_init_with_discovery`]) is
11//! therefore **boot-time only**. The registry lives behind a [`OnceLock`] and
12//! is handed out as `&'static ProviderRegistry` to ~15 call sites, so there is
13//! no sound way to mutate it after install; a model that appears upstream mid
14//! process is served only after the next restart. The augmentation runs in the
15//! one window where the config is still owned — between `ConfigLoader::load`
16//! and `install` — and the config is re-validated afterwards so a discovered
17//! model can never bypass the gateway pricing gate. A second
18//! `try_init_with_discovery` after any init is a no-op returning the installed
19//! config; the report from the one pass that did run is kept behind
20//! [`ServicesBootstrap::discovery_report`].
21//!
22//! Copyright (c) systemprompt.io — Business Source License 1.1.
23//! See <https://systemprompt.io> for licensing details.
24
25use std::future::Future;
26use std::path::Path;
27use std::pin::Pin;
28use std::sync::OnceLock;
29
30use systemprompt_models::services::{
31    DiscoveryReport, GatewayConfig, ProviderRegistry, ServicesConfig,
32};
33
34use crate::config_loader::ConfigLoader;
35use crate::error::{ConfigLoadError, ConfigLoadResult};
36
37static SERVICES: OnceLock<ServicesConfig> = OnceLock::new();
38pub type DiscoveryFuture<'a> = Pin<Box<dyn Future<Output = DiscoveryReport> + Send + 'a>>;
39
40static DISCOVERY: OnceLock<DiscoveryReport> = OnceLock::new();
41
42#[derive(Debug, Clone, Copy)]
43pub struct ServicesBootstrap;
44
45impl ServicesBootstrap {
46    pub fn init() -> ConfigLoadResult<&'static ServicesConfig> {
47        if SERVICES.get().is_some() {
48            return Err(ConfigLoadError::AlreadyInitialized);
49        }
50        let services = ConfigLoader::load()?;
51        Self::install(services)
52    }
53
54    pub fn init_from_path(path: &Path) -> ConfigLoadResult<&'static ServicesConfig> {
55        if SERVICES.get().is_some() {
56            return Err(ConfigLoadError::AlreadyInitialized);
57        }
58        let services = ConfigLoader::load_from_path(path)?;
59        Self::install(services)
60    }
61
62    // Why: the augmenter borrows the registry across an await, so it is a
63    // boxed future tied to that borrow — a plain `FnOnce(&mut _) -> Fut` cannot
64    // name the lifetime and every real async fn fails the higher-ranked bound.
65    pub async fn try_init_with_discovery<F>(augment: F) -> ConfigLoadResult<&'static ServicesConfig>
66    where
67        F: for<'a> FnOnce(&'a mut ProviderRegistry) -> DiscoveryFuture<'a>,
68    {
69        if let Some(services) = SERVICES.get() {
70            return Ok(services);
71        }
72        let mut services = ConfigLoader::load()?;
73        let report = augment(&mut services.providers).await;
74        services
75            .validate()
76            .map_err(|e| ConfigLoadError::Validation(e.to_string()))?;
77        let installed = Self::install(services)?;
78        if DISCOVERY.set(report).is_err() {
79            tracing::warn!(
80                "catalog discovery report already recorded for this process; keeping the first"
81            );
82        }
83        Ok(installed)
84    }
85
86    #[must_use]
87    pub fn discovery_report() -> Option<&'static DiscoveryReport> {
88        DISCOVERY.get()
89    }
90
91    pub fn try_init() -> ConfigLoadResult<&'static ServicesConfig> {
92        if let Some(services) = SERVICES.get() {
93            return Ok(services);
94        }
95        Self::init()
96    }
97
98    pub fn get() -> ConfigLoadResult<&'static ServicesConfig> {
99        SERVICES.get().ok_or(ConfigLoadError::NotInitialized)
100    }
101
102    pub fn providers() -> ConfigLoadResult<&'static ProviderRegistry> {
103        Self::get().map(|s| &s.providers)
104    }
105
106    pub fn gateway() -> ConfigLoadResult<Option<&'static GatewayConfig>> {
107        Self::get().map(ServicesConfig::gateway_config)
108    }
109
110    #[must_use]
111    pub fn is_initialized() -> bool {
112        SERVICES.get().is_some()
113    }
114
115    fn install(services: ServicesConfig) -> ConfigLoadResult<&'static ServicesConfig> {
116        SERVICES
117            .set(services)
118            .map_err(|_already| ConfigLoadError::AlreadyInitialized)?;
119        SERVICES.get().ok_or(ConfigLoadError::NotInitialized)
120    }
121}