Skip to main content

soma_config/
python_environment.rs

1//! Configuration for production-managed Python provider environments.
2
3use serde::{Deserialize, Serialize};
4
5/// Product configuration for immutable PEP 723 Python environments.
6///
7/// The lifecycle is disabled by default. When enabled, every identity-bearing
8/// field is required so cache keys never depend on ambient interpreter state.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(default)]
11pub struct PythonEnvironmentConfig {
12    pub enabled: bool,
13    pub cache_root: String,
14    pub uv_program: String,
15    pub uv_version: String,
16    pub python_executable: String,
17    pub runtime_implementation: String,
18    pub runtime_version: String,
19    pub runtime_platform: String,
20    pub wheel_platform_tag: String,
21    pub sdk_wheel: String,
22    pub sdk_wheel_sha256: String,
23    pub offline: bool,
24    pub update: bool,
25    pub policy_version: u32,
26}
27
28impl Default for PythonEnvironmentConfig {
29    fn default() -> Self {
30        Self {
31            enabled: false,
32            cache_root: String::new(),
33            uv_program: String::new(),
34            uv_version: String::new(),
35            python_executable: String::new(),
36            runtime_implementation: String::new(),
37            runtime_version: String::new(),
38            runtime_platform: String::new(),
39            wheel_platform_tag: String::new(),
40            sdk_wheel: String::new(),
41            sdk_wheel_sha256: String::new(),
42            offline: false,
43            update: false,
44            policy_version: 2,
45        }
46    }
47}
48
49impl PythonEnvironmentConfig {
50    pub(crate) fn validate(&self) -> anyhow::Result<()> {
51        if !self.enabled {
52            return Ok(());
53        }
54        for (name, value) in [
55            ("SOMA_PYTHON_ENVIRONMENT_CACHE_ROOT", &self.cache_root),
56            ("SOMA_PYTHON_ENVIRONMENT_UV_PROGRAM", &self.uv_program),
57            ("SOMA_PYTHON_ENVIRONMENT_UV_VERSION", &self.uv_version),
58            (
59                "SOMA_PYTHON_ENVIRONMENT_PYTHON_EXECUTABLE",
60                &self.python_executable,
61            ),
62            (
63                "SOMA_PYTHON_ENVIRONMENT_RUNTIME_IMPLEMENTATION",
64                &self.runtime_implementation,
65            ),
66            (
67                "SOMA_PYTHON_ENVIRONMENT_RUNTIME_VERSION",
68                &self.runtime_version,
69            ),
70            (
71                "SOMA_PYTHON_ENVIRONMENT_RUNTIME_PLATFORM",
72                &self.runtime_platform,
73            ),
74            (
75                "SOMA_PYTHON_ENVIRONMENT_WHEEL_PLATFORM_TAG",
76                &self.wheel_platform_tag,
77            ),
78            ("SOMA_PYTHON_ENVIRONMENT_SDK_WHEEL", &self.sdk_wheel),
79            (
80                "SOMA_PYTHON_ENVIRONMENT_SDK_WHEEL_SHA256",
81                &self.sdk_wheel_sha256,
82            ),
83        ] {
84            if value.trim().is_empty() {
85                anyhow::bail!("{name} is required when Python environments are enabled");
86            }
87        }
88        let digest = self.sdk_wheel_sha256.trim();
89        if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
90            anyhow::bail!(
91                "SOMA_PYTHON_ENVIRONMENT_SDK_WHEEL_SHA256 must be exactly 64 hexadecimal characters"
92            );
93        }
94        if self.policy_version == 0 {
95            anyhow::bail!("SOMA_PYTHON_ENVIRONMENT_POLICY_VERSION must be greater than zero");
96        }
97        if self.offline && self.update {
98            anyhow::bail!(
99                "SOMA_PYTHON_ENVIRONMENT_UPDATE cannot be enabled with SOMA_PYTHON_ENVIRONMENT_OFFLINE"
100            );
101        }
102        for (name, value) in [
103            ("SOMA_PYTHON_ENVIRONMENT_CACHE_ROOT", &self.cache_root),
104            ("SOMA_PYTHON_ENVIRONMENT_UV_PROGRAM", &self.uv_program),
105            (
106                "SOMA_PYTHON_ENVIRONMENT_PYTHON_EXECUTABLE",
107                &self.python_executable,
108            ),
109            ("SOMA_PYTHON_ENVIRONMENT_SDK_WHEEL", &self.sdk_wheel),
110        ] {
111            if !std::path::Path::new(value).is_absolute() {
112                anyhow::bail!("{name} must be an absolute path");
113            }
114        }
115        Ok(())
116    }
117}