Skip to main content

orchestrator_config/config/
runner.rs

1use serde::{Deserialize, Serialize};
2
3/// Policy used to constrain shell execution.
4#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
5#[serde(rename_all = "snake_case")]
6pub enum RunnerPolicy {
7    /// Permit arbitrary shells and arguments.
8    Unsafe,
9    /// Allow only configured shells, args, and environment variables.
10    #[default]
11    Allowlist,
12}
13
14/// Execution backend used by the runner.
15#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum RunnerExecutorKind {
18    /// Execute commands through a shell process.
19    #[default]
20    Shell,
21}
22
23/// Configuration for command execution in orchestrated steps.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RunnerConfig {
26    /// Default shell binary used to launch commands.
27    pub shell: String,
28    /// Default argument passed before the command string.
29    #[serde(default = "default_shell_arg")]
30    pub shell_arg: String,
31    /// Security policy applied to shell execution.
32    #[serde(default)]
33    pub policy: RunnerPolicy,
34    /// Execution backend used for step commands.
35    #[serde(default)]
36    pub executor: RunnerExecutorKind,
37    /// Shell binaries allowed when `policy` is `Allowlist`.
38    #[serde(default = "default_allowed_shells")]
39    pub allowed_shells: Vec<String>,
40    /// Shell arguments allowed when `policy` is `Allowlist`.
41    #[serde(default = "default_allowed_shell_args")]
42    pub allowed_shell_args: Vec<String>,
43    /// Environment variables passed through to the runner.
44    #[serde(default = "default_env_allowlist")]
45    pub env_allowlist: Vec<String>,
46    /// Case-insensitive patterns redacted from logs and telemetry.
47    #[serde(default = "default_redaction_patterns")]
48    pub redaction_patterns: Vec<String>,
49}
50
51fn default_shell_arg() -> String {
52    "-lc".to_string()
53}
54
55fn default_allowed_shells() -> Vec<String> {
56    vec![
57        "/bin/bash".to_string(),
58        "/bin/zsh".to_string(),
59        "/bin/sh".to_string(),
60    ]
61}
62
63fn default_allowed_shell_args() -> Vec<String> {
64    vec!["-lc".to_string(), "-c".to_string()]
65}
66
67fn default_env_allowlist() -> Vec<String> {
68    vec![
69        "PATH".to_string(),
70        "HOME".to_string(),
71        "USER".to_string(),
72        "LANG".to_string(),
73        "TERM".to_string(),
74    ]
75}
76
77fn default_redaction_patterns() -> Vec<String> {
78    vec![
79        "token".to_string(),
80        "password".to_string(),
81        "secret".to_string(),
82        "api_key".to_string(),
83        "authorization".to_string(),
84    ]
85}
86
87impl Default for RunnerConfig {
88    fn default() -> Self {
89        Self {
90            shell: "/bin/bash".to_string(),
91            shell_arg: default_shell_arg(),
92            policy: RunnerPolicy::Allowlist,
93            executor: RunnerExecutorKind::Shell,
94            allowed_shells: default_allowed_shells(),
95            allowed_shell_args: default_allowed_shell_args(),
96            env_allowlist: default_env_allowlist(),
97            redaction_patterns: default_redaction_patterns(),
98        }
99    }
100}
101
102/// Resume behavior configuration
103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104pub struct ResumeConfig {
105    /// Automatically resume eligible tasks after restart.
106    #[serde(default)]
107    pub auto: bool,
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_runner_config_default() {
116        let cfg = RunnerConfig::default();
117        assert_eq!(cfg.shell, "/bin/bash");
118        assert_eq!(cfg.shell_arg, "-lc");
119        assert_eq!(cfg.policy, RunnerPolicy::Allowlist);
120        assert_eq!(cfg.executor, RunnerExecutorKind::Shell);
121        assert_eq!(cfg.allowed_shells.len(), 3);
122        assert!(cfg.allowed_shells.contains(&"/bin/bash".to_string()));
123        assert_eq!(cfg.allowed_shell_args, vec!["-lc", "-c"]);
124        assert!(cfg.env_allowlist.contains(&"PATH".to_string()));
125        assert!(cfg.env_allowlist.contains(&"HOME".to_string()));
126        assert!(cfg.redaction_patterns.contains(&"token".to_string()));
127        assert!(cfg.redaction_patterns.contains(&"secret".to_string()));
128    }
129
130    #[test]
131    fn test_runner_policy_default() {
132        let policy = RunnerPolicy::default();
133        assert_eq!(policy, RunnerPolicy::Allowlist);
134    }
135
136    #[test]
137    fn test_runner_executor_kind_default() {
138        let kind = RunnerExecutorKind::default();
139        assert_eq!(kind, RunnerExecutorKind::Shell);
140    }
141
142    #[test]
143    fn test_runner_config_serde_round_trip() {
144        let cfg = RunnerConfig::default();
145        let json = serde_json::to_string(&cfg).expect("serialize runner config");
146        let cfg2: RunnerConfig = serde_json::from_str(&json).expect("deserialize runner config");
147        assert_eq!(cfg2.shell, cfg.shell);
148        assert_eq!(cfg2.policy, cfg.policy);
149    }
150
151    #[test]
152    fn test_runner_config_deserialize_minimal() {
153        let json = r#"{"shell": "/bin/sh"}"#;
154        let cfg: RunnerConfig = serde_json::from_str(json).expect("deserialize minimal runner");
155        assert_eq!(cfg.shell, "/bin/sh");
156        // defaults should kick in
157        assert_eq!(cfg.shell_arg, "-lc");
158        assert_eq!(cfg.policy, RunnerPolicy::Allowlist);
159        assert!(!cfg.allowed_shells.is_empty());
160    }
161
162    #[test]
163    fn test_unsafe_serializes_as_unsafe() {
164        let cfg = RunnerConfig {
165            policy: RunnerPolicy::Unsafe,
166            ..RunnerConfig::default()
167        };
168        let json = serde_json::to_string(&cfg).expect("serialize unsafe runner");
169        assert!(json.contains(r#""policy":"unsafe""#));
170    }
171}