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