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_walk_depth: usize,
58 pub(crate) max_walk_entries: usize,
59 pub(crate) max_json_depth: usize,
60 pub(crate) http_timeout: Duration,
61 pub(crate) max_response_bytes: u64,
62 pub(crate) max_sleep_secs: f64,
63 pub(crate) llm_default_timeout_secs: u64,
64 pub(crate) llm_max_response_bytes: u64,
65 pub(crate) llm_max_batch_concurrency: usize,
66}
67
68impl std::fmt::Debug for Config {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("Config")
71 .field("path_policy", &self.path_policy.policy_name())
72 .field("http_policy", &self.http_policy.policy_name())
73 .field("env_policy", &self.env_policy.policy_name())
74 .field("llm_policy", &self.llm_policy.policy_name())
75 .field("max_walk_depth", &self.max_walk_depth)
76 .field("max_walk_entries", &self.max_walk_entries)
77 .field("max_json_depth", &self.max_json_depth)
78 .field("http_timeout", &self.http_timeout)
79 .field("max_response_bytes", &self.max_response_bytes)
80 .field("max_sleep_secs", &self.max_sleep_secs)
81 .field("llm_default_timeout_secs", &self.llm_default_timeout_secs)
82 .field("llm_max_response_bytes", &self.llm_max_response_bytes)
83 .field("llm_max_batch_concurrency", &self.llm_max_batch_concurrency)
84 .finish()
85 }
86}
87
88impl Default for Config {
89 fn default() -> Self {
90 Self {
91 path_policy: Box::new(Unrestricted),
92 http_policy: Box::new(Unrestricted),
93 env_policy: Box::new(Unrestricted),
94 llm_policy: Box::new(Unrestricted),
95 max_walk_depth: 256,
96 max_walk_entries: 10_000,
97 max_json_depth: 128,
98 http_timeout: Duration::from_secs(30),
99 max_response_bytes: 10 * 1024 * 1024, max_sleep_secs: 86_400.0,
101 llm_default_timeout_secs: 120,
102 llm_max_response_bytes: 10 * 1024 * 1024, llm_max_batch_concurrency: 8,
104 }
105 }
106}
107
108impl Config {
109 pub fn builder() -> ConfigBuilder {
111 ConfigBuilder {
112 inner: Config::default(),
113 }
114 }
115}
116
117pub struct ConfigBuilder {
119 inner: Config,
120}
121
122impl ConfigBuilder {
123 pub fn path_policy(mut self, policy: impl PathPolicy) -> Self {
127 self.inner.path_policy = Box::new(policy);
128 self
129 }
130
131 pub fn http_policy(mut self, policy: impl HttpPolicy) -> Self {
135 self.inner.http_policy = Box::new(policy);
136 self
137 }
138
139 pub fn env_policy(mut self, policy: impl EnvPolicy) -> Self {
143 self.inner.env_policy = Box::new(policy);
144 self
145 }
146
147 pub fn llm_policy(mut self, policy: impl LlmPolicy) -> Self {
151 self.inner.llm_policy = Box::new(policy);
152 self
153 }
154
155 pub fn llm_default_timeout_secs(mut self, secs: u64) -> Self {
159 self.inner.llm_default_timeout_secs = secs;
160 self
161 }
162
163 pub fn llm_max_response_bytes(mut self, bytes: u64) -> Self {
167 self.inner.llm_max_response_bytes = bytes;
168 self
169 }
170
171 pub fn llm_max_batch_concurrency(mut self, n: usize) -> Self {
175 self.inner.llm_max_batch_concurrency = n;
176 self
177 }
178
179 pub fn max_walk_depth(mut self, depth: usize) -> Self {
183 self.inner.max_walk_depth = depth;
184 self
185 }
186
187 pub fn max_walk_entries(mut self, entries: usize) -> Self {
191 self.inner.max_walk_entries = entries;
192 self
193 }
194
195 pub fn max_json_depth(mut self, depth: usize) -> Self {
199 self.inner.max_json_depth = depth;
200 self
201 }
202
203 pub fn http_timeout(mut self, timeout: Duration) -> Self {
207 self.inner.http_timeout = timeout;
208 self
209 }
210
211 pub fn max_response_bytes(mut self, bytes: u64) -> Self {
215 self.inner.max_response_bytes = bytes;
216 self
217 }
218
219 pub fn max_sleep_secs(mut self, secs: f64) -> Self {
226 self.inner.max_sleep_secs = secs;
227 self
228 }
229
230 pub fn build(self) -> Result<Config, ConfigError> {
234 let secs = self.inner.max_sleep_secs;
235 if !secs.is_finite() || secs < 0.0 {
236 return Err(ConfigError::new(format!(
237 "max_sleep_secs must be finite and non-negative, got {secs}"
238 )));
239 }
240 Ok(self.inner)
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 #[cfg(feature = "sandbox")]
248 use crate::policy::Sandboxed;
249
250 #[test]
251 fn default_config_values() {
252 let config = Config::default();
253 assert_eq!(config.max_walk_depth, 256);
254 assert_eq!(config.max_walk_entries, 10_000);
255 assert_eq!(config.max_json_depth, 128);
256 assert_eq!(config.http_timeout, Duration::from_secs(30));
257 assert_eq!(config.max_response_bytes, 10 * 1024 * 1024);
258 assert!((config.max_sleep_secs - 86_400.0).abs() < f64::EPSILON);
259 assert_eq!(config.llm_default_timeout_secs, 120);
260 assert_eq!(config.llm_max_response_bytes, 10 * 1024 * 1024);
261 assert_eq!(config.llm_max_batch_concurrency, 8);
262 }
263
264 #[test]
265 fn builder_overrides() {
266 let config = Config::builder()
267 .max_walk_depth(10)
268 .max_walk_entries(500)
269 .max_json_depth(32)
270 .http_timeout(Duration::from_secs(5))
271 .max_response_bytes(1024)
272 .max_sleep_secs(60.0)
273 .build()
274 .unwrap();
275
276 assert_eq!(config.max_walk_depth, 10);
277 assert_eq!(config.max_walk_entries, 500);
278 assert_eq!(config.max_json_depth, 32);
279 assert_eq!(config.http_timeout, Duration::from_secs(5));
280 assert_eq!(config.max_response_bytes, 1024);
281 assert!((config.max_sleep_secs - 60.0).abs() < f64::EPSILON);
282 }
283
284 #[cfg(feature = "sandbox")]
285 #[test]
286 fn builder_accepts_custom_policy() {
287 let config = Config::builder()
288 .path_policy(Sandboxed::new(["/tmp"]).unwrap())
289 .build()
290 .unwrap();
291
292 assert_eq!(config.max_walk_depth, 256); }
295
296 #[test]
297 fn builder_rejects_nan_sleep() {
298 let result = Config::builder().max_sleep_secs(f64::NAN).build();
299 assert!(result.is_err());
300 assert!(result
301 .unwrap_err()
302 .to_string()
303 .contains("max_sleep_secs must be finite and non-negative"));
304 }
305
306 #[test]
307 fn builder_rejects_infinite_sleep() {
308 let result = Config::builder().max_sleep_secs(f64::INFINITY).build();
309 assert!(result.is_err());
310 }
311
312 #[test]
313 fn builder_rejects_negative_sleep() {
314 let result = Config::builder().max_sleep_secs(-1.0).build();
315 assert!(result.is_err());
316 }
317
318 #[test]
319 fn config_debug_does_not_panic() {
320 let config = Config::default();
321 let s = format!("{config:?}");
322 assert!(s.contains("max_walk_depth"));
323 assert!(
324 s.contains("Unrestricted"),
325 "Debug should show policy type names, got: {s}"
326 );
327 }
328}