Skip to main content

systemprompt_cli/session/creation/
mod.rs

1//! Creation of CLI sessions for local and cloud-tenant profiles.
2//!
3//! Resolves an admin user, mints a session token, and records the session row
4//! plus context for both the local (`create_local_session`) and tenant
5//! (`create_session_for_tenant`) paths.
6//!
7//! The local-trial path *resolves* rather than provisions, and does so by
8//! `system_admin.username`. It previously looked the admin up by a hardcoded
9//! `admin@localhost.dev` and created one on a miss, which turned `users.email`
10//! into a key shared with a migration instead of a fact about a person — and
11//! surfaced as a fabricated identity on the bridge device-link consent screen.
12//! Cloud and tenant paths still key on email, because there the address comes
13//! from real credentials.
14//!
15//! Copyright (c) systemprompt.io — Business Source License 1.1.
16//! See <https://systemprompt.io> for licensing details.
17
18pub mod helpers;
19
20use anyhow::{Context, Result};
21use systemprompt_cloud::{CliSession, CloudCredentials, SessionKey};
22use systemprompt_logging::CliService;
23use systemprompt_models::Profile;
24
25use super::api::create_local_session_row;
26use super::resolution::ProfileContext;
27use crate::CliConfig;
28use helpers::{
29    SessionComponents, build_cli_session, connect_database, create_cli_context,
30    generate_admin_token, load_secrets, resolve_admin_with_fallback,
31    resolve_credentialed_user_email, resolve_local_admin, resolve_tenant_admin_with_fallback,
32};
33
34pub(super) async fn create_local_session(
35    profile: &Profile,
36    profile_ctx: &ProfileContext<'_>,
37    session_key: &SessionKey,
38    config: &CliConfig,
39    session_email_hint: Option<&str>,
40) -> Result<CliSession> {
41    profile
42        .validate()
43        .with_context(|| format!("Failed to validate profile: {}", profile_ctx.name))?;
44
45    let secrets = load_secrets().context("Failed to load secrets")?;
46
47    if config.is_interactive() {
48        CliService::info("Creating local CLI session...");
49        CliService::key_value("Profile", profile_ctx.name);
50    }
51
52    let db_pool = connect_database(&secrets).await?;
53
54    let admin_user = if profile.is_local_trial() && session_email_hint.is_none() {
55        resolve_local_admin(&db_pool, &profile.system_admin.username).await?
56    } else {
57        let user_email = resolve_credentialed_user_email(session_email_hint).await?;
58        resolve_admin_with_fallback(&db_pool, user_email.as_str(), session_email_hint, "local")
59            .await?
60    };
61
62    if config.is_interactive() {
63        CliService::key_value("User", &admin_user.email);
64    }
65
66    let session_id = create_local_session_row(
67        &db_pool,
68        &admin_user.id,
69        chrono::Duration::hours(crate::session::api::DEFAULT_CLI_SESSION_HOURS),
70    )
71    .await
72    .context("Failed to create local CLI session row in the database")?;
73
74    let context_id =
75        create_cli_context(db_pool, &admin_user, &session_id, profile_ctx.name).await?;
76    let session_token = generate_admin_token(&profile.security.issuer, &admin_user, &session_id)?;
77
78    if config.is_interactive() {
79        CliService::success("Local session created");
80        CliService::key_value("Session ID", session_id.as_str());
81        CliService::key_value("Context ID", context_id.as_str());
82    }
83
84    build_cli_session(
85        profile_ctx,
86        session_key,
87        SessionComponents {
88            session_token,
89            session_id,
90            context_id,
91        },
92        &admin_user,
93        &profile.security.issuer,
94    )
95}
96
97pub(super) struct TenantSessionParams<'a> {
98    pub creds: &'a CloudCredentials,
99    pub profile: &'a Profile,
100    pub profile_ctx: &'a ProfileContext<'a>,
101    pub session_key: &'a SessionKey,
102    pub config: &'a CliConfig,
103    pub session_email_hint: Option<&'a str>,
104}
105
106pub(super) async fn create_session_for_tenant(
107    params: TenantSessionParams<'_>,
108) -> Result<CliSession> {
109    let TenantSessionParams {
110        creds,
111        profile,
112        profile_ctx,
113        session_key,
114        config,
115        session_email_hint,
116    } = params;
117    profile
118        .validate()
119        .with_context(|| format!("Failed to validate profile: {}", profile_ctx.name))?;
120
121    let user_email = session_email_hint.unwrap_or(creds.user_email.as_str());
122    let secrets = load_secrets().context("Failed to load secrets")?;
123
124    if config.is_interactive() {
125        CliService::info("Creating CLI session...");
126        CliService::key_value("Profile", profile_ctx.name);
127        CliService::key_value("User", user_email);
128    }
129
130    let db_pool = connect_database(&secrets).await?;
131    let admin_user =
132        resolve_tenant_admin_with_fallback(&db_pool, creds, user_email, session_email_hint).await?;
133
134    let session_id = create_local_session_row(
135        &db_pool,
136        &admin_user.id,
137        chrono::Duration::hours(crate::session::api::DEFAULT_CLI_SESSION_HOURS),
138    )
139    .await
140    .context("Failed to create local tenant CLI session row in the database")?;
141
142    let context_id =
143        create_cli_context(db_pool, &admin_user, &session_id, profile_ctx.name).await?;
144    let session_token = generate_admin_token(&profile.security.issuer, &admin_user, &session_id)?;
145
146    if config.is_interactive() {
147        CliService::success("Session created");
148        CliService::key_value("Session ID", session_id.as_str());
149        CliService::key_value("Context ID", context_id.as_str());
150    }
151
152    build_cli_session(
153        profile_ctx,
154        session_key,
155        SessionComponents {
156            session_token,
157            session_id,
158            context_id,
159        },
160        &admin_user,
161        &profile.security.issuer,
162    )
163}