1use serde::{Deserialize, Serialize};
11
12use crate::python_environment::PythonEnvironmentConfig;
13mod env;
14use env::{
15 boolean as env_bool, list as env_list, optional_parse as env_opt_parse,
16 optional_string as env_opt_str, parse as env_parse, string as env_str,
17};
18#[path = "python_runner_profile.rs"]
19mod python_runner_profile;
20pub use python_runner_profile::{PythonExecutionProfile, PythonRunnerMode};
21
22const SERVICE_HOME_DIRNAME: &str = ".soma";
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27#[serde(default)]
28pub struct Config {
29 pub mcp: McpConfig,
30 pub soma: SomaConfig,
31 pub python: PythonRunnerConfig,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(default)]
37pub struct PythonRunnerConfig {
38 pub environment: PythonEnvironmentConfig,
39 pub mode: PythonRunnerMode,
40 pub execution_profile: PythonExecutionProfile,
41 pub broker_allowed_providers: Vec<String>,
43 pub broker_allowed_http_hosts: Vec<String>,
45 pub broker_secret_names: Vec<String>,
47 pub broker_state_namespaces: Vec<String>,
49 pub broker_allow_logging: bool,
50 pub broker_allow_metrics: bool,
51 pub broker_allow_progress: bool,
52 pub startup_timeout_ms: u64,
53 pub request_timeout_ms: u64,
54 pub shutdown_grace_ms: u64,
55 pub max_restarts: u32,
56 pub restart_window_ms: u64,
57 pub restart_backoff_ms: u64,
58 pub max_stderr_bytes: usize,
59 pub max_pending_bytes: usize,
60 pub max_workers: usize,
61 pub max_candidate_starts: usize,
62}
63
64impl Default for PythonRunnerConfig {
65 fn default() -> Self {
66 Self {
67 environment: PythonEnvironmentConfig::default(),
68 mode: PythonRunnerMode::OneShot,
69 execution_profile: PythonExecutionProfile::Trusted,
70 broker_allowed_providers: Vec::new(),
71 broker_allowed_http_hosts: Vec::new(),
72 broker_secret_names: Vec::new(),
73 broker_state_namespaces: Vec::new(),
74 broker_allow_logging: false,
75 broker_allow_metrics: false,
76 broker_allow_progress: false,
77 startup_timeout_ms: 10_000,
78 request_timeout_ms: 10_000,
79 shutdown_grace_ms: 2_000,
80 max_restarts: 3,
81 restart_window_ms: 60_000,
82 restart_backoff_ms: 250,
83 max_stderr_bytes: 64 * 1024,
84 max_pending_bytes: 512 * 1024,
85 max_workers: 32,
86 max_candidate_starts: 4,
87 }
88 }
89}
90
91impl PythonRunnerConfig {
92 fn validate(&self) -> anyhow::Result<()> {
93 if self.execution_profile != PythonExecutionProfile::Trusted
94 && self.mode != PythonRunnerMode::Persistent
95 {
96 anyhow::bail!(
97 "SOMA_PYTHON_EXECUTION_PROFILE={} requires SOMA_PYTHON_RUNNER_MODE=persistent",
98 match self.execution_profile {
99 PythonExecutionProfile::Disabled => "disabled",
100 PythonExecutionProfile::Trusted => "trusted",
101 PythonExecutionProfile::Brokered => "brokered",
102 }
103 );
104 }
105 for (name, value) in [
106 (
107 "SOMA_PYTHON_RUNNER_STARTUP_TIMEOUT_MS",
108 self.startup_timeout_ms,
109 ),
110 (
111 "SOMA_PYTHON_RUNNER_REQUEST_TIMEOUT_MS",
112 self.request_timeout_ms,
113 ),
114 (
115 "SOMA_PYTHON_RUNNER_SHUTDOWN_GRACE_MS",
116 self.shutdown_grace_ms,
117 ),
118 (
119 "SOMA_PYTHON_RUNNER_RESTART_WINDOW_MS",
120 self.restart_window_ms,
121 ),
122 ] {
123 if value == 0 {
124 anyhow::bail!("{name} must be greater than zero");
125 }
126 }
127 for (name, value) in [
128 ("SOMA_PYTHON_RUNNER_MAX_STDERR_BYTES", self.max_stderr_bytes),
129 (
130 "SOMA_PYTHON_RUNNER_MAX_PENDING_BYTES",
131 self.max_pending_bytes,
132 ),
133 ("SOMA_PYTHON_RUNNER_MAX_WORKERS", self.max_workers),
134 (
135 "SOMA_PYTHON_RUNNER_MAX_CANDIDATE_STARTS",
136 self.max_candidate_starts,
137 ),
138 ] {
139 if value == 0 {
140 anyhow::bail!("{name} must be greater than zero");
141 }
142 }
143 self.environment.validate()?;
144 Ok(())
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, Default)]
154#[serde(default)]
155pub struct SomaConfig {
156 pub api_url: String,
159 pub api_key: String,
161 pub runtime_mode: RuntimeMode,
163}
164
165#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
166#[serde(rename_all = "lowercase")]
167pub enum RuntimeMode {
168 #[default]
170 Auto,
171 Local,
173 Remote,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum EffectiveRuntimeMode {
179 Local,
180 Remote,
181}
182
183impl SomaConfig {
184 pub fn effective_runtime_mode(&self) -> EffectiveRuntimeMode {
185 match self.runtime_mode {
186 RuntimeMode::Auto if self.api_url.trim().is_empty() => EffectiveRuntimeMode::Local,
187 RuntimeMode::Auto => EffectiveRuntimeMode::Remote,
188 RuntimeMode::Local => EffectiveRuntimeMode::Local,
189 RuntimeMode::Remote => EffectiveRuntimeMode::Remote,
190 }
191 }
192
193 pub fn is_remote_adapter(&self) -> bool {
194 self.effective_runtime_mode() == EffectiveRuntimeMode::Remote
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200#[serde(default)]
201pub struct McpConfig {
202 #[serde(default = "default_mcp_host")]
205 pub host: String,
206 #[serde(default = "default_mcp_port")]
208 pub port: u16,
209 #[serde(default = "default_server_name")]
211 pub server_name: String,
212 pub no_auth: bool,
214 pub trusted_gateway: bool,
218 pub conformance_fixtures: bool,
223 pub api_token: Option<String>,
225 pub static_token_write: bool,
231 pub allowed_hosts: Vec<String>,
233 pub allowed_origins: Vec<String>,
235 pub trace_headers: TraceHeaderMode,
240 pub auth: AuthConfig,
242}
243
244impl McpConfig {
245 pub fn bind_addr(&self) -> String {
246 format!("{}:{}", self.host, self.port)
247 }
248
249 pub fn is_loopback(&self) -> bool {
255 let host = &self.host;
256 host == "localhost"
259 || host
260 .trim_start_matches('[')
261 .trim_end_matches(']')
262 .parse::<std::net::IpAddr>()
263 .map(|ip| ip.is_loopback())
264 .unwrap_or(false)
265 }
266}
267
268#[derive(Debug, Clone, Default, Serialize, Deserialize)]
277#[serde(default)]
278pub struct AuthConfig {
279 pub mode: AuthMode,
280 pub public_url: Option<String>,
282 pub google_client_id: Option<String>,
284 pub google_client_secret: Option<String>,
286 pub google_callback_path: Option<String>,
288 pub google_scopes: Vec<String>,
290 pub authelia_issuer_url: Option<String>,
292 pub authelia_client_id: Option<String>,
294 pub authelia_client_secret: Option<String>,
296 pub authelia_callback_path: Option<String>,
298 pub authelia_scopes: Vec<String>,
300 pub github_client_id: Option<String>,
302 pub github_client_secret: Option<String>,
304 pub github_callback_path: Option<String>,
306 pub github_scopes: Vec<String>,
309 pub default_provider: Option<String>,
312 pub admin_email: String,
314 pub allowed_emails: Vec<String>,
315 pub bootstrap_secret: Option<String>,
317 pub sqlite_path: Option<String>,
319 pub key_path: Option<String>,
321 pub access_token_ttl_secs: Option<u64>,
323 pub refresh_token_ttl_secs: Option<u64>,
325 pub auth_code_ttl_secs: Option<u64>,
327 pub register_rpm: Option<u32>,
329 pub authorize_rpm: Option<u32>,
331 pub max_pending_oauth_states: Option<usize>,
333 pub allowed_client_redirect_uris: Vec<String>,
336 pub token_encryption_key: Option<String>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
342#[serde(rename_all = "lowercase")]
343pub enum AuthMode {
344 #[default]
345 Bearer,
346 OAuth,
347}
348
349#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
350#[serde(rename_all = "kebab-case")]
351pub enum TraceHeaderMode {
352 #[default]
354 Off,
355 Trusted,
358 TrustedWithBaggage,
361}
362
363impl TraceHeaderMode {
364 pub const fn as_str(self) -> &'static str {
365 match self {
366 Self::Off => "off",
367 Self::Trusted => "trusted",
368 Self::TrustedWithBaggage => "trusted-with-baggage",
369 }
370 }
371}
372
373impl std::fmt::Display for TraceHeaderMode {
374 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 formatter.write_str(self.as_str())
376 }
377}
378
379fn default_mcp_host() -> String {
382 "127.0.0.1".into()
385}
386fn default_mcp_port() -> u16 {
387 40060
388}
389fn default_server_name() -> String {
390 "soma".into()
391}
392
393impl Default for McpConfig {
394 fn default() -> Self {
395 Self {
396 host: default_mcp_host(),
397 port: default_mcp_port(),
398 server_name: default_server_name(),
399 no_auth: false,
400 trusted_gateway: false,
401 conformance_fixtures: false,
402 api_token: None,
403 static_token_write: false,
404 allowed_hosts: Vec::new(),
405 allowed_origins: Vec::new(),
406 trace_headers: TraceHeaderMode::default(),
407 auth: AuthConfig::default(),
408 }
409 }
410}
411
412pub fn default_data_dir() -> anyhow::Result<std::path::PathBuf> {
426 if let Some(path) = std::env::var_os("SOMA_HOME") {
427 return Ok(std::path::PathBuf::from(path));
428 }
429
430 if std::path::Path::new("/.dockerenv").exists()
434 || std::env::var("RUNNING_IN_CONTAINER").is_ok()
435 || std::env::var("container").is_ok()
436 {
437 return Ok(std::path::PathBuf::from("/data"));
438 }
439
440 let home = dirs::home_dir().ok_or_else(|| {
442 anyhow::anyhow!("cannot determine home directory — set HOME or RUNNING_IN_CONTAINER=1")
443 })?;
444 Ok(home.join(SERVICE_HOME_DIRNAME))
445}
446
447pub fn load_dotenv() {
456 let Ok(dir) = default_data_dir() else {
457 return;
458 };
459 let env_path = dir.join(".env");
460 match std::fs::symlink_metadata(&env_path) {
464 Ok(md) if md.file_type().is_symlink() => {
465 eprintln!(
466 "error: refusing to load symlinked .env at {} (potential symlink attack)",
467 env_path.display()
468 );
469 std::process::exit(1);
470 }
471 Ok(_) => {
472 let _ = dotenvy::from_path(&env_path);
473 }
474 Err(_) => {}
476 }
477}
478
479impl Config {
482 pub fn load() -> anyhow::Result<Self> {
483 let mut config = Config::default();
484
485 let candidate_paths = {
489 let mut paths = vec![];
490 if let Some(home) = std::env::var_os("HOME") {
491 paths.push(
492 std::path::PathBuf::from(home)
493 .join(SERVICE_HOME_DIRNAME)
494 .join("config.toml"),
495 );
496 }
497 paths.push(std::path::PathBuf::from("config.toml"));
498 paths
499 };
500
501 for path in &candidate_paths {
502 match std::fs::read_to_string(path) {
503 Ok(contents) => {
504 config = toml::from_str(&contents)
505 .map_err(|e| anyhow::anyhow!("Failed to parse {}: {e}", path.display()))?;
506 break;
507 }
508 Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
509 Err(e) => return Err(anyhow::anyhow!("Failed to read {}: {e}", path.display())),
510 }
511 }
512
513 env_str("SOMA_MCP_HOST", &mut config.mcp.host);
515 env_parse("SOMA_MCP_PORT", &mut config.mcp.port)?;
516 env_str("SOMA_MCP_SERVER_NAME", &mut config.mcp.server_name);
517 env_bool("SOMA_MCP_NO_AUTH", &mut config.mcp.no_auth)?;
518 env_bool("SOMA_NOAUTH", &mut config.mcp.trusted_gateway)?;
519 env_bool(
520 "SOMA_MCP_CONFORMANCE_FIXTURES",
521 &mut config.mcp.conformance_fixtures,
522 )?;
523 env_opt_str("SOMA_MCP_TOKEN", &mut config.mcp.api_token);
524 env_bool(
525 "SOMA_MCP_STATIC_TOKEN_WRITE",
526 &mut config.mcp.static_token_write,
527 )?;
528 env_list("SOMA_MCP_ALLOWED_HOSTS", &mut config.mcp.allowed_hosts);
529 env_list("SOMA_MCP_ALLOWED_ORIGINS", &mut config.mcp.allowed_origins);
530 env_opt_str("SOMA_MCP_PUBLIC_URL", &mut config.mcp.auth.public_url);
531 env_str(
532 "SOMA_MCP_AUTH_ADMIN_EMAIL",
533 &mut config.mcp.auth.admin_email,
534 );
535 env_opt_str(
536 "SOMA_MCP_GOOGLE_CLIENT_ID",
537 &mut config.mcp.auth.google_client_id,
538 );
539 env_opt_str(
540 "SOMA_MCP_GOOGLE_CLIENT_SECRET",
541 &mut config.mcp.auth.google_client_secret,
542 );
543 env_opt_str(
544 "SOMA_MCP_GOOGLE_CALLBACK_PATH",
545 &mut config.mcp.auth.google_callback_path,
546 );
547 env_list("SOMA_MCP_GOOGLE_SCOPES", &mut config.mcp.auth.google_scopes);
548 env_opt_str(
549 "SOMA_MCP_AUTHELIA_ISSUER_URL",
550 &mut config.mcp.auth.authelia_issuer_url,
551 );
552 env_opt_str(
553 "SOMA_MCP_AUTHELIA_CLIENT_ID",
554 &mut config.mcp.auth.authelia_client_id,
555 );
556 env_opt_str(
557 "SOMA_MCP_AUTHELIA_CLIENT_SECRET",
558 &mut config.mcp.auth.authelia_client_secret,
559 );
560 env_opt_str(
561 "SOMA_MCP_AUTHELIA_CALLBACK_PATH",
562 &mut config.mcp.auth.authelia_callback_path,
563 );
564 env_list(
565 "SOMA_PYTHON_BROKER_ALLOWED_PROVIDERS",
566 &mut config.python.broker_allowed_providers,
567 );
568 env_list(
569 "SOMA_MCP_AUTHELIA_SCOPES",
570 &mut config.mcp.auth.authelia_scopes,
571 );
572 env_opt_str(
573 "SOMA_MCP_GITHUB_CLIENT_ID",
574 &mut config.mcp.auth.github_client_id,
575 );
576 env_opt_str(
577 "SOMA_MCP_GITHUB_CLIENT_SECRET",
578 &mut config.mcp.auth.github_client_secret,
579 );
580 env_opt_str(
581 "SOMA_MCP_GITHUB_CALLBACK_PATH",
582 &mut config.mcp.auth.github_callback_path,
583 );
584 env_list("SOMA_MCP_GITHUB_SCOPES", &mut config.mcp.auth.github_scopes);
585 env_opt_str(
586 "SOMA_MCP_AUTH_DEFAULT_PROVIDER",
587 &mut config.mcp.auth.default_provider,
588 );
589 env_opt_str(
590 "SOMA_MCP_AUTH_BOOTSTRAP_SECRET",
591 &mut config.mcp.auth.bootstrap_secret,
592 );
593 env_opt_str(
594 "SOMA_MCP_AUTH_SQLITE_PATH",
595 &mut config.mcp.auth.sqlite_path,
596 );
597 env_opt_str("SOMA_MCP_AUTH_KEY_PATH", &mut config.mcp.auth.key_path);
598 env_opt_parse(
599 "SOMA_MCP_AUTH_ACCESS_TOKEN_TTL_SECS",
600 &mut config.mcp.auth.access_token_ttl_secs,
601 )?;
602 env_opt_parse(
603 "SOMA_MCP_AUTH_REFRESH_TOKEN_TTL_SECS",
604 &mut config.mcp.auth.refresh_token_ttl_secs,
605 )?;
606 env_opt_parse(
607 "SOMA_MCP_AUTH_CODE_TTL_SECS",
608 &mut config.mcp.auth.auth_code_ttl_secs,
609 )?;
610 env_opt_parse(
611 "SOMA_MCP_AUTH_REGISTER_REQUESTS_PER_MINUTE",
612 &mut config.mcp.auth.register_rpm,
613 )?;
614 env_opt_parse(
615 "SOMA_MCP_AUTH_AUTHORIZE_REQUESTS_PER_MINUTE",
616 &mut config.mcp.auth.authorize_rpm,
617 )?;
618 env_opt_parse(
619 "SOMA_MCP_AUTH_MAX_PENDING_OAUTH_STATES",
620 &mut config.mcp.auth.max_pending_oauth_states,
621 )?;
622 env_list(
623 "SOMA_MCP_AUTH_ALLOWED_REDIRECT_URIS",
624 &mut config.mcp.auth.allowed_client_redirect_uris,
625 );
626 env_opt_str(
627 "SOMA_MCP_TOKEN_ENCRYPTION_KEY",
628 &mut config.mcp.auth.token_encryption_key,
629 );
630 if let Ok(v) = std::env::var("SOMA_MCP_AUTH_MODE")
631 && !v.is_empty()
632 {
633 config.mcp.auth.mode = match v.to_lowercase().as_str() {
634 "oauth" => AuthMode::OAuth,
635 "bearer" => AuthMode::Bearer,
636 other => {
637 return Err(anyhow::anyhow!(
638 "invalid SOMA_MCP_AUTH_MODE {:?}: must be \"bearer\" or \"oauth\"",
639 other
640 ));
641 }
642 };
643 }
644 if let Ok(v) = std::env::var("SOMA_MCP_TRACE_HEADERS")
645 && !v.is_empty()
646 {
647 config.mcp.trace_headers = match v.to_lowercase().as_str() {
648 "off" => TraceHeaderMode::Off,
649 "trusted" => TraceHeaderMode::Trusted,
650 "trusted-with-baggage" => TraceHeaderMode::TrustedWithBaggage,
651 other => {
652 return Err(anyhow::anyhow!(
653 "invalid SOMA_MCP_TRACE_HEADERS {:?}: must be \"off\", \"trusted\", \
654 or \"trusted-with-baggage\"",
655 other
656 ));
657 }
658 };
659 }
660
661 env_str("SOMA_API_URL", &mut config.soma.api_url);
663 env_str("SOMA_API_KEY", &mut config.soma.api_key);
664 if let Ok(v) = std::env::var("SOMA_RUNTIME_MODE")
665 && !v.is_empty()
666 {
667 config.soma.runtime_mode = match v.to_lowercase().as_str() {
668 "auto" => RuntimeMode::Auto,
669 "local" => RuntimeMode::Local,
670 "remote" | "api" => RuntimeMode::Remote,
671 other => {
672 return Err(anyhow::anyhow!(
673 "invalid SOMA_RUNTIME_MODE {:?}: must be \"auto\", \"local\", or \"remote\"",
674 other
675 ));
676 }
677 };
678 }
679
680 if let Ok(value) = std::env::var("SOMA_PYTHON_RUNNER_MODE")
681 && !value.is_empty()
682 {
683 config.python.mode = match value.to_ascii_lowercase().as_str() {
684 "one-shot" | "oneshot" => PythonRunnerMode::OneShot,
685 "persistent" => PythonRunnerMode::Persistent,
686 other => anyhow::bail!(
687 "invalid SOMA_PYTHON_RUNNER_MODE {other:?}: must be \"one-shot\" or \"persistent\""
688 ),
689 };
690 }
691 if let Ok(value) = std::env::var("SOMA_PYTHON_EXECUTION_PROFILE")
692 && !value.is_empty()
693 {
694 config.python.execution_profile = match value.to_ascii_lowercase().as_str() {
695 "disabled" => PythonExecutionProfile::Disabled,
696 "trusted" => PythonExecutionProfile::Trusted,
697 "brokered" => PythonExecutionProfile::Brokered,
698 other => anyhow::bail!(
699 "invalid SOMA_PYTHON_EXECUTION_PROFILE {other:?}: must be \"disabled\", \"trusted\", or \"brokered\""
700 ),
701 };
702 }
703 env_list(
704 "SOMA_PYTHON_BROKER_ALLOWED_HTTP_HOSTS",
705 &mut config.python.broker_allowed_http_hosts,
706 );
707 env_list(
708 "SOMA_PYTHON_BROKER_SECRET_NAMES",
709 &mut config.python.broker_secret_names,
710 );
711 env_list(
712 "SOMA_PYTHON_BROKER_STATE_NAMESPACES",
713 &mut config.python.broker_state_namespaces,
714 );
715 env_bool(
716 "SOMA_PYTHON_BROKER_ALLOW_LOGGING",
717 &mut config.python.broker_allow_logging,
718 )?;
719 env_bool(
720 "SOMA_PYTHON_BROKER_ALLOW_METRICS",
721 &mut config.python.broker_allow_metrics,
722 )?;
723 env_bool(
724 "SOMA_PYTHON_BROKER_ALLOW_PROGRESS",
725 &mut config.python.broker_allow_progress,
726 )?;
727 env_parse(
728 "SOMA_PYTHON_RUNNER_STARTUP_TIMEOUT_MS",
729 &mut config.python.startup_timeout_ms,
730 )?;
731 env_parse(
732 "SOMA_PYTHON_RUNNER_REQUEST_TIMEOUT_MS",
733 &mut config.python.request_timeout_ms,
734 )?;
735 env_parse(
736 "SOMA_PYTHON_RUNNER_SHUTDOWN_GRACE_MS",
737 &mut config.python.shutdown_grace_ms,
738 )?;
739 env_parse(
740 "SOMA_PYTHON_RUNNER_MAX_RESTARTS",
741 &mut config.python.max_restarts,
742 )?;
743 env_parse(
744 "SOMA_PYTHON_RUNNER_RESTART_WINDOW_MS",
745 &mut config.python.restart_window_ms,
746 )?;
747 env_parse(
748 "SOMA_PYTHON_RUNNER_RESTART_BACKOFF_MS",
749 &mut config.python.restart_backoff_ms,
750 )?;
751 env_parse(
752 "SOMA_PYTHON_RUNNER_MAX_STDERR_BYTES",
753 &mut config.python.max_stderr_bytes,
754 )?;
755 env_parse(
756 "SOMA_PYTHON_RUNNER_MAX_PENDING_BYTES",
757 &mut config.python.max_pending_bytes,
758 )?;
759 env_parse(
760 "SOMA_PYTHON_RUNNER_MAX_WORKERS",
761 &mut config.python.max_workers,
762 )?;
763 env_parse(
764 "SOMA_PYTHON_RUNNER_MAX_CANDIDATE_STARTS",
765 &mut config.python.max_candidate_starts,
766 )?;
767 env_bool(
768 "SOMA_PYTHON_ENVIRONMENT_ENABLED",
769 &mut config.python.environment.enabled,
770 )?;
771 env_str(
772 "SOMA_PYTHON_ENVIRONMENT_CACHE_ROOT",
773 &mut config.python.environment.cache_root,
774 );
775 env_str(
776 "SOMA_PYTHON_ENVIRONMENT_UV_PROGRAM",
777 &mut config.python.environment.uv_program,
778 );
779 env_str(
780 "SOMA_PYTHON_ENVIRONMENT_UV_VERSION",
781 &mut config.python.environment.uv_version,
782 );
783 env_str(
784 "SOMA_PYTHON_ENVIRONMENT_PYTHON_EXECUTABLE",
785 &mut config.python.environment.python_executable,
786 );
787 env_str(
788 "SOMA_PYTHON_ENVIRONMENT_RUNTIME_IMPLEMENTATION",
789 &mut config.python.environment.runtime_implementation,
790 );
791 env_str(
792 "SOMA_PYTHON_ENVIRONMENT_RUNTIME_VERSION",
793 &mut config.python.environment.runtime_version,
794 );
795 env_str(
796 "SOMA_PYTHON_ENVIRONMENT_RUNTIME_PLATFORM",
797 &mut config.python.environment.runtime_platform,
798 );
799 env_str(
800 "SOMA_PYTHON_ENVIRONMENT_WHEEL_PLATFORM_TAG",
801 &mut config.python.environment.wheel_platform_tag,
802 );
803 env_str(
804 "SOMA_PYTHON_ENVIRONMENT_SDK_WHEEL",
805 &mut config.python.environment.sdk_wheel,
806 );
807 env_str(
808 "SOMA_PYTHON_ENVIRONMENT_SDK_WHEEL_SHA256",
809 &mut config.python.environment.sdk_wheel_sha256,
810 );
811 env_bool(
812 "SOMA_PYTHON_ENVIRONMENT_OFFLINE",
813 &mut config.python.environment.offline,
814 )?;
815 env_bool(
816 "SOMA_PYTHON_ENVIRONMENT_UPDATE",
817 &mut config.python.environment.update,
818 )?;
819 env_parse(
820 "SOMA_PYTHON_ENVIRONMENT_POLICY_VERSION",
821 &mut config.python.environment.policy_version,
822 )?;
823 config.python.validate()?;
824
825 Ok(config)
826 }
827}
828
829#[cfg(test)]
830#[path = "config_tests.rs"]
831mod tests;