skill_context/
runtime.rs

1//! Runtime-specific override types.
2//!
3//! This module defines runtime-specific configuration overrides
4//! for WASM, Docker, and native execution environments.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Runtime-specific overrides.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub struct RuntimeOverrides {
13    /// WASM-specific configuration.
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub wasm: Option<WasmOverrides>,
16
17    /// Docker-specific configuration.
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub docker: Option<DockerOverrides>,
20
21    /// Native execution configuration.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub native: Option<NativeOverrides>,
24}
25
26impl RuntimeOverrides {
27    /// Create new empty runtime overrides.
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Set WASM overrides.
33    pub fn with_wasm(mut self, wasm: WasmOverrides) -> Self {
34        self.wasm = Some(wasm);
35        self
36    }
37
38    /// Set Docker overrides.
39    pub fn with_docker(mut self, docker: DockerOverrides) -> Self {
40        self.docker = Some(docker);
41        self
42    }
43
44    /// Set native overrides.
45    pub fn with_native(mut self, native: NativeOverrides) -> Self {
46        self.native = Some(native);
47        self
48    }
49
50    /// Check if any runtime overrides are set.
51    pub fn is_empty(&self) -> bool {
52        self.wasm.is_none() && self.docker.is_none() && self.native.is_none()
53    }
54}
55
56/// WASM runtime overrides.
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub struct WasmOverrides {
60    /// Stack size in bytes.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub stack_size: Option<usize>,
63
64    /// Enable/disable specific WASI capabilities.
65    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
66    pub wasi_capabilities: HashMap<String, bool>,
67
68    /// Fuel limit for execution metering.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub fuel_limit: Option<u64>,
71
72    /// Enable epoch-based interruption.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub epoch_interruption: Option<bool>,
75
76    /// Memory pages limit.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub max_memory_pages: Option<u32>,
79
80    /// Enable debug info.
81    #[serde(default)]
82    pub debug_info: bool,
83}
84
85impl WasmOverrides {
86    /// Create new WASM overrides.
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Set stack size.
92    pub fn with_stack_size(mut self, size: usize) -> Self {
93        self.stack_size = Some(size);
94        self
95    }
96
97    /// Set a WASI capability.
98    pub fn with_wasi_capability(mut self, capability: impl Into<String>, enabled: bool) -> Self {
99        self.wasi_capabilities.insert(capability.into(), enabled);
100        self
101    }
102
103    /// Enable a WASI capability.
104    pub fn enable_capability(self, capability: impl Into<String>) -> Self {
105        self.with_wasi_capability(capability, true)
106    }
107
108    /// Disable a WASI capability.
109    pub fn disable_capability(self, capability: impl Into<String>) -> Self {
110        self.with_wasi_capability(capability, false)
111    }
112
113    /// Set fuel limit.
114    pub fn with_fuel_limit(mut self, limit: u64) -> Self {
115        self.fuel_limit = Some(limit);
116        self
117    }
118
119    /// Enable epoch interruption.
120    pub fn with_epoch_interruption(mut self) -> Self {
121        self.epoch_interruption = Some(true);
122        self
123    }
124
125    /// Set max memory pages.
126    pub fn with_max_memory_pages(mut self, pages: u32) -> Self {
127        self.max_memory_pages = Some(pages);
128        self
129    }
130
131    /// Enable debug info.
132    pub fn with_debug_info(mut self) -> Self {
133        self.debug_info = true;
134        self
135    }
136
137    /// Check if a WASI capability is enabled.
138    pub fn is_capability_enabled(&self, capability: &str) -> Option<bool> {
139        self.wasi_capabilities.get(capability).copied()
140    }
141
142    /// Get stack size in bytes, with a default.
143    pub fn stack_size_or_default(&self) -> usize {
144        self.stack_size.unwrap_or(1024 * 1024) // 1MB default
145    }
146}
147
148/// Docker runtime overrides.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(rename_all = "snake_case")]
151pub struct DockerOverrides {
152    /// Override container image.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub image: Option<String>,
155
156    /// Additional docker run arguments.
157    #[serde(default, skip_serializing_if = "Vec::is_empty")]
158    pub extra_args: Vec<String>,
159
160    /// Override entrypoint.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub entrypoint: Option<String>,
163
164    /// Override command.
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub command: Option<Vec<String>>,
167
168    /// User to run as (uid:gid).
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub user: Option<String>,
171
172    /// GPU configuration.
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub gpus: Option<String>,
175
176    /// Platform for multi-arch support.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub platform: Option<String>,
179
180    /// Privileged mode (dangerous!).
181    #[serde(default)]
182    pub privileged: bool,
183
184    /// Security options.
185    #[serde(default, skip_serializing_if = "Vec::is_empty")]
186    pub security_opt: Vec<String>,
187
188    /// Sysctls.
189    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
190    pub sysctls: HashMap<String, String>,
191
192    /// Container labels.
193    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
194    pub labels: HashMap<String, String>,
195
196    /// Restart policy.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub restart: Option<String>,
199
200    /// Remove container after execution.
201    #[serde(default = "default_true")]
202    pub rm: bool,
203
204    /// Init process.
205    #[serde(default)]
206    pub init: bool,
207
208    /// Hostname.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub hostname: Option<String>,
211
212    /// IPC mode.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub ipc: Option<String>,
215
216    /// PID mode.
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub pid: Option<String>,
219
220    /// Capabilities to add.
221    #[serde(default, skip_serializing_if = "Vec::is_empty")]
222    pub cap_add: Vec<String>,
223
224    /// Capabilities to drop.
225    #[serde(default, skip_serializing_if = "Vec::is_empty")]
226    pub cap_drop: Vec<String>,
227}
228
229fn default_true() -> bool {
230    true
231}
232
233impl Default for DockerOverrides {
234    fn default() -> Self {
235        Self {
236            image: None,
237            extra_args: Vec::new(),
238            entrypoint: None,
239            command: None,
240            user: None,
241            gpus: None,
242            platform: None,
243            privileged: false,
244            security_opt: Vec::new(),
245            sysctls: HashMap::new(),
246            labels: HashMap::new(),
247            restart: None,
248            rm: true, // Default to removing container after execution
249            init: false,
250            hostname: None,
251            ipc: None,
252            pid: None,
253            cap_add: Vec::new(),
254            cap_drop: Vec::new(),
255        }
256    }
257}
258
259impl DockerOverrides {
260    /// Create new Docker overrides.
261    pub fn new() -> Self {
262        Self::default()
263    }
264
265    /// Set container image.
266    pub fn with_image(mut self, image: impl Into<String>) -> Self {
267        self.image = Some(image.into());
268        self
269    }
270
271    /// Add extra docker run argument.
272    pub fn with_extra_arg(mut self, arg: impl Into<String>) -> Self {
273        self.extra_args.push(arg.into());
274        self
275    }
276
277    /// Set entrypoint.
278    pub fn with_entrypoint(mut self, entrypoint: impl Into<String>) -> Self {
279        self.entrypoint = Some(entrypoint.into());
280        self
281    }
282
283    /// Set command.
284    pub fn with_command(mut self, command: Vec<String>) -> Self {
285        self.command = Some(command);
286        self
287    }
288
289    /// Set user.
290    pub fn with_user(mut self, user: impl Into<String>) -> Self {
291        self.user = Some(user.into());
292        self
293    }
294
295    /// Enable GPU access.
296    pub fn with_gpus(mut self, gpus: impl Into<String>) -> Self {
297        self.gpus = Some(gpus.into());
298        self
299    }
300
301    /// Enable all GPUs.
302    pub fn with_all_gpus(self) -> Self {
303        self.with_gpus("all")
304    }
305
306    /// Set platform.
307    pub fn with_platform(mut self, platform: impl Into<String>) -> Self {
308        self.platform = Some(platform.into());
309        self
310    }
311
312    /// Enable privileged mode (dangerous!).
313    pub fn privileged(mut self) -> Self {
314        self.privileged = true;
315        self
316    }
317
318    /// Add security option.
319    pub fn with_security_opt(mut self, opt: impl Into<String>) -> Self {
320        self.security_opt.push(opt.into());
321        self
322    }
323
324    /// Disable new privileges.
325    pub fn with_no_new_privileges(self) -> Self {
326        self.with_security_opt("no-new-privileges")
327    }
328
329    /// Add sysctl.
330    pub fn with_sysctl(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
331        self.sysctls.insert(key.into(), value.into());
332        self
333    }
334
335    /// Add label.
336    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
337        self.labels.insert(key.into(), value.into());
338        self
339    }
340
341    /// Set restart policy.
342    pub fn with_restart(mut self, policy: impl Into<String>) -> Self {
343        self.restart = Some(policy.into());
344        self
345    }
346
347    /// Keep container after execution.
348    pub fn keep_container(mut self) -> Self {
349        self.rm = false;
350        self
351    }
352
353    /// Enable init process.
354    pub fn with_init(mut self) -> Self {
355        self.init = true;
356        self
357    }
358
359    /// Set hostname.
360    pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
361        self.hostname = Some(hostname.into());
362        self
363    }
364
365    /// Add a capability.
366    pub fn add_capability(mut self, cap: impl Into<String>) -> Self {
367        self.cap_add.push(cap.into());
368        self
369    }
370
371    /// Drop a capability.
372    pub fn drop_capability(mut self, cap: impl Into<String>) -> Self {
373        self.cap_drop.push(cap.into());
374        self
375    }
376
377    /// Drop all capabilities.
378    pub fn drop_all_capabilities(self) -> Self {
379        self.drop_capability("ALL")
380    }
381
382    /// Build docker run arguments from these overrides.
383    pub fn to_docker_args(&self) -> Vec<String> {
384        let mut args = Vec::new();
385
386        if self.rm {
387            args.push("--rm".to_string());
388        }
389
390        if self.init {
391            args.push("--init".to_string());
392        }
393
394        if self.privileged {
395            args.push("--privileged".to_string());
396        }
397
398        if let Some(ref user) = self.user {
399            args.push("--user".to_string());
400            args.push(user.clone());
401        }
402
403        if let Some(ref gpus) = self.gpus {
404            args.push("--gpus".to_string());
405            args.push(gpus.clone());
406        }
407
408        if let Some(ref platform) = self.platform {
409            args.push("--platform".to_string());
410            args.push(platform.clone());
411        }
412
413        if let Some(ref entrypoint) = self.entrypoint {
414            args.push("--entrypoint".to_string());
415            args.push(entrypoint.clone());
416        }
417
418        if let Some(ref hostname) = self.hostname {
419            args.push("--hostname".to_string());
420            args.push(hostname.clone());
421        }
422
423        if let Some(ref ipc) = self.ipc {
424            args.push("--ipc".to_string());
425            args.push(ipc.clone());
426        }
427
428        if let Some(ref pid) = self.pid {
429            args.push("--pid".to_string());
430            args.push(pid.clone());
431        }
432
433        if let Some(ref restart) = self.restart {
434            args.push("--restart".to_string());
435            args.push(restart.clone());
436        }
437
438        for opt in &self.security_opt {
439            args.push("--security-opt".to_string());
440            args.push(opt.clone());
441        }
442
443        for (key, value) in &self.sysctls {
444            args.push("--sysctl".to_string());
445            args.push(format!("{}={}", key, value));
446        }
447
448        for (key, value) in &self.labels {
449            args.push("--label".to_string());
450            args.push(format!("{}={}", key, value));
451        }
452
453        for cap in &self.cap_add {
454            args.push("--cap-add".to_string());
455            args.push(cap.clone());
456        }
457
458        for cap in &self.cap_drop {
459            args.push("--cap-drop".to_string());
460            args.push(cap.clone());
461        }
462
463        args.extend(self.extra_args.clone());
464
465        args
466    }
467}
468
469/// Native execution overrides.
470#[derive(Debug, Clone, Serialize, Deserialize)]
471#[serde(rename_all = "snake_case")]
472pub struct NativeOverrides {
473    /// Working directory.
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub working_dir: Option<String>,
476
477    /// Shell to use.
478    #[serde(default, skip_serializing_if = "Option::is_none")]
479    pub shell: Option<String>,
480
481    /// PATH additions.
482    #[serde(default, skip_serializing_if = "Vec::is_empty")]
483    pub path_additions: Vec<String>,
484
485    /// Run as different user.
486    #[serde(default, skip_serializing_if = "Option::is_none")]
487    pub run_as: Option<String>,
488
489    /// Clear environment before execution.
490    #[serde(default)]
491    pub clear_env: bool,
492
493    /// Inherit environment from parent.
494    #[serde(default = "default_true")]
495    pub inherit_env: bool,
496}
497
498impl Default for NativeOverrides {
499    fn default() -> Self {
500        Self {
501            working_dir: None,
502            shell: None,
503            path_additions: Vec::new(),
504            run_as: None,
505            clear_env: false,
506            inherit_env: true, // Default to inheriting environment
507        }
508    }
509}
510
511impl NativeOverrides {
512    /// Create new native overrides.
513    pub fn new() -> Self {
514        Self::default()
515    }
516
517    /// Set working directory.
518    pub fn with_working_dir(mut self, dir: impl Into<String>) -> Self {
519        self.working_dir = Some(dir.into());
520        self
521    }
522
523    /// Set shell.
524    pub fn with_shell(mut self, shell: impl Into<String>) -> Self {
525        self.shell = Some(shell.into());
526        self
527    }
528
529    /// Add to PATH.
530    pub fn with_path_addition(mut self, path: impl Into<String>) -> Self {
531        self.path_additions.push(path.into());
532        self
533    }
534
535    /// Run as user.
536    pub fn with_run_as(mut self, user: impl Into<String>) -> Self {
537        self.run_as = Some(user.into());
538        self
539    }
540
541    /// Clear environment before execution.
542    pub fn with_clear_env(mut self) -> Self {
543        self.clear_env = true;
544        self.inherit_env = false;
545        self
546    }
547
548    /// Don't inherit environment.
549    pub fn without_inherit_env(mut self) -> Self {
550        self.inherit_env = false;
551        self
552    }
553
554    /// Get the shell to use, with a default.
555    pub fn shell_or_default(&self) -> &str {
556        self.shell.as_deref().unwrap_or_else(|| {
557            if cfg!(windows) {
558                "cmd.exe"
559            } else {
560                "/bin/sh"
561            }
562        })
563    }
564
565    /// Build the PATH environment variable.
566    pub fn build_path(&self, existing_path: Option<&str>) -> String {
567        let separator = if cfg!(windows) { ";" } else { ":" };
568        let additions = self.path_additions.join(separator);
569
570        match (additions.is_empty(), existing_path) {
571            (true, Some(p)) => p.to_string(),
572            (true, None) => String::new(),
573            (false, Some(p)) if self.inherit_env => format!("{}{}{}",additions, separator, p),
574            (false, _) => additions,
575        }
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn test_runtime_overrides_builder() {
585        let overrides = RuntimeOverrides::new()
586            .with_wasm(WasmOverrides::new().with_fuel_limit(1000))
587            .with_docker(DockerOverrides::new().with_image("python:3.11"));
588
589        assert!(overrides.wasm.is_some());
590        assert!(overrides.docker.is_some());
591        assert!(!overrides.is_empty());
592    }
593
594    #[test]
595    fn test_wasm_overrides() {
596        let wasm = WasmOverrides::new()
597            .with_stack_size(2 * 1024 * 1024)
598            .with_fuel_limit(100_000)
599            .enable_capability("filesystem")
600            .disable_capability("network")
601            .with_debug_info();
602
603        assert_eq!(wasm.stack_size, Some(2 * 1024 * 1024));
604        assert_eq!(wasm.fuel_limit, Some(100_000));
605        assert_eq!(wasm.is_capability_enabled("filesystem"), Some(true));
606        assert_eq!(wasm.is_capability_enabled("network"), Some(false));
607        assert!(wasm.debug_info);
608    }
609
610    #[test]
611    fn test_docker_overrides() {
612        let docker = DockerOverrides::new()
613            .with_image("python:3.11-slim")
614            .with_user("1000:1000")
615            .with_no_new_privileges()
616            .drop_all_capabilities()
617            .add_capability("NET_BIND_SERVICE")
618            .with_label("app", "skill-engine");
619
620        assert_eq!(docker.image, Some("python:3.11-slim".to_string()));
621        assert_eq!(docker.user, Some("1000:1000".to_string()));
622        assert!(docker.security_opt.contains(&"no-new-privileges".to_string()));
623        assert!(docker.cap_drop.contains(&"ALL".to_string()));
624        assert!(docker.cap_add.contains(&"NET_BIND_SERVICE".to_string()));
625    }
626
627    #[test]
628    fn test_docker_args() {
629        let docker = DockerOverrides::new()
630            .with_user("1000:1000")
631            .with_all_gpus()
632            .with_init()
633            .with_no_new_privileges();
634
635        let args = docker.to_docker_args();
636
637        assert!(args.contains(&"--rm".to_string()));
638        assert!(args.contains(&"--init".to_string()));
639        assert!(args.contains(&"--user".to_string()));
640        assert!(args.contains(&"--gpus".to_string()));
641        assert!(args.contains(&"--security-opt".to_string()));
642    }
643
644    #[test]
645    fn test_native_overrides() {
646        let native = NativeOverrides::new()
647            .with_working_dir("/app")
648            .with_shell("/bin/bash")
649            .with_path_addition("/custom/bin");
650
651        assert_eq!(native.working_dir, Some("/app".to_string()));
652        assert_eq!(native.shell_or_default(), "/bin/bash");
653    }
654
655    #[test]
656    fn test_native_path_building() {
657        let native = NativeOverrides::new()
658            .with_path_addition("/usr/local/bin")
659            .with_path_addition("/opt/bin");
660
661        let path = native.build_path(Some("/usr/bin"));
662        assert!(path.contains("/usr/local/bin"));
663        assert!(path.contains("/opt/bin"));
664        assert!(path.contains("/usr/bin"));
665    }
666
667    #[test]
668    fn test_runtime_overrides_serialization() {
669        let overrides = RuntimeOverrides::new()
670            .with_wasm(WasmOverrides::new().with_fuel_limit(1000))
671            .with_docker(DockerOverrides::new().with_image("python:3.11"));
672
673        let json = serde_json::to_string(&overrides).unwrap();
674        let deserialized: RuntimeOverrides = serde_json::from_str(&json).unwrap();
675
676        assert!(deserialized.wasm.is_some());
677        assert!(deserialized.docker.is_some());
678    }
679}