Skip to main content

systemprompt_loader/
boot.rs

1//! The owned boot sequence: profile, secrets, config, services root.
2//!
3//! [`BootContext`] is what the entry crates build once at startup and hand to
4//! every reader — the CLI command context, the API server and the
5//! `AppContext` builder. It holds the loaded profile and the secrets service,
6//! and the stages that follow them (the resolved [`Config`] and the services
7//! root) as write-once cells filled by the `try_init_*`
8//! methods in boot order. A stage that has not run answers
9//! `NotInitialized`; a stage that has run keeps its first value, so the
10//! methods are safe to call from every path that may be the first to need it.
11//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use std::path::Path;
16use std::sync::{Arc, OnceLock};
17
18use systemprompt_config::{ConfigError, ConfigResult, LoadedProfile, SecretsService};
19use systemprompt_manifest::Config;
20use systemprompt_manifest::profile::Profile;
21use systemprompt_manifest::services::ServicesConfig;
22use systemprompt_models::errors::GlobalConfigError;
23
24use crate::bundle::{BundleResult, ServicesSourceBootstrap};
25use crate::config_loader::ConfigLoader;
26use crate::error::ConfigLoadResult;
27use systemprompt_manifest::services::root::ActiveServicesRoot;
28
29#[derive(Clone)]
30pub struct BootContext {
31    inner: Arc<BootInner>,
32}
33
34struct BootInner {
35    profile: Arc<LoadedProfile>,
36    secrets: Arc<SecretsService>,
37    config: OnceLock<Arc<Config>>,
38    services_root: OnceLock<ActiveServicesRoot>,
39}
40
41impl std::fmt::Debug for BootContext {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("BootContext")
44            .field("profile", &self.inner.profile.path())
45            .field("config", &self.inner.config.get().is_some())
46            .field("services_root", &self.inner.services_root.get())
47            .finish_non_exhaustive()
48    }
49}
50
51impl BootContext {
52    #[must_use]
53    pub fn new(profile: Arc<LoadedProfile>, secrets: Arc<SecretsService>) -> Self {
54        Self {
55            inner: Arc::new(BootInner {
56                profile,
57                secrets,
58                config: OnceLock::new(),
59                services_root: OnceLock::new(),
60            }),
61        }
62    }
63
64    #[must_use]
65    pub fn profile(&self) -> &Profile {
66        self.inner.profile.profile()
67    }
68
69    #[must_use]
70    pub fn loaded_profile(&self) -> &Arc<LoadedProfile> {
71        &self.inner.profile
72    }
73
74    #[must_use]
75    pub fn profile_path(&self) -> &Path {
76        self.inner.profile.path()
77    }
78
79    #[must_use]
80    pub fn secrets(&self) -> &Arc<SecretsService> {
81        &self.inner.secrets
82    }
83
84    pub fn config(&self) -> Result<&Arc<Config>, GlobalConfigError> {
85        self.inner
86            .config
87            .get()
88            .ok_or(GlobalConfigError::NotInitialized)
89    }
90
91    #[must_use]
92    pub fn is_config_initialized(&self) -> bool {
93        self.inner.config.get().is_some()
94    }
95
96    pub fn try_init_config(&self, services_root: Option<&Path>) -> ConfigResult<&Arc<Config>> {
97        if let Some(config) = self.inner.config.get() {
98            return Ok(config);
99        }
100        let config = systemprompt_config::build_from_profile(
101            &self.inner.profile,
102            &self.inner.secrets.get(),
103            services_root,
104        )?;
105        self.install_config(config)
106    }
107
108    pub fn install_config(&self, config: Config) -> ConfigResult<&Arc<Config>> {
109        self.inner
110            .config
111            .set(Arc::new(config))
112            .map_err(|_config| ConfigError::AlreadyInitialized)?;
113        self.inner
114            .config
115            .get()
116            .ok_or_else(|| ConfigError::AlreadyInitialized)
117    }
118
119    #[must_use]
120    pub fn services_root(&self) -> Option<&ActiveServicesRoot> {
121        self.inner.services_root.get()
122    }
123
124    pub fn require_services_root(&self) -> ActiveServicesRoot {
125        self.inner
126            .services_root
127            .get()
128            .cloned()
129            .unwrap_or_else(|| ActiveServicesRoot::baked(self.profile()))
130    }
131
132    pub fn install_services_root(&self, root: ActiveServicesRoot) -> &ActiveServicesRoot {
133        self.inner.services_root.get_or_init(|| root)
134    }
135
136    pub async fn try_resolve_services_root(
137        &self,
138        core_version: &str,
139    ) -> BundleResult<&ActiveServicesRoot> {
140        if let Some(active) = self.inner.services_root.get() {
141            return Ok(active);
142        }
143        let secrets = self.inner.secrets.get();
144        let root = ServicesSourceBootstrap::resolve(
145            self.profile(),
146            |name| secrets.get(name).cloned(),
147            core_version,
148        )
149        .await?;
150        Ok(self.install_services_root(root))
151    }
152
153    pub fn load_services_config(&self) -> ConfigLoadResult<ServicesConfig> {
154        self.config_loader().load()
155    }
156
157    #[must_use]
158    pub fn config_loader(&self) -> ConfigLoader {
159        ConfigLoader::for_profile(self.profile(), &self.require_services_root().path)
160    }
161}