Skip to main content

systemprompt_cli/session/resolution/
helpers.rs

1//! Helpers resolving an existing CLI session for reuse.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use anyhow::{Context, Result};
10use systemprompt_agent::repository::context::ContextRepository;
11use systemprompt_cloud::{
12    CliSession, CredentialsBootstrap, SessionBinding, SessionIdentity, SessionKey, SessionStore,
13};
14use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
15use systemprompt_database::{Database, DbPool};
16use systemprompt_identifiers::{Email, ProfileName};
17use systemprompt_logging::CliService;
18use systemprompt_manifest::Profile;
19use systemprompt_models::auth::UserType;
20
21use super::ProfileContext;
22use crate::env_overrides::EnvOverrides;
23use crate::paths::ResolvedPaths;
24use crate::session::context::CliSessionContext;
25
26pub fn try_session_from_env(profile: &Profile, env: &EnvOverrides) -> Option<CliSessionContext> {
27    if !env.is_remote_cli {
28        return None;
29    }
30
31    let session_id = env.session.session_id.clone()?;
32    let context_id = env.session.context_id.clone()?;
33    let user_id = env.session.user_id.clone()?;
34    let auth_token = env.session.auth_token.clone()?;
35
36    let profile_name = ProfileName::try_new("remote").ok()?;
37    let email = Email::try_new("remote@cli.local").ok()?;
38    let session = CliSession::builder(
39        SessionBinding::new(profile_name, profile.security.issuer.clone()),
40        auth_token,
41        session_id,
42        context_id,
43        SessionIdentity::new(user_id, email, UserType::Admin),
44    )
45    .build();
46
47    Some(CliSessionContext {
48        session,
49        profile: profile.clone(),
50    })
51}
52
53pub fn extract_profile_name(profile_path: &Path) -> Result<ProfileName> {
54    let profile_dir = profile_path
55        .parent()
56        .ok_or_else(|| anyhow::anyhow!("Invalid profile path: no parent directory"))?;
57    let dir_name = profile_dir
58        .file_name()
59        .and_then(|n| n.to_str())
60        .ok_or_else(|| anyhow::anyhow!("Invalid profile directory name"))?;
61    ProfileName::try_new(dir_name)
62        .with_context(|| format!("Profile directory '{dir_name}' is not a valid profile name"))
63}
64
65pub(super) async fn create_new_session(
66    profile: &Profile,
67    profile_ctx: &ProfileContext<'_>,
68    session_key: &SessionKey,
69    config: &crate::CliConfig,
70    session_email_hint: Option<&str>,
71) -> Result<CliSession> {
72    if session_key.is_local() || profile.is_local_trial() {
73        return crate::session::creation::create_local_session(
74            profile,
75            profile_ctx,
76            session_key,
77            config,
78            session_email_hint,
79        )
80        .await;
81    }
82
83    CredentialsBootstrap::try_init()
84        .await
85        .context("Failed to initialize credentials. Run 'systemprompt cloud auth login'.")?;
86
87    let creds = CredentialsBootstrap::require()
88        .map_err(|_e| {
89            anyhow::anyhow!(
90                "Cloud authentication required.\n\nRun 'systemprompt cloud auth login' to \
91                 authenticate."
92            )
93        })?
94        .clone();
95
96    crate::session::creation::create_session_for_tenant(
97        crate::session::creation::TenantSessionParams {
98            creds: &creds,
99            profile,
100            profile_ctx,
101            session_key,
102            config,
103            session_email_hint,
104        },
105    )
106    .await
107}
108
109pub fn resolve_profile_path_from_session(
110    session: &CliSession,
111    active_profile: Option<&ProfileName>,
112) -> Result<Option<PathBuf>> {
113    if let Some(expected) = active_profile
114        && session.profile_name != *expected
115    {
116        anyhow::bail!(
117            "No session for active profile '{}'.\n\nRun 'systemprompt admin session login' to \
118                 authenticate.",
119            expected
120        );
121    }
122    match &session.profile_path {
123        Some(path) if path.exists() => Ok(Some(path.clone())),
124        _ => Ok(None),
125    }
126}
127
128pub fn resolve_profile_path_without_session(
129    paths: &ResolvedPaths,
130    store: &SessionStore,
131    active_key: &SessionKey,
132    active_profile: Option<&ProfileName>,
133) -> Result<PathBuf> {
134    if let Some(profile_name) = active_profile {
135        let profile_dir = paths.profiles_dir().join(profile_name.as_str());
136        let config_path = systemprompt_cloud::ProfilePath::Config.resolve(&profile_dir);
137        if config_path.exists() {
138            anyhow::bail!(
139                "No session for active profile '{}'.\n\nRun 'systemprompt admin session login' to \
140                 authenticate, or 'systemprompt admin session switch <profile>' to change \
141                 profiles.",
142                profile_name
143            );
144        }
145    }
146
147    store
148        .get_session(active_key)
149        .and_then(|s| s.profile_path.as_ref())
150        .filter(|p| p.exists())
151        .cloned()
152        .ok_or_else(|| {
153            let profile_hint = active_profile.map_or("unknown", ProfileName::as_str);
154            anyhow::anyhow!(
155                "No session for active profile '{}'.\n\nRun 'systemprompt admin session login' to \
156                 authenticate, or 'systemprompt admin session switch <profile>' to change \
157                 profiles.",
158                profile_hint
159            )
160        })
161}
162
163pub(super) async fn initialize_profile_bootstraps(profile_path: &Path) -> Result<()> {
164    if !ProfileBootstrap::is_initialized() {
165        ProfileBootstrap::init_from_path(profile_path).with_context(|| {
166            format!(
167                "Failed to initialize profile from {}",
168                profile_path.display()
169            )
170        })?;
171    }
172
173    if !SecretsBootstrap::is_initialized() {
174        SecretsBootstrap::try_init()
175            .await
176            .with_context(|| "Failed to initialize secrets for session")?;
177    }
178
179    Ok(())
180}
181
182pub(super) async fn try_validate_context(
183    session: &mut CliSession,
184    profile_name: &ProfileName,
185) -> Option<CliSession> {
186    let secrets = SecretsBootstrap::get()
187        .map_err(|e| tracing::debug!(error = %e, "Failed to get secrets for context validation"))
188        .ok()?;
189    let db = Database::new_postgres(&secrets.database_url)
190        .await
191        .map_err(
192            |e| tracing::debug!(error = %e, "Failed to connect to database for context validation"),
193        )
194        .ok()?;
195    let db_pool = DbPool::from(Arc::new(db));
196    revalidate_context(&db_pool, session, profile_name).await
197}
198
199pub async fn revalidate_context(
200    db_pool: &DbPool,
201    session: &mut CliSession,
202    profile_name: &ProfileName,
203) -> Option<CliSession> {
204    let context_repo = ContextRepository::new(db_pool);
205
206    let is_valid = context_repo
207        .validate_context_ownership(&session.context_id, &session.user_id)
208        .await
209        .is_ok();
210
211    if is_valid {
212        return None;
213    }
214
215    CliService::warning("Session context is stale, recovering session context...");
216
217    let new_context_id = context_repo
218        .get_or_create_cli_context(
219            &session.user_id,
220            &session.session_id,
221            &format!("CLI Session - {}", profile_name),
222        )
223        .await
224        .map_err(|e| tracing::warn!(error = %e, "Failed to recover session context"))
225        .ok()?;
226
227    session.set_context_id(new_context_id);
228    Some(session.clone())
229}