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