systemprompt_cli/session/resolution/
helpers.rs1use 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}