yggdrasil/infra/
runner_config.rs1use serde::{Deserialize, Serialize};
2#[cfg(feature = "server")]
3use std::env;
4#[cfg(feature = "server")]
5use std::sync::LazyLock;
6
7#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
8pub struct ResourceLimits {
9 pub cpu_cores: f64,
10 pub memory_mb: u64,
11 pub timeout_secs: u64,
12 pub output_bytes: u64,
13 pub allow_network: bool,
14}
15
16#[cfg(feature = "server")]
17pub struct RunnerConfig {
18 pub max_cpu_cores: f64,
19 pub max_memory_mb: u64,
20 pub max_timeout_secs: u64,
21 pub max_output_bytes: u64,
22 pub max_source_bytes: u64,
23 pub allow_network: bool,
24 pub max_concurrent: usize,
25 pub queue_timeout_secs: u64,
26 pub task_ttl_secs: u64,
27 pub docker_socket_path: String,
28 pub languages: Option<Vec<String>>,
31}
32
33#[cfg(feature = "server")]
34fn parse_allow_network(v: &str) -> bool {
35 let l = v.to_lowercase();
36 l == "true" || l == "1" || l == "yes"
37}
38
39#[cfg(feature = "server")]
40pub static RUNNER_CONFIG: LazyLock<RunnerConfig> = LazyLock::new(|| {
41 let languages = env::var("CODE_RUNNER_LANGUAGES").ok().map(|s| {
44 s.split(',')
45 .map(|t| t.trim().to_lowercase())
46 .filter(|t| !t.is_empty())
47 .collect()
48 });
49
50 RunnerConfig {
51 max_cpu_cores: env::var("CODE_RUNNER_MAX_CPU_CORES")
52 .ok()
53 .and_then(|v| v.parse().ok())
54 .unwrap_or(2.0),
55 max_memory_mb: env::var("CODE_RUNNER_MAX_MEMORY_MB")
56 .ok()
57 .and_then(|v| v.parse().ok())
58 .unwrap_or(1024),
59 max_timeout_secs: env::var("CODE_RUNNER_MAX_TIMEOUT_SECS")
60 .ok()
61 .and_then(|v| v.parse().ok())
62 .unwrap_or(30),
63 max_output_bytes: env::var("CODE_RUNNER_MAX_OUTPUT_BYTES")
64 .ok()
65 .and_then(|v| v.parse().ok())
66 .unwrap_or(1048576),
67 max_source_bytes: env::var("CODE_RUNNER_MAX_SOURCE_BYTES")
68 .ok()
69 .and_then(|v| v.parse().ok())
70 .unwrap_or(65536),
71 allow_network: env::var("CODE_RUNNER_ALLOW_NETWORK")
72 .ok()
73 .map(|v| parse_allow_network(&v))
74 .unwrap_or(false),
75 max_concurrent: env::var("CODE_RUNNER_MAX_CONCURRENT")
76 .ok()
77 .and_then(|v| v.parse().ok())
78 .unwrap_or(4),
79 queue_timeout_secs: env::var("CODE_RUNNER_QUEUE_TIMEOUT_SECS")
80 .ok()
81 .and_then(|v| v.parse().ok())
82 .unwrap_or(30),
83 task_ttl_secs: env::var("CODE_RUNNER_TASK_TTL_SECS")
84 .ok()
85 .and_then(|v| v.parse().ok())
86 .unwrap_or(300),
87 docker_socket_path: env::var("DOCKER_SOCKET_PATH")
88 .unwrap_or_else(|_| "/var/run/docker.sock".to_string()),
89 languages,
90 }
91});
92
93#[cfg(feature = "server")]
94pub fn clamp_limits(merged: ResourceLimits, lang_allows_network: bool) -> ResourceLimits {
95 clamp_limits_impl(merged, lang_allows_network, &RUNNER_CONFIG)
96}
97
98#[cfg(feature = "server")]
99fn clamp_limits_impl(
100 merged: ResourceLimits,
101 lang_allows_network: bool,
102 config: &RunnerConfig,
103) -> ResourceLimits {
104 let max_cpu = if config.max_cpu_cores.is_nan() {
105 2.0
106 } else {
107 config.max_cpu_cores
108 };
109 let min_cpu = 0.1f64.min(max_cpu);
110 let cpu_cores = if merged.cpu_cores.is_nan() {
111 min_cpu
112 } else {
113 merged.cpu_cores.clamp(min_cpu, max_cpu)
114 };
115
116 let min_mem = 16.min(config.max_memory_mb);
117 let memory_mb = merged.memory_mb.clamp(min_mem, config.max_memory_mb);
118
119 let min_timeout = 1.min(config.max_timeout_secs);
120 let timeout_secs = merged
121 .timeout_secs
122 .clamp(min_timeout, config.max_timeout_secs);
123
124 ResourceLimits {
125 cpu_cores,
126 memory_mb,
127 timeout_secs,
128 output_bytes: merged.output_bytes.min(config.max_output_bytes),
129 allow_network: merged.allow_network && config.allow_network && lang_allows_network,
130 }
131}
132
133#[cfg(all(test, feature = "server"))]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_clamp_limits() {
139 let raw = ResourceLimits {
140 cpu_cores: 5.0,
141 memory_mb: 4096,
142 timeout_secs: 120,
143 output_bytes: 9999999,
144 allow_network: true,
145 };
146 let clamped = clamp_limits(raw, true);
147 assert!(clamped.cpu_cores <= 2.0);
148 assert!(clamped.memory_mb <= 1024);
149 assert!(clamped.timeout_secs <= 30);
150 assert!(clamped.output_bytes <= 1048576);
151 assert!(!clamped.allow_network);
152 }
153
154 #[test]
155 fn test_clamp_limits_safeguarded() {
156 let config = RunnerConfig {
157 max_cpu_cores: 0.05,
158 max_memory_mb: 8,
159 max_timeout_secs: 0,
160 max_output_bytes: 100,
161 max_source_bytes: 100,
162 allow_network: true,
163 max_concurrent: 1,
164 queue_timeout_secs: 1,
165 task_ttl_secs: 1,
166 docker_socket_path: "".to_string(),
167 languages: None,
168 };
169 let raw = ResourceLimits {
170 cpu_cores: 1.0,
171 memory_mb: 64,
172 timeout_secs: 10,
173 output_bytes: 50,
174 allow_network: true,
175 };
176 let clamped = clamp_limits_impl(raw, true, &config);
177 assert_eq!(clamped.cpu_cores, 0.05);
178 assert_eq!(clamped.memory_mb, 8);
179 assert_eq!(clamped.timeout_secs, 0);
180 assert_eq!(clamped.output_bytes, 50);
181 assert!(clamped.allow_network);
182 }
183
184 #[test]
185 fn test_parse_allow_network() {
186 assert!(parse_allow_network("true"));
187 assert!(parse_allow_network("TRUE"));
188 assert!(parse_allow_network("True"));
189 assert!(parse_allow_network("1"));
190 assert!(parse_allow_network("yes"));
191 assert!(parse_allow_network("YES"));
192 assert!(parse_allow_network("Yes"));
193 assert!(!parse_allow_network("false"));
194 assert!(!parse_allow_network("0"));
195 assert!(!parse_allow_network("no"));
196 }
197}