systemprompt_cli/session/
resolution.rs1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use anyhow::{Context, Result};
5use systemprompt_agent::repository::context::ContextRepository;
6use systemprompt_cloud::{CliSession, CredentialsBootstrap, SessionKey, SessionStore};
7use systemprompt_database::{Database, DbPool};
8use systemprompt_identifiers::{ContextId, Email, ProfileName, SessionId, SessionToken, UserId};
9use systemprompt_loader::ProfileLoader;
10use systemprompt_logging::CliService;
11use systemprompt_models::auth::UserType;
12use systemprompt_models::profile_bootstrap::ProfileBootstrap;
13use systemprompt_models::{Profile, SecretsBootstrap};
14
15use super::context::CliSessionContext;
16use crate::paths::ResolvedPaths;
17use crate::CliConfig;
18
19pub(super) struct ProfileContext<'a> {
20 pub name: &'a str,
21 pub path: PathBuf,
22}
23
24fn try_session_from_env(profile: &Profile) -> Option<CliSessionContext> {
25 if std::env::var("SYSTEMPROMPT_CLI_REMOTE").is_err() {
26 return None;
27 }
28
29 let session_id = std::env::var("SYSTEMPROMPT_SESSION_ID").ok()?;
30 let context_id = std::env::var("SYSTEMPROMPT_CONTEXT_ID").ok()?;
31 let user_id = std::env::var("SYSTEMPROMPT_USER_ID").ok()?;
32 let auth_token = std::env::var("SYSTEMPROMPT_AUTH_TOKEN").ok()?;
33
34 let profile_name = ProfileName::new("remote");
35 let email = Email::new("remote@cli.local");
36 let session = CliSession::builder(
37 profile_name,
38 SessionToken::new(auth_token),
39 SessionId::new(session_id),
40 ContextId::new(context_id),
41 )
42 .with_user(UserId::new(user_id), email)
43 .with_user_type(UserType::Admin)
44 .build();
45
46 Some(CliSessionContext {
47 session,
48 profile: profile.clone(),
49 })
50}
51
52async fn get_session_for_profile(
53 profile_input: &str,
54 config: &CliConfig,
55) -> Result<CliSessionContext> {
56 let (profile_path, profile) = crate::shared::resolve_profile_with_data(profile_input)
57 .map_err(|e| anyhow::anyhow!("{}", e))?;
58
59 if !ProfileBootstrap::is_initialized() {
60 ProfileBootstrap::init_from_path(&profile_path)
61 .with_context(|| format!("Failed to initialize profile '{}'", profile_input))?;
62 }
63
64 if !SecretsBootstrap::is_initialized() {
65 SecretsBootstrap::try_init().with_context(|| {
66 "Failed to initialize secrets. Check your profile's secrets configuration."
67 })?;
68 }
69
70 get_session_for_loaded_profile(&profile, &profile_path, config).await
71}
72
73async fn get_session_for_loaded_profile(
74 profile: &Profile,
75 profile_path: &Path,
76 config: &CliConfig,
77) -> Result<CliSessionContext> {
78 if let Some(ctx) = try_session_from_env(profile) {
79 return Ok(ctx);
80 }
81
82 let profile_dir = profile_path
83 .parent()
84 .ok_or_else(|| anyhow::anyhow!("Invalid profile path: no parent directory"))?;
85 let profile_name = profile_dir
86 .file_name()
87 .and_then(|n| n.to_str())
88 .ok_or_else(|| anyhow::anyhow!("Invalid profile directory name"))?
89 .to_string();
90
91 let tenant_id = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_deref());
92 let session_key = SessionKey::from_tenant_id(tenant_id);
93
94 let sessions_dir = ResolvedPaths::discover().sessions_dir()?;
95
96 let mut store = SessionStore::load_or_create(&sessions_dir)?;
97
98 if let Some(mut session) = store.get_valid_session(&session_key).cloned() {
99 session.touch();
100
101 if let Some(refreshed) = try_validate_context(&mut session, &profile_name).await {
102 session = refreshed;
103 }
104
105 store.upsert_session(&session_key, session.clone());
106 store.save(&sessions_dir)?;
107 return Ok(CliSessionContext {
108 session,
109 profile: profile.clone(),
110 });
111 }
112
113 let profile_ctx = ProfileContext {
114 name: &profile_name,
115 path: profile_path.to_path_buf(),
116 };
117
118 let session = if session_key.is_local() {
119 super::creation::create_local_session(profile, &profile_ctx, &session_key, config).await?
120 } else {
121 CredentialsBootstrap::try_init()
122 .await
123 .context("Failed to initialize credentials. Run 'systemprompt cloud auth login'.")?;
124
125 let creds = CredentialsBootstrap::require()
126 .map_err(|_| {
127 anyhow::anyhow!(
128 "Cloud authentication required.\n\nRun 'systemprompt cloud auth login' to \
129 authenticate."
130 )
131 })?
132 .clone();
133
134 super::creation::create_session_for_tenant(
135 &creds,
136 profile,
137 &profile_ctx,
138 &session_key,
139 config,
140 )
141 .await?
142 };
143
144 store.upsert_session(&session_key, session.clone());
145 store.set_active_with_profile(&session_key, &profile_name);
146 store.save(&sessions_dir)?;
147
148 if session.session_token.as_str().is_empty() {
149 anyhow::bail!("Session token is empty. Session creation failed.");
150 }
151
152 Ok(CliSessionContext {
153 session,
154 profile: profile.clone(),
155 })
156}
157
158async fn try_session_from_active_key(config: &CliConfig) -> Result<Option<CliSessionContext>> {
159 let paths = ResolvedPaths::discover();
160 let sessions_dir = paths.sessions_dir()?;
161 let store = SessionStore::load_or_create(&sessions_dir)?;
162
163 let Some(ref active_key_str) = store.active_key else {
164 return Ok(None);
165 };
166
167 let active_key = store
168 .active_session_key()
169 .ok_or_else(|| anyhow::anyhow!("Invalid active session key: {}", active_key_str))?;
170
171 let active_profile = store.active_profile_name.as_deref();
172
173 let profile_path = if let Some(session) = store.active_session() {
174 if let Some(expected) = active_profile {
175 if session.profile_name.as_str() != expected {
176 anyhow::bail!(
177 "No session for active profile '{}'.\n\nRun 'systemprompt admin session \
178 login' to authenticate.",
179 expected
180 );
181 }
182 }
183 match &session.profile_path {
184 Some(path) if path.exists() => path.clone(),
185 _ => return Ok(None),
186 }
187 } else {
188 if let Some(profile_name) = active_profile {
189 let profile_dir = paths.profiles_dir().join(profile_name);
190 let config_path = systemprompt_cloud::ProfilePath::Config.resolve(&profile_dir);
191 if config_path.exists() {
192 anyhow::bail!(
193 "No session for active profile '{}'.\n\nRun 'systemprompt admin session \
194 login' to authenticate, or 'systemprompt admin session switch <profile>' to \
195 change profiles.",
196 profile_name
197 );
198 }
199 }
200
201 let raw_session = store.get_session(&active_key);
202 if let Some(path) = raw_session
203 .and_then(|s| s.profile_path.as_ref())
204 .filter(|p| p.exists())
205 {
206 path.clone()
207 } else {
208 let profile_hint = active_profile.unwrap_or("unknown");
209 anyhow::bail!(
210 "No session for active profile '{}'.\n\nRun 'systemprompt admin session login' to \
211 authenticate, or 'systemprompt admin session switch <profile>' to change \
212 profiles.",
213 profile_hint
214 );
215 }
216 };
217
218 let profile = ProfileLoader::load_from_path(&profile_path).with_context(|| {
219 format!(
220 "Failed to load profile from stored path: {}",
221 profile_path.display()
222 )
223 })?;
224
225 if !ProfileBootstrap::is_initialized() {
226 ProfileBootstrap::init_from_path(&profile_path).with_context(|| {
227 format!(
228 "Failed to initialize profile from {}",
229 profile_path.display()
230 )
231 })?;
232 }
233
234 if !SecretsBootstrap::is_initialized() {
235 SecretsBootstrap::try_init().with_context(|| "Failed to initialize secrets for session")?;
236 }
237
238 let ctx = get_session_for_loaded_profile(&profile, &profile_path, config).await?;
239 Ok(Some(ctx))
240}
241
242pub async fn get_or_create_session(config: &CliConfig) -> Result<CliSessionContext> {
243 let ctx = resolve_session(config).await?;
244
245 if config.is_interactive() {
246 let tenant = ctx
247 .session
248 .tenant_key
249 .as_ref()
250 .map_or("local", systemprompt_identifiers::TenantId::as_str);
251 CliService::session_context_with_url(
252 ctx.session.profile_name.as_str(),
253 &ctx.session.session_id,
254 Some(tenant),
255 Some(&ctx.profile.server.api_external_url),
256 );
257 }
258
259 Ok(ctx)
260}
261
262async fn resolve_session(config: &CliConfig) -> Result<CliSessionContext> {
263 if let Some(ref profile_name) = config.profile_override {
264 return get_session_for_profile(profile_name, config).await;
265 }
266
267 let env_profile_set = std::env::var("SYSTEMPROMPT_PROFILE").is_ok();
268
269 if !env_profile_set {
270 if let Some(ctx) = try_session_from_active_key(config).await? {
271 return Ok(ctx);
272 }
273 }
274
275 let profile = ProfileBootstrap::get()
276 .map_err(|_| {
277 anyhow::anyhow!(
278 "Profile required.\n\nSet SYSTEMPROMPT_PROFILE environment variable to your \
279 profile.yaml path, or use --profile <name>."
280 )
281 })?
282 .clone();
283
284 let profile_path_str = ProfileBootstrap::get_path().map_err(|_| {
285 anyhow::anyhow!(
286 "Profile path required.\n\nSet SYSTEMPROMPT_PROFILE environment variable or use \
287 --profile <name>."
288 )
289 })?;
290
291 let profile_path = Path::new(profile_path_str);
292 get_session_for_loaded_profile(&profile, profile_path, config).await
293}
294
295async fn try_validate_context(session: &mut CliSession, profile_name: &str) -> Option<CliSession> {
296 let secrets = SecretsBootstrap::get().ok()?;
297 let db = Database::new_postgres(&secrets.database_url).await.ok()?;
298 let db_pool = DbPool::from(Arc::new(db));
299 let context_repo = ContextRepository::new(db_pool);
300
301 let is_valid = context_repo
302 .validate_context_ownership(&session.context_id, &session.user_id)
303 .await
304 .is_ok();
305
306 if is_valid {
307 return None;
308 }
309
310 CliService::warning("Session context is stale, creating new context...");
311
312 let new_context_id = context_repo
313 .create_context(
314 &session.user_id,
315 Some(&session.session_id),
316 &format!("CLI Session - {}", profile_name),
317 )
318 .await
319 .ok()?;
320
321 session.set_context_id(new_context_id);
322 Some(session.clone())
323}