Skip to main content

systemprompt_loader/
profile_loader.rs

1//! Reads, validates, and writes profile YAML files (with embedded
2//! gateway / cloud catalogues).
3//!
4//! [`ProfileLoader`] is a thin shim over
5//! [`systemprompt_config::load_profile_with_catalog`] that adds:
6//!
7//! - on-disk path conventions (`profiles/<name>.secrets.profile.yaml`),
8//! - serialization with a leading "do not commit secrets" header, and
9//! - directory enumeration for the `systemprompt cloud` CLI commands.
10//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use std::path::Path;
15use systemprompt_config::load_profile_with_catalog;
16use systemprompt_models::Profile;
17
18use crate::error::{ProfileLoadError, ProfileLoadResult};
19
20#[derive(Debug, Clone, Copy)]
21pub struct ProfileLoader;
22
23impl ProfileLoader {
24    pub fn load_from_path(profile_path: &Path) -> ProfileLoadResult<Profile> {
25        load_profile_with_catalog(profile_path).map_err(ProfileLoadError::from)
26    }
27
28    pub fn load(services_path: &Path, profile_name: &str) -> ProfileLoadResult<Profile> {
29        let profile_path = services_path
30            .join("profiles")
31            .join(format!("{profile_name}.secrets.profile.yaml"));
32
33        Self::load_from_path(&profile_path)
34    }
35
36    pub fn load_from_path_and_validate(profile_path: &Path) -> ProfileLoadResult<Profile> {
37        let profile = Self::load_from_path(profile_path)?;
38        profile.validate().map_err(ProfileLoadError::from)?;
39        Ok(profile)
40    }
41
42    pub fn load_and_validate(
43        services_path: &Path,
44        profile_name: &str,
45    ) -> ProfileLoadResult<Profile> {
46        let profile = Self::load(services_path, profile_name)?;
47        profile.validate().map_err(ProfileLoadError::from)?;
48        Ok(profile)
49    }
50
51    pub fn save(profile: &Profile, services_path: &Path) -> ProfileLoadResult<()> {
52        let profiles_dir = services_path.join("profiles");
53        std::fs::create_dir_all(&profiles_dir).map_err(|e| ProfileLoadError::Io {
54            path: profiles_dir.clone(),
55            source: e,
56        })?;
57
58        let profile_path = profiles_dir.join(format!("{}.secrets.profile.yaml", profile.name));
59        let content = profile.to_yaml().map_err(ProfileLoadError::from)?;
60
61        let content_with_header = format!(
62            "# systemprompt.io Profile: {}\n# \n# WARNING: This file contains secrets.\n# DO NOT \
63             commit to version control.\n\n{content}",
64            profile.display_name
65        );
66
67        std::fs::write(&profile_path, content_with_header).map_err(|e| ProfileLoadError::Io {
68            path: profile_path,
69            source: e,
70        })
71    }
72
73    #[must_use]
74    pub fn list_available(services_path: &Path) -> Vec<String> {
75        let profiles_dir = services_path.join("profiles");
76
77        if !profiles_dir.exists() {
78            return Vec::new();
79        }
80
81        match std::fs::read_dir(&profiles_dir) {
82            Ok(entries) => entries
83                .filter_map(Result::ok)
84                .filter_map(|e| {
85                    let name = e.file_name().to_string_lossy().to_string();
86                    name.strip_suffix(".secrets.profile.yaml")
87                        .map(str::to_owned)
88                })
89                .collect(),
90            Err(e) => {
91                tracing::warn!(
92                    error = %e,
93                    path = %profiles_dir.display(),
94                    "Failed to read profiles directory"
95                );
96                Vec::new()
97            },
98        }
99    }
100}