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, 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::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        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) async 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()
174            .await
175            .with_context(|| "Failed to initialize secrets for session")?;
176    }
177
178    Ok(())
179}
180
181pub(super) async fn try_validate_context(
182    session: &mut CliSession,
183    profile_name: &str,
184) -> Option<CliSession> {
185    let secrets = SecretsBootstrap::get()
186        .map_err(|e| tracing::debug!(error = %e, "Failed to get secrets for context validation"))
187        .ok()?;
188    let db = Database::new_postgres(&secrets.database_url)
189        .await
190        .map_err(
191            |e| tracing::debug!(error = %e, "Failed to connect to database for context validation"),
192        )
193        .ok()?;
194    let db_pool = DbPool::from(Arc::new(db));
195    revalidate_context(&db_pool, session, profile_name).await
196}
197
198pub async fn revalidate_context(
199    db_pool: &DbPool,
200    session: &mut CliSession,
201    profile_name: &str,
202) -> Option<CliSession> {
203    let context_repo = ContextRepository::new(db_pool)
204        .map_err(|e| tracing::debug!(error = %e, "Failed to build context repository"))
205        .ok()?;
206
207    let is_valid = context_repo
208        .validate_context_ownership(&session.context_id, &session.user_id)
209        .await
210        .is_ok();
211
212    if is_valid {
213        return None;
214    }
215
216    CliService::warning("Session context is stale, recovering session context...");
217
218    let new_context_id = context_repo
219        .get_or_create_cli_context(
220            &session.user_id,
221            &session.session_id,
222            &format!("CLI Session - {}", profile_name),
223        )
224        .await
225        .map_err(|e| tracing::warn!(error = %e, "Failed to recover session context"))
226        .ok()?;
227
228    session.set_context_id(new_context_id);
229    Some(session.clone())
230}