systemprompt_cli/session/resolution/
helpers.rs1use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use anyhow::{Context, Result};
10use systemprompt_agent::repository::context::ContextRepository;
11use systemprompt_cloud::{
12 CliSession, CredentialsBootstrap, SessionIdentity, SessionKey, SessionStore,
13};
14use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
15use systemprompt_database::{Database, DbPool};
16use systemprompt_identifiers::{Email, ProfileName, SessionToken};
17use systemprompt_logging::CliService;
18use systemprompt_models::Profile;
19use systemprompt_models::auth::UserType;
20
21use super::ProfileContext;
22use crate::env_overrides::EnvOverrides;
23use crate::paths::ResolvedPaths;
24use crate::session::context::CliSessionContext;
25
26pub fn try_session_from_env(profile: &Profile, env: &EnvOverrides) -> Option<CliSessionContext> {
27 if !env.is_remote_cli {
28 return None;
29 }
30
31 let session_id = env.session.session_id.clone()?;
32 let context_id = env.session.context_id.clone()?;
33 let user_id = env.session.user_id.clone()?;
34 let auth_token = env.session.auth_token.clone()?;
35
36 let profile_name = ProfileName::new("remote");
37 let email = Email::new("remote@cli.local");
38 let session = CliSession::builder(
39 profile_name,
40 SessionToken::new(auth_token),
41 session_id,
42 context_id,
43 SessionIdentity::new(user_id, email, UserType::Admin),
44 )
45 .build();
46
47 Some(CliSessionContext {
48 session,
49 profile: profile.clone(),
50 })
51}
52
53pub fn extract_profile_name(profile_path: &Path) -> Result<String> {
54 let profile_dir = profile_path
55 .parent()
56 .ok_or_else(|| anyhow::anyhow!("Invalid profile path: no parent directory"))?;
57 profile_dir
58 .file_name()
59 .and_then(|n| n.to_str())
60 .map(String::from)
61 .ok_or_else(|| anyhow::anyhow!("Invalid profile directory name"))
62}
63
64pub(super) async fn create_new_session(
65 profile: &Profile,
66 profile_ctx: &ProfileContext<'_>,
67 session_key: &SessionKey,
68 config: &crate::CliConfig,
69 session_email_hint: Option<&str>,
70) -> Result<CliSession> {
71 if session_key.is_local() || profile.is_local_trial() {
72 return crate::session::creation::create_local_session(
73 profile,
74 profile_ctx,
75 session_key,
76 config,
77 session_email_hint,
78 )
79 .await;
80 }
81
82 CredentialsBootstrap::try_init()
83 .await
84 .context("Failed to initialize credentials. Run 'systemprompt cloud auth login'.")?;
85
86 let creds = CredentialsBootstrap::require()
87 .map_err(|_e| {
88 anyhow::anyhow!(
89 "Cloud authentication required.\n\nRun 'systemprompt cloud auth login' to \
90 authenticate."
91 )
92 })?
93 .clone();
94
95 crate::session::creation::create_session_for_tenant(
96 crate::session::creation::TenantSessionParams {
97 creds: &creds,
98 profile,
99 profile_ctx,
100 session_key,
101 config,
102 session_email_hint,
103 },
104 )
105 .await
106}
107
108pub fn resolve_profile_path_from_session(
109 session: &CliSession,
110 active_profile: Option<&str>,
111) -> Result<Option<PathBuf>> {
112 if let Some(expected) = active_profile
113 && session.profile_name.as_str() != expected
114 {
115 anyhow::bail!(
116 "No session for active profile '{}'.\n\nRun 'systemprompt admin session login' to \
117 authenticate.",
118 expected
119 );
120 }
121 match &session.profile_path {
122 Some(path) if path.exists() => Ok(Some(path.clone())),
123 _ => Ok(None),
124 }
125}
126
127pub fn resolve_profile_path_without_session(
128 paths: &ResolvedPaths,
129 store: &SessionStore,
130 active_key: &SessionKey,
131 active_profile: Option<&str>,
132) -> Result<PathBuf> {
133 if let Some(profile_name) = active_profile {
134 let profile_dir = paths.profiles_dir().join(profile_name);
135 let config_path = systemprompt_cloud::ProfilePath::Config.resolve(&profile_dir);
136 if config_path.exists() {
137 anyhow::bail!(
138 "No session for active profile '{}'.\n\nRun 'systemprompt admin session login' to \
139 authenticate, or 'systemprompt admin session switch <profile>' to change \
140 profiles.",
141 profile_name
142 );
143 }
144 }
145
146 store
147 .get_session(active_key)
148 .and_then(|s| s.profile_path.as_ref())
149 .filter(|p| p.exists())
150 .cloned()
151 .ok_or_else(|| {
152 let profile_hint = active_profile.unwrap_or("unknown");
153 anyhow::anyhow!(
154 "No session for active profile '{}'.\n\nRun 'systemprompt admin session login' to \
155 authenticate, or 'systemprompt admin session switch <profile>' to change \
156 profiles.",
157 profile_hint
158 )
159 })
160}
161
162pub(super) fn initialize_profile_bootstraps(profile_path: &Path) -> Result<()> {
163 if !ProfileBootstrap::is_initialized() {
164 ProfileBootstrap::init_from_path(profile_path).with_context(|| {
165 format!(
166 "Failed to initialize profile from {}",
167 profile_path.display()
168 )
169 })?;
170 }
171
172 if !SecretsBootstrap::is_initialized() {
173 SecretsBootstrap::try_init().with_context(|| "Failed to initialize secrets for session")?;
174 }
175
176 Ok(())
177}
178
179pub(super) async fn try_validate_context(
180 session: &mut CliSession,
181 profile_name: &str,
182) -> Option<CliSession> {
183 let secrets = SecretsBootstrap::get()
184 .map_err(|e| tracing::debug!(error = %e, "Failed to get secrets for context validation"))
185 .ok()?;
186 let db = Database::new_postgres(&secrets.database_url)
187 .await
188 .map_err(
189 |e| tracing::debug!(error = %e, "Failed to connect to database for context validation"),
190 )
191 .ok()?;
192 let db_pool = DbPool::from(Arc::new(db));
193 revalidate_context(&db_pool, session, profile_name).await
194}
195
196pub async fn revalidate_context(
197 db_pool: &DbPool,
198 session: &mut CliSession,
199 profile_name: &str,
200) -> Option<CliSession> {
201 let context_repo = ContextRepository::new(db_pool)
202 .map_err(|e| tracing::debug!(error = %e, "Failed to build context repository"))
203 .ok()?;
204
205 let is_valid = context_repo
206 .validate_context_ownership(&session.context_id, &session.user_id)
207 .await
208 .is_ok();
209
210 if is_valid {
211 return None;
212 }
213
214 CliService::warning("Session context is stale, recovering session context...");
215
216 let new_context_id = context_repo
217 .get_or_create_cli_context(
218 &session.user_id,
219 &session.session_id,
220 &format!("CLI Session - {}", profile_name),
221 )
222 .await
223 .map_err(|e| tracing::warn!(error = %e, "Failed to recover session context"))
224 .ok()?;
225
226 session.set_context_id(new_context_id);
227 Some(session.clone())
228}