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