1use std::time::Duration;
21
22use crate::policy::{EnvPolicy, HttpPolicy, LlmPolicy, PathPolicy, Unrestricted};
23
24#[derive(Debug, Clone)]
26pub struct ConfigError(String);
27
28impl ConfigError {
29 pub fn new(message: impl Into<String>) -> Self {
31 Self(message.into())
32 }
33
34 pub fn message(&self) -> &str {
36 &self.0
37 }
38}
39
40impl std::fmt::Display for ConfigError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.write_str(&self.0)
43 }
44}
45
46impl std::error::Error for ConfigError {}
47
48pub struct Config {
53 pub(crate) path_policy: Box<dyn PathPolicy>,
54 pub(crate) http_policy: Box<dyn HttpPolicy>,
55 pub(crate) env_policy: Box<dyn EnvPolicy>,
56 pub(crate) llm_policy: Box<dyn LlmPolicy>,
57 pub(crate) max_read_bytes: Option<u64>,
58 pub(crate) max_walk_depth: usize,
59 pub(crate) max_walk_entries: usize,
60 pub(crate) max_json_depth: usize,
61 pub(crate) http_timeout: Duration,
62 pub(crate) max_response_bytes: u64,
63 pub(crate) max_sleep_secs: f64,
64 pub(crate) llm_default_timeout_secs: u64,
65 pub(crate) llm_max_response_bytes: u64,
66 pub(crate) llm_max_batch_concurrency: usize,
67}
68
69impl std::fmt::Debug for Config {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("Config")
72 .field("path_policy", &self.path_policy.policy_name())
73 .field("http_policy", &self.http_policy.policy_name())
74 .field("env_policy", &self.env_policy.policy_name())
75 .field("llm_policy", &self.llm_policy.policy_name())
76 .field("max_read_bytes", &self.max_read_bytes)
77 .field("max_walk_depth", &self.max_walk_depth)
78 .field("max_walk_entries", &self.max_walk_entries)
79 .field("max_json_depth", &self.max_json_depth)
80 .field("http_timeout", &self.http_timeout)
81 .field("max_response_bytes", &self.max_response_bytes)
82 .field("max_sleep_secs", &self.max_sleep_secs)
83 .field("llm_default_timeout_secs", &self.llm_default_timeout_secs)
84 .field("llm_max_response_bytes", &self.llm_max_response_bytes)
85 .field("llm_max_batch_concurrency", &self.llm_max_batch_concurrency)
86 .finish()
87 }
88}
89
90impl Default for Config {
91 fn default() -> Self {
92 Self {
93 path_policy: Box::new(Unrestricted),
94 http_policy: Box::new(Unrestricted),
95 env_policy: Box::new(Unrestricted),
96 llm_policy: Box::new(Unrestricted),
97 max_read_bytes: None,
98 max_walk_depth: 256,
99 max_walk_entries: 10_000,
100 max_json_depth: 128,
101 http_timeout: Duration::from_secs(30),
102 max_response_bytes: 10 * 1024 * 1024, max_sleep_secs: 86_400.0,
104 llm_default_timeout_secs: 120,
105 llm_max_response_bytes: 10 * 1024 * 1024, llm_max_batch_concurrency: 8,
107 }
108 }
109}
110
111impl Config {
112 pub fn builder() -> ConfigBuilder {
114 ConfigBuilder {
115 inner: Config::default(),
116 }
117 }
118}
119
120pub struct ConfigBuilder {
122 inner: Config,
123}
124
125impl ConfigBuilder {
126 pub fn path_policy(mut self, policy: impl PathPolicy) -> Self {
130 self.inner.path_policy = Box::new(policy);
131 self
132 }
133
134 pub fn http_policy(mut self, policy: impl HttpPolicy) -> Self {
138 self.inner.http_policy = Box::new(policy);
139 self
140 }
141
142 pub fn env_policy(mut self, policy: impl EnvPolicy) -> Self {
146 self.inner.env_policy = Box::new(policy);
147 self
148 }
149
150 pub fn llm_policy(mut self, policy: impl LlmPolicy) -> Self {
154 self.inner.llm_policy = Box::new(policy);
155 self
156 }
157
158 pub fn llm_default_timeout_secs(mut self, secs: u64) -> Self {
162 self.inner.llm_default_timeout_secs = secs;
163 self
164 }
165
166 pub fn llm_max_response_bytes(mut self, bytes: u64) -> Self {
170 self.inner.llm_max_response_bytes = bytes;
171 self
172 }
173
174 pub fn llm_max_batch_concurrency(mut self, n: usize) -> Self {
178 self.inner.llm_max_batch_concurrency = n;
179 self
180 }
181
182 pub fn max_read_bytes(mut self, bytes: u64) -> Self {
187 self.inner.max_read_bytes = Some(bytes);
188 self
189 }
190
191 pub fn max_walk_depth(mut self, depth: usize) -> Self {
195 self.inner.max_walk_depth = depth;
196 self
197 }
198
199 pub fn max_walk_entries(mut self, entries: usize) -> Self {
203 self.inner.max_walk_entries = entries;
204 self
205 }
206
207 pub fn max_json_depth(mut self, depth: usize) -> Self {
211 self.inner.max_json_depth = depth;
212 self
213 }
214
215 pub fn http_timeout(mut self, timeout: Duration) -> Self {
219 self.inner.http_timeout = timeout;
220 self
221 }
222
223 pub fn max_response_bytes(mut self, bytes: u64) -> Self {
227 self.inner.max_response_bytes = bytes;
228 self
229 }
230
231 pub fn max_sleep_secs(mut self, secs: f64) -> Self {
238 self.inner.max_sleep_secs = secs;
239 self
240 }
241
242 pub fn build(self) -> Result<Config, ConfigError> {
246 let secs = self.inner.max_sleep_secs;
247 if !secs.is_finite() || secs < 0.0 {
248 return Err(ConfigError::new(format!(
249 "max_sleep_secs must be finite and non-negative, got {secs}"
250 )));
251 }
252 Ok(self.inner)
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 #[cfg(feature = "sandbox")]
260 use crate::policy::Sandboxed;
261
262 #[test]
263 fn default_config_values() {
264 let config = Config::default();
265 assert_eq!(config.max_read_bytes, None);
266 assert_eq!(config.max_walk_depth, 256);
267 assert_eq!(config.max_walk_entries, 10_000);
268 assert_eq!(config.max_json_depth, 128);
269 assert_eq!(config.http_timeout, Duration::from_secs(30));
270 assert_eq!(config.max_response_bytes, 10 * 1024 * 1024);
271 assert!((config.max_sleep_secs - 86_400.0).abs() < f64::EPSILON);
272 assert_eq!(config.llm_default_timeout_secs, 120);
273 assert_eq!(config.llm_max_response_bytes, 10 * 1024 * 1024);
274 assert_eq!(config.llm_max_batch_concurrency, 8);
275 }
276
277 #[test]
278 fn builder_overrides() {
279 let config = Config::builder()
280 .max_read_bytes(4096)
281 .max_walk_depth(10)
282 .max_walk_entries(500)
283 .max_json_depth(32)
284 .http_timeout(Duration::from_secs(5))
285 .max_response_bytes(1024)
286 .max_sleep_secs(60.0)
287 .build()
288 .unwrap();
289
290 assert_eq!(config.max_read_bytes, Some(4096));
291 assert_eq!(config.max_walk_depth, 10);
292 assert_eq!(config.max_walk_entries, 500);
293 assert_eq!(config.max_json_depth, 32);
294 assert_eq!(config.http_timeout, Duration::from_secs(5));
295 assert_eq!(config.max_response_bytes, 1024);
296 assert!((config.max_sleep_secs - 60.0).abs() < f64::EPSILON);
297 }
298
299 #[cfg(feature = "sandbox")]
300 #[test]
301 fn builder_accepts_custom_policy() {
302 let config = Config::builder()
303 .path_policy(Sandboxed::new(["/tmp"]).unwrap())
304 .build()
305 .unwrap();
306
307 assert_eq!(config.max_walk_depth, 256); }
310
311 #[test]
312 fn builder_rejects_nan_sleep() {
313 let result = Config::builder().max_sleep_secs(f64::NAN).build();
314 assert!(result.is_err());
315 assert!(result
316 .unwrap_err()
317 .to_string()
318 .contains("max_sleep_secs must be finite and non-negative"));
319 }
320
321 #[test]
322 fn builder_rejects_infinite_sleep() {
323 let result = Config::builder().max_sleep_secs(f64::INFINITY).build();
324 assert!(result.is_err());
325 }
326
327 #[test]
328 fn builder_rejects_negative_sleep() {
329 let result = Config::builder().max_sleep_secs(-1.0).build();
330 assert!(result.is_err());
331 }
332
333 #[test]
334 fn config_debug_does_not_panic() {
335 let config = Config::default();
336 let s = format!("{config:?}");
337 assert!(s.contains("max_walk_depth"));
338 assert!(
339 s.contains("Unrestricted"),
340 "Debug should show policy type names, got: {s}"
341 );
342 }
343}