1use serde::{Deserialize, Serialize};
18use std::path::PathBuf;
19
20#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
24pub enum FsAccess {
25 #[default]
28 None,
29 ReadOnly(Vec<PathBuf>),
32 ReadWrite(Vec<PathBuf>),
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
39pub enum NetAccess {
40 #[default]
42 None,
43 OutboundHttp(Option<Vec<String>>),
47 OutboundFull(Option<Vec<String>>),
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
57pub enum ListenAccess {
58 #[default]
61 None,
62 Ports(Vec<u16>),
64 PortRange(u16, u16),
66 Any,
68}
69
70impl ListenAccess {
71 #[must_use]
73 pub fn allows(&self, port: u16) -> bool {
74 match self {
75 Self::None => false,
76 Self::Ports(ports) => ports.contains(&port),
77 Self::PortRange(lo, hi) => (*lo..=*hi).contains(&port),
78 Self::Any => true,
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
85pub enum EnvAccess {
86 #[default]
88 None,
89 AllowList(Vec<String>),
91 Full,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
101pub struct Manifold {
102 pub fs: FsAccess,
103 pub net: NetAccess,
104 pub crypto: bool,
105 pub child_process: bool,
106 pub env: EnvAccess,
107 pub allow_exit: bool,
111 pub http_timeout_ms: Option<u64>,
116 #[serde(default)]
121 pub listen: ListenAccess,
122}
123
124impl Manifold {
125 pub const fn sealed() -> Self {
129 Self {
130 fs: FsAccess::None,
131 net: NetAccess::None,
132 crypto: false,
133 child_process: false,
134 env: EnvAccess::None,
135 allow_exit: false,
136 http_timeout_ms: None,
137 listen: ListenAccess::None,
138 }
139 }
140
141 pub fn open() -> Self {
144 Self {
145 fs: FsAccess::ReadWrite(Vec::new()),
146 net: NetAccess::OutboundFull(None),
147 crypto: true,
148 child_process: true,
149 env: EnvAccess::Full,
150 allow_exit: true,
151 http_timeout_ms: None,
152 listen: ListenAccess::Any,
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn sealed_is_the_default() {
163 assert_eq!(Manifold::default(), Manifold::sealed());
164 }
165
166 #[test]
167 fn sealed_has_no_capabilities() {
168 let m = Manifold::sealed();
169 assert!(matches!(m.fs, FsAccess::None));
170 assert!(matches!(m.net, NetAccess::None));
171 assert!(!m.crypto);
172 assert!(!m.child_process);
173 assert!(matches!(m.env, EnvAccess::None));
174 assert!(!m.allow_exit);
175 assert!(matches!(m.listen, ListenAccess::None));
176 }
177
178 #[test]
179 fn open_grants_everything() {
180 let m = Manifold::open();
181 assert!(matches!(m.fs, FsAccess::ReadWrite(_)));
182 assert!(matches!(m.net, NetAccess::OutboundFull(_)));
183 assert!(m.crypto);
184 assert!(m.child_process);
185 assert!(matches!(m.env, EnvAccess::Full));
186 assert!(m.allow_exit);
187 assert!(matches!(m.listen, ListenAccess::Any));
188 }
189
190 #[test]
191 fn listen_allows_matches_grant_shapes() {
192 assert!(!ListenAccess::None.allows(80));
193 let ports = ListenAccess::Ports(vec![8080, 9090]);
194 assert!(ports.allows(8080));
195 assert!(ports.allows(9090));
196 assert!(!ports.allows(8081));
197 let range = ListenAccess::PortRange(9000, 9100);
198 assert!(range.allows(9000));
199 assert!(range.allows(9100));
200 assert!(!range.allows(8999));
201 assert!(!range.allows(9101));
202 assert!(ListenAccess::Any.allows(1));
203 assert!(ListenAccess::Any.allows(65535));
204 }
205
206 #[test]
207 fn listen_serde_roundtrips_every_variant() {
208 for v in [
209 ListenAccess::None,
210 ListenAccess::Ports(vec![9090]),
211 ListenAccess::Ports(vec![80, 443, 8080]),
212 ListenAccess::PortRange(9000, 9100),
213 ListenAccess::Any,
214 ] {
215 let json = serde_json::to_string(&v).expect("serialize");
216 let back: ListenAccess = serde_json::from_str(&json).expect("deserialize");
217 assert_eq!(v, back, "roundtrip drift for {json}");
218 }
219 }
220
221 #[test]
222 fn listen_serde_is_externally_tagged_like_the_other_axes() {
223 assert_eq!(
225 serde_json::to_string(&ListenAccess::None).expect("ser"),
226 r#""None""#
227 );
228 assert_eq!(
229 serde_json::to_string(&ListenAccess::Ports(vec![9090])).expect("ser"),
230 r#"{"Ports":[9090]}"#
231 );
232 assert_eq!(
233 serde_json::to_string(&ListenAccess::PortRange(9000, 9100)).expect("ser"),
234 r#"{"PortRange":[9000,9100]}"#
235 );
236 assert_eq!(
237 serde_json::to_string(&ListenAccess::Any).expect("ser"),
238 r#""Any""#
239 );
240 }
241
242 #[test]
243 fn manifold_without_listen_field_parses_as_none() {
244 let legacy = r#"{
247 "fs": "None",
248 "net": "None",
249 "crypto": false,
250 "child_process": false,
251 "env": "None",
252 "allow_exit": false,
253 "http_timeout_ms": null
254 }"#;
255 let m: Manifold = serde_json::from_str(legacy).expect("legacy manifold parses");
256 assert_eq!(m, Manifold::sealed());
257 assert!(matches!(m.listen, ListenAccess::None));
258 }
259
260 #[test]
261 fn manifold_listen_roundtrips_through_json() {
262 let mut m = Manifold::sealed();
263 m.listen = ListenAccess::PortRange(18200, 18210);
264 let json = serde_json::to_string(&m).expect("serialize");
265 let back: Manifold = serde_json::from_str(&json).expect("deserialize");
266 assert_eq!(m, back);
267 }
268}