Skip to main content

systemprompt_cli/session/resolution/
mod.rs

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