systemprompt_cloud/
context.rs1use systemprompt_models::profile_bootstrap::ProfileBootstrap;
2use systemprompt_models::Profile;
3
4use crate::api_client::CloudApiClient;
5use crate::credentials::CloudCredentials;
6use crate::error::{CloudError, CloudResult};
7use crate::paths::{get_cloud_paths, CloudPath};
8use crate::tenants::{StoredTenant, TenantStore};
9
10#[derive(Debug, Clone)]
11pub struct ResolvedTenant {
12 pub id: String,
13 pub name: String,
14 pub app_id: Option<String>,
15 pub hostname: Option<String>,
16 pub region: Option<String>,
17}
18
19impl From<StoredTenant> for ResolvedTenant {
20 fn from(tenant: StoredTenant) -> Self {
21 Self {
22 id: tenant.id,
23 name: tenant.name,
24 app_id: tenant.app_id,
25 hostname: tenant.hostname,
26 region: tenant.region,
27 }
28 }
29}
30
31#[derive(Debug)]
32pub struct CloudContext {
33 pub credentials: CloudCredentials,
34 pub profile: Option<&'static Profile>,
35 pub tenant: Option<ResolvedTenant>,
36 pub api_client: CloudApiClient,
37}
38
39impl CloudContext {
40 pub fn new_authenticated() -> CloudResult<Self> {
41 let cloud_paths = get_cloud_paths().map_err(|_| CloudError::NotAuthenticated)?;
42 let creds_path = cloud_paths.resolve(CloudPath::Credentials);
43 let credentials = CloudCredentials::load_and_validate_from_path(&creds_path)
44 .map_err(|_| CloudError::NotAuthenticated)?;
45
46 let api_client = CloudApiClient::new(&credentials.api_url, &credentials.api_token);
47
48 Ok(Self {
49 credentials,
50 profile: None,
51 tenant: None,
52 api_client,
53 })
54 }
55
56 pub fn with_profile(mut self) -> CloudResult<Self> {
57 let profile = ProfileBootstrap::get().map_err(|e| CloudError::ProfileRequired {
58 message: e.to_string(),
59 })?;
60
61 self.profile = Some(profile);
62
63 if let Some(ref cloud_config) = profile.cloud {
64 if let Some(ref tenant_id) = cloud_config.tenant_id {
65 self.tenant = Self::resolve_tenant(tenant_id)?;
66 }
67 }
68
69 Ok(self)
70 }
71
72 fn resolve_tenant(tenant_id: &str) -> CloudResult<Option<ResolvedTenant>> {
73 let cloud_paths = get_cloud_paths().map_err(|_| CloudError::TenantsNotSynced)?;
74 let tenants_path = cloud_paths.resolve(CloudPath::Tenants);
75
76 if !tenants_path.exists() {
77 return Ok(None);
78 }
79
80 let store =
81 TenantStore::load_from_path(&tenants_path).map_err(|_| CloudError::TenantsNotSynced)?;
82
83 store.find_tenant(tenant_id).map_or_else(
84 || {
85 Err(CloudError::TenantNotFound {
86 tenant_id: tenant_id.to_string(),
87 })
88 },
89 |tenant| Ok(Some(ResolvedTenant::from(tenant.clone()))),
90 )
91 }
92
93 pub fn tenant_id(&self) -> CloudResult<&str> {
94 self.tenant
95 .as_ref()
96 .map(|t| t.id.as_str())
97 .ok_or(CloudError::TenantNotConfigured)
98 }
99
100 pub fn app_id(&self) -> CloudResult<&str> {
101 self.tenant
102 .as_ref()
103 .and_then(|t| t.app_id.as_deref())
104 .ok_or(CloudError::AppNotConfigured)
105 }
106
107 #[must_use]
108 pub fn tenant_name(&self) -> &str {
109 self.tenant.as_ref().map_or("unknown", |t| t.name.as_str())
110 }
111
112 #[must_use]
113 pub fn hostname(&self) -> Option<&str> {
114 self.tenant.as_ref().and_then(|t| t.hostname.as_deref())
115 }
116
117 pub fn profile(&self) -> CloudResult<&'static Profile> {
118 self.profile.ok_or_else(|| CloudError::ProfileRequired {
119 message: "Profile not loaded in context".into(),
120 })
121 }
122
123 #[must_use]
124 pub const fn has_tenant(&self) -> bool {
125 self.tenant.is_some()
126 }
127}