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//! A freshly minted session is stored under its key, but only a profile
10//! chosen from the stored session may become the active one: an explicit
11//! `--profile` or `SYSTEMPROMPT_PROFILE` override, or a discovered profile,
12//! is a one-shot target and must not leave the next bare invocation pointed
13//! at it. `admin session switch|login` remain the ways to change the active
14//! profile.
15//!
16//! Copyright (c) systemprompt.io — Business Source License 1.1.
17//! See <https://systemprompt.io> for licensing details.
18
19pub 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}