Skip to main content

systemprompt_cli/runner/routing/
mod.rs

1//! Decides whether a command runs locally or is forwarded to a remote tenant.
2//!
3//! [`determine_execution_target`] resolves the active profile and tenant store
4//! into an [`ExecutionTarget`]; [`execute_remote`] adapts the terminal to the
5//! SSE transport in `systemprompt_client::RemoteCliExecutor`.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::io::{self, Write};
11
12use anyhow::{Context, Result};
13use systemprompt_client::{OutputSink, RemoteCliExecutor, RemoteCliRequest};
14use systemprompt_cloud::{SessionKey, SessionStore, StoredTenant, TenantStore};
15use systemprompt_config::ProfileBootstrap;
16use systemprompt_identifiers::{ContextId, SessionToken};
17use systemprompt_logging::CliService;
18
19use crate::paths::ResolvedPaths;
20
21#[doc(hidden)]
22#[derive(Debug)]
23pub enum ExecutionTarget {
24    Local,
25    Remote {
26        hostname: String,
27        token: SessionToken,
28        context: ContextId,
29    },
30}
31
32pub fn determine_execution_target() -> Result<ExecutionTarget> {
33    let Ok(profile) = ProfileBootstrap::get() else {
34        tracing::debug!("No profile loaded, routing to local execution");
35        return Ok(ExecutionTarget::Local);
36    };
37
38    if profile.target.is_local() {
39        tracing::debug!(
40            profile_name = %profile.name,
41            "Profile target is local, routing to local execution"
42        );
43        return Ok(ExecutionTarget::Local);
44    }
45
46    let Some(tenant_id) = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_ref()) else {
47        tracing::debug!(
48            profile_name = %profile.name,
49            "Profile has no tenant_id, routing to local execution"
50        );
51        return Ok(ExecutionTarget::Local);
52    };
53
54    tracing::debug!(
55        profile_name = %profile.name,
56        tenant_id = %tenant_id,
57        "Profile has tenant_id, resolving remote execution target"
58    );
59
60    let tenant = resolve_tenant(profile, tenant_id)?;
61    let hostname = tenant
62        .hostname
63        .as_ref()
64        .context("Tenant has no hostname configured")?
65        .clone();
66
67    let session_key = SessionKey::Tenant(tenant_id.clone());
68    let session = load_session_for_key(profile, &session_key, &profile.security.issuer)?;
69
70    tracing::info!(
71        hostname = %hostname,
72        tenant_id = %tenant_id,
73        "Routing to remote execution"
74    );
75
76    Ok(ExecutionTarget::Remote {
77        hostname,
78        token: session.session_token,
79        context: session.context_id,
80    })
81}
82
83pub fn resolve_tenant(
84    profile: &systemprompt_models::Profile,
85    tenant: &systemprompt_identifiers::TenantId,
86) -> Result<StoredTenant> {
87    let tenants_path = ResolvedPaths::from_profile(profile).tenants_path();
88
89    let store = TenantStore::load_from_path(&tenants_path).with_context(|| {
90        format!(
91            "Failed to load tenants from {}. Run 'systemprompt cloud tenant list' to sync.",
92            tenants_path.display()
93        )
94    })?;
95
96    store
97        .find_tenant(tenant)
98        .cloned()
99        .with_context(|| format!("Tenant '{}' not found in local tenant store", tenant))
100}
101
102pub fn load_session_for_key(
103    profile: &systemprompt_models::Profile,
104    session_key: &SessionKey,
105    issuer: &str,
106) -> Result<systemprompt_cloud::CliSession> {
107    let sessions_dir = ResolvedPaths::from_profile(profile).sessions_dir();
108
109    let store = SessionStore::load_or_create(&sessions_dir)?;
110
111    store
112        .get_valid_session(session_key, issuer)
113        .cloned()
114        .context("No active session. Run 'systemprompt admin session login'.")
115}
116
117struct StdioSink {
118    stdout: io::Stdout,
119    stderr: io::Stderr,
120}
121
122impl OutputSink for StdioSink {
123    fn stdout_chunk(&mut self, data: &str) -> io::Result<()> {
124        write!(self.stdout, "{}", data)?;
125        self.stdout.flush()
126    }
127
128    fn stderr_chunk(&mut self, data: &str) -> io::Result<()> {
129        write!(self.stderr, "{}", data)?;
130        self.stderr.flush()
131    }
132
133    fn error_message(&mut self, message: &str) {
134        CliService::error(message);
135    }
136}
137
138pub async fn execute_remote(
139    hostname: &str,
140    token: &str,
141    context: &str,
142    args: &[String],
143    timeout_secs: u64,
144) -> Result<i32> {
145    let executor = RemoteCliExecutor::new(&format!("https://{hostname}"), timeout_secs)
146        .context("Failed to create HTTP client")?;
147    let mut sink = StdioSink {
148        stdout: io::stdout(),
149        stderr: io::stderr(),
150    };
151    let request = RemoteCliRequest {
152        token,
153        context,
154        args,
155    };
156    Ok(executor.execute(request, &mut sink).await?)
157}