Skip to main content

oxicode_sdk/security/
exec_policy.rs

1//! Execution policy configuration for command allowlisting.
2//!
3//! Defines which binaries agents can execute and how arguments are validated.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8/// How the execution allowlist is enforced.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10#[serde(rename_all = "snake_case")]
11pub enum AllowlistMode {
12    /// All binaries allowed (default for development).
13    #[default]
14    Permissive,
15    /// Only explicitly listed binaries allowed.
16    Enforced,
17}
18
19/// Execution policy — controls which binaries agents can run.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ExecPolicy {
22    /// Allowlist enforcement mode.
23    #[serde(default)]
24    pub allowlist_mode: AllowlistMode,
25    /// Explicitly allowed binary names.
26    #[serde(default)]
27    pub allowed_commands: HashSet<String>,
28    /// Default safe commands always allowed.
29    #[serde(default)]
30    pub default_safe_commands: HashSet<String>,
31}
32
33impl ExecPolicy {
34    /// Create a permissive policy (all allowed).
35    pub fn permissive() -> Self {
36        Self {
37            allowlist_mode: AllowlistMode::Permissive,
38            allowed_commands: HashSet::new(),
39            default_safe_commands: Self::safe_defaults(),
40        }
41    }
42
43    /// Create an enforced policy with only the given commands.
44    pub fn enforced(commands: Vec<&str>) -> Self {
45        Self {
46            allowlist_mode: AllowlistMode::Enforced,
47            allowed_commands: commands.into_iter().map(String::from).collect(),
48            default_safe_commands: Self::safe_defaults(),
49        }
50    }
51
52    /// Check if a binary is allowed.
53    pub fn is_binary_allowed(&self, binary: &str) -> bool {
54        match self.allowlist_mode {
55            AllowlistMode::Permissive => true,
56            AllowlistMode::Enforced => {
57                self.allowed_commands.contains(binary)
58                    || self.default_safe_commands.contains(binary)
59            }
60        }
61    }
62
63    fn safe_defaults() -> HashSet<String> {
64        [
65            "git", "grep", "find", "cat", "ls", "head", "tail", "wc", "sort", "uniq",
66        ]
67        .iter()
68        .map(|s| s.to_string())
69        .collect()
70    }
71}
72
73impl Default for ExecPolicy {
74    fn default() -> Self {
75        Self::permissive()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn permissive_allows_all() {
85        let policy = ExecPolicy::permissive();
86        assert!(policy.is_binary_allowed("rm"));
87        assert!(policy.is_binary_allowed("anything"));
88    }
89
90    #[test]
91    fn enforced_allows_listed() {
92        let policy = ExecPolicy::enforced(vec!["echo", "git"]);
93        assert!(policy.is_binary_allowed("echo"));
94        assert!(policy.is_binary_allowed("git"));
95        assert!(!policy.is_binary_allowed("rm"));
96    }
97
98    #[test]
99    fn enforced_safe_defaults() {
100        let policy = ExecPolicy::enforced(vec![]);
101        // Safe defaults should always be allowed
102        assert!(policy.is_binary_allowed("git"));
103        assert!(policy.is_binary_allowed("grep"));
104        assert!(policy.is_binary_allowed("cat"));
105    }
106}