Skip to main content

systemprompt_cli/runner/
routing_decision.rs

1//! Where a routed command runs once the execution target is known.
2//!
3//! [`decide_routing`] turns the resolved target into one [`RoutingDecision`]:
4//! a remote run, or a local continuation the profile and the command's
5//! [`RoutingClass`] / [`DataImpact`] allow. A cloud profile that cannot route
6//! never silently falls back for a destructive command.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use anyhow::{Result, bail};
12
13use crate::descriptor::{DataImpact, RoutingClass};
14
15/// Where a command runs once the routing target is known.
16#[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}