Skip to main content

systemprompt_cli/session/
resolution.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use anyhow::{Context, Result};
5use systemprompt_agent::repository::context::ContextRepository;
6use systemprompt_cloud::{CliSession, CredentialsBootstrap, SessionKey, SessionStore};
7use systemprompt_database::{Database, DbPool};
8use systemprompt_identifiers::{ContextId, Email, ProfileName, SessionId, SessionToken, UserId};
9use systemprompt_loader::ProfileLoader;
10use systemprompt_logging::CliService;
11use systemprompt_models::auth::UserType;
12use systemprompt_models::profile_bootstrap::ProfileBootstrap;
13use systemprompt_models::{Profile, SecretsBootstrap};
14
15use super::context::CliSessionContext;
16use crate::paths::ResolvedPaths;
17use crate::CliConfig;
18
19pub(super) struct ProfileContext<'a> {
20    pub name: &'a str,
21    pub path: PathBuf,
22}
23
24fn try_session_from_env(profile: &Profile) -> Option<CliSessionContext> {
25    if std::env::var("SYSTEMPROMPT_CLI_REMOTE").is_err() {
26        return None;
27    }
28
29    let session_id = std::env::var("SYSTEMPROMPT_SESSION_ID").ok()?;
30    let context_id = std::env::var("SYSTEMPROMPT_CONTEXT_ID").ok()?;
31    let user_id = std::env::var("SYSTEMPROMPT_USER_ID").ok()?;
32    let auth_token = std::env::var("SYSTEMPROMPT_AUTH_TOKEN").ok()?;
33
34    let profile_name = ProfileName::new("remote");
35    let email = Email::new("remote@cli.local");
36    let session = CliSession::builder(
37        profile_name,
38        SessionToken::new(auth_token),
39        SessionId::new(session_id),
40        ContextId::new(context_id),
41    )
42    .with_user(UserId::new(user_id), email)
43    .with_user_type(UserType::Admin)
44    .build();
45
46    Some(CliSessionContext {
47        session,
48        profile: profile.clone(),
49    })
50}
51
52async fn get_session_for_profile(
53    profile_input: &str,
54    config: &CliConfig,
55) -> Result<CliSessionContext> {
56    let (profile_path, profile) = crate::shared::resolve_profile_with_data(profile_input)
57        .map_err(|e| anyhow::anyhow!("{}", e))?;
58
59    if !ProfileBootstrap::is_initialized() {
60        ProfileBootstrap::init_from_path(&profile_path)
61            .with_context(|| format!("Failed to initialize profile '{}'", profile_input))?;
62    }
63
64    if !SecretsBootstrap::is_initialized() {
65        SecretsBootstrap::try_init().with_context(|| {
66            "Failed to initialize secrets. Check your profile's secrets configuration."
67        })?;
68    }
69
70    get_session_for_loaded_profile(&profile, &profile_path, config).await
71}
72
73async fn get_session_for_loaded_profile(
74    profile: &Profile,
75    profile_path: &Path,
76    config: &CliConfig,
77) -> Result<CliSessionContext> {
78    if let Some(ctx) = try_session_from_env(profile) {
79        return Ok(ctx);
80    }
81
82    let profile_dir = profile_path
83        .parent()
84        .ok_or_else(|| anyhow::anyhow!("Invalid profile path: no parent directory"))?;
85    let profile_name = profile_dir
86        .file_name()
87        .and_then(|n| n.to_str())
88        .ok_or_else(|| anyhow::anyhow!("Invalid profile directory name"))?
89        .to_string();
90
91    let tenant_id = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_deref());
92    let session_key = SessionKey::from_tenant_id(tenant_id);
93
94    let sessions_dir = ResolvedPaths::discover().sessions_dir()?;
95
96    let mut store = SessionStore::load_or_create(&sessions_dir)?;
97
98    if let Some(mut session) = store.get_valid_session(&session_key).cloned() {
99        session.touch();
100
101        if let Some(refreshed) = try_validate_context(&mut session, &profile_name).await {
102            session = refreshed;
103        }
104
105        store.upsert_session(&session_key, session.clone());
106        store.save(&sessions_dir)?;
107        return Ok(CliSessionContext {
108            session,
109            profile: profile.clone(),
110        });
111    }
112
113    let profile_ctx = ProfileContext {
114        name: &profile_name,
115        path: profile_path.to_path_buf(),
116    };
117
118    let session = if session_key.is_local() {
119        super::creation::create_local_session(profile, &profile_ctx, &session_key, config).await?
120    } else {
121        CredentialsBootstrap::try_init()
122            .await
123            .context("Failed to initialize credentials. Run 'systemprompt cloud auth login'.")?;
124
125        let creds = CredentialsBootstrap::require()
126            .map_err(|_| {
127                anyhow::anyhow!(
128                    "Cloud authentication required.\n\nRun 'systemprompt cloud auth login' to \
129                     authenticate."
130                )
131            })?
132            .clone();
133
134        super::creation::create_session_for_tenant(
135            &creds,
136            profile,
137            &profile_ctx,
138            &session_key,
139            config,
140        )
141        .await?
142    };
143
144    store.upsert_session(&session_key, session.clone());
145    store.set_active_with_profile(&session_key, &profile_name);
146    store.save(&sessions_dir)?;
147
148    if session.session_token.as_str().is_empty() {
149        anyhow::bail!("Session token is empty. Session creation failed.");
150    }
151
152    Ok(CliSessionContext {
153        session,
154        profile: profile.clone(),
155    })
156}
157
158async fn try_session_from_active_key(config: &CliConfig) -> Result<Option<CliSessionContext>> {
159    let paths = ResolvedPaths::discover();
160    let sessions_dir = paths.sessions_dir()?;
161    let store = SessionStore::load_or_create(&sessions_dir)?;
162
163    let Some(ref active_key_str) = store.active_key else {
164        return Ok(None);
165    };
166
167    let active_key = store
168        .active_session_key()
169        .ok_or_else(|| anyhow::anyhow!("Invalid active session key: {}", active_key_str))?;
170
171    let active_profile = store.active_profile_name.as_deref();
172
173    let profile_path = if let Some(session) = store.active_session() {
174        if let Some(expected) = active_profile {
175            if session.profile_name.as_str() != expected {
176                anyhow::bail!(
177                    "No session for active profile '{}'.\n\nRun 'systemprompt admin session \
178                     login' to authenticate.",
179                    expected
180                );
181            }
182        }
183        match &session.profile_path {
184            Some(path) if path.exists() => path.clone(),
185            _ => return Ok(None),
186        }
187    } else {
188        if let Some(profile_name) = active_profile {
189            let profile_dir = paths.profiles_dir().join(profile_name);
190            let config_path = systemprompt_cloud::ProfilePath::Config.resolve(&profile_dir);
191            if config_path.exists() {
192                anyhow::bail!(
193                    "No session for active profile '{}'.\n\nRun 'systemprompt admin session \
194                     login' to authenticate, or 'systemprompt admin session switch <profile>' to \
195                     change profiles.",
196                    profile_name
197                );
198            }
199        }
200
201        let raw_session = store.get_session(&active_key);
202        if let Some(path) = raw_session
203            .and_then(|s| s.profile_path.as_ref())
204            .filter(|p| p.exists())
205        {
206            path.clone()
207        } else {
208            let profile_hint = active_profile.unwrap_or("unknown");
209            anyhow::bail!(
210                "No session for active profile '{}'.\n\nRun 'systemprompt admin session login' to \
211                 authenticate, or 'systemprompt admin session switch <profile>' to change \
212                 profiles.",
213                profile_hint
214            );
215        }
216    };
217
218    let profile = ProfileLoader::load_from_path(&profile_path).with_context(|| {
219        format!(
220            "Failed to load profile from stored path: {}",
221            profile_path.display()
222        )
223    })?;
224
225    if !ProfileBootstrap::is_initialized() {
226        ProfileBootstrap::init_from_path(&profile_path).with_context(|| {
227            format!(
228                "Failed to initialize profile from {}",
229                profile_path.display()
230            )
231        })?;
232    }
233
234    if !SecretsBootstrap::is_initialized() {
235        SecretsBootstrap::try_init().with_context(|| "Failed to initialize secrets for session")?;
236    }
237
238    let ctx = get_session_for_loaded_profile(&profile, &profile_path, config).await?;
239    Ok(Some(ctx))
240}
241
242pub async fn get_or_create_session(config: &CliConfig) -> Result<CliSessionContext> {
243    let ctx = resolve_session(config).await?;
244
245    if config.is_interactive() {
246        let tenant = ctx
247            .session
248            .tenant_key
249            .as_ref()
250            .map_or("local", systemprompt_identifiers::TenantId::as_str);
251        CliService::session_context_with_url(
252            ctx.session.profile_name.as_str(),
253            &ctx.session.session_id,
254            Some(tenant),
255            Some(&ctx.profile.server.api_external_url),
256        );
257    }
258
259    Ok(ctx)
260}
261
262async fn resolve_session(config: &CliConfig) -> Result<CliSessionContext> {
263    if let Some(ref profile_name) = config.profile_override {
264        return get_session_for_profile(profile_name, config).await;
265    }
266
267    let env_profile_set = std::env::var("SYSTEMPROMPT_PROFILE").is_ok();
268
269    if !env_profile_set {
270        if let Some(ctx) = try_session_from_active_key(config).await? {
271            return Ok(ctx);
272        }
273    }
274
275    let profile = ProfileBootstrap::get()
276        .map_err(|_| {
277            anyhow::anyhow!(
278                "Profile required.\n\nSet SYSTEMPROMPT_PROFILE environment variable to your \
279                 profile.yaml path, or use --profile <name>."
280            )
281        })?
282        .clone();
283
284    let profile_path_str = ProfileBootstrap::get_path().map_err(|_| {
285        anyhow::anyhow!(
286            "Profile path required.\n\nSet SYSTEMPROMPT_PROFILE environment variable or use \
287             --profile <name>."
288        )
289    })?;
290
291    let profile_path = Path::new(profile_path_str);
292    get_session_for_loaded_profile(&profile, profile_path, config).await
293}
294
295async fn try_validate_context(session: &mut CliSession, profile_name: &str) -> Option<CliSession> {
296    let secrets = SecretsBootstrap::get().ok()?;
297    let db = Database::new_postgres(&secrets.database_url).await.ok()?;
298    let db_pool = DbPool::from(Arc::new(db));
299    let context_repo = ContextRepository::new(db_pool);
300
301    let is_valid = context_repo
302        .validate_context_ownership(&session.context_id, &session.user_id)
303        .await
304        .is_ok();
305
306    if is_valid {
307        return None;
308    }
309
310    CliService::warning("Session context is stale, creating new context...");
311
312    let new_context_id = context_repo
313        .create_context(
314            &session.user_id,
315            Some(&session.session_id),
316            &format!("CLI Session - {}", profile_name),
317        )
318        .await
319        .ok()?;
320
321    session.set_context_id(new_context_id);
322    Some(session.clone())
323}