Skip to main content

zeph_config/
tools.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Pure-data tool configuration types.
5//!
6//! Contains all TOML-deserializable configuration structs for tool execution. Runtime
7//! types (executors, permission policy enforcement) remain in `zeph-tools`. That crate
8//! re-exports the types here so existing import paths continue to resolve.
9
10use std::collections::HashMap;
11use std::path::PathBuf;
12
13use serde::{Deserialize, Serialize};
14
15use crate::providers::ProviderName;
16use zeph_common::SkillTrustLevel;
17
18// ── Permissions ──────────────────────────────────────────────────────────────
19
20/// Tool access level controlling agent autonomy.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
22#[serde(rename_all = "lowercase")]
23#[non_exhaustive]
24pub enum AutonomyLevel {
25    /// Read-only tools: `read`, `find_path`, `grep`, `list_directory`, `web_scrape`, `fetch`
26    ReadOnly,
27    /// Default: rule-based permissions with confirmations.
28    #[default]
29    Supervised,
30    /// All tools allowed, no confirmations.
31    Full,
32}
33
34/// Action a permission rule resolves to.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
36#[serde(rename_all = "lowercase")]
37#[non_exhaustive]
38pub enum PermissionAction {
39    /// Allow the tool call unconditionally.
40    Allow,
41    /// Prompt the user before allowing.
42    Ask,
43    /// Deny the tool call.
44    Deny,
45}
46
47/// Single permission rule: glob `pattern` + action.
48#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct PermissionRule {
50    /// Glob pattern matched against the tool input string.
51    pub pattern: String,
52    /// Action to take when the pattern matches.
53    pub action: PermissionAction,
54}
55
56/// TOML-deserializable permissions config section.
57#[derive(Debug, Clone, Deserialize, Serialize, Default)]
58pub struct PermissionsConfig {
59    /// Per-tool permission rules. Key is `tool_id`.
60    #[serde(flatten)]
61    pub tools: HashMap<String, Vec<PermissionRule>>,
62}
63
64// ── Verifiers ────────────────────────────────────────────────────────────────
65
66fn default_true() -> bool {
67    true
68}
69
70fn default_shell_tools() -> Vec<String> {
71    vec![
72        "bash".to_string(),
73        "shell".to_string(),
74        "terminal".to_string(),
75    ]
76}
77
78fn default_guarded_tools() -> Vec<String> {
79    vec!["fetch".to_string(), "web_scrape".to_string()]
80}
81
82/// Configuration for the destructive command verifier.
83#[derive(Debug, Clone, Deserialize, Serialize)]
84pub struct DestructiveVerifierConfig {
85    /// Enable the verifier. Default: `true`.
86    #[serde(default = "default_true")]
87    pub enabled: bool,
88    /// Explicit path prefixes under which destructive commands are permitted.
89    #[serde(default)]
90    pub allowed_paths: Vec<String>,
91    /// Additional command patterns to treat as destructive (substring match).
92    #[serde(default)]
93    pub extra_patterns: Vec<String>,
94    /// Tool names to treat as shell executors (case-insensitive).
95    #[serde(default = "default_shell_tools")]
96    pub shell_tools: Vec<String>,
97}
98
99impl Default for DestructiveVerifierConfig {
100    fn default() -> Self {
101        Self {
102            enabled: true,
103            allowed_paths: Vec::new(),
104            extra_patterns: Vec::new(),
105            shell_tools: default_shell_tools(),
106        }
107    }
108}
109
110/// Configuration for the injection pattern verifier.
111#[derive(Debug, Clone, Deserialize, Serialize)]
112pub struct InjectionVerifierConfig {
113    /// Enable the verifier. Default: `true`.
114    #[serde(default = "default_true")]
115    pub enabled: bool,
116    /// Additional injection patterns to block (regex strings).
117    #[serde(default)]
118    pub extra_patterns: Vec<String>,
119    /// URLs explicitly permitted even if they match SSRF patterns.
120    #[serde(default)]
121    pub allowlisted_urls: Vec<String>,
122}
123
124impl Default for InjectionVerifierConfig {
125    fn default() -> Self {
126        Self {
127            enabled: true,
128            extra_patterns: Vec::new(),
129            allowlisted_urls: Vec::new(),
130        }
131    }
132}
133
134/// Configuration for the URL grounding verifier.
135#[derive(Debug, Clone, Deserialize, Serialize)]
136pub struct UrlGroundingVerifierConfig {
137    /// Enable the verifier. Default: `true`.
138    #[serde(default = "default_true")]
139    pub enabled: bool,
140    /// Tool IDs subject to URL grounding checks.
141    #[serde(default = "default_guarded_tools")]
142    pub guarded_tools: Vec<String>,
143}
144
145impl Default for UrlGroundingVerifierConfig {
146    fn default() -> Self {
147        Self {
148            enabled: true,
149            guarded_tools: default_guarded_tools(),
150        }
151    }
152}
153
154/// Configuration for the firewall verifier.
155#[derive(Debug, Clone, Deserialize, Serialize)]
156pub struct FirewallVerifierConfig {
157    /// Enable the verifier. Default: `true`.
158    #[serde(default = "default_true")]
159    pub enabled: bool,
160    /// Glob patterns for additional paths to block.
161    #[serde(default)]
162    pub blocked_paths: Vec<String>,
163    /// Additional environment variable names to block from tool arguments.
164    #[serde(default)]
165    pub blocked_env_vars: Vec<String>,
166    /// Tool IDs exempt from firewall scanning.
167    #[serde(default)]
168    pub exempt_tools: Vec<String>,
169}
170
171impl Default for FirewallVerifierConfig {
172    fn default() -> Self {
173        Self {
174            enabled: true,
175            blocked_paths: Vec::new(),
176            blocked_env_vars: Vec::new(),
177            exempt_tools: Vec::new(),
178        }
179    }
180}
181
182/// Top-level configuration for all pre-execution verifiers.
183#[derive(Debug, Clone, Deserialize, Serialize)]
184pub struct PreExecutionVerifierConfig {
185    /// Enable all verifiers globally. Default: `true`.
186    #[serde(default = "default_true")]
187    pub enabled: bool,
188    /// Destructive command verifier settings.
189    #[serde(default)]
190    pub destructive_commands: DestructiveVerifierConfig,
191    /// Injection pattern verifier settings.
192    #[serde(default)]
193    pub injection_patterns: InjectionVerifierConfig,
194    /// URL grounding verifier settings.
195    #[serde(default)]
196    pub url_grounding: UrlGroundingVerifierConfig,
197    /// Firewall verifier settings.
198    #[serde(default)]
199    pub firewall: FirewallVerifierConfig,
200}
201
202impl Default for PreExecutionVerifierConfig {
203    fn default() -> Self {
204        Self {
205            enabled: true,
206            destructive_commands: DestructiveVerifierConfig::default(),
207            injection_patterns: InjectionVerifierConfig::default(),
208            url_grounding: UrlGroundingVerifierConfig::default(),
209            firewall: FirewallVerifierConfig::default(),
210        }
211    }
212}
213
214// ── Policy ───────────────────────────────────────────────────────────────────
215
216/// Effect applied when a policy rule matches.
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
218#[serde(rename_all = "snake_case")]
219#[non_exhaustive]
220pub enum PolicyEffect {
221    /// Allow the tool call.
222    Allow,
223    /// Deny the tool call.
224    Deny,
225}
226
227/// Default effect when no policy rule matches.
228#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
229#[serde(rename_all = "lowercase")]
230#[non_exhaustive]
231pub enum DefaultEffect {
232    /// Allow the call when no rule matches.
233    Allow,
234    /// Deny the call when no rule matches (default, fail-closed).
235    #[default]
236    Deny,
237}
238
239fn default_deny() -> DefaultEffect {
240    DefaultEffect::Deny
241}
242
243/// TOML-deserializable policy configuration.
244#[derive(Debug, Clone, Deserialize, Serialize, Default)]
245pub struct PolicyConfig {
246    /// Whether to enforce policy rules. When false, all calls are allowed.
247    #[serde(default)]
248    pub enabled: bool,
249    /// Fallback effect when no rule matches.
250    #[serde(default = "default_deny")]
251    pub default_effect: DefaultEffect,
252    /// Inline policy rules.
253    #[serde(default)]
254    pub rules: Vec<PolicyRuleConfig>,
255    /// Optional external policy file (TOML). When set, overrides inline rules.
256    pub policy_file: Option<String>,
257}
258
259/// A single policy rule as read from TOML.
260#[derive(Debug, Clone, Deserialize, Serialize)]
261pub struct PolicyRuleConfig {
262    /// Effect when the rule matches.
263    pub effect: PolicyEffect,
264    /// Glob pattern matching the tool id. Required.
265    pub tool: String,
266    /// Path globs matched against path-like params. Rule fires if ANY path matches.
267    #[serde(default)]
268    pub paths: Vec<String>,
269    /// Env var names that must all be present in the policy context.
270    #[serde(default)]
271    pub env: Vec<String>,
272    /// Minimum required trust level (rule fires only when context trust <= threshold).
273    pub trust_level: Option<SkillTrustLevel>,
274    /// Regex matched against individual string param values.
275    pub args_match: Option<String>,
276    /// Named capabilities associated with this rule.
277    #[serde(default)]
278    pub capabilities: Vec<String>,
279}
280
281// ── Sandbox ──────────────────────────────────────────────────────────────────
282
283/// Baseline restriction profile for the OS-level sandbox.
284#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
285#[serde(rename_all = "kebab-case")]
286#[non_exhaustive]
287pub enum SandboxProfile {
288    /// Read-only to `allow_read` paths, no writes, no network.
289    ReadOnly,
290    /// Read/write to configured paths; network egress blocked.
291    #[default]
292    Workspace,
293    /// Workspace-level filesystem access plus unrestricted network egress.
294    #[serde(rename = "network-allow-all", alias = "network")]
295    NetworkAllowAll,
296    /// Sandbox disabled. The subprocess inherits the parent's full capabilities.
297    Off,
298}
299
300fn default_sandbox_profile() -> SandboxProfile {
301    SandboxProfile::Workspace
302}
303
304/// Backend used to enforce OS-level sandboxing.
305///
306/// Serialises with `kebab-case` names so TOML values match the original string convention
307/// (`"auto"`, `"seatbelt"`, `"landlock-bwrap"`, `"noop"`).
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
309#[serde(rename_all = "kebab-case")]
310#[non_exhaustive]
311pub enum SandboxBackend {
312    /// Automatically select the best available backend for the current OS.
313    #[default]
314    Auto,
315    /// macOS `sandbox-exec` (Seatbelt) profile.
316    Seatbelt,
317    /// Linux Landlock + bubblewrap combination.
318    LandlockBwrap,
319    /// Disable sandboxing (testing / unsupported platforms).
320    Noop,
321}
322
323/// OS-level subprocess sandbox configuration (`[tools.sandbox]` TOML section).
324#[derive(Debug, Clone, Deserialize, Serialize)]
325pub struct SandboxConfig {
326    /// Enable OS-level sandbox. Default: `false`.
327    #[serde(default)]
328    pub enabled: bool,
329    /// Enforcement profile controlling the baseline restrictions.
330    #[serde(default = "default_sandbox_profile")]
331    pub profile: SandboxProfile,
332    /// Additional paths granted read access.
333    #[serde(default)]
334    pub allow_read: Vec<PathBuf>,
335    /// Additional paths granted write access.
336    #[serde(default)]
337    pub allow_write: Vec<PathBuf>,
338    /// When `true`, sandbox initialization failure aborts startup (fail-closed). Default: `true`.
339    #[serde(default = "default_true")]
340    pub strict: bool,
341    /// OS backend used to enforce sandboxing.
342    ///
343    /// Accepts `"auto"`, `"seatbelt"`, `"landlock-bwrap"`, or `"noop"` in TOML.
344    #[serde(default)]
345    pub backend: SandboxBackend,
346    /// Hostnames denied network egress from sandboxed subprocesses.
347    #[serde(default)]
348    pub denied_domains: Vec<String>,
349    /// When `true`, failure to activate an effective OS sandbox aborts startup.
350    #[serde(default)]
351    pub fail_if_unavailable: bool,
352}
353
354impl Default for SandboxConfig {
355    fn default() -> Self {
356        Self {
357            enabled: false,
358            profile: default_sandbox_profile(),
359            allow_read: Vec::new(),
360            allow_write: Vec::new(),
361            strict: true,
362            backend: SandboxBackend::Auto,
363            denied_domains: Vec::new(),
364            fail_if_unavailable: false,
365        }
366    }
367}
368
369// ── Output filter config ─────────────────────────────────────────────────────
370
371/// Configuration for tool output security filter.
372#[derive(Debug, Clone, Deserialize, Serialize)]
373pub struct SecurityFilterConfig {
374    /// Enable security filtering. Default: `true`.
375    #[serde(default = "default_true")]
376    pub enabled: bool,
377    /// Additional regex patterns to block in tool output.
378    #[serde(default)]
379    pub extra_patterns: Vec<String>,
380}
381
382impl Default for SecurityFilterConfig {
383    fn default() -> Self {
384        Self {
385            enabled: true,
386            extra_patterns: Vec::new(),
387        }
388    }
389}
390
391/// Configuration for output filters.
392#[derive(Debug, Clone, Deserialize, Serialize)]
393pub struct FilterConfig {
394    /// Master switch for output filtering. Default: `true`.
395    #[serde(default = "default_true")]
396    pub enabled: bool,
397    /// Security filter settings.
398    #[serde(default)]
399    pub security: SecurityFilterConfig,
400    /// Directory containing a `filters.toml` override file.
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub filters_path: Option<PathBuf>,
403}
404
405impl Default for FilterConfig {
406    fn default() -> Self {
407        Self {
408            enabled: true,
409            security: SecurityFilterConfig::default(),
410            filters_path: None,
411        }
412    }
413}
414
415// ── ToolsConfig sub-types ────────────────────────────────────────────────────
416
417fn default_overflow_threshold() -> usize {
418    50_000
419}
420
421fn default_retention_days() -> u64 {
422    7
423}
424
425fn default_max_overflow_bytes() -> usize {
426    10 * 1024 * 1024
427}
428
429/// Configuration for large tool response offload to `SQLite`.
430#[derive(Debug, Clone, Deserialize, Serialize)]
431pub struct OverflowConfig {
432    /// Character threshold above which tool output is offloaded. Default: `50000`.
433    #[serde(default = "default_overflow_threshold")]
434    pub threshold: usize,
435    /// Days to retain offloaded entries. Default: `7`.
436    #[serde(default = "default_retention_days")]
437    pub retention_days: u64,
438    /// Maximum bytes per overflow entry. `0` means unlimited. Default: `10 MiB`.
439    #[serde(default = "default_max_overflow_bytes")]
440    pub max_overflow_bytes: usize,
441}
442
443impl Default for OverflowConfig {
444    fn default() -> Self {
445        Self {
446            threshold: default_overflow_threshold(),
447            retention_days: default_retention_days(),
448            max_overflow_bytes: default_max_overflow_bytes(),
449        }
450    }
451}
452
453fn default_anomaly_window() -> usize {
454    10
455}
456
457fn default_anomaly_error_threshold() -> f64 {
458    0.5
459}
460
461fn default_anomaly_critical_threshold() -> f64 {
462    0.8
463}
464
465/// Configuration for the sliding-window anomaly detector.
466#[derive(Debug, Clone, Deserialize, Serialize)]
467pub struct AnomalyConfig {
468    /// Enable the anomaly detector. Default: `true`.
469    #[serde(default = "default_true")]
470    pub enabled: bool,
471    /// Number of recent tool calls in the sliding window. Default: `10`.
472    #[serde(default = "default_anomaly_window")]
473    pub window_size: usize,
474    /// Error-rate fraction triggering a WARN. Default: `0.5`.
475    #[serde(default = "default_anomaly_error_threshold")]
476    pub error_threshold: f64,
477    /// Error-rate fraction triggering a CRIT. Default: `0.8`.
478    #[serde(default = "default_anomaly_critical_threshold")]
479    pub critical_threshold: f64,
480    /// Emit a WARN when a reasoning model produces a quality failure. Default: `true`.
481    #[serde(default = "default_true")]
482    pub reasoning_model_warning: bool,
483}
484
485impl Default for AnomalyConfig {
486    fn default() -> Self {
487        Self {
488            enabled: true,
489            window_size: default_anomaly_window(),
490            error_threshold: default_anomaly_error_threshold(),
491            critical_threshold: default_anomaly_critical_threshold(),
492            reasoning_model_warning: true,
493        }
494    }
495}
496
497fn default_cache_ttl_secs() -> u64 {
498    300
499}
500
501/// Configuration for the tool result cache.
502#[derive(Debug, Clone, Deserialize, Serialize)]
503pub struct ResultCacheConfig {
504    /// Whether caching is enabled. Default: `true`.
505    #[serde(default = "default_true")]
506    pub enabled: bool,
507    /// Time-to-live in seconds. `0` means entries never expire. Default: `300`.
508    #[serde(default = "default_cache_ttl_secs")]
509    pub ttl_secs: u64,
510}
511
512impl Default for ResultCacheConfig {
513    fn default() -> Self {
514        Self {
515            enabled: true,
516            ttl_secs: default_cache_ttl_secs(),
517        }
518    }
519}
520
521fn default_tafc_complexity_threshold() -> f64 {
522    0.6
523}
524
525/// Configuration for Think-Augmented Function Calling (TAFC).
526#[derive(Debug, Clone, Deserialize, Serialize)]
527pub struct TafcConfig {
528    /// Enable TAFC schema augmentation. Default: `false`.
529    #[serde(default)]
530    pub enabled: bool,
531    /// Complexity threshold tau in [0.0, 1.0]; tools >= tau are augmented. Default: `0.6`.
532    #[serde(default = "default_tafc_complexity_threshold")]
533    pub complexity_threshold: f64,
534}
535
536impl Default for TafcConfig {
537    fn default() -> Self {
538        Self {
539            enabled: false,
540            complexity_threshold: default_tafc_complexity_threshold(),
541        }
542    }
543}
544
545impl TafcConfig {
546    /// Validate and clamp `complexity_threshold` to [0.0, 1.0]. Resets NaN/Infinity to 0.6.
547    #[must_use]
548    pub fn validated(mut self) -> Self {
549        if self.complexity_threshold.is_finite() {
550            self.complexity_threshold = self.complexity_threshold.clamp(0.0, 1.0);
551        } else {
552            self.complexity_threshold = 0.6;
553        }
554        self
555    }
556}
557
558fn default_utility_exempt_tools() -> Vec<String> {
559    vec!["invoke_skill".to_string(), "load_skill".to_string()]
560}
561
562fn default_utility_threshold() -> f32 {
563    0.1
564}
565
566fn default_utility_gain_weight() -> f32 {
567    1.0
568}
569
570fn default_utility_cost_weight() -> f32 {
571    0.5
572}
573
574fn default_utility_redundancy_weight() -> f32 {
575    0.3
576}
577
578fn default_utility_uncertainty_bonus() -> f32 {
579    0.2
580}
581
582/// Configuration for utility-guided tool dispatch.
583#[derive(Debug, Clone, Deserialize, Serialize)]
584#[serde(default)]
585pub struct UtilityScoringConfig {
586    /// Enable utility-guided gating. Default: `false`.
587    pub enabled: bool,
588    /// Minimum utility score required to execute a tool call. Default: `0.1`.
589    #[serde(default = "default_utility_threshold")]
590    pub threshold: f32,
591    /// Weight for the estimated gain component. Must be >= 0. Default: `1.0`.
592    #[serde(default = "default_utility_gain_weight")]
593    pub gain_weight: f32,
594    /// Weight for the step cost component. Must be >= 0. Default: `0.5`.
595    #[serde(default = "default_utility_cost_weight")]
596    pub cost_weight: f32,
597    /// Weight for the redundancy penalty. Must be >= 0. Default: `0.3`.
598    #[serde(default = "default_utility_redundancy_weight")]
599    pub redundancy_weight: f32,
600    /// Weight for the exploration bonus. Must be >= 0. Default: `0.2`.
601    #[serde(default = "default_utility_uncertainty_bonus")]
602    pub uncertainty_bonus: f32,
603    /// Tool names that bypass the utility gate unconditionally.
604    #[serde(default = "default_utility_exempt_tools")]
605    pub exempt_tools: Vec<String>,
606}
607
608impl Default for UtilityScoringConfig {
609    fn default() -> Self {
610        Self {
611            enabled: false,
612            threshold: default_utility_threshold(),
613            gain_weight: default_utility_gain_weight(),
614            cost_weight: default_utility_cost_weight(),
615            redundancy_weight: default_utility_redundancy_weight(),
616            uncertainty_bonus: default_utility_uncertainty_bonus(),
617            exempt_tools: default_utility_exempt_tools(),
618        }
619    }
620}
621
622impl UtilityScoringConfig {
623    /// Validate that all weights and threshold are non-negative and finite.
624    ///
625    /// # Errors
626    ///
627    /// Returns a description of the first invalid field found.
628    pub fn validate(&self) -> Result<(), String> {
629        let fields = [
630            ("threshold", self.threshold),
631            ("gain_weight", self.gain_weight),
632            ("cost_weight", self.cost_weight),
633            ("redundancy_weight", self.redundancy_weight),
634            ("uncertainty_bonus", self.uncertainty_bonus),
635        ];
636        for (name, val) in fields {
637            if !val.is_finite() {
638                return Err(format!("[tools.utility] {name} must be finite, got {val}"));
639            }
640            if val < 0.0 {
641                return Err(format!("[tools.utility] {name} must be >= 0, got {val}"));
642            }
643        }
644        Ok(())
645    }
646}
647
648/// Dependency specification for a single tool.
649#[derive(Debug, Clone, Default, Deserialize, Serialize)]
650pub struct ToolDependency {
651    /// Hard prerequisites: tool is hidden until ALL of these have completed successfully.
652    #[serde(default, skip_serializing_if = "Vec::is_empty")]
653    pub requires: Vec<String>,
654    /// Soft prerequisites: tool gets a similarity boost when these have completed.
655    #[serde(default, skip_serializing_if = "Vec::is_empty")]
656    pub prefers: Vec<String>,
657}
658
659fn default_boost_per_dep() -> f32 {
660    0.15
661}
662
663fn default_max_total_boost() -> f32 {
664    0.2
665}
666
667/// Configuration for the tool dependency graph feature.
668#[derive(Debug, Clone, Deserialize, Serialize)]
669pub struct DependencyConfig {
670    /// Whether dependency gating is enabled. Default: `false`.
671    #[serde(default)]
672    pub enabled: bool,
673    /// Similarity boost added per satisfied `prefers` dependency. Default: `0.15`.
674    #[serde(default = "default_boost_per_dep")]
675    pub boost_per_dep: f32,
676    /// Maximum total boost applied regardless of how many `prefers` deps are met. Default: `0.2`.
677    #[serde(default = "default_max_total_boost")]
678    pub max_total_boost: f32,
679    /// Per-tool dependency rules. Key is `tool_id`.
680    #[serde(default)]
681    pub rules: HashMap<String, ToolDependency>,
682}
683
684impl Default for DependencyConfig {
685    fn default() -> Self {
686        Self {
687            enabled: false,
688            boost_per_dep: default_boost_per_dep(),
689            max_total_boost: default_max_total_boost(),
690            rules: HashMap::new(),
691        }
692    }
693}
694
695fn default_retry_max_attempts() -> usize {
696    2
697}
698
699fn default_retry_base_ms() -> u64 {
700    500
701}
702
703fn default_retry_max_ms() -> u64 {
704    5_000
705}
706
707fn default_retry_budget_secs() -> u64 {
708    30
709}
710
711/// Configuration for tool error retry behavior.
712#[derive(Debug, Clone, Deserialize, Serialize)]
713pub struct RetryConfig {
714    /// Maximum retry attempts for transient errors per tool call. `0` = disabled.
715    #[serde(default = "default_retry_max_attempts")]
716    pub max_attempts: usize,
717    /// Base delay (ms) for exponential backoff.
718    #[serde(default = "default_retry_base_ms")]
719    pub base_ms: u64,
720    /// Maximum delay cap (ms) for exponential backoff.
721    #[serde(default = "default_retry_max_ms")]
722    pub max_ms: u64,
723    /// Maximum wall-clock time (seconds) for all retries of a single tool call. `0` = unlimited.
724    #[serde(default = "default_retry_budget_secs")]
725    pub budget_secs: u64,
726    /// Provider name for LLM-based parameter reformatting on `InvalidParameters`/`TypeMismatch`.
727    /// Empty string = disabled.
728    #[serde(default)]
729    pub parameter_reformat_provider: ProviderName,
730}
731
732impl Default for RetryConfig {
733    fn default() -> Self {
734        Self {
735            max_attempts: default_retry_max_attempts(),
736            base_ms: default_retry_base_ms(),
737            max_ms: default_retry_max_ms(),
738            budget_secs: default_retry_budget_secs(),
739            parameter_reformat_provider: ProviderName::default(),
740        }
741    }
742}
743
744fn default_adversarial_timeout_ms() -> u64 {
745    3_000
746}
747
748/// Configuration for the LLM-based adversarial policy agent.
749#[derive(Debug, Clone, Deserialize, Serialize)]
750pub struct AdversarialPolicyConfig {
751    /// Enable the adversarial policy agent. Default: `false`.
752    #[serde(default)]
753    pub enabled: bool,
754    /// Provider name for the policy validation LLM.
755    #[serde(default)]
756    pub policy_provider: ProviderName,
757    /// Path to a plain-text policy file.
758    pub policy_file: Option<String>,
759    /// Whether to allow tool calls when the policy LLM fails. Default: `false` (fail-closed).
760    #[serde(default)]
761    pub fail_open: bool,
762    /// Timeout in milliseconds for a single policy LLM call. Default: `3000`.
763    #[serde(default = "default_adversarial_timeout_ms")]
764    pub timeout_ms: u64,
765    /// Tool names always allowed through the adversarial policy gate.
766    #[serde(default = "AdversarialPolicyConfig::default_exempt_tools")]
767    pub exempt_tools: Vec<String>,
768}
769
770impl Default for AdversarialPolicyConfig {
771    fn default() -> Self {
772        Self {
773            enabled: false,
774            policy_provider: ProviderName::default(),
775            policy_file: None,
776            fail_open: false,
777            timeout_ms: default_adversarial_timeout_ms(),
778            exempt_tools: Self::default_exempt_tools(),
779        }
780    }
781}
782
783impl AdversarialPolicyConfig {
784    #[must_use]
785    pub fn default_exempt_tools() -> Vec<String> {
786        vec![
787            "memory_save".into(),
788            "memory_search".into(),
789            "read_overflow".into(),
790            "load_skill".into(),
791            "invoke_skill".into(),
792            "schedule_deferred".into(),
793            // Read-only scheduler intrinsic must never be blocked by the adversarial
794            // probe: it carries no side-effects and the embed provider may be unavailable.
795            "list_tasks".into(),
796        ]
797    }
798}
799
800/// Per-path read allow/deny sandbox for the file tool.
801///
802/// Evaluation order: deny-then-allow. If a path matches `deny_read` and does NOT
803/// match `allow_read`, access is denied. Empty `deny_read` means no read restrictions.
804#[derive(Debug, Clone, Default, Deserialize, Serialize)]
805pub struct FileConfig {
806    /// Glob patterns for paths denied for reading. Evaluated first.
807    #[serde(default)]
808    pub deny_read: Vec<String>,
809    /// Glob patterns for paths allowed for reading. Evaluated second (overrides deny).
810    #[serde(default)]
811    pub allow_read: Vec<String>,
812}
813
814/// OAP-style declarative authorization config.
815#[derive(Debug, Clone, Default, Deserialize, Serialize)]
816pub struct AuthorizationConfig {
817    /// Enable OAP authorization checks. Default: `false`.
818    #[serde(default)]
819    pub enabled: bool,
820    /// Per-tool authorization rules appended after `[tools.policy]` rules at startup.
821    #[serde(default)]
822    pub rules: Vec<PolicyRuleConfig>,
823}
824
825/// Audit log destination.
826///
827/// Deserializes from a string in TOML: `"stdout"`, `"stderr"`, or a file path.
828#[derive(Debug, Clone, PartialEq, Eq, Default)]
829#[non_exhaustive]
830pub enum AuditDestination {
831    /// Write audit entries to standard output.
832    #[default]
833    Stdout,
834    /// Write audit entries to standard error.
835    Stderr,
836    /// Write audit entries to the given file path (appended, mode 0o600).
837    File(std::path::PathBuf),
838}
839
840impl AuditDestination {
841    /// Returns `true` if the destination is `stdout`.
842    #[must_use]
843    pub fn is_stdout(&self) -> bool {
844        matches!(self, Self::Stdout)
845    }
846}
847
848impl serde::Serialize for AuditDestination {
849    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
850        match self {
851            Self::Stdout => s.serialize_str("stdout"),
852            Self::Stderr => s.serialize_str("stderr"),
853            Self::File(p) => s.serialize_str(&p.display().to_string()),
854        }
855    }
856}
857
858impl<'de> serde::Deserialize<'de> for AuditDestination {
859    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
860        let s = String::deserialize(d)?;
861        Ok(match s.as_str() {
862            "stdout" => Self::Stdout,
863            "stderr" => Self::Stderr,
864            path => Self::File(std::path::PathBuf::from(path)),
865        })
866    }
867}
868
869/// Configuration for audit logging of tool executions.
870#[derive(Debug, Deserialize, Serialize)]
871pub struct AuditConfig {
872    /// Enable audit logging. Default: `true`.
873    #[serde(default = "default_true")]
874    pub enabled: bool,
875    /// Log destination. Default: [`AuditDestination::Stdout`].
876    #[serde(default)]
877    pub destination: AuditDestination,
878    /// When `true`, log a per-tool risk summary at startup. Default: `false`.
879    #[serde(default)]
880    pub tool_risk_summary: bool,
881}
882
883impl Default for AuditConfig {
884    fn default() -> Self {
885        Self {
886            enabled: true,
887            destination: AuditDestination::default(),
888            tool_risk_summary: false,
889        }
890    }
891}
892
893fn default_timeout() -> u64 {
894    30
895}
896
897fn default_confirm_patterns() -> Vec<String> {
898    vec![
899        "rm ".into(),
900        "git push -f".into(),
901        "git push --force".into(),
902        "drop table".into(),
903        "drop database".into(),
904        "truncate ".into(),
905        "$(".into(),
906        "`".into(),
907        "<(".into(),
908        ">(".into(),
909        "<<<".into(),
910        "eval ".into(),
911    ]
912}
913
914fn default_max_background_runs() -> usize {
915    8
916}
917
918fn default_background_timeout_secs() -> u64 {
919    1800
920}
921
922/// Shell-specific configuration: timeout, command blocklist, and allowlist overrides.
923#[derive(Debug, Deserialize, Serialize)]
924#[allow(clippy::struct_excessive_bools)]
925pub struct ShellConfig {
926    /// Shell command timeout in seconds. Default: `30`.
927    #[serde(default = "default_timeout")]
928    pub timeout: u64,
929    /// Commands blocked from execution.
930    #[serde(default)]
931    pub blocked_commands: Vec<String>,
932    /// Commands explicitly allowed (overrides blocklist).
933    #[serde(default)]
934    pub allowed_commands: Vec<String>,
935    /// Filesystem paths the shell is permitted to access.
936    #[serde(default)]
937    pub allowed_paths: Vec<String>,
938    /// Allow outbound network from shell. Default: `true`.
939    #[serde(default = "default_true")]
940    pub allow_network: bool,
941    /// Patterns that trigger a confirmation prompt before execution.
942    #[serde(default = "default_confirm_patterns")]
943    pub confirm_patterns: Vec<String>,
944    /// Environment variable name prefixes to strip from subprocess environment.
945    #[serde(default = "ShellConfig::default_env_blocklist")]
946    pub env_blocklist: Vec<String>,
947    /// Enable transactional mode: snapshot files before write commands. Default: `false`.
948    #[serde(default)]
949    pub transactional: bool,
950    /// Glob patterns for paths eligible for snapshotting.
951    #[serde(default)]
952    pub transaction_scope: Vec<String>,
953    /// Automatically rollback when exit code >= 2. Default: `false`.
954    #[serde(default)]
955    pub auto_rollback: bool,
956    /// Exit codes that trigger auto-rollback.
957    #[serde(default)]
958    pub auto_rollback_exit_codes: Vec<i32>,
959    /// When `true`, snapshot failure aborts execution. Default: `false`.
960    #[serde(default)]
961    pub snapshot_required: bool,
962    /// Maximum cumulative bytes for transaction snapshots. `0` = unlimited.
963    #[serde(default)]
964    pub max_snapshot_bytes: u64,
965    /// Maximum concurrent background shell runs. Default: `8`.
966    #[serde(default = "default_max_background_runs")]
967    pub max_background_runs: usize,
968    /// Timeout in seconds for each background shell run. Default: `1800`.
969    #[serde(default = "default_background_timeout_secs")]
970    pub background_timeout_secs: u64,
971    /// Cumulative risk score threshold for multi-step chain blocking. Default: `0.7`.
972    ///
973    /// When the `RiskChainAccumulator` (zeph-tools) exceeds this score within a single turn,
974    /// the command is blocked. Set to `None` to use the built-in default of `0.7`.
975    #[serde(default)]
976    pub risk_chain_threshold: Option<f32>,
977}
978
979impl Default for ShellConfig {
980    fn default() -> Self {
981        Self {
982            timeout: default_timeout(),
983            blocked_commands: Vec::new(),
984            allowed_commands: Vec::new(),
985            allowed_paths: Vec::new(),
986            allow_network: true,
987            confirm_patterns: default_confirm_patterns(),
988            env_blocklist: Self::default_env_blocklist(),
989            transactional: false,
990            transaction_scope: Vec::new(),
991            auto_rollback: false,
992            auto_rollback_exit_codes: Vec::new(),
993            snapshot_required: false,
994            max_snapshot_bytes: 0,
995            max_background_runs: default_max_background_runs(),
996            background_timeout_secs: default_background_timeout_secs(),
997            risk_chain_threshold: None,
998        }
999    }
1000}
1001
1002impl ShellConfig {
1003    /// Default environment variable prefixes to strip from subprocess environment.
1004    #[must_use]
1005    pub fn default_env_blocklist() -> Vec<String> {
1006        vec![
1007            "ZEPH_".into(),
1008            "AWS_".into(),
1009            "AZURE_".into(),
1010            "GCP_".into(),
1011            "GOOGLE_".into(),
1012            "OPENAI_".into(),
1013            "ANTHROPIC_".into(),
1014            "HF_".into(),
1015            "HUGGING".into(),
1016        ]
1017    }
1018}
1019
1020fn default_scrape_timeout() -> u64 {
1021    15
1022}
1023
1024fn default_max_body_bytes() -> usize {
1025    4_194_304
1026}
1027
1028fn default_ipi_filter_threshold() -> f32 {
1029    0.6
1030}
1031
1032/// Configuration for the web scrape tool.
1033#[derive(Debug, Deserialize, Serialize)]
1034pub struct ScrapeConfig {
1035    /// Timeout in seconds for scrape requests. Default: `15`.
1036    #[serde(default = "default_scrape_timeout")]
1037    pub timeout: u64,
1038    /// Maximum response body bytes. Default: `4 MiB`.
1039    #[serde(default = "default_max_body_bytes")]
1040    pub max_body_bytes: usize,
1041    /// Domain allowlist. Empty = all public domains allowed.
1042    #[serde(default)]
1043    pub allowed_domains: Vec<String>,
1044    /// Domain denylist. Always enforced, regardless of allowlist state.
1045    #[serde(default)]
1046    pub denied_domains: Vec<String>,
1047    /// IPI filter score threshold. Responses with score >= this value get a warning
1048    /// prepended and injection fragments replaced with `[FILTERED]`. Default: `0.6`.
1049    #[serde(default = "default_ipi_filter_threshold")]
1050    pub ipi_filter_threshold: f32,
1051}
1052
1053impl Default for ScrapeConfig {
1054    fn default() -> Self {
1055        Self {
1056            timeout: default_scrape_timeout(),
1057            max_body_bytes: default_max_body_bytes(),
1058            allowed_domains: Vec::new(),
1059            denied_domains: Vec::new(),
1060            ipi_filter_threshold: default_ipi_filter_threshold(),
1061        }
1062    }
1063}
1064
1065/// Speculative tool execution mode.
1066#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
1067#[serde(rename_all = "kebab-case")]
1068#[non_exhaustive]
1069pub enum SpeculationMode {
1070    /// No speculation; uses existing synchronous path.
1071    #[default]
1072    Off,
1073    /// LLM-decoding level: fires tools when streaming partial JSON has all required fields.
1074    Decoding,
1075    /// Application-level pattern (PASTE): predicts top-K calls from `SQLite` history.
1076    Pattern,
1077    /// Both decoding and pattern speculation active.
1078    Both,
1079}
1080
1081/// Pattern-based (PASTE) speculative execution config.
1082#[derive(Debug, Clone, Deserialize, Serialize)]
1083pub struct SpeculativePatternConfig {
1084    /// Enable PASTE pattern learning and prediction. Default: `false`.
1085    #[serde(default)]
1086    pub enabled: bool,
1087    /// Minimum observed occurrences before a prediction is issued.
1088    #[serde(default = "default_min_observations")]
1089    pub min_observations: u32,
1090    /// Exponential decay half-life in days for pattern scoring.
1091    #[serde(default = "default_half_life_days")]
1092    pub half_life_days: f64,
1093    /// LLM provider name for optional reranking. Empty = disabled.
1094    #[serde(default)]
1095    pub rerank_provider: ProviderName,
1096}
1097
1098fn default_min_observations() -> u32 {
1099    5
1100}
1101
1102fn default_half_life_days() -> f64 {
1103    14.0
1104}
1105
1106impl Default for SpeculativePatternConfig {
1107    fn default() -> Self {
1108        Self {
1109            enabled: false,
1110            min_observations: default_min_observations(),
1111            half_life_days: default_half_life_days(),
1112            rerank_provider: ProviderName::default(),
1113        }
1114    }
1115}
1116
1117/// Shell command regex allowlist for speculative execution.
1118#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1119pub struct SpeculativeAllowlistConfig {
1120    /// Regexes matched against the full `bash` command string.
1121    #[serde(default)]
1122    pub shell: Vec<String>,
1123}
1124
1125fn default_max_in_flight() -> usize {
1126    4
1127}
1128
1129fn default_confidence_threshold() -> f32 {
1130    0.55
1131}
1132
1133fn default_max_wasted_per_minute() -> u64 {
1134    100
1135}
1136
1137fn default_ttl_seconds() -> u64 {
1138    30
1139}
1140
1141/// Top-level configuration for speculative tool execution.
1142#[derive(Debug, Clone, Deserialize, Serialize)]
1143pub struct SpeculativeConfig {
1144    /// Speculation mode. Default: `off`.
1145    #[serde(default)]
1146    pub mode: SpeculationMode,
1147    /// Maximum concurrent in-flight speculative tasks.
1148    #[serde(default = "default_max_in_flight")]
1149    pub max_in_flight: usize,
1150    /// Minimum confidence score [0, 1] to dispatch a speculative task.
1151    #[serde(default = "default_confidence_threshold")]
1152    pub confidence_threshold: f32,
1153    /// Circuit-breaker: disable speculation for 60 s when wasted ms exceeds this per minute.
1154    #[serde(default = "default_max_wasted_per_minute")]
1155    pub max_wasted_per_minute: u64,
1156    /// Per-handle wall-clock TTL in seconds before the handle is cancelled.
1157    #[serde(default = "default_ttl_seconds")]
1158    pub ttl_seconds: u64,
1159    /// Emit `AuditEntry` for speculative dispatches. Default: `true`.
1160    #[serde(default = "default_true")]
1161    pub audit: bool,
1162    /// PASTE pattern learning config.
1163    #[serde(default)]
1164    pub pattern: SpeculativePatternConfig,
1165    /// Per-executor command allowlists.
1166    #[serde(default)]
1167    pub allowlist: SpeculativeAllowlistConfig,
1168}
1169
1170impl Default for SpeculativeConfig {
1171    fn default() -> Self {
1172        Self {
1173            mode: SpeculationMode::Off,
1174            max_in_flight: default_max_in_flight(),
1175            confidence_threshold: default_confidence_threshold(),
1176            max_wasted_per_minute: default_max_wasted_per_minute(),
1177            ttl_seconds: default_ttl_seconds(),
1178            audit: true,
1179            pattern: SpeculativePatternConfig::default(),
1180            allowlist: SpeculativeAllowlistConfig::default(),
1181        }
1182    }
1183}
1184
1185/// Configuration for egress network event logging.
1186#[derive(Debug, Clone, Deserialize, Serialize)]
1187#[serde(default)]
1188#[allow(clippy::struct_excessive_bools)]
1189pub struct EgressConfig {
1190    /// Master switch for egress event emission. Default: `true`.
1191    pub enabled: bool,
1192    /// Emit events for requests blocked by SSRF/domain/scheme checks. Default: `true`.
1193    pub log_blocked: bool,
1194    /// Include `response_bytes` in the JSONL record. Default: `true`.
1195    pub log_response_bytes: bool,
1196    /// Show real hostname in TUI egress panel. Default: `true`.
1197    pub log_hosts_to_tui: bool,
1198}
1199
1200impl Default for EgressConfig {
1201    fn default() -> Self {
1202        Self {
1203            enabled: true,
1204            log_blocked: true,
1205            log_response_bytes: true,
1206            log_hosts_to_tui: true,
1207        }
1208    }
1209}
1210
1211// ── ToolCompressionConfig ─────────────────────────────────────────────────────
1212
1213fn default_compression_min_lines() -> usize {
1214    10
1215}
1216
1217fn default_compression_max_rules() -> u32 {
1218    200
1219}
1220
1221fn default_regex_compile_timeout_ms() -> u64 {
1222    500
1223}
1224
1225fn default_evolution_min_interval_secs() -> u64 {
1226    3600
1227}
1228
1229/// TACO self-evolving tool output compression configuration (`[tools.compression]` TOML section).
1230///
1231/// When enabled, a `RuleBasedCompressor` is wrapped around the root tool executor.
1232/// Rules are loaded from the `compression_rules` `SQLite` table and optionally evolved by an
1233/// LLM provider specified in `evolution_provider`.
1234///
1235/// # Example (TOML)
1236///
1237/// ```toml
1238/// [tools.compression]
1239/// enabled = true
1240/// evolution_provider = "fast"
1241/// min_lines_to_compress = 15
1242/// ```
1243#[derive(Debug, Clone, Deserialize, Serialize)]
1244#[serde(default)]
1245pub struct ToolCompressionConfig {
1246    /// Enable rule-based tool output compression. Default: `false`.
1247    pub enabled: bool,
1248    /// Minimum output line count before compression is attempted. Default: `10`.
1249    #[serde(default = "default_compression_min_lines")]
1250    pub min_lines_to_compress: usize,
1251    /// LLM provider name for self-evolution. Empty string = evolution disabled. Default: `""`.
1252    #[serde(default)]
1253    pub evolution_provider: ProviderName,
1254    /// Minimum interval in seconds between self-evolution runs. Default: `3600`.
1255    #[serde(default = "default_evolution_min_interval_secs")]
1256    pub evolution_min_interval_secs: u64,
1257    /// Maximum number of rules to keep in the DB (prune lowest-hit rules above this). Default: `200`.
1258    #[serde(default = "default_compression_max_rules")]
1259    pub max_rules: u32,
1260    /// Timeout in milliseconds for safe regex compilation. Default: `500`.
1261    #[serde(default = "default_regex_compile_timeout_ms")]
1262    pub regex_compile_timeout_ms: u64,
1263}
1264
1265impl Default for ToolCompressionConfig {
1266    fn default() -> Self {
1267        Self {
1268            enabled: false,
1269            min_lines_to_compress: default_compression_min_lines(),
1270            evolution_provider: ProviderName::default(),
1271            evolution_min_interval_secs: default_evolution_min_interval_secs(),
1272            max_rules: default_compression_max_rules(),
1273            regex_compile_timeout_ms: default_regex_compile_timeout_ms(),
1274        }
1275    }
1276}
1277
1278// ── ToolsConfig ───────────────────────────────────────────────────────────────
1279
1280/// Top-level configuration for tool execution.
1281///
1282/// Deserialized from `[tools]` in TOML. The `permission_policy()` method (which constructs
1283/// a runtime `PermissionPolicy`) lives in `zeph-tools` as a free function to avoid
1284/// importing runtime types into this leaf crate.
1285#[derive(Debug, Deserialize, Serialize)]
1286pub struct ToolsConfig {
1287    /// Enable all tools. Default: `true`.
1288    #[serde(default = "default_true")]
1289    pub enabled: bool,
1290    /// Summarize long tool output before injection into context. Default: `true`.
1291    #[serde(default = "default_true")]
1292    pub summarize_output: bool,
1293    /// Shell tool configuration.
1294    #[serde(default)]
1295    pub shell: ShellConfig,
1296    /// Web scrape tool configuration.
1297    #[serde(default)]
1298    pub scrape: ScrapeConfig,
1299    /// Audit log configuration.
1300    #[serde(default)]
1301    pub audit: AuditConfig,
1302    /// Declarative permissions. Overrides legacy `shell.blocked_commands` when set.
1303    #[serde(default)]
1304    pub permissions: Option<PermissionsConfig>,
1305    /// Output filter configuration.
1306    #[serde(default)]
1307    pub filters: FilterConfig,
1308    /// Large response offload configuration.
1309    #[serde(default)]
1310    pub overflow: OverflowConfig,
1311    /// Sliding-window anomaly detector.
1312    #[serde(default)]
1313    pub anomaly: AnomalyConfig,
1314    /// Tool result cache.
1315    #[serde(default)]
1316    pub result_cache: ResultCacheConfig,
1317    /// Think-Augmented Function Calling.
1318    #[serde(default)]
1319    pub tafc: TafcConfig,
1320    /// Tool dependency graph.
1321    #[serde(default)]
1322    pub dependencies: DependencyConfig,
1323    /// Error retry configuration.
1324    #[serde(default)]
1325    pub retry: RetryConfig,
1326    /// Declarative policy compiler for tool call authorization.
1327    #[serde(default)]
1328    pub policy: PolicyConfig,
1329    /// LLM-based adversarial policy agent.
1330    #[serde(default)]
1331    pub adversarial_policy: AdversarialPolicyConfig,
1332    /// Utility-guided tool dispatch gate.
1333    #[serde(default)]
1334    pub utility: UtilityScoringConfig,
1335    /// Per-path read allow/deny sandbox for the file tool.
1336    #[serde(default)]
1337    pub file: FileConfig,
1338    /// OAP declarative pre-action authorization.
1339    #[serde(default)]
1340    pub authorization: AuthorizationConfig,
1341    /// Maximum tool calls allowed per agent session. `None` = unlimited.
1342    #[serde(default)]
1343    pub max_tool_calls_per_session: Option<u32>,
1344    /// Speculative tool execution configuration.
1345    #[serde(default)]
1346    pub speculative: SpeculativeConfig,
1347    /// OS-level subprocess sandbox configuration.
1348    #[serde(default)]
1349    pub sandbox: SandboxConfig,
1350    /// Egress network event logging configuration.
1351    #[serde(default)]
1352    pub egress: EgressConfig,
1353    /// TACO self-evolving tool output compression configuration.
1354    #[serde(default)]
1355    pub compression: ToolCompressionConfig,
1356}
1357
1358impl Default for ToolsConfig {
1359    fn default() -> Self {
1360        Self {
1361            enabled: true,
1362            summarize_output: true,
1363            shell: ShellConfig::default(),
1364            scrape: ScrapeConfig::default(),
1365            audit: AuditConfig::default(),
1366            permissions: None,
1367            filters: FilterConfig::default(),
1368            overflow: OverflowConfig::default(),
1369            anomaly: AnomalyConfig::default(),
1370            result_cache: ResultCacheConfig::default(),
1371            tafc: TafcConfig::default(),
1372            dependencies: DependencyConfig::default(),
1373            retry: RetryConfig::default(),
1374            policy: PolicyConfig::default(),
1375            adversarial_policy: AdversarialPolicyConfig::default(),
1376            utility: UtilityScoringConfig::default(),
1377            file: FileConfig::default(),
1378            authorization: AuthorizationConfig::default(),
1379            max_tool_calls_per_session: None,
1380            speculative: SpeculativeConfig::default(),
1381            sandbox: SandboxConfig::default(),
1382            egress: EgressConfig::default(),
1383            compression: ToolCompressionConfig::default(),
1384        }
1385    }
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390    use super::*;
1391
1392    #[test]
1393    fn deserialize_default_config() {
1394        let toml_str = r#"
1395            enabled = true
1396
1397            [shell]
1398            timeout = 60
1399            blocked_commands = ["rm -rf /", "sudo"]
1400        "#;
1401
1402        let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1403        assert!(config.enabled);
1404        assert_eq!(config.shell.timeout, 60);
1405        assert_eq!(config.shell.blocked_commands.len(), 2);
1406    }
1407
1408    #[test]
1409    fn empty_blocked_commands() {
1410        let config: ToolsConfig = toml::from_str(r"[shell]\ntimeout = 30\n").unwrap_or_default();
1411        assert!(config.enabled);
1412    }
1413
1414    #[test]
1415    fn default_tools_config() {
1416        let config = ToolsConfig::default();
1417        assert!(config.enabled);
1418        assert!(config.summarize_output);
1419        assert_eq!(config.shell.timeout, 30);
1420        assert!(config.shell.blocked_commands.is_empty());
1421        assert!(config.audit.enabled);
1422    }
1423
1424    #[test]
1425    fn audit_destination_serde_roundtrip() {
1426        let cases = [
1427            ("\"stdout\"", AuditDestination::Stdout),
1428            ("\"stderr\"", AuditDestination::Stderr),
1429            (
1430                "\"/var/log/audit.log\"",
1431                AuditDestination::File("/var/log/audit.log".into()),
1432            ),
1433        ];
1434        for (json_str, expected) in cases {
1435            let got: AuditDestination = serde_json::from_str(json_str).unwrap();
1436            assert_eq!(got, expected);
1437            let serialized = serde_json::to_string(&got).unwrap();
1438            let roundtrip: AuditDestination = serde_json::from_str(&serialized).unwrap();
1439            assert_eq!(roundtrip, expected);
1440        }
1441    }
1442
1443    #[test]
1444    fn audit_destination_toml_in_config() {
1445        let cases = [
1446            (
1447                r#"[audit]
1448destination = "stdout""#,
1449                AuditDestination::Stdout,
1450            ),
1451            (
1452                r#"[audit]
1453destination = "stderr""#,
1454                AuditDestination::Stderr,
1455            ),
1456            (
1457                r#"[audit]
1458destination = "/var/log/zeph-audit.log""#,
1459                AuditDestination::File("/var/log/zeph-audit.log".into()),
1460            ),
1461        ];
1462        for (toml_str, expected) in cases {
1463            let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1464            assert_eq!(config.audit.destination, expected);
1465        }
1466    }
1467}