aa_security/policy/
capability.rs1use std::collections::BTreeSet;
16use std::fmt;
17use std::str::FromStr;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub enum Capability {
27 FileRead,
29 FileWrite,
31 NetworkOutbound,
33 NetworkInbound,
35 TerminalExec,
37 McpTool(String),
39 Model(String),
41 AgentSpawn,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Default)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub struct CapabilitySet {
49 pub allow: BTreeSet<Capability>,
51 pub deny: BTreeSet<Capability>,
53}
54
55impl FromStr for Capability {
56 type Err = String;
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 match s {
60 "file_read" => Ok(Capability::FileRead),
61 "file_write" => Ok(Capability::FileWrite),
62 "network_outbound" => Ok(Capability::NetworkOutbound),
63 "network_inbound" => Ok(Capability::NetworkInbound),
64 "terminal_exec" => Ok(Capability::TerminalExec),
65 "agent_spawn" => Ok(Capability::AgentSpawn),
66 _ => {
67 if let Some(name) = s.strip_prefix("mcp_tool:") {
68 if name.is_empty() {
69 return Err("mcp_tool: name must not be empty".to_string());
70 }
71 Ok(Capability::McpTool(name.to_string()))
72 } else if let Some(name) = s.strip_prefix("model:") {
73 if name.is_empty() {
74 return Err("model: name must not be empty".to_string());
75 }
76 Ok(Capability::Model(name.to_string()))
77 } else {
78 Err(format!("unknown capability: '{s}'"))
79 }
80 }
81 }
82 }
83}
84
85impl fmt::Display for Capability {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 Capability::FileRead => f.write_str("file_read"),
89 Capability::FileWrite => f.write_str("file_write"),
90 Capability::NetworkOutbound => f.write_str("network_outbound"),
91 Capability::NetworkInbound => f.write_str("network_inbound"),
92 Capability::TerminalExec => f.write_str("terminal_exec"),
93 Capability::AgentSpawn => f.write_str("agent_spawn"),
94 Capability::McpTool(name) => write!(f, "mcp_tool:{name}"),
95 Capability::Model(name) => write!(f, "model:{name}"),
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn parses_each_simple_variant() {
106 assert_eq!("file_read".parse::<Capability>().unwrap(), Capability::FileRead);
107 assert_eq!("file_write".parse::<Capability>().unwrap(), Capability::FileWrite);
108 assert_eq!(
109 "network_outbound".parse::<Capability>().unwrap(),
110 Capability::NetworkOutbound
111 );
112 assert_eq!("terminal_exec".parse::<Capability>().unwrap(), Capability::TerminalExec);
113 }
114
115 #[test]
116 fn parses_parameterised_variants() {
117 assert_eq!(
118 "mcp_tool:git".parse::<Capability>().unwrap(),
119 Capability::McpTool("git".to_string())
120 );
121 assert_eq!(
122 "model:gpt-4".parse::<Capability>().unwrap(),
123 Capability::Model("gpt-4".to_string())
124 );
125 }
126
127 #[test]
128 fn rejects_empty_parameterised_name() {
129 assert!("mcp_tool:".parse::<Capability>().is_err());
130 assert!("model:".parse::<Capability>().is_err());
131 }
132
133 #[test]
134 fn rejects_unknown_capability() {
135 assert!("teleport".parse::<Capability>().is_err());
136 }
137
138 #[test]
139 fn display_round_trips() {
140 for cap in [
141 Capability::FileRead,
142 Capability::FileWrite,
143 Capability::NetworkOutbound,
144 Capability::NetworkInbound,
145 Capability::TerminalExec,
146 Capability::AgentSpawn,
147 Capability::McpTool("bash".to_string()),
148 Capability::Model("claude".to_string()),
149 ] {
150 let rendered = cap.to_string();
151 assert_eq!(rendered.parse::<Capability>().unwrap(), cap);
152 }
153 }
154}