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