use std::time::Duration;
use crate::policy::{EnvPolicy, HttpPolicy, LlmPolicy, PathPolicy, Unrestricted};
#[derive(Debug, Clone)]
pub struct ConfigError(String);
impl ConfigError {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
pub fn message(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ConfigError {}
pub struct Config {
pub(crate) path_policy: Box<dyn PathPolicy>,
pub(crate) http_policy: Box<dyn HttpPolicy>,
pub(crate) env_policy: Box<dyn EnvPolicy>,
pub(crate) llm_policy: Box<dyn LlmPolicy>,
pub(crate) max_read_bytes: Option<u64>,
pub(crate) max_walk_depth: usize,
pub(crate) max_walk_entries: usize,
pub(crate) max_json_depth: usize,
pub(crate) http_timeout: Duration,
pub(crate) max_response_bytes: u64,
pub(crate) max_sleep_secs: f64,
pub(crate) llm_default_timeout_secs: u64,
pub(crate) llm_max_response_bytes: u64,
pub(crate) llm_max_batch_concurrency: usize,
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("path_policy", &self.path_policy.policy_name())
.field("http_policy", &self.http_policy.policy_name())
.field("env_policy", &self.env_policy.policy_name())
.field("llm_policy", &self.llm_policy.policy_name())
.field("max_read_bytes", &self.max_read_bytes)
.field("max_walk_depth", &self.max_walk_depth)
.field("max_walk_entries", &self.max_walk_entries)
.field("max_json_depth", &self.max_json_depth)
.field("http_timeout", &self.http_timeout)
.field("max_response_bytes", &self.max_response_bytes)
.field("max_sleep_secs", &self.max_sleep_secs)
.field("llm_default_timeout_secs", &self.llm_default_timeout_secs)
.field("llm_max_response_bytes", &self.llm_max_response_bytes)
.field("llm_max_batch_concurrency", &self.llm_max_batch_concurrency)
.finish()
}
}
impl Default for Config {
fn default() -> Self {
Self {
path_policy: Box::new(Unrestricted),
http_policy: Box::new(Unrestricted),
env_policy: Box::new(Unrestricted),
llm_policy: Box::new(Unrestricted),
max_read_bytes: None,
max_walk_depth: 256,
max_walk_entries: 10_000,
max_json_depth: 128,
http_timeout: Duration::from_secs(30),
max_response_bytes: 10 * 1024 * 1024, max_sleep_secs: 86_400.0,
llm_default_timeout_secs: 120,
llm_max_response_bytes: 10 * 1024 * 1024, llm_max_batch_concurrency: 8,
}
}
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder {
inner: Config::default(),
}
}
}
pub struct ConfigBuilder {
inner: Config,
}
impl ConfigBuilder {
pub fn path_policy(mut self, policy: impl PathPolicy) -> Self {
self.inner.path_policy = Box::new(policy);
self
}
pub fn http_policy(mut self, policy: impl HttpPolicy) -> Self {
self.inner.http_policy = Box::new(policy);
self
}
pub fn env_policy(mut self, policy: impl EnvPolicy) -> Self {
self.inner.env_policy = Box::new(policy);
self
}
pub fn llm_policy(mut self, policy: impl LlmPolicy) -> Self {
self.inner.llm_policy = Box::new(policy);
self
}
pub fn llm_default_timeout_secs(mut self, secs: u64) -> Self {
self.inner.llm_default_timeout_secs = secs;
self
}
pub fn llm_max_response_bytes(mut self, bytes: u64) -> Self {
self.inner.llm_max_response_bytes = bytes;
self
}
pub fn llm_max_batch_concurrency(mut self, n: usize) -> Self {
self.inner.llm_max_batch_concurrency = n;
self
}
pub fn max_read_bytes(mut self, bytes: u64) -> Self {
self.inner.max_read_bytes = Some(bytes);
self
}
pub fn max_walk_depth(mut self, depth: usize) -> Self {
self.inner.max_walk_depth = depth;
self
}
pub fn max_walk_entries(mut self, entries: usize) -> Self {
self.inner.max_walk_entries = entries;
self
}
pub fn max_json_depth(mut self, depth: usize) -> Self {
self.inner.max_json_depth = depth;
self
}
pub fn http_timeout(mut self, timeout: Duration) -> Self {
self.inner.http_timeout = timeout;
self
}
pub fn max_response_bytes(mut self, bytes: u64) -> Self {
self.inner.max_response_bytes = bytes;
self
}
pub fn max_sleep_secs(mut self, secs: f64) -> Self {
self.inner.max_sleep_secs = secs;
self
}
pub fn build(self) -> Result<Config, ConfigError> {
let secs = self.inner.max_sleep_secs;
if !secs.is_finite() || secs < 0.0 {
return Err(ConfigError::new(format!(
"max_sleep_secs must be finite and non-negative, got {secs}"
)));
}
Ok(self.inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "sandbox")]
use crate::policy::Sandboxed;
#[test]
fn default_config_values() {
let config = Config::default();
assert_eq!(config.max_read_bytes, None);
assert_eq!(config.max_walk_depth, 256);
assert_eq!(config.max_walk_entries, 10_000);
assert_eq!(config.max_json_depth, 128);
assert_eq!(config.http_timeout, Duration::from_secs(30));
assert_eq!(config.max_response_bytes, 10 * 1024 * 1024);
assert!((config.max_sleep_secs - 86_400.0).abs() < f64::EPSILON);
assert_eq!(config.llm_default_timeout_secs, 120);
assert_eq!(config.llm_max_response_bytes, 10 * 1024 * 1024);
assert_eq!(config.llm_max_batch_concurrency, 8);
}
#[test]
fn builder_overrides() {
let config = Config::builder()
.max_read_bytes(4096)
.max_walk_depth(10)
.max_walk_entries(500)
.max_json_depth(32)
.http_timeout(Duration::from_secs(5))
.max_response_bytes(1024)
.max_sleep_secs(60.0)
.build()
.unwrap();
assert_eq!(config.max_read_bytes, Some(4096));
assert_eq!(config.max_walk_depth, 10);
assert_eq!(config.max_walk_entries, 500);
assert_eq!(config.max_json_depth, 32);
assert_eq!(config.http_timeout, Duration::from_secs(5));
assert_eq!(config.max_response_bytes, 1024);
assert!((config.max_sleep_secs - 60.0).abs() < f64::EPSILON);
}
#[cfg(feature = "sandbox")]
#[test]
fn builder_accepts_custom_policy() {
let config = Config::builder()
.path_policy(Sandboxed::new(["/tmp"]).unwrap())
.build()
.unwrap();
assert_eq!(config.max_walk_depth, 256); }
#[test]
fn builder_rejects_nan_sleep() {
let result = Config::builder().max_sleep_secs(f64::NAN).build();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("max_sleep_secs must be finite and non-negative"));
}
#[test]
fn builder_rejects_infinite_sleep() {
let result = Config::builder().max_sleep_secs(f64::INFINITY).build();
assert!(result.is_err());
}
#[test]
fn builder_rejects_negative_sleep() {
let result = Config::builder().max_sleep_secs(-1.0).build();
assert!(result.is_err());
}
#[test]
fn config_debug_does_not_panic() {
let config = Config::default();
let s = format!("{config:?}");
assert!(s.contains("max_walk_depth"));
assert!(
s.contains("Unrestricted"),
"Debug should show policy type names, got: {s}"
);
}
}