1use crate::{
2 error::{AppError, AppResult},
3 types::{
4 CapabilityMode, ExecutionPlan, NamespaceConfig, ResourceEnforcementPlan,
5 RuntimeCapabilities, SandboxProfile, SubmitTaskRequest,
6 },
7};
8
9pub fn resolve_execution_plan(
10 request: &SubmitTaskRequest,
11 capabilities: &RuntimeCapabilities,
12 default_mode: CapabilityMode,
13) -> AppResult<ExecutionPlan> {
14 let strict = effective_capability_mode(request, default_mode) == CapabilityMode::Strict
15 || request
16 .control_context
17 .as_ref()
18 .map(|context| context.requires_strict_sandbox)
19 .unwrap_or(false);
20
21 let mut degraded = false;
22 let mut fallback_reasons = Vec::new();
23 let mut effective_sandbox = request.sandbox.clone();
24
25 if matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox)
26 && !capabilities.sandbox.linux_sandbox
27 {
28 if strict {
29 return Err(AppError::UnsupportedCapability(
30 "sandbox.profile=linux_sandbox is unavailable on this runtime".into(),
31 ));
32 }
33 degraded = true;
34 fallback_reasons
35 .push("linux_sandbox is unavailable; falling back to process sandbox".into());
36 effective_sandbox.profile = SandboxProfile::Process;
37 effective_sandbox.chroot = false;
38 effective_sandbox.rootfs = None;
39 effective_sandbox.namespaces = None;
40 }
41
42 if effective_sandbox.chroot && !capabilities.sandbox.chroot {
43 if strict {
44 return Err(AppError::UnsupportedCapability(
45 "sandbox.chroot is unavailable on this runtime".into(),
46 ));
47 }
48 degraded = true;
49 fallback_reasons.push("chroot is unavailable; running without chroot".into());
50 effective_sandbox.chroot = false;
51 effective_sandbox.rootfs = None;
52 }
53
54 if matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox) {
55 let requested_namespaces = effective_sandbox.effective_namespaces();
56 let adjusted = NamespaceConfig {
57 mount: namespace_or_fallback(
58 requested_namespaces.mount,
59 capabilities.sandbox.namespaces.mount,
60 strict,
61 &mut degraded,
62 &mut fallback_reasons,
63 "mount",
64 )?,
65 pid: namespace_or_fallback(
66 requested_namespaces.pid,
67 capabilities.sandbox.namespaces.pid,
68 strict,
69 &mut degraded,
70 &mut fallback_reasons,
71 "pid",
72 )?,
73 uts: namespace_or_fallback(
74 requested_namespaces.uts,
75 capabilities.sandbox.namespaces.uts,
76 strict,
77 &mut degraded,
78 &mut fallback_reasons,
79 "uts",
80 )?,
81 ipc: namespace_or_fallback(
82 requested_namespaces.ipc,
83 capabilities.sandbox.namespaces.ipc,
84 strict,
85 &mut degraded,
86 &mut fallback_reasons,
87 "ipc",
88 )?,
89 net: namespace_or_fallback(
90 requested_namespaces.net,
91 capabilities.sandbox.namespaces.net,
92 strict,
93 &mut degraded,
94 &mut fallback_reasons,
95 "net",
96 )?,
97 };
98 effective_sandbox.namespaces = Some(adjusted);
99 }
100
101 let cgroup_enforced = matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox)
102 && capabilities.resources.cgroup_writable;
103 let cpu_time_enforced =
104 request.limits.cpu_time_sec.is_none() || capabilities.resources.rlimit_cpu;
105 let memory_enforced =
106 request.limits.memory_bytes.is_none() || capabilities.resources.rlimit_memory;
107 let pids_enforced =
108 request.limits.pids_max.is_none() || (cgroup_enforced && capabilities.resources.pids_limit);
109 let oom_detection = request.limits.memory_bytes.is_some()
110 && cgroup_enforced
111 && capabilities.resources.oom_detection;
112
113 if request.limits.cpu_time_sec.is_some() && !cpu_time_enforced {
114 if strict {
115 return Err(AppError::UnsupportedCapability(
116 "cpu_time_sec enforcement is unavailable on this runtime".into(),
117 ));
118 }
119 degraded = true;
120 fallback_reasons.push("cpu_time_sec enforcement is unavailable".into());
121 }
122
123 if request.limits.memory_bytes.is_some() && !memory_enforced {
124 if strict {
125 return Err(AppError::UnsupportedCapability(
126 "memory_bytes enforcement is unavailable on this runtime".into(),
127 ));
128 }
129 degraded = true;
130 fallback_reasons.push("memory_bytes enforcement is unavailable".into());
131 }
132
133 if request.limits.pids_max.is_some() && !pids_enforced {
134 if strict {
135 return Err(AppError::UnsupportedCapability(
136 "pids_max enforcement requires writable cgroup support".into(),
137 ));
138 }
139 degraded = true;
140 fallback_reasons.push("pids_max enforcement requires writable cgroup support".into());
141 }
142
143 Ok(ExecutionPlan {
144 capability_mode: effective_capability_mode(request, default_mode),
145 requested_sandbox: request.sandbox.clone(),
146 effective_sandbox,
147 resource_enforcement: ResourceEnforcementPlan {
148 wall_time_ms: request.limits.wall_time_ms,
149 cpu_time_sec: request.limits.cpu_time_sec,
150 cpu_time_enforced: request.limits.cpu_time_sec.is_some() && cpu_time_enforced,
151 memory_bytes: request.limits.memory_bytes,
152 memory_enforced: request.limits.memory_bytes.is_some() && memory_enforced,
153 pids_max: request.limits.pids_max,
154 pids_enforced: request.limits.pids_max.is_some() && pids_enforced,
155 cgroup_enforced,
156 oom_detection,
157 },
158 degraded,
159 fallback_reasons,
160 capability_warnings: capabilities.warnings.clone(),
161 })
162}
163
164pub fn effective_capability_mode(
165 request: &SubmitTaskRequest,
166 default_mode: CapabilityMode,
167) -> CapabilityMode {
168 request
169 .policy
170 .as_ref()
171 .map(|policy| policy.capability_mode)
172 .unwrap_or(default_mode)
173}
174
175fn namespace_or_fallback(
176 requested: bool,
177 supported: bool,
178 strict: bool,
179 degraded: &mut bool,
180 fallback_reasons: &mut Vec<String>,
181 namespace: &str,
182) -> AppResult<bool> {
183 if !requested {
184 return Ok(false);
185 }
186 if supported {
187 return Ok(true);
188 }
189 if strict {
190 return Err(AppError::UnsupportedCapability(format!(
191 "{namespace} namespace is unavailable on this runtime"
192 )));
193 }
194 *degraded = true;
195 fallback_reasons.push(format!(
196 "{namespace} namespace is unavailable; running without it"
197 ));
198 Ok(false)
199}
200
201#[cfg(test)]
202mod tests {
203 use std::collections::BTreeMap;
204
205 use chrono::Utc;
206
207 use super::*;
208 use crate::types::{
209 ControlContext, ExecutionCapabilities, ExecutionKind, ExecutionSpec, NamespaceCapabilities,
210 ResourceCapabilities, ResourceCapacity, ResourceLimits, RuntimePlatform,
211 SandboxCapabilities, SandboxPolicy, StorageCapabilities, TaskPolicy,
212 };
213
214 fn capabilities(linux_sandbox: bool, cgroup_writable: bool) -> RuntimeCapabilities {
215 RuntimeCapabilities {
216 runtime_id: "test".into(),
217 snapshot_version: "v1".into(),
218 collected_at: Utc::now(),
219 platform: RuntimePlatform {
220 os: "test".into(),
221 arch: "test".into(),
222 containerized: false,
223 kubernetes: false,
224 },
225 execution: ExecutionCapabilities {
226 command: true,
227 script: true,
228 process_group: true,
229 },
230 sandbox: SandboxCapabilities {
231 process: true,
232 linux_sandbox,
233 chroot: linux_sandbox,
234 namespaces: NamespaceCapabilities {
235 mount: linux_sandbox,
236 pid: linux_sandbox,
237 uts: linux_sandbox,
238 ipc: linux_sandbox,
239 net: linux_sandbox,
240 },
241 },
242 storage: StorageCapabilities {
243 data_dir_writable: true,
244 },
245 resources: ResourceCapabilities {
246 rlimit_cpu: true,
247 rlimit_memory: true,
248 cgroup_v2: cgroup_writable,
249 cgroup_writable,
250 memory_limit: true,
251 pids_limit: cgroup_writable,
252 oom_detection: cgroup_writable,
253 cpu_quota: false,
254 ledger: true,
255 capacity: ResourceCapacity {
256 task_slots: 4,
257 memory_bytes: Some(1024),
258 pids: Some(64),
259 },
260 },
261 stable_semantics: vec![],
262 enhanced_semantics: vec![],
263 warnings: vec![],
264 degraded: false,
265 overrides: BTreeMap::new(),
266 }
267 }
268
269 fn request() -> SubmitTaskRequest {
270 SubmitTaskRequest {
271 task_id: None,
272 execution: ExecutionSpec {
273 kind: ExecutionKind::Command,
274 program: Some("/bin/echo".into()),
275 args: vec!["ok".into()],
276 script: None,
277 interpreter: None,
278 env: Default::default(),
279 },
280 limits: ResourceLimits::default(),
281 sandbox: SandboxPolicy::default(),
282 policy: None,
283 control_context: None,
284 metadata: BTreeMap::new(),
285 }
286 }
287
288 #[test]
289 fn process_request_stays_stable() {
290 let plan = resolve_execution_plan(
291 &request(),
292 &capabilities(false, false),
293 CapabilityMode::Adaptive,
294 )
295 .expect("plan");
296 assert!(!plan.degraded);
297 assert_eq!(plan.effective_sandbox.profile, SandboxProfile::Process);
298 }
299
300 #[test]
301 fn adaptive_linux_sandbox_falls_back() {
302 let mut request = request();
303 request.sandbox.profile = SandboxProfile::LinuxSandbox;
304
305 let plan = resolve_execution_plan(
306 &request,
307 &capabilities(false, false),
308 CapabilityMode::Adaptive,
309 )
310 .expect("plan");
311 assert!(plan.degraded);
312 assert_eq!(plan.effective_sandbox.profile, SandboxProfile::Process);
313 }
314
315 #[test]
316 fn strict_linux_sandbox_rejects() {
317 let mut request = request();
318 request.sandbox.profile = SandboxProfile::LinuxSandbox;
319 request.policy = Some(TaskPolicy {
320 capability_mode: CapabilityMode::Strict,
321 });
322
323 let err = resolve_execution_plan(
324 &request,
325 &capabilities(false, false),
326 CapabilityMode::Adaptive,
327 )
328 .expect_err("strict should reject");
329 assert!(matches!(err, AppError::UnsupportedCapability(_)));
330 }
331
332 #[test]
333 fn control_context_can_enforce_strict_sandbox() {
334 let mut request = request();
335 request.sandbox.profile = SandboxProfile::LinuxSandbox;
336 request.control_context = Some(ControlContext {
337 requires_strict_sandbox: true,
338 ..ControlContext::default()
339 });
340
341 let err = resolve_execution_plan(
342 &request,
343 &capabilities(false, false),
344 CapabilityMode::Adaptive,
345 )
346 .expect_err("control context should reject");
347 assert!(matches!(err, AppError::UnsupportedCapability(_)));
348 }
349}