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