systemprompt_cli/session/resolution/
mod.rs1pub mod helpers;
20
21use std::path::{Path, PathBuf};
22
23use anyhow::{Context, Result};
24use systemprompt_cloud::{SessionKey, SessionStore};
25use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
26use systemprompt_identifiers::ProfileName;
27use systemprompt_loader::ProfileLoader;
28use systemprompt_logging::CliService;
29use systemprompt_manifest::Profile;
30
31use super::context::CliSessionContext;
32use crate::cli_settings::{OutputFormat, VerbosityLevel};
33use crate::context::CommandContext;
34use crate::paths::ResolvedPaths;
35use crate::shared::ProfileSource;
36use helpers::{
37 create_new_session, extract_profile_name, initialize_profile_bootstraps,
38 resolve_profile_path_from_session, resolve_profile_path_without_session, try_session_from_env,
39 try_validate_context,
40};
41
42#[derive(Debug)]
43pub struct ProfileContext<'a> {
44 pub name: &'a ProfileName,
45 pub path: PathBuf,
46}
47
48async fn get_session_for_profile(
49 profile_input: &str,
50 ctx: &CommandContext,
51) -> Result<CliSessionContext> {
52 let (profile_path, profile) = crate::shared::resolve_profile_with_data(profile_input)
53 .map_err(|e| anyhow::anyhow!("{}", e))?;
54
55 if !ProfileBootstrap::is_initialized() {
56 ProfileBootstrap::init_from_path(&profile_path)
57 .with_context(|| format!("Failed to initialize profile '{}'", profile_input))?;
58 }
59
60 if !SecretsBootstrap::is_initialized() {
61 SecretsBootstrap::try_init().await.with_context(
62 || "Failed to initialize secrets. Check your profile's secrets configuration.",
63 )?;
64 }
65
66 get_session_for_loaded_profile(&profile, &profile_path, ProfileSource::Cli, ctx).await
67}
68
69async fn get_session_for_loaded_profile(
70 profile: &Profile,
71 profile_path: &Path,
72 source: ProfileSource,
73 ctx: &CommandContext,
74) -> Result<CliSessionContext> {
75 if let Some(session_ctx) = try_session_from_env(profile, &ctx.env) {
76 return Ok(session_ctx);
77 }
78
79 let profile_name = extract_profile_name(profile_path)?;
80 let tenant_id = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_ref());
81 let session_key = SessionKey::from_tenant_id(tenant_id);
82 let sessions_dir = ResolvedPaths::discover().sessions_dir();
83 let mut store = SessionStore::load_or_create(&sessions_dir)?;
84
85 if let Some(mut session) = store
86 .get_valid_session(&session_key, &profile.security.issuer)
87 .cloned()
88 {
89 session.touch();
90
91 if let Some(refreshed) = try_validate_context(&mut session, &profile_name).await {
92 session = refreshed;
93 }
94
95 store.upsert_session(&session_key, session.clone());
96 store.save(&sessions_dir)?;
97 return Ok(CliSessionContext {
98 session,
99 profile: profile.clone(),
100 });
101 }
102
103 let session_email_hint = store
104 .get_session(&session_key)
105 .map(|s| s.user_email.to_string());
106
107 let profile_ctx = ProfileContext {
108 name: &profile_name,
109 path: profile_path.to_path_buf(),
110 };
111
112 let session = create_new_session(
113 profile,
114 &profile_ctx,
115 &session_key,
116 &ctx.cli,
117 session_email_hint.as_deref(),
118 )
119 .await?;
120
121 record_new_session(&mut store, &session_key, &session, &profile_name, source);
122 store.save(&sessions_dir)?;
123
124 if session.session_token.as_str().is_empty() {
125 anyhow::bail!("Session token is empty. Session creation failed.");
126 }
127
128 Ok(CliSessionContext {
129 session,
130 profile: profile.clone(),
131 })
132}
133
134pub fn record_new_session(
135 store: &mut SessionStore,
136 session_key: &SessionKey,
137 session: &systemprompt_cloud::CliSession,
138 profile_name: &ProfileName,
139 source: ProfileSource,
140) {
141 store.upsert_session(session_key, session.clone());
142 if source == ProfileSource::Session {
143 store.set_active_with_profile(session_key, profile_name);
144 }
145}
146
147async fn try_session_from_active_key(ctx: &CommandContext) -> Result<Option<CliSessionContext>> {
148 let paths = ResolvedPaths::discover();
149 let sessions_dir = paths.sessions_dir();
150 let store = SessionStore::load_or_create(&sessions_dir)?;
151
152 let Some(ref active_key_str) = store.active_key else {
153 return Ok(None);
154 };
155
156 let active_key = store
157 .active_session_key()
158 .ok_or_else(|| anyhow::anyhow!("Invalid active session key: {}", active_key_str))?;
159
160 let active_profile = store.active_profile_name.as_ref();
161
162 let profile_path = if let Some(session) = store.active_session_for_profile_discovery() {
163 match resolve_profile_path_from_session(session, active_profile)? {
164 Some(path) => path,
165 None => return Ok(None),
166 }
167 } else {
168 resolve_profile_path_without_session(&paths, &store, &active_key, active_profile)?
169 };
170
171 let profile = ProfileLoader::load_from_path(&profile_path).with_context(|| {
172 format!(
173 "Failed to load profile from stored path: {}",
174 profile_path.display()
175 )
176 })?;
177
178 initialize_profile_bootstraps(&profile_path).await?;
179
180 let session_ctx =
181 get_session_for_loaded_profile(&profile, &profile_path, ProfileSource::Session, ctx)
182 .await?;
183 Ok(Some(session_ctx))
184}
185
186pub async fn get_or_create_session(ctx: &CommandContext) -> Result<CliSessionContext> {
187 let session_ctx = resolve_session(ctx).await?;
188
189 let config = &ctx.cli;
190 let banner_requested = config.verbosity >= VerbosityLevel::Verbose;
191 let banner_warranted = session_ctx.profile.target.is_cloud();
192 if config.is_interactive()
193 && config.output_format == OutputFormat::Table
194 && config.verbosity != VerbosityLevel::Quiet
195 && (banner_requested || banner_warranted)
196 {
197 let tenant = session_ctx
198 .session
199 .tenant_key
200 .as_ref()
201 .map_or("local", systemprompt_identifiers::TenantId::as_str);
202 CliService::session_context_with_url(
203 session_ctx.session.profile_name.as_str(),
204 &session_ctx.session.session_id,
205 Some(tenant),
206 Some(&session_ctx.profile.server.api_external_url),
207 );
208 }
209
210 Ok(session_ctx)
211}
212
213async fn resolve_session(ctx: &CommandContext) -> Result<CliSessionContext> {
214 if let Some(ref profile_name) = ctx.cli.profile_override {
215 return get_session_for_profile(profile_name, ctx).await;
216 }
217
218 if ctx.env.profile.is_none()
219 && let Some(session_ctx) = try_session_from_active_key(ctx).await?
220 {
221 return Ok(session_ctx);
222 }
223
224 let profile = ProfileBootstrap::get()
225 .map_err(|_e| {
226 anyhow::anyhow!(
227 "Profile required.\n\nSet SYSTEMPROMPT_PROFILE environment variable to your \
228 profile.yaml path, or use --profile <name>."
229 )
230 })?
231 .clone();
232
233 let profile_path_str = ProfileBootstrap::get_path().map_err(|_e| {
234 anyhow::anyhow!(
235 "Profile path required.\n\nSet SYSTEMPROMPT_PROFILE environment variable or use \
236 --profile <name>."
237 )
238 })?;
239
240 let source = if ctx.env.profile.is_some() {
241 ProfileSource::Env
242 } else {
243 ProfileSource::Discovery
244 };
245 let profile_path = Path::new(profile_path_str);
246 get_session_for_loaded_profile(&profile, profile_path, source, ctx).await
247}