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//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12pub 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.get_valid_session(&session_key).cloned() {
76        session.touch();
77
78        if let Some(refreshed) = try_validate_context(&mut session, &profile_name).await {
79            session = refreshed;
80        }
81
82        store.upsert_session(&session_key, session.clone());
83        store.save(&sessions_dir)?;
84        return Ok(CliSessionContext {
85            session,
86            profile: profile.clone(),
87        });
88    }
89
90    let session_email_hint = store
91        .get_session(&session_key)
92        .map(|s| s.user_email.to_string());
93
94    let profile_ctx = ProfileContext {
95        name: &profile_name,
96        path: profile_path.to_path_buf(),
97    };
98
99    let session = create_new_session(
100        profile,
101        &profile_ctx,
102        &session_key,
103        &ctx.cli,
104        session_email_hint.as_deref(),
105    )
106    .await?;
107
108    store.upsert_session(&session_key, session.clone());
109    store.set_active_with_profile(&session_key, &profile_name);
110    store.save(&sessions_dir)?;
111
112    if session.session_token.as_str().is_empty() {
113        anyhow::bail!("Session token is empty. Session creation failed.");
114    }
115
116    Ok(CliSessionContext {
117        session,
118        profile: profile.clone(),
119    })
120}
121
122async fn try_session_from_active_key(ctx: &CommandContext) -> Result<Option<CliSessionContext>> {
123    let paths = ResolvedPaths::discover();
124    let sessions_dir = paths.sessions_dir();
125    let store = SessionStore::load_or_create(&sessions_dir)?;
126
127    let Some(ref active_key_str) = store.active_key else {
128        return Ok(None);
129    };
130
131    let active_key = store
132        .active_session_key()
133        .ok_or_else(|| anyhow::anyhow!("Invalid active session key: {}", active_key_str))?;
134
135    let active_profile = store.active_profile_name.as_deref();
136
137    let profile_path = if let Some(session) = store.active_session() {
138        match resolve_profile_path_from_session(session, active_profile)? {
139            Some(path) => path,
140            None => return Ok(None),
141        }
142    } else {
143        resolve_profile_path_without_session(&paths, &store, &active_key, active_profile)?
144    };
145
146    let profile = ProfileLoader::load_from_path(&profile_path).with_context(|| {
147        format!(
148            "Failed to load profile from stored path: {}",
149            profile_path.display()
150        )
151    })?;
152
153    initialize_profile_bootstraps(&profile_path)?;
154
155    let session_ctx = get_session_for_loaded_profile(&profile, &profile_path, ctx).await?;
156    Ok(Some(session_ctx))
157}
158
159pub async fn get_or_create_session(ctx: &CommandContext) -> Result<CliSessionContext> {
160    let session_ctx = resolve_session(ctx).await?;
161
162    let config = &ctx.cli;
163    let banner_requested = config.verbosity >= VerbosityLevel::Verbose;
164    let banner_warranted = session_ctx.profile.target.is_cloud();
165    if config.is_interactive()
166        && config.output_format == OutputFormat::Table
167        && config.verbosity != VerbosityLevel::Quiet
168        && (banner_requested || banner_warranted)
169    {
170        let tenant = session_ctx
171            .session
172            .tenant_key
173            .as_ref()
174            .map_or("local", systemprompt_identifiers::TenantId::as_str);
175        CliService::session_context_with_url(
176            session_ctx.session.profile_name.as_str(),
177            &session_ctx.session.session_id,
178            Some(tenant),
179            Some(&session_ctx.profile.server.api_external_url),
180        );
181    }
182
183    Ok(session_ctx)
184}
185
186async fn resolve_session(ctx: &CommandContext) -> Result<CliSessionContext> {
187    if let Some(ref profile_name) = ctx.cli.profile_override {
188        return get_session_for_profile(profile_name, ctx).await;
189    }
190
191    if ctx.env.profile.is_none()
192        && let Some(session_ctx) = try_session_from_active_key(ctx).await?
193    {
194        return Ok(session_ctx);
195    }
196
197    let profile = ProfileBootstrap::get()
198        .map_err(|_e| {
199            anyhow::anyhow!(
200                "Profile required.\n\nSet SYSTEMPROMPT_PROFILE environment variable to your \
201                 profile.yaml path, or use --profile <name>."
202            )
203        })?
204        .clone();
205
206    let profile_path_str = ProfileBootstrap::get_path().map_err(|_e| {
207        anyhow::anyhow!(
208            "Profile path required.\n\nSet SYSTEMPROMPT_PROFILE environment variable or use \
209             --profile <name>."
210        )
211    })?;
212
213    let profile_path = Path::new(profile_path_str);
214    get_session_for_loaded_profile(&profile, profile_path, ctx).await
215}