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