Skip to main content

openmoat_core/
kind.rs

1//! The kinds of atomic action a rule can name.
2//!
3//! The same names appear in policy files (`fs.read:`), in `defaults`
4//! (`net: deny`), in synthetic rule ids (`default.net`) and in user output, so
5//! they are defined once here.
6
7use std::fmt;
8use std::str::FromStr;
9
10/// One kind of atomic action. Ordered as policy files list them.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum Kind {
13    /// A command or a whole pipeline.
14    Shell,
15    /// A file read.
16    FsRead,
17    /// A file write, create, delete or move.
18    FsWrite,
19    /// A network host.
20    Net,
21    /// A host a read-only fetch tool (`WebFetch`) requests a URL from: a
22    /// narrower kind of `net`, so `net` rules apply to it as well (ADR-017).
23    Fetch,
24    /// An environment variable read.
25    EnvRead,
26    /// An environment variable set.
27    EnvSet,
28    /// An MCP tool name.
29    Mcp,
30}
31
32impl Kind {
33    /// Every kind, in policy-file order.
34    pub const ALL: [Self; 8] = [
35        Self::Shell,
36        Self::FsRead,
37        Self::FsWrite,
38        Self::Net,
39        Self::Fetch,
40        Self::EnvRead,
41        Self::EnvSet,
42        Self::Mcp,
43    ];
44
45    /// The name used in policy files and rule ids (`fs.read`).
46    #[must_use]
47    pub fn as_str(self) -> &'static str {
48        match self {
49            Self::Shell => "shell",
50            Self::FsRead => "fs.read",
51            Self::FsWrite => "fs.write",
52            Self::Net => "net",
53            Self::Fetch => "fetch",
54            Self::EnvRead => "env.read",
55            Self::EnvSet => "env.set",
56            Self::Mcp => "mcp",
57        }
58    }
59
60    /// The rule-list and `defaults` keys that apply to an atom of this kind,
61    /// most specific first: a fetch is also network access, so `net` lists
62    /// match it and `defaults.net` is its fallback when `defaults.fetch` is unset.
63    #[must_use]
64    pub fn rule_kinds(self) -> &'static [Self] {
65        match self {
66            Self::Fetch => &[Self::Fetch, Self::Net],
67            Self::Shell => &[Self::Shell],
68            Self::FsRead => &[Self::FsRead],
69            Self::FsWrite => &[Self::FsWrite],
70            Self::Net => &[Self::Net],
71            Self::EnvRead => &[Self::EnvRead],
72            Self::EnvSet => &[Self::EnvSet],
73            Self::Mcp => &[Self::Mcp],
74        }
75    }
76}
77
78impl fmt::Display for Kind {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        f.write_str(self.as_str())
81    }
82}
83
84/// Serialised as its policy-file name (`fs.read`), as in the compiled IR.
85impl serde::Serialize for Kind {
86    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
87        serializer.serialize_str(self.as_str())
88    }
89}
90
91/// A name that is not one of [`Kind::ALL`].
92#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
93#[error("unknown action kind `{0}`")]
94pub struct UnknownKind(pub String);
95
96impl FromStr for Kind {
97    type Err = UnknownKind;
98
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        Self::ALL
101            .into_iter()
102            .find(|k| k.as_str() == s)
103            .ok_or_else(|| UnknownKind(s.to_owned()))
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn names_round_trip() {
113        for kind in Kind::ALL {
114            assert_eq!(kind.as_str().parse::<Kind>(), Ok(kind));
115        }
116        assert_eq!(
117            "network".parse::<Kind>(),
118            Err(UnknownKind("network".into()))
119        );
120    }
121
122    #[test]
123    fn only_fetch_borrows_another_kinds_rules() {
124        assert_eq!(Kind::Fetch.rule_kinds(), &[Kind::Fetch, Kind::Net]);
125        for kind in Kind::ALL.into_iter().filter(|k| *k != Kind::Fetch) {
126            assert_eq!(kind.rule_kinds(), &[kind]);
127        }
128    }
129}