systemprompt_cli/runner/routing/
mod.rs1use 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}