1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
10pub struct Capability(pub String);
11
12impl Capability {
13 pub fn new(value: impl Into<String>) -> Self {
14 Self(value.into())
15 }
16
17 pub fn fs_read(glob: &str) -> Self {
18 Self(format!("fs:read:{glob}"))
19 }
20
21 pub fn fs_write(glob: &str) -> Self {
22 Self(format!("fs:write:{glob}"))
23 }
24
25 pub fn net_egress(host: &str) -> Self {
26 Self(format!("net:egress:{host}"))
27 }
28
29 pub fn exec(command: &str) -> Self {
30 Self(format!("exec:cmd:{command}"))
31 }
32
33 pub fn secrets(scope: &str) -> Self {
34 Self(format!("secrets:read:{scope}"))
35 }
36
37 pub fn as_str(&self) -> &str {
38 &self.0
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct PolicySet {
45 pub allow_capabilities: Vec<Capability>,
46 pub gate_capabilities: Vec<Capability>,
47 pub max_tool_runtime_secs: u64,
48 pub max_events_per_turn: u64,
49}
50
51impl PolicySet {
52 pub fn anonymous() -> Self {
61 Self {
62 allow_capabilities: vec![Capability::new("fs:read:/session/**")],
63 gate_capabilities: vec![
65 Capability::new("fs:write:**"),
66 Capability::new("net:egress:*"),
67 Capability::new("secrets:read:*"),
68 ],
69 max_tool_runtime_secs: 30,
70 max_events_per_turn: 5,
71 }
72 }
73
74 pub fn free() -> Self {
81 Self {
82 allow_capabilities: vec![
83 Capability::new("fs:read:/session/**"),
84 Capability::new("net:egress:*"),
85 Capability::new("exec:cmd:cat"),
87 Capability::new("exec:cmd:ls"),
88 Capability::new("exec:cmd:echo"),
89 Capability::new("exec:cmd:grep"),
90 Capability::new("exec:cmd:jq"),
91 Capability::new("exec:cmd:python3"),
92 Capability::new("exec:cmd:find"),
93 Capability::new("exec:cmd:head"),
94 Capability::new("exec:cmd:tail"),
95 Capability::new("exec:cmd:sort"),
96 Capability::new("exec:cmd:wc"),
97 ],
98 gate_capabilities: vec![
100 Capability::new("fs:write:**"),
101 Capability::new("secrets:read:*"),
102 ],
103 max_tool_runtime_secs: 30,
104 max_events_per_turn: 15,
105 }
106 }
107
108 pub fn pro() -> Self {
111 Self {
112 allow_capabilities: vec![Capability::new("*")],
113 gate_capabilities: vec![],
114 max_tool_runtime_secs: 60,
115 max_events_per_turn: 50,
116 }
117 }
118
119 pub fn enterprise() -> Self {
122 Self {
123 allow_capabilities: vec![Capability::new("*")],
124 gate_capabilities: vec![],
125 max_tool_runtime_secs: 120,
126 max_events_per_turn: 200,
127 }
128 }
129}
130
131impl Default for PolicySet {
132 fn default() -> Self {
133 Self {
134 allow_capabilities: vec![
135 Capability::fs_read("/session/**"),
136 Capability::fs_write("/session/artifacts/**"),
137 Capability::exec("git"),
138 ],
139 gate_capabilities: vec![Capability::new("payments:initiate")],
140 max_tool_runtime_secs: 30,
141 max_events_per_turn: 256,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
151#[serde(rename_all = "snake_case")]
152pub enum SubscriptionTier {
153 #[default]
155 Anonymous,
156 Free,
158 Pro,
160 Enterprise,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct PolicyEvaluation {
167 pub allowed: Vec<Capability>,
168 pub requires_approval: Vec<Capability>,
169 pub denied: Vec<Capability>,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn capability_factory_methods() {
178 assert_eq!(Capability::fs_read("/tmp").as_str(), "fs:read:/tmp");
179 assert_eq!(Capability::fs_write("/out").as_str(), "fs:write:/out");
180 assert_eq!(
181 Capability::net_egress("api.com").as_str(),
182 "net:egress:api.com"
183 );
184 assert_eq!(Capability::exec("git").as_str(), "exec:cmd:git");
185 assert_eq!(Capability::secrets("prod").as_str(), "secrets:read:prod");
186 }
187
188 #[test]
189 fn policy_set_default() {
190 let ps = PolicySet::default();
191 assert_eq!(ps.allow_capabilities.len(), 3);
192 assert_eq!(ps.gate_capabilities.len(), 1);
193 assert_eq!(ps.max_tool_runtime_secs, 30);
194 }
195
196 #[test]
197 fn capability_serde_roundtrip() {
198 let cap = Capability::fs_read("/session/**");
199 let json = serde_json::to_string(&cap).unwrap();
200 let back: Capability = serde_json::from_str(&json).unwrap();
201 assert_eq!(cap, back);
202 }
203
204 #[test]
205 fn policy_set_anonymous() {
206 let ps = PolicySet::anonymous();
207 assert_eq!(ps.allow_capabilities.len(), 1);
208 assert_eq!(ps.allow_capabilities[0].as_str(), "fs:read:/session/**");
209 assert_eq!(ps.gate_capabilities.len(), 3);
211 assert_eq!(ps.max_tool_runtime_secs, 30);
212 assert_eq!(ps.max_events_per_turn, 5);
213 let exec_cap = Capability::new("exec:cmd:*");
215 assert!(!ps.allow_capabilities.contains(&exec_cap));
216 assert!(!ps.gate_capabilities.contains(&exec_cap));
217 }
218
219 #[test]
220 fn policy_set_free() {
221 let ps = PolicySet::free();
222 assert_eq!(ps.allow_capabilities.len(), 13);
224 assert_eq!(ps.gate_capabilities.len(), 2);
226 assert_eq!(ps.max_tool_runtime_secs, 30);
227 assert_eq!(ps.max_events_per_turn, 15);
228 assert!(
230 ps.allow_capabilities
231 .contains(&Capability::new("net:egress:*"))
232 );
233 assert!(
235 ps.allow_capabilities
236 .contains(&Capability::new("exec:cmd:cat"))
237 );
238 assert!(
239 ps.allow_capabilities
240 .contains(&Capability::new("exec:cmd:grep"))
241 );
242 assert!(
244 !ps.gate_capabilities
245 .contains(&Capability::new("exec:cmd:*"))
246 );
247 }
248
249 #[test]
250 fn policy_set_pro() {
251 let ps = PolicySet::pro();
252 assert_eq!(ps.allow_capabilities.len(), 1);
253 assert_eq!(ps.allow_capabilities[0].as_str(), "*");
254 assert_eq!(ps.gate_capabilities.len(), 0);
255 assert_eq!(ps.max_tool_runtime_secs, 60);
256 assert_eq!(ps.max_events_per_turn, 50);
257 assert!(ps.allow_capabilities.contains(&Capability::new("*")));
259 }
260
261 #[test]
262 fn policy_set_enterprise() {
263 let ps = PolicySet::enterprise();
264 assert_eq!(ps.allow_capabilities.len(), 1);
265 assert_eq!(ps.allow_capabilities[0].as_str(), "*");
266 assert_eq!(ps.gate_capabilities.len(), 0);
267 assert_eq!(ps.max_tool_runtime_secs, 120);
268 assert_eq!(ps.max_events_per_turn, 200);
269 assert!(ps.allow_capabilities.contains(&Capability::new("*")));
271 }
272}