systemprompt_cli/runner/
routing_decision.rs1use anyhow::{Result, bail};
12
13use crate::descriptor::{DataImpact, RoutingClass};
14
15#[derive(Debug, PartialEq, Eq)]
17pub enum RoutingDecision {
18 ExecuteRemote {
19 hostname: String,
20 token: systemprompt_identifiers::SessionToken,
21 context: systemprompt_identifiers::ContextId,
22 },
23 ContinueLocal,
24}
25
26pub fn decide_routing(
27 target: Result<super::routing::ExecutionTarget>,
28 profile: &systemprompt_manifest::Profile,
29 class: RoutingClass,
30 impact: DataImpact,
31) -> Result<RoutingDecision> {
32 use super::routing::ExecutionTarget;
33
34 let is_cloud = profile.target.is_cloud();
35 match target {
36 Ok(ExecutionTarget::Remote {
37 hostname,
38 token,
39 context,
40 }) => Ok(RoutingDecision::ExecuteRemote {
41 hostname,
42 token,
43 context,
44 }),
45 Ok(ExecutionTarget::Local) if is_cloud => {
46 allow_local_execution(profile, class, "no tenant is configured")?;
47 Ok(RoutingDecision::ContinueLocal)
48 },
49 Err(e) if is_cloud => {
50 let reason = format!("routing failed: {}", e);
51 if impact == DataImpact::Destructive {
52 bail!(
53 "Cloud profile '{}' could not route this destructive command remotely ({}); \
54 it never falls back to direct database access.\n{}",
55 profile.name,
56 reason,
57 remediation_for(&reason)
58 );
59 }
60 allow_local_execution(profile, class, &reason)?;
61 Ok(RoutingDecision::ContinueLocal)
62 },
63 Ok(ExecutionTarget::Local) => Ok(RoutingDecision::ContinueLocal),
64 Err(e) => {
65 tracing::debug!(error = %e, "Routing failed on a local profile; continuing locally");
66 Ok(RoutingDecision::ContinueLocal)
67 },
68 }
69}
70
71pub fn allow_local_execution(
72 profile: &systemprompt_manifest::Profile,
73 class: RoutingClass,
74 reason: &str,
75) -> Result<()> {
76 if profile.database.external_db_access {
77 tracing::debug!(
78 profile_name = %profile.name,
79 reason = reason,
80 "Cloud profile allowing local execution via external_db_access"
81 );
82 return Ok(());
83 }
84
85 if class == RoutingClass::ReadOnly {
86 tracing::warn!(
87 profile_name = %profile.name,
88 reason = reason,
89 "Cloud profile could not route remotely; reading local data instead"
90 );
91 return Ok(());
92 }
93
94 bail!(
95 "Cloud profile '{}' requires remote execution but {}.\n{}",
96 profile.name,
97 reason,
98 remediation_for(reason)
99 )
100}
101
102pub fn remediation_for(reason: &str) -> &'static str {
103 if reason.contains("load tenants") || reason.contains("tenant") {
104 "Run 'systemprompt cloud tenant list' to sync the tenant store, and check you are in the \
105 project directory this profile belongs to."
106 } else {
107 "Run 'systemprompt admin session login' to authenticate."
108 }
109}