use serde::{Deserialize, Serialize};
use crate::permissions::{AutonomyLevel, PermissionPolicy, PermissionsConfig};
use crate::policy::{PolicyConfig, PolicyRuleConfig};
fn default_true() -> bool {
true
}
fn default_adversarial_timeout_ms() -> u64 {
3_000
}
fn default_timeout() -> u64 {
30
}
fn default_cache_ttl_secs() -> u64 {
300
}
fn default_confirm_patterns() -> Vec<String> {
vec![
"rm ".into(),
"git push -f".into(),
"git push --force".into(),
"drop table".into(),
"drop database".into(),
"truncate ".into(),
"$(".into(),
"`".into(),
"<(".into(),
">(".into(),
"<<<".into(),
"eval ".into(),
]
}
fn default_audit_destination() -> String {
"stdout".into()
}
fn default_overflow_threshold() -> usize {
50_000
}
fn default_retention_days() -> u64 {
7
}
fn default_max_overflow_bytes() -> usize {
10 * 1024 * 1024 }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OverflowConfig {
#[serde(default = "default_overflow_threshold")]
pub threshold: usize,
#[serde(default = "default_retention_days")]
pub retention_days: u64,
#[serde(default = "default_max_overflow_bytes")]
pub max_overflow_bytes: usize,
}
impl Default for OverflowConfig {
fn default() -> Self {
Self {
threshold: default_overflow_threshold(),
retention_days: default_retention_days(),
max_overflow_bytes: default_max_overflow_bytes(),
}
}
}
fn default_anomaly_window() -> usize {
10
}
fn default_anomaly_error_threshold() -> f64 {
0.5
}
fn default_anomaly_critical_threshold() -> f64 {
0.8
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AnomalyConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_anomaly_window")]
pub window_size: usize,
#[serde(default = "default_anomaly_error_threshold")]
pub error_threshold: f64,
#[serde(default = "default_anomaly_critical_threshold")]
pub critical_threshold: f64,
#[serde(default = "default_true")]
pub reasoning_model_warning: bool,
}
impl Default for AnomalyConfig {
fn default() -> Self {
Self {
enabled: true,
window_size: default_anomaly_window(),
error_threshold: default_anomaly_error_threshold(),
critical_threshold: default_anomaly_critical_threshold(),
reasoning_model_warning: true,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResultCacheConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_cache_ttl_secs")]
pub ttl_secs: u64,
}
impl Default for ResultCacheConfig {
fn default() -> Self {
Self {
enabled: true,
ttl_secs: default_cache_ttl_secs(),
}
}
}
fn default_tafc_complexity_threshold() -> f64 {
0.6
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TafcConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_tafc_complexity_threshold")]
pub complexity_threshold: f64,
}
impl Default for TafcConfig {
fn default() -> Self {
Self {
enabled: false,
complexity_threshold: default_tafc_complexity_threshold(),
}
}
}
impl TafcConfig {
#[must_use]
pub fn validated(mut self) -> Self {
if self.complexity_threshold.is_finite() {
self.complexity_threshold = self.complexity_threshold.clamp(0.0, 1.0);
} else {
self.complexity_threshold = 0.6;
}
self
}
}
fn default_utility_threshold() -> f32 {
0.1
}
fn default_utility_gain_weight() -> f32 {
1.0
}
fn default_utility_cost_weight() -> f32 {
0.5
}
fn default_utility_redundancy_weight() -> f32 {
0.3
}
fn default_utility_uncertainty_bonus() -> f32 {
0.2
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct UtilityScoringConfig {
pub enabled: bool,
#[serde(default = "default_utility_threshold")]
pub threshold: f32,
#[serde(default = "default_utility_gain_weight")]
pub gain_weight: f32,
#[serde(default = "default_utility_cost_weight")]
pub cost_weight: f32,
#[serde(default = "default_utility_redundancy_weight")]
pub redundancy_weight: f32,
#[serde(default = "default_utility_uncertainty_bonus")]
pub uncertainty_bonus: f32,
#[serde(default)]
pub exempt_tools: Vec<String>,
}
impl Default for UtilityScoringConfig {
fn default() -> Self {
Self {
enabled: false,
threshold: default_utility_threshold(),
gain_weight: default_utility_gain_weight(),
cost_weight: default_utility_cost_weight(),
redundancy_weight: default_utility_redundancy_weight(),
uncertainty_bonus: default_utility_uncertainty_bonus(),
exempt_tools: vec!["invoke_skill".to_string(), "load_skill".to_string()],
}
}
}
impl UtilityScoringConfig {
pub fn validate(&self) -> Result<(), String> {
let fields = [
("threshold", self.threshold),
("gain_weight", self.gain_weight),
("cost_weight", self.cost_weight),
("redundancy_weight", self.redundancy_weight),
("uncertainty_bonus", self.uncertainty_bonus),
];
for (name, val) in fields {
if !val.is_finite() {
return Err(format!("[tools.utility] {name} must be finite, got {val}"));
}
if val < 0.0 {
return Err(format!("[tools.utility] {name} must be >= 0, got {val}"));
}
}
Ok(())
}
}
fn default_boost_per_dep() -> f32 {
0.15
}
fn default_max_total_boost() -> f32 {
0.2
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ToolDependency {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub prefers: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DependencyConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_boost_per_dep")]
pub boost_per_dep: f32,
#[serde(default = "default_max_total_boost")]
pub max_total_boost: f32,
#[serde(default)]
pub rules: std::collections::HashMap<String, ToolDependency>,
}
impl Default for DependencyConfig {
fn default() -> Self {
Self {
enabled: false,
boost_per_dep: default_boost_per_dep(),
max_total_boost: default_max_total_boost(),
rules: std::collections::HashMap::new(),
}
}
}
fn default_retry_max_attempts() -> usize {
2
}
fn default_retry_base_ms() -> u64 {
500
}
fn default_retry_max_ms() -> u64 {
5_000
}
fn default_retry_budget_secs() -> u64 {
30
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RetryConfig {
#[serde(default = "default_retry_max_attempts")]
pub max_attempts: usize,
#[serde(default = "default_retry_base_ms")]
pub base_ms: u64,
#[serde(default = "default_retry_max_ms")]
pub max_ms: u64,
#[serde(default = "default_retry_budget_secs")]
pub budget_secs: u64,
#[serde(default)]
pub parameter_reformat_provider: String,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: default_retry_max_attempts(),
base_ms: default_retry_base_ms(),
max_ms: default_retry_max_ms(),
budget_secs: default_retry_budget_secs(),
parameter_reformat_provider: String::new(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AdversarialPolicyConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub policy_provider: String,
pub policy_file: Option<String>,
#[serde(default)]
pub fail_open: bool,
#[serde(default = "default_adversarial_timeout_ms")]
pub timeout_ms: u64,
#[serde(default = "AdversarialPolicyConfig::default_exempt_tools")]
pub exempt_tools: Vec<String>,
}
impl Default for AdversarialPolicyConfig {
fn default() -> Self {
Self {
enabled: false,
policy_provider: String::new(),
policy_file: None,
fail_open: false,
timeout_ms: default_adversarial_timeout_ms(),
exempt_tools: Self::default_exempt_tools(),
}
}
}
impl AdversarialPolicyConfig {
fn default_exempt_tools() -> Vec<String> {
vec![
"memory_save".into(),
"memory_search".into(),
"read_overflow".into(),
"load_skill".into(),
"invoke_skill".into(),
"schedule_deferred".into(),
]
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct FileConfig {
#[serde(default)]
pub deny_read: Vec<String>,
#[serde(default)]
pub allow_read: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolsConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_true")]
pub summarize_output: bool,
#[serde(default)]
pub shell: ShellConfig,
#[serde(default)]
pub scrape: ScrapeConfig,
#[serde(default)]
pub audit: AuditConfig,
#[serde(default)]
pub permissions: Option<PermissionsConfig>,
#[serde(default)]
pub filters: crate::filter::FilterConfig,
#[serde(default)]
pub overflow: OverflowConfig,
#[serde(default)]
pub anomaly: AnomalyConfig,
#[serde(default)]
pub result_cache: ResultCacheConfig,
#[serde(default)]
pub tafc: TafcConfig,
#[serde(default)]
pub dependencies: DependencyConfig,
#[serde(default)]
pub retry: RetryConfig,
#[serde(default)]
pub policy: PolicyConfig,
#[serde(default)]
pub adversarial_policy: AdversarialPolicyConfig,
#[serde(default)]
pub utility: UtilityScoringConfig,
#[serde(default)]
pub file: FileConfig,
#[serde(default)]
pub authorization: AuthorizationConfig,
#[serde(default)]
pub max_tool_calls_per_session: Option<u32>,
#[serde(default)]
pub speculative: SpeculativeConfig,
#[serde(default)]
pub sandbox: SandboxConfig,
#[serde(default)]
pub egress: EgressConfig,
}
impl ToolsConfig {
#[must_use]
pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
let policy = if let Some(ref perms) = self.permissions {
PermissionPolicy::from(perms.clone())
} else {
PermissionPolicy::from_legacy(
&self.shell.blocked_commands,
&self.shell.confirm_patterns,
)
};
policy.with_autonomy(autonomy_level)
}
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct ShellConfig {
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default)]
pub blocked_commands: Vec<String>,
#[serde(default)]
pub allowed_commands: Vec<String>,
#[serde(default)]
pub allowed_paths: Vec<String>,
#[serde(default = "default_true")]
pub allow_network: bool,
#[serde(default = "default_confirm_patterns")]
pub confirm_patterns: Vec<String>,
#[serde(default = "ShellConfig::default_env_blocklist")]
pub env_blocklist: Vec<String>,
#[serde(default)]
pub transactional: bool,
#[serde(default)]
pub transaction_scope: Vec<String>,
#[serde(default)]
pub auto_rollback: bool,
#[serde(default)]
pub auto_rollback_exit_codes: Vec<i32>,
#[serde(default)]
pub snapshot_required: bool,
#[serde(default)]
pub max_snapshot_bytes: u64,
}
impl ShellConfig {
#[must_use]
pub fn default_env_blocklist() -> Vec<String> {
vec![
"ZEPH_".into(),
"AWS_".into(),
"AZURE_".into(),
"GCP_".into(),
"GOOGLE_".into(),
"OPENAI_".into(),
"ANTHROPIC_".into(),
"HF_".into(),
"HUGGING".into(),
]
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AuditConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_audit_destination")]
pub destination: String,
#[serde(default)]
pub tool_risk_summary: bool,
}
impl Default for ToolsConfig {
fn default() -> Self {
Self {
enabled: true,
summarize_output: true,
shell: ShellConfig::default(),
scrape: ScrapeConfig::default(),
audit: AuditConfig::default(),
permissions: None,
filters: crate::filter::FilterConfig::default(),
overflow: OverflowConfig::default(),
anomaly: AnomalyConfig::default(),
result_cache: ResultCacheConfig::default(),
tafc: TafcConfig::default(),
dependencies: DependencyConfig::default(),
retry: RetryConfig::default(),
policy: PolicyConfig::default(),
adversarial_policy: AdversarialPolicyConfig::default(),
utility: UtilityScoringConfig::default(),
file: FileConfig::default(),
authorization: AuthorizationConfig::default(),
max_tool_calls_per_session: None,
speculative: SpeculativeConfig::default(),
sandbox: SandboxConfig::default(),
egress: EgressConfig::default(),
}
}
}
fn default_max_in_flight() -> usize {
4
}
fn default_confidence_threshold() -> f32 {
0.55
}
fn default_max_wasted_per_minute() -> u64 {
100
}
fn default_ttl_seconds() -> u64 {
30
}
fn default_min_observations() -> u32 {
5
}
fn default_half_life_days() -> f64 {
14.0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SpeculationMode {
#[default]
Off,
Decoding,
Pattern,
Both,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SpeculativePatternConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_min_observations")]
pub min_observations: u32,
#[serde(default = "default_half_life_days")]
pub half_life_days: f64,
#[serde(default)]
pub rerank_provider: String,
}
impl Default for SpeculativePatternConfig {
fn default() -> Self {
Self {
enabled: false,
min_observations: default_min_observations(),
half_life_days: default_half_life_days(),
rerank_provider: String::new(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct SpeculativeAllowlistConfig {
#[serde(default)]
pub shell: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SpeculativeConfig {
#[serde(default)]
pub mode: SpeculationMode,
#[serde(default = "default_max_in_flight")]
pub max_in_flight: usize,
#[serde(default = "default_confidence_threshold")]
pub confidence_threshold: f32,
#[serde(default = "default_max_wasted_per_minute")]
pub max_wasted_per_minute: u64,
#[serde(default = "default_ttl_seconds")]
pub ttl_seconds: u64,
#[serde(default = "default_true")]
pub audit: bool,
#[serde(default)]
pub pattern: SpeculativePatternConfig,
#[serde(default)]
pub allowlist: SpeculativeAllowlistConfig,
}
impl Default for SpeculativeConfig {
fn default() -> Self {
Self {
mode: SpeculationMode::Off,
max_in_flight: default_max_in_flight(),
confidence_threshold: default_confidence_threshold(),
max_wasted_per_minute: default_max_wasted_per_minute(),
ttl_seconds: default_ttl_seconds(),
audit: true,
pattern: SpeculativePatternConfig::default(),
allowlist: SpeculativeAllowlistConfig::default(),
}
}
}
impl Default for ShellConfig {
fn default() -> Self {
Self {
timeout: default_timeout(),
blocked_commands: Vec::new(),
allowed_commands: Vec::new(),
allowed_paths: Vec::new(),
allow_network: true,
confirm_patterns: default_confirm_patterns(),
env_blocklist: Self::default_env_blocklist(),
transactional: false,
transaction_scope: Vec::new(),
auto_rollback: false,
auto_rollback_exit_codes: Vec::new(),
snapshot_required: false,
max_snapshot_bytes: 0,
}
}
}
impl Default for AuditConfig {
fn default() -> Self {
Self {
enabled: true,
destination: default_audit_destination(),
tool_risk_summary: false,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct AuthorizationConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub rules: Vec<PolicyRuleConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
#[allow(clippy::struct_excessive_bools)]
pub struct EgressConfig {
pub enabled: bool,
pub log_blocked: bool,
pub log_response_bytes: bool,
pub log_hosts_to_tui: bool,
}
impl Default for EgressConfig {
fn default() -> Self {
Self {
enabled: true,
log_blocked: true,
log_response_bytes: true,
log_hosts_to_tui: true,
}
}
}
fn default_scrape_timeout() -> u64 {
15
}
fn default_max_body_bytes() -> usize {
4_194_304
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ScrapeConfig {
#[serde(default = "default_scrape_timeout")]
pub timeout: u64,
#[serde(default = "default_max_body_bytes")]
pub max_body_bytes: usize,
#[serde(default)]
pub allowed_domains: Vec<String>,
#[serde(default)]
pub denied_domains: Vec<String>,
}
impl Default for ScrapeConfig {
fn default() -> Self {
Self {
timeout: default_scrape_timeout(),
max_body_bytes: default_max_body_bytes(),
allowed_domains: Vec::new(),
denied_domains: Vec::new(),
}
}
}
fn default_sandbox_profile() -> crate::sandbox::SandboxProfile {
crate::sandbox::SandboxProfile::Workspace
}
fn default_sandbox_backend() -> String {
"auto".into()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SandboxConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_sandbox_profile")]
pub profile: crate::sandbox::SandboxProfile,
#[serde(default)]
pub allow_read: Vec<std::path::PathBuf>,
#[serde(default)]
pub allow_write: Vec<std::path::PathBuf>,
#[serde(default = "default_true")]
pub strict: bool,
#[serde(default = "default_sandbox_backend")]
pub backend: String,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
enabled: false,
profile: default_sandbox_profile(),
allow_read: Vec::new(),
allow_write: Vec::new(),
strict: true,
backend: default_sandbox_backend(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_default_config() {
let toml_str = r#"
enabled = true
[shell]
timeout = 60
blocked_commands = ["rm -rf /", "sudo"]
"#;
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert!(config.enabled);
assert_eq!(config.shell.timeout, 60);
assert_eq!(config.shell.blocked_commands.len(), 2);
assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
assert_eq!(config.shell.blocked_commands[1], "sudo");
}
#[test]
fn empty_blocked_commands() {
let toml_str = r"
[shell]
timeout = 30
";
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert!(config.enabled);
assert_eq!(config.shell.timeout, 30);
assert!(config.shell.blocked_commands.is_empty());
}
#[test]
fn default_tools_config() {
let config = ToolsConfig::default();
assert!(config.enabled);
assert!(config.summarize_output);
assert_eq!(config.shell.timeout, 30);
assert!(config.shell.blocked_commands.is_empty());
assert!(config.audit.enabled);
}
#[test]
fn tools_summarize_output_default_true() {
let config = ToolsConfig::default();
assert!(config.summarize_output);
}
#[test]
fn tools_summarize_output_parsing() {
let toml_str = r"
summarize_output = true
";
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert!(config.summarize_output);
}
#[test]
fn default_shell_config() {
let config = ShellConfig::default();
assert_eq!(config.timeout, 30);
assert!(config.blocked_commands.is_empty());
assert!(config.allowed_paths.is_empty());
assert!(config.allow_network);
assert!(!config.confirm_patterns.is_empty());
}
#[test]
fn deserialize_omitted_fields_use_defaults() {
let toml_str = "";
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert!(config.enabled);
assert_eq!(config.shell.timeout, 30);
assert!(config.shell.blocked_commands.is_empty());
assert!(config.shell.allow_network);
assert!(!config.shell.confirm_patterns.is_empty());
assert_eq!(config.scrape.timeout, 15);
assert_eq!(config.scrape.max_body_bytes, 4_194_304);
assert!(config.audit.enabled);
assert_eq!(config.audit.destination, "stdout");
assert!(config.summarize_output);
}
#[test]
fn default_scrape_config() {
let config = ScrapeConfig::default();
assert_eq!(config.timeout, 15);
assert_eq!(config.max_body_bytes, 4_194_304);
}
#[test]
fn deserialize_scrape_config() {
let toml_str = r"
[scrape]
timeout = 30
max_body_bytes = 2097152
";
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.scrape.timeout, 30);
assert_eq!(config.scrape.max_body_bytes, 2_097_152);
}
#[test]
fn tools_config_default_includes_scrape() {
let config = ToolsConfig::default();
assert_eq!(config.scrape.timeout, 15);
assert_eq!(config.scrape.max_body_bytes, 4_194_304);
}
#[test]
fn deserialize_allowed_commands() {
let toml_str = r#"
[shell]
timeout = 30
allowed_commands = ["curl", "wget"]
"#;
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
}
#[test]
fn default_allowed_commands_empty() {
let config = ShellConfig::default();
assert!(config.allowed_commands.is_empty());
}
#[test]
fn deserialize_shell_security_fields() {
let toml_str = r#"
[shell]
allowed_paths = ["/tmp", "/home/user"]
allow_network = false
confirm_patterns = ["rm ", "drop table"]
"#;
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
assert!(!config.shell.allow_network);
assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
}
#[test]
fn deserialize_audit_config() {
let toml_str = r#"
[audit]
enabled = true
destination = "/var/log/zeph-audit.log"
"#;
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert!(config.audit.enabled);
assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
}
#[test]
fn default_audit_config() {
let config = AuditConfig::default();
assert!(config.enabled);
assert_eq!(config.destination, "stdout");
}
#[test]
fn permission_policy_from_legacy_fields() {
let config = ToolsConfig {
shell: ShellConfig {
blocked_commands: vec!["sudo".to_owned()],
confirm_patterns: vec!["rm ".to_owned()],
..ShellConfig::default()
},
..ToolsConfig::default()
};
let policy = config.permission_policy(AutonomyLevel::Supervised);
assert_eq!(
policy.check("bash", "sudo apt"),
crate::permissions::PermissionAction::Deny
);
assert_eq!(
policy.check("bash", "rm file"),
crate::permissions::PermissionAction::Ask
);
}
#[test]
fn permission_policy_from_explicit_config() {
let toml_str = r#"
[permissions]
[[permissions.bash]]
pattern = "*sudo*"
action = "deny"
"#;
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
let policy = config.permission_policy(AutonomyLevel::Supervised);
assert_eq!(
policy.check("bash", "sudo rm"),
crate::permissions::PermissionAction::Deny
);
}
#[test]
fn permission_policy_default_uses_legacy() {
let config = ToolsConfig::default();
assert!(config.permissions.is_none());
let policy = config.permission_policy(AutonomyLevel::Supervised);
assert!(!config.shell.confirm_patterns.is_empty());
assert!(policy.rules().contains_key("bash"));
}
#[test]
fn deserialize_overflow_config_full() {
let toml_str = r"
[overflow]
threshold = 100000
retention_days = 14
max_overflow_bytes = 5242880
";
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.overflow.threshold, 100_000);
assert_eq!(config.overflow.retention_days, 14);
assert_eq!(config.overflow.max_overflow_bytes, 5_242_880);
}
#[test]
fn deserialize_overflow_config_unknown_dir_field_is_ignored() {
let toml_str = r#"
[overflow]
threshold = 75000
dir = "/tmp/overflow"
"#;
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.overflow.threshold, 75_000);
}
#[test]
fn deserialize_overflow_config_partial_uses_defaults() {
let toml_str = r"
[overflow]
threshold = 75000
";
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.overflow.threshold, 75_000);
assert_eq!(config.overflow.retention_days, 7);
}
#[test]
fn deserialize_overflow_config_omitted_uses_defaults() {
let config: ToolsConfig = toml::from_str("").unwrap();
assert_eq!(config.overflow.threshold, 50_000);
assert_eq!(config.overflow.retention_days, 7);
assert_eq!(config.overflow.max_overflow_bytes, 10 * 1024 * 1024);
}
#[test]
fn result_cache_config_defaults() {
let config = ResultCacheConfig::default();
assert!(config.enabled);
assert_eq!(config.ttl_secs, 300);
}
#[test]
fn deserialize_result_cache_config() {
let toml_str = r"
[result_cache]
enabled = false
ttl_secs = 60
";
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert!(!config.result_cache.enabled);
assert_eq!(config.result_cache.ttl_secs, 60);
}
#[test]
fn result_cache_omitted_uses_defaults() {
let config: ToolsConfig = toml::from_str("").unwrap();
assert!(config.result_cache.enabled);
assert_eq!(config.result_cache.ttl_secs, 300);
}
#[test]
fn result_cache_ttl_zero_is_valid() {
let toml_str = r"
[result_cache]
ttl_secs = 0
";
let config: ToolsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.result_cache.ttl_secs, 0);
}
#[test]
fn adversarial_policy_default_exempt_tools_contains_skill_ops() {
let exempt = AdversarialPolicyConfig::default_exempt_tools();
assert!(
exempt.contains(&"load_skill".to_string()),
"default exempt_tools must contain load_skill"
);
assert!(
exempt.contains(&"invoke_skill".to_string()),
"default exempt_tools must contain invoke_skill"
);
}
#[test]
fn utility_scoring_default_exempt_tools_contains_skill_ops() {
let cfg = UtilityScoringConfig::default();
assert!(
cfg.exempt_tools.contains(&"invoke_skill".to_string()),
"UtilityScoringConfig default exempt_tools must contain invoke_skill"
);
assert!(
cfg.exempt_tools.contains(&"load_skill".to_string()),
"UtilityScoringConfig default exempt_tools must contain load_skill"
);
}
}