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    // Why: a local-trial install has no credentials to name a user with, so the
55    // admin is resolved by `system_admin.username` — the same key the runtime
56    // resolves on — rather than by matching a hardcoded email. Anything else here
57    // (a session hint, cloud credentials) is a real address and stays email-keyed.
58    let admin_user = if profile.is_local_trial() && session_email_hint.is_none() {
59        resolve_local_admin(&db_pool, &profile.system_admin.username).await?
60    } else {
61        let user_email = resolve_credentialed_user_email(session_email_hint).await?;
62        resolve_admin_with_fallback(&db_pool, user_email.as_str(), session_email_hint, "local")
63            .await?
64    };
65
66    if config.is_interactive() {
67        CliService::key_value("User", &admin_user.email);
68    }
69
70    let session_id = create_local_session_row(
71        &db_pool,
72        &admin_user.id,
73        chrono::Duration::hours(crate::session::api::DEFAULT_CLI_SESSION_HOURS),
74    )
75    .await
76    .context("Failed to create local CLI session row in the database")?;
77
78    let context_id =
79        create_cli_context(db_pool, &admin_user, &session_id, profile_ctx.name).await?;
80    let session_token = generate_admin_token(&profile.security.issuer, &admin_user, &session_id)?;
81
82    if config.is_interactive() {
83        CliService::success("Local session created");
84        CliService::key_value("Session ID", session_id.as_str());
85        CliService::key_value("Context ID", context_id.as_str());
86    }
87
88    build_cli_session(
89        profile_ctx,
90        session_key,
91        SessionComponents {
92            session_token,
93            session_id,
94            context_id,
95        },
96        &admin_user,
97        &profile.security.issuer,
98    )
99}
100
101pub(super) struct TenantSessionParams<'a> {
102    pub creds: &'a CloudCredentials,
103    pub profile: &'a Profile,
104    pub profile_ctx: &'a ProfileContext<'a>,
105    pub session_key: &'a SessionKey,
106    pub config: &'a CliConfig,
107    pub session_email_hint: Option<&'a str>,
108}
109
110pub(super) async fn create_session_for_tenant(
111    params: TenantSessionParams<'_>,
112) -> Result<CliSession> {
113    let TenantSessionParams {
114        creds,
115        profile,
116        profile_ctx,
117        session_key,
118        config,
119        session_email_hint,
120    } = params;
121    profile
122        .validate()
123        .with_context(|| format!("Failed to validate profile: {}", profile_ctx.name))?;
124
125    let user_email = session_email_hint.unwrap_or(creds.user_email.as_str());
126    let secrets = load_secrets().context("Failed to load secrets")?;
127
128    if config.is_interactive() {
129        CliService::info("Creating CLI session...");
130        CliService::key_value("Profile", profile_ctx.name);
131        CliService::key_value("User", user_email);
132    }
133
134    let db_pool = connect_database(&secrets).await?;
135    let admin_user =
136        resolve_tenant_admin_with_fallback(&db_pool, creds, user_email, session_email_hint).await?;
137
138    let session_id = create_local_session_row(
139        &db_pool,
140        &admin_user.id,
141        chrono::Duration::hours(crate::session::api::DEFAULT_CLI_SESSION_HOURS),
142    )
143    .await
144    .context("Failed to create local tenant CLI session row in the database")?;
145
146    let context_id =
147        create_cli_context(db_pool, &admin_user, &session_id, profile_ctx.name).await?;
148    let session_token = generate_admin_token(&profile.security.issuer, &admin_user, &session_id)?;
149
150    if config.is_interactive() {
151        CliService::success("Session created");
152        CliService::key_value("Session ID", session_id.as_str());
153        CliService::key_value("Context ID", context_id.as_str());
154    }
155
156    build_cli_session(
157        profile_ctx,
158        session_key,
159        SessionComponents {
160            session_token,
161            session_id,
162            context_id,
163        },
164        &admin_user,
165        &profile.security.issuer,
166    )
167}