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    /// Provider name for LLM-assisted policy checks. Empty = disabled.
258    #[serde(default)]
259    pub policy_provider: ProviderName,
260}
261
262/// A single policy rule as read from TOML.
263#[derive(Debug, Clone, Deserialize, Serialize)]
264pub struct PolicyRuleConfig {
265    /// Effect when the rule matches.
266    pub effect: PolicyEffect,
267    /// Glob pattern matching the tool id. Required.
268    pub tool: String,
269    /// Path globs matched against path-like params. Rule fires if ANY path matches.
270    #[serde(default)]
271    pub paths: Vec<String>,
272    /// Env var names that must all be present in the policy context.
273    #[serde(default)]
274    pub env: Vec<String>,
275    /// Minimum required trust level (rule fires only when context trust <= threshold).
276    pub trust_level: Option<SkillTrustLevel>,
277    /// Regex matched against individual string param values.
278    pub args_match: Option<String>,
279    /// Named capabilities associated with this rule.
280    #[serde(default)]
281    pub capabilities: Vec<String>,
282}
283
284// ── Sandbox ──────────────────────────────────────────────────────────────────
285
286/// Baseline restriction profile for the OS-level sandbox.
287#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
288#[serde(rename_all = "kebab-case")]
289#[non_exhaustive]
290pub enum SandboxProfile {
291    /// Read-only to `allow_read` paths, no writes, no network.
292    ReadOnly,
293    /// Read/write to configured paths; network egress blocked.
294    #[default]
295    Workspace,
296    /// Workspace-level filesystem access plus unrestricted network egress.
297    #[serde(rename = "network-allow-all", alias = "network")]
298    NetworkAllowAll,
299    /// Sandbox disabled. The subprocess inherits the parent's full capabilities.
300    Off,
301}
302
303fn default_sandbox_profile() -> SandboxProfile {
304    SandboxProfile::Workspace
305}
306
307/// Backend used to enforce OS-level sandboxing.
308///
309/// Serialises with `kebab-case` names so TOML values match the original string convention
310/// (`"auto"`, `"seatbelt"`, `"landlock-bwrap"`, `"noop"`).
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
312#[serde(rename_all = "kebab-case")]
313#[non_exhaustive]
314pub enum SandboxBackend {
315    /// Automatically select the best available backend for the current OS.
316    #[default]
317    Auto,
318    /// macOS `sandbox-exec` (Seatbelt) profile.
319    Seatbelt,
320    /// Linux Landlock + bubblewrap combination.
321    LandlockBwrap,
322    /// Disable sandboxing (testing / unsupported platforms).
323    Noop,
324}
325
326/// OS-level subprocess sandbox configuration (`[tools.sandbox]` TOML section).
327#[derive(Debug, Clone, Deserialize, Serialize)]
328pub struct SandboxConfig {
329    /// Enable OS-level sandbox. Default: `false`.
330    #[serde(default)]
331    pub enabled: bool,
332    /// Enforcement profile controlling the baseline restrictions.
333    #[serde(default = "default_sandbox_profile")]
334    pub profile: SandboxProfile,
335    /// Additional paths granted read access.
336    #[serde(default)]
337    pub allow_read: Vec<PathBuf>,
338    /// Additional paths granted write access.
339    #[serde(default)]
340    pub allow_write: Vec<PathBuf>,
341    /// When `true`, sandbox initialization failure aborts startup (fail-closed). Default: `true`.
342    #[serde(default = "default_true")]
343    pub strict: bool,
344    /// OS backend used to enforce sandboxing.
345    ///
346    /// Accepts `"auto"`, `"seatbelt"`, `"landlock-bwrap"`, or `"noop"` in TOML.
347    #[serde(default)]
348    pub backend: SandboxBackend,
349    /// Hostnames denied network egress from sandboxed subprocesses.
350    #[serde(default)]
351    pub denied_domains: Vec<String>,
352    /// When `true`, failure to activate an effective OS sandbox aborts startup.
353    #[serde(default)]
354    pub fail_if_unavailable: bool,
355}
356
357impl Default for SandboxConfig {
358    fn default() -> Self {
359        Self {
360            enabled: false,
361            profile: default_sandbox_profile(),
362            allow_read: Vec::new(),
363            allow_write: Vec::new(),
364            strict: true,
365            backend: SandboxBackend::Auto,
366            denied_domains: Vec::new(),
367            fail_if_unavailable: false,
368        }
369    }
370}
371
372// ── Output filter config ─────────────────────────────────────────────────────
373
374/// Configuration for tool output security filter.
375#[derive(Debug, Clone, Deserialize, Serialize)]
376pub struct SecurityFilterConfig {
377    /// Enable security filtering. Default: `true`.
378    #[serde(default = "default_true")]
379    pub enabled: bool,
380    /// Additional regex patterns to block in tool output.
381    #[serde(default)]
382    pub extra_patterns: Vec<String>,
383}
384
385impl Default for SecurityFilterConfig {
386    fn default() -> Self {
387        Self {
388            enabled: true,
389            extra_patterns: Vec::new(),
390        }
391    }
392}
393
394/// Configuration for output filters.
395#[derive(Debug, Clone, Deserialize, Serialize)]
396pub struct FilterConfig {
397    /// Master switch for output filtering. Default: `true`.
398    #[serde(default = "default_true")]
399    pub enabled: bool,
400    /// Security filter settings.
401    #[serde(default)]
402    pub security: SecurityFilterConfig,
403    /// Directory containing a `filters.toml` override file.
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub filters_path: Option<PathBuf>,
406}
407
408impl Default for FilterConfig {
409    fn default() -> Self {
410        Self {
411            enabled: true,
412            security: SecurityFilterConfig::default(),
413            filters_path: None,
414        }
415    }
416}
417
418// ── ToolsConfig sub-types ────────────────────────────────────────────────────
419
420fn default_overflow_threshold() -> usize {
421    50_000
422}
423
424fn default_retention_days() -> u64 {
425    7
426}
427
428fn default_max_overflow_bytes() -> usize {
429    10 * 1024 * 1024
430}
431
432/// Configuration for large tool response offload to `SQLite`.
433#[derive(Debug, Clone, Deserialize, Serialize)]
434pub struct OverflowConfig {
435    /// Character threshold above which tool output is offloaded. Default: `50000`.
436    #[serde(default = "default_overflow_threshold")]
437    pub threshold: usize,
438    /// Days to retain offloaded entries. Default: `7`.
439    #[serde(default = "default_retention_days")]
440    pub retention_days: u64,
441    /// Maximum bytes per overflow entry. `0` means unlimited. Default: `10 MiB`.
442    #[serde(default = "default_max_overflow_bytes")]
443    pub max_overflow_bytes: usize,
444}
445
446impl Default for OverflowConfig {
447    fn default() -> Self {
448        Self {
449            threshold: default_overflow_threshold(),
450            retention_days: default_retention_days(),
451            max_overflow_bytes: default_max_overflow_bytes(),
452        }
453    }
454}
455
456fn default_anomaly_window() -> usize {
457    10
458}
459
460fn default_anomaly_error_threshold() -> f64 {
461    0.5
462}
463
464fn default_anomaly_critical_threshold() -> f64 {
465    0.8
466}
467
468/// Configuration for the sliding-window anomaly detector.
469#[derive(Debug, Clone, Deserialize, Serialize)]
470pub struct AnomalyConfig {
471    /// Enable the anomaly detector. Default: `true`.
472    #[serde(default = "default_true")]
473    pub enabled: bool,
474    /// Number of recent tool calls in the sliding window. Default: `10`.
475    #[serde(default = "default_anomaly_window")]
476    pub window_size: usize,
477    /// Error-rate fraction triggering a WARN. Default: `0.5`.
478    #[serde(default = "default_anomaly_error_threshold")]
479    pub error_threshold: f64,
480    /// Error-rate fraction triggering a CRIT. Default: `0.8`.
481    #[serde(default = "default_anomaly_critical_threshold")]
482    pub critical_threshold: f64,
483    /// Emit a WARN when a reasoning model produces a quality failure. Default: `true`.
484    #[serde(default = "default_true")]
485    pub reasoning_model_warning: bool,
486}
487
488impl Default for AnomalyConfig {
489    fn default() -> Self {
490        Self {
491            enabled: true,
492            window_size: default_anomaly_window(),
493            error_threshold: default_anomaly_error_threshold(),
494            critical_threshold: default_anomaly_critical_threshold(),
495            reasoning_model_warning: true,
496        }
497    }
498}
499
500fn default_cache_ttl_secs() -> u64 {
501    300
502}
503
504/// Configuration for the tool result cache.
505#[derive(Debug, Clone, Deserialize, Serialize)]
506pub struct ResultCacheConfig {
507    /// Whether caching is enabled. Default: `true`.
508    #[serde(default = "default_true")]
509    pub enabled: bool,
510    /// Time-to-live in seconds. `0` means entries never expire. Default: `300`.
511    #[serde(default = "default_cache_ttl_secs")]
512    pub ttl_secs: u64,
513}
514
515impl Default for ResultCacheConfig {
516    fn default() -> Self {
517        Self {
518            enabled: true,
519            ttl_secs: default_cache_ttl_secs(),
520        }
521    }
522}
523
524fn default_tafc_complexity_threshold() -> f64 {
525    0.6
526}
527
528/// Configuration for Think-Augmented Function Calling (TAFC).
529#[derive(Debug, Clone, Deserialize, Serialize)]
530pub struct TafcConfig {
531    /// Enable TAFC schema augmentation. Default: `false`.
532    #[serde(default)]
533    pub enabled: bool,
534    /// Complexity threshold tau in [0.0, 1.0]; tools >= tau are augmented. Default: `0.6`.
535    #[serde(default = "default_tafc_complexity_threshold")]
536    pub complexity_threshold: f64,
537}
538
539impl Default for TafcConfig {
540    fn default() -> Self {
541        Self {
542            enabled: false,
543            complexity_threshold: default_tafc_complexity_threshold(),
544        }
545    }
546}
547
548impl TafcConfig {
549    /// Validate and clamp `complexity_threshold` to [0.0, 1.0]. Resets NaN/Infinity to 0.6.
550    #[must_use]
551    pub fn validated(mut self) -> Self {
552        if self.complexity_threshold.is_finite() {
553            self.complexity_threshold = self.complexity_threshold.clamp(0.0, 1.0);
554        } else {
555            self.complexity_threshold = 0.6;
556        }
557        self
558    }
559}
560
561fn default_utility_exempt_tools() -> Vec<String> {
562    vec!["invoke_skill".to_string(), "load_skill".to_string()]
563}
564
565fn default_utility_threshold() -> f32 {
566    0.1
567}
568
569fn default_utility_gain_weight() -> f32 {
570    1.0
571}
572
573fn default_utility_cost_weight() -> f32 {
574    0.5
575}
576
577fn default_utility_redundancy_weight() -> f32 {
578    0.3
579}
580
581fn default_utility_uncertainty_bonus() -> f32 {
582    0.2
583}
584
585/// Configuration for utility-guided tool dispatch.
586#[derive(Debug, Clone, Deserialize, Serialize)]
587#[serde(default)]
588pub struct UtilityScoringConfig {
589    /// Enable utility-guided gating. Default: `false`.
590    pub enabled: bool,
591    /// Minimum utility score required to execute a tool call. Default: `0.1`.
592    #[serde(default = "default_utility_threshold")]
593    pub threshold: f32,
594    /// Weight for the estimated gain component. Must be >= 0. Default: `1.0`.
595    #[serde(default = "default_utility_gain_weight")]
596    pub gain_weight: f32,
597    /// Weight for the step cost component. Must be >= 0. Default: `0.5`.
598    #[serde(default = "default_utility_cost_weight")]
599    pub cost_weight: f32,
600    /// Weight for the redundancy penalty. Must be >= 0. Default: `0.3`.
601    #[serde(default = "default_utility_redundancy_weight")]
602    pub redundancy_weight: f32,
603    /// Weight for the exploration bonus. Must be >= 0. Default: `0.2`.
604    #[serde(default = "default_utility_uncertainty_bonus")]
605    pub uncertainty_bonus: f32,
606    /// Tool names that bypass the utility gate unconditionally.
607    #[serde(default = "default_utility_exempt_tools")]
608    pub exempt_tools: Vec<String>,
609    /// Consecutive low-utility calls before early-stopping the loop. 0 = disabled.
610    ///
611    /// Exempt tools (`invoke_skill`, `load_skill`) do not count toward this window.
612    /// The counter resets between outer loop iterations.
613    #[serde(default)]
614    pub utility_window: usize,
615    /// Tool names that always receive the `0.75` "direct action" gain tier, matching
616    /// `diagnostics`/`edit`/etc in the built-in `default_gain` table.
617    ///
618    /// Opt-in override for tool ids the built-in table has no entry for — most notably
619    /// MCP-registered tools, whose ids are `{server_id}_{name}` (see
620    /// `McpTool::sanitized_id`) and therefore never match a hardcoded name. Without an
621    /// entry here, such a tool falls to the generic `0.5` bucket and can stall behind a
622    /// `Retrieve -> redundant retry -> vetoed` cycle on its first call (#5659). Default:
623    /// empty (no behavior change for existing configs).
624    #[serde(default)]
625    pub high_gain_tools: Vec<String>,
626}
627
628impl Default for UtilityScoringConfig {
629    fn default() -> Self {
630        Self {
631            enabled: false,
632            threshold: default_utility_threshold(),
633            gain_weight: default_utility_gain_weight(),
634            cost_weight: default_utility_cost_weight(),
635            redundancy_weight: default_utility_redundancy_weight(),
636            uncertainty_bonus: default_utility_uncertainty_bonus(),
637            exempt_tools: default_utility_exempt_tools(),
638            utility_window: 0,
639            high_gain_tools: Vec::new(),
640        }
641    }
642}
643
644impl UtilityScoringConfig {
645    /// Validate that all weights and threshold are non-negative and finite.
646    ///
647    /// # Errors
648    ///
649    /// Returns a description of the first invalid field found.
650    #[must_use = "validation result must be checked"]
651    pub fn validate(&self) -> Result<(), String> {
652        let fields = [
653            ("threshold", self.threshold),
654            ("gain_weight", self.gain_weight),
655            ("cost_weight", self.cost_weight),
656            ("redundancy_weight", self.redundancy_weight),
657            ("uncertainty_bonus", self.uncertainty_bonus),
658        ];
659        for (name, val) in fields {
660            if !val.is_finite() {
661                return Err(format!("[tools.utility] {name} must be finite, got {val}"));
662            }
663            if val < 0.0 {
664                return Err(format!("[tools.utility] {name} must be >= 0, got {val}"));
665            }
666        }
667        Ok(())
668    }
669}
670
671/// Dependency specification for a single tool.
672#[derive(Debug, Clone, Default, Deserialize, Serialize)]
673pub struct ToolDependency {
674    /// Hard prerequisites: tool is hidden until ALL of these have completed successfully.
675    #[serde(default, skip_serializing_if = "Vec::is_empty")]
676    pub requires: Vec<String>,
677    /// Soft prerequisites: tool gets a similarity boost when these have completed.
678    #[serde(default, skip_serializing_if = "Vec::is_empty")]
679    pub prefers: Vec<String>,
680}
681
682fn default_boost_per_dep() -> f32 {
683    0.15
684}
685
686fn default_max_total_boost() -> f32 {
687    0.2
688}
689
690/// Configuration for the tool dependency graph feature.
691#[derive(Debug, Clone, Deserialize, Serialize)]
692pub struct DependencyConfig {
693    /// Whether dependency gating is enabled. Default: `false`.
694    #[serde(default)]
695    pub enabled: bool,
696    /// Similarity boost added per satisfied `prefers` dependency. Default: `0.15`.
697    #[serde(default = "default_boost_per_dep")]
698    pub boost_per_dep: f32,
699    /// Maximum total boost applied regardless of how many `prefers` deps are met. Default: `0.2`.
700    #[serde(default = "default_max_total_boost")]
701    pub max_total_boost: f32,
702    /// Per-tool dependency rules. Key is `tool_id`.
703    #[serde(default)]
704    pub rules: HashMap<String, ToolDependency>,
705}
706
707impl Default for DependencyConfig {
708    fn default() -> Self {
709        Self {
710            enabled: false,
711            boost_per_dep: default_boost_per_dep(),
712            max_total_boost: default_max_total_boost(),
713            rules: HashMap::new(),
714        }
715    }
716}
717
718fn default_retry_max_attempts() -> usize {
719    2
720}
721
722fn default_retry_base_ms() -> u64 {
723    500
724}
725
726fn default_retry_max_ms() -> u64 {
727    5_000
728}
729
730fn default_retry_budget_secs() -> u64 {
731    30
732}
733
734/// Configuration for tool error retry behavior.
735#[derive(Debug, Clone, Deserialize, Serialize)]
736pub struct RetryConfig {
737    /// Maximum retry attempts for transient errors per tool call. `0` = disabled.
738    #[serde(default = "default_retry_max_attempts")]
739    pub max_attempts: usize,
740    /// Base delay (ms) for exponential backoff.
741    #[serde(default = "default_retry_base_ms")]
742    pub base_ms: u64,
743    /// Maximum delay cap (ms) for exponential backoff.
744    #[serde(default = "default_retry_max_ms")]
745    pub max_ms: u64,
746    /// Maximum wall-clock time (seconds) for all retries of a single tool call. `0` = unlimited.
747    #[serde(default = "default_retry_budget_secs")]
748    pub budget_secs: u64,
749    /// Provider name for LLM-based parameter reformatting on `InvalidParameters`/`TypeMismatch`.
750    /// Empty string = disabled.
751    #[serde(default)]
752    pub parameter_reformat_provider: ProviderName,
753}
754
755impl Default for RetryConfig {
756    fn default() -> Self {
757        Self {
758            max_attempts: default_retry_max_attempts(),
759            base_ms: default_retry_base_ms(),
760            max_ms: default_retry_max_ms(),
761            budget_secs: default_retry_budget_secs(),
762            parameter_reformat_provider: ProviderName::default(),
763        }
764    }
765}
766
767fn default_adversarial_timeout_ms() -> u64 {
768    3_000
769}
770
771/// Configuration for the LLM-based adversarial policy agent.
772#[derive(Debug, Clone, Deserialize, Serialize)]
773pub struct AdversarialPolicyConfig {
774    /// Enable the adversarial policy agent. Default: `false`.
775    #[serde(default)]
776    pub enabled: bool,
777    /// Provider name for the policy validation LLM.
778    #[serde(default)]
779    pub policy_provider: ProviderName,
780    /// Path to a plain-text policy file.
781    pub policy_file: Option<String>,
782    /// Whether to allow tool calls when the policy LLM fails. Default: `false` (fail-closed).
783    #[serde(default)]
784    pub fail_open: bool,
785    /// Timeout in milliseconds for a single policy LLM call. Default: `3000`.
786    #[serde(default = "default_adversarial_timeout_ms")]
787    pub timeout_ms: u64,
788    /// Tool names always allowed through the adversarial policy gate.
789    #[serde(default = "AdversarialPolicyConfig::default_exempt_tools")]
790    pub exempt_tools: Vec<String>,
791}
792
793impl Default for AdversarialPolicyConfig {
794    fn default() -> Self {
795        Self {
796            enabled: false,
797            policy_provider: ProviderName::default(),
798            policy_file: None,
799            fail_open: false,
800            timeout_ms: default_adversarial_timeout_ms(),
801            exempt_tools: Self::default_exempt_tools(),
802        }
803    }
804}
805
806impl AdversarialPolicyConfig {
807    #[must_use]
808    pub fn default_exempt_tools() -> Vec<String> {
809        vec![
810            "memory_save".into(),
811            "memory_search".into(),
812            "read_overflow".into(),
813            "load_skill".into(),
814            "invoke_skill".into(),
815            "schedule_deferred".into(),
816            // Read-only scheduler intrinsic must never be blocked by the adversarial
817            // probe: it carries no side-effects and the embed provider may be unavailable.
818            "list_tasks".into(),
819        ]
820    }
821}
822
823/// Per-path read allow/deny sandbox for the file tool.
824///
825/// Evaluation order: deny-then-allow. If a path matches `deny_read` and does NOT
826/// match `allow_read`, access is denied. Empty `deny_read` means no read restrictions.
827#[derive(Debug, Clone, Default, Deserialize, Serialize)]
828pub struct FileConfig {
829    /// Glob patterns for paths denied for reading. Evaluated first.
830    #[serde(default)]
831    pub deny_read: Vec<String>,
832    /// Glob patterns for paths allowed for reading. Evaluated second (overrides deny).
833    #[serde(default)]
834    pub allow_read: Vec<String>,
835}
836
837/// OAP-style declarative authorization config.
838#[derive(Debug, Clone, Default, Deserialize, Serialize)]
839pub struct AuthorizationConfig {
840    /// Enable OAP authorization checks. Default: `false`.
841    #[serde(default)]
842    pub enabled: bool,
843    /// Per-tool authorization rules appended after `[tools.policy]` rules at startup.
844    #[serde(default)]
845    pub rules: Vec<PolicyRuleConfig>,
846}
847
848/// Audit log destination.
849///
850/// Deserializes from a string in TOML: `"stdout"`, `"stderr"`, or a file path.
851#[derive(Debug, Clone, PartialEq, Eq, Default)]
852#[non_exhaustive]
853pub enum AuditDestination {
854    /// Write audit entries to standard output.
855    #[default]
856    Stdout,
857    /// Write audit entries to standard error.
858    Stderr,
859    /// Write audit entries to the given file path (appended, mode 0o600).
860    File(std::path::PathBuf),
861}
862
863impl AuditDestination {
864    /// Returns `true` if the destination is `stdout`.
865    #[must_use]
866    pub fn is_stdout(&self) -> bool {
867        matches!(self, Self::Stdout)
868    }
869}
870
871impl serde::Serialize for AuditDestination {
872    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
873        match self {
874            Self::Stdout => s.serialize_str("stdout"),
875            Self::Stderr => s.serialize_str("stderr"),
876            Self::File(p) => s.serialize_str(&p.display().to_string()),
877        }
878    }
879}
880
881impl<'de> serde::Deserialize<'de> for AuditDestination {
882    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
883        let s = String::deserialize(d)?;
884        Ok(match s.as_str() {
885            "stdout" => Self::Stdout,
886            "stderr" => Self::Stderr,
887            path => Self::File(std::path::PathBuf::from(path)),
888        })
889    }
890}
891
892/// Configuration for audit logging of tool executions.
893#[derive(Debug, Deserialize, Serialize)]
894pub struct AuditConfig {
895    /// Enable audit logging. Default: `true`.
896    #[serde(default = "default_true")]
897    pub enabled: bool,
898    /// Log destination. Default: [`AuditDestination::Stdout`].
899    #[serde(default)]
900    pub destination: AuditDestination,
901    /// When `true`, log a per-tool risk summary at startup. Default: `false`.
902    #[serde(default)]
903    pub tool_risk_summary: bool,
904}
905
906impl Default for AuditConfig {
907    fn default() -> Self {
908        Self {
909            enabled: true,
910            destination: AuditDestination::default(),
911            tool_risk_summary: false,
912        }
913    }
914}
915
916fn default_timeout() -> u64 {
917    30
918}
919
920fn default_confirm_patterns() -> Vec<String> {
921    vec![
922        "rm ".into(),
923        "git push -f".into(),
924        "git push --force".into(),
925        "drop table".into(),
926        "drop database".into(),
927        "truncate ".into(),
928        "$(".into(),
929        "`".into(),
930        "<(".into(),
931        ">(".into(),
932        "<<<".into(),
933        "eval ".into(),
934    ]
935}
936
937fn default_max_background_runs() -> usize {
938    8
939}
940
941fn default_background_timeout_secs() -> u64 {
942    1800
943}
944
945fn default_max_checkpoints() -> usize {
946    20
947}
948
949/// Shell-specific configuration: timeout, command blocklist, and allowlist overrides.
950#[derive(Debug, Deserialize, Serialize)]
951#[allow(clippy::struct_excessive_bools)]
952pub struct ShellConfig {
953    /// Shell command timeout in seconds. Default: `30`.
954    #[serde(default = "default_timeout")]
955    pub timeout: u64,
956    /// Commands blocked from execution.
957    #[serde(default)]
958    pub blocked_commands: Vec<String>,
959    /// Commands explicitly allowed (overrides blocklist).
960    #[serde(default)]
961    pub allowed_commands: Vec<String>,
962    /// Filesystem paths the shell is permitted to access.
963    #[serde(default)]
964    pub allowed_paths: Vec<String>,
965    /// Allow outbound network from shell. Default: `true`.
966    #[serde(default = "default_true")]
967    pub allow_network: bool,
968    /// Patterns that trigger a confirmation prompt before execution.
969    #[serde(default = "default_confirm_patterns")]
970    pub confirm_patterns: Vec<String>,
971    /// Environment variable name prefixes to strip from subprocess environment.
972    #[serde(default = "ShellConfig::default_env_blocklist")]
973    pub env_blocklist: Vec<String>,
974    /// Enable transactional mode: snapshot files before write commands. Default: `false`.
975    #[serde(default)]
976    pub transactional: bool,
977    /// Glob patterns for paths eligible for snapshotting.
978    #[serde(default)]
979    pub transaction_scope: Vec<String>,
980    /// Automatically rollback when exit code >= 2. Default: `false`.
981    #[serde(default)]
982    pub auto_rollback: bool,
983    /// Exit codes that trigger auto-rollback.
984    #[serde(default)]
985    pub auto_rollback_exit_codes: Vec<i32>,
986    /// When `true`, snapshot failure aborts execution. Default: `false`.
987    #[serde(default)]
988    pub snapshot_required: bool,
989    /// Maximum cumulative bytes for transaction snapshots. `0` = unlimited.
990    #[serde(default)]
991    pub max_snapshot_bytes: u64,
992    /// Maximum concurrent background shell runs. Default: `8`.
993    #[serde(default = "default_max_background_runs")]
994    pub max_background_runs: usize,
995    /// Timeout in seconds for each background shell run. Default: `1800`.
996    #[serde(default = "default_background_timeout_secs")]
997    pub background_timeout_secs: u64,
998    /// Cumulative risk score threshold for multi-step chain blocking. Default: `0.7`.
999    ///
1000    /// When the `RiskChainAccumulator` (zeph-tools) exceeds this score within a single turn,
1001    /// the command is blocked. Set to `None` to use the built-in default of `0.7`.
1002    #[serde(default)]
1003    pub risk_chain_threshold: Option<f32>,
1004    /// Enable session-scoped checkpoint history for `/undo` and `/redo`. Default: `false`.
1005    ///
1006    /// When `true`, file snapshots are captured before each write command and stored
1007    /// in an in-memory stack for the duration of the session. Checkpoints are lost
1008    /// when the agent process exits.
1009    #[serde(default)]
1010    pub checkpoints_enabled: bool,
1011    /// Maximum number of checkpoints retained in the undo stack. Default: `20`.
1012    ///
1013    /// When the stack reaches this limit, the oldest entry is evicted to make room.
1014    /// Set to `0` for no limit (not recommended for long-running sessions).
1015    #[serde(default = "default_max_checkpoints")]
1016    pub max_checkpoints: usize,
1017}
1018
1019impl Default for ShellConfig {
1020    fn default() -> Self {
1021        Self {
1022            timeout: default_timeout(),
1023            blocked_commands: Vec::new(),
1024            allowed_commands: Vec::new(),
1025            allowed_paths: Vec::new(),
1026            allow_network: true,
1027            confirm_patterns: default_confirm_patterns(),
1028            env_blocklist: Self::default_env_blocklist(),
1029            transactional: false,
1030            transaction_scope: Vec::new(),
1031            auto_rollback: false,
1032            auto_rollback_exit_codes: Vec::new(),
1033            snapshot_required: false,
1034            max_snapshot_bytes: 0,
1035            max_background_runs: default_max_background_runs(),
1036            background_timeout_secs: default_background_timeout_secs(),
1037            risk_chain_threshold: None,
1038            checkpoints_enabled: false,
1039            max_checkpoints: default_max_checkpoints(),
1040        }
1041    }
1042}
1043
1044impl ShellConfig {
1045    /// Default environment variable prefixes to strip from subprocess environment.
1046    #[must_use]
1047    pub fn default_env_blocklist() -> Vec<String> {
1048        vec![
1049            "ZEPH_".into(),
1050            "AWS_".into(),
1051            "AZURE_".into(),
1052            "GCP_".into(),
1053            "GOOGLE_".into(),
1054            "OPENAI_".into(),
1055            "ANTHROPIC_".into(),
1056            "HF_".into(),
1057            "HUGGING".into(),
1058        ]
1059    }
1060}
1061
1062fn default_scrape_timeout() -> u64 {
1063    15
1064}
1065
1066fn default_max_body_bytes() -> usize {
1067    4_194_304
1068}
1069
1070fn default_ipi_filter_threshold() -> f32 {
1071    0.6
1072}
1073
1074/// Configuration for the web scrape tool.
1075#[derive(Debug, Deserialize, Serialize)]
1076pub struct ScrapeConfig {
1077    /// Timeout in seconds for scrape requests. Default: `15`.
1078    #[serde(default = "default_scrape_timeout")]
1079    pub timeout: u64,
1080    /// Maximum response body bytes. Default: `4 MiB`.
1081    #[serde(default = "default_max_body_bytes")]
1082    pub max_body_bytes: usize,
1083    /// Domain allowlist. Empty = all public domains allowed.
1084    #[serde(default)]
1085    pub allowed_domains: Vec<String>,
1086    /// Domain denylist. Always enforced, regardless of allowlist state.
1087    #[serde(default)]
1088    pub denied_domains: Vec<String>,
1089    /// IPI filter score threshold. Responses with score >= this value get a warning
1090    /// prepended and injection fragments replaced with `[FILTERED]`. Default: `0.6`.
1091    #[serde(default = "default_ipi_filter_threshold")]
1092    pub ipi_filter_threshold: f32,
1093}
1094
1095impl Default for ScrapeConfig {
1096    fn default() -> Self {
1097        Self {
1098            timeout: default_scrape_timeout(),
1099            max_body_bytes: default_max_body_bytes(),
1100            allowed_domains: Vec::new(),
1101            denied_domains: Vec::new(),
1102            ipi_filter_threshold: default_ipi_filter_threshold(),
1103        }
1104    }
1105}
1106
1107/// Speculative tool execution mode.
1108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
1109#[serde(rename_all = "kebab-case")]
1110#[non_exhaustive]
1111pub enum SpeculationMode {
1112    /// No speculation; uses existing synchronous path.
1113    #[default]
1114    Off,
1115    /// LLM-decoding level: fires tools when streaming partial JSON has all required fields.
1116    Decoding,
1117    /// Application-level pattern (PASTE): predicts top-K calls from `SQLite` history.
1118    Pattern,
1119    /// Both decoding and pattern speculation active.
1120    Both,
1121}
1122
1123/// Pattern-based (PASTE) speculative execution config.
1124#[derive(Debug, Clone, Deserialize, Serialize)]
1125pub struct SpeculativePatternConfig {
1126    /// Enable PASTE pattern learning and prediction. Default: `false`.
1127    #[serde(default)]
1128    pub enabled: bool,
1129    /// Minimum observed occurrences before a prediction is issued.
1130    #[serde(default = "default_min_observations")]
1131    pub min_observations: u32,
1132    /// Exponential decay half-life in days for pattern scoring.
1133    #[serde(default = "default_half_life_days")]
1134    pub half_life_days: f64,
1135    /// LLM provider name for optional reranking. Empty = disabled.
1136    #[serde(default)]
1137    pub rerank_provider: ProviderName,
1138}
1139
1140fn default_min_observations() -> u32 {
1141    5
1142}
1143
1144fn default_half_life_days() -> f64 {
1145    14.0
1146}
1147
1148impl Default for SpeculativePatternConfig {
1149    fn default() -> Self {
1150        Self {
1151            enabled: false,
1152            min_observations: default_min_observations(),
1153            half_life_days: default_half_life_days(),
1154            rerank_provider: ProviderName::default(),
1155        }
1156    }
1157}
1158
1159/// Shell command regex allowlist for speculative execution.
1160#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1161pub struct SpeculativeAllowlistConfig {
1162    /// Regexes matched against the full `bash` command string.
1163    #[serde(default)]
1164    pub shell: Vec<String>,
1165}
1166
1167fn default_max_in_flight() -> usize {
1168    4
1169}
1170
1171fn default_confidence_threshold() -> f32 {
1172    0.55
1173}
1174
1175fn default_max_wasted_per_minute() -> u64 {
1176    100
1177}
1178
1179fn default_ttl_seconds() -> u64 {
1180    30
1181}
1182
1183/// Top-level configuration for speculative tool execution.
1184#[derive(Debug, Clone, Deserialize, Serialize)]
1185pub struct SpeculativeConfig {
1186    /// Speculation mode. Default: `off`.
1187    #[serde(default)]
1188    pub mode: SpeculationMode,
1189    /// Maximum concurrent in-flight speculative tasks.
1190    #[serde(default = "default_max_in_flight")]
1191    pub max_in_flight: usize,
1192    /// Minimum confidence score [0, 1] to dispatch a speculative task.
1193    #[serde(default = "default_confidence_threshold")]
1194    pub confidence_threshold: f32,
1195    /// Circuit-breaker: disable speculation for 60 s when wasted ms exceeds this per minute.
1196    #[serde(default = "default_max_wasted_per_minute")]
1197    pub max_wasted_per_minute: u64,
1198    /// Per-handle wall-clock TTL in seconds before the handle is cancelled.
1199    #[serde(default = "default_ttl_seconds")]
1200    pub ttl_seconds: u64,
1201    /// Emit `AuditEntry` for speculative dispatches. Default: `true`.
1202    #[serde(default = "default_true")]
1203    pub audit: bool,
1204    /// PASTE pattern learning config.
1205    #[serde(default)]
1206    pub pattern: SpeculativePatternConfig,
1207    /// Per-executor command allowlists.
1208    #[serde(default)]
1209    pub allowlist: SpeculativeAllowlistConfig,
1210}
1211
1212impl Default for SpeculativeConfig {
1213    fn default() -> Self {
1214        Self {
1215            mode: SpeculationMode::Off,
1216            max_in_flight: default_max_in_flight(),
1217            confidence_threshold: default_confidence_threshold(),
1218            max_wasted_per_minute: default_max_wasted_per_minute(),
1219            ttl_seconds: default_ttl_seconds(),
1220            audit: true,
1221            pattern: SpeculativePatternConfig::default(),
1222            allowlist: SpeculativeAllowlistConfig::default(),
1223        }
1224    }
1225}
1226
1227/// Configuration for egress network event logging.
1228#[derive(Debug, Clone, Deserialize, Serialize)]
1229#[serde(default)]
1230#[allow(clippy::struct_excessive_bools)]
1231pub struct EgressConfig {
1232    /// Master switch for egress event emission. Default: `true`.
1233    pub enabled: bool,
1234    /// Emit events for requests blocked by SSRF/domain/scheme checks. Default: `true`.
1235    pub log_blocked: bool,
1236    /// Include `response_bytes` in the JSONL record. Default: `true`.
1237    pub log_response_bytes: bool,
1238    /// Show real hostname in TUI egress panel. Default: `true`.
1239    pub log_hosts_to_tui: bool,
1240}
1241
1242impl Default for EgressConfig {
1243    fn default() -> Self {
1244        Self {
1245            enabled: true,
1246            log_blocked: true,
1247            log_response_bytes: true,
1248            log_hosts_to_tui: true,
1249        }
1250    }
1251}
1252
1253// ── ToolCompressionConfig ─────────────────────────────────────────────────────
1254
1255fn default_compression_min_lines() -> usize {
1256    10
1257}
1258
1259fn default_compression_max_rules() -> u32 {
1260    200
1261}
1262
1263fn default_regex_compile_timeout_ms() -> u64 {
1264    500
1265}
1266
1267fn default_evolution_min_interval_secs() -> u64 {
1268    3600
1269}
1270
1271/// TACO self-evolving tool output compression configuration (`[tools.compression]` TOML section).
1272///
1273/// When enabled, a `RuleBasedCompressor` is wrapped around the root tool executor.
1274/// Rules are loaded from the `compression_rules` `SQLite` table and optionally evolved by an
1275/// LLM provider specified in `evolution_provider`.
1276///
1277/// # Example (TOML)
1278///
1279/// ```toml
1280/// [tools.compression]
1281/// enabled = true
1282/// evolution_provider = "fast"
1283/// min_lines_to_compress = 15
1284/// ```
1285#[derive(Debug, Clone, Deserialize, Serialize)]
1286#[serde(default)]
1287pub struct ToolCompressionConfig {
1288    /// Enable rule-based tool output compression. Default: `false`.
1289    pub enabled: bool,
1290    /// Minimum output line count before compression is attempted. Default: `10`.
1291    #[serde(default = "default_compression_min_lines")]
1292    pub min_lines_to_compress: usize,
1293    /// LLM provider name for self-evolution. Empty string = evolution disabled. Default: `""`.
1294    #[serde(default)]
1295    pub evolution_provider: ProviderName,
1296    /// Minimum interval in seconds between self-evolution runs. Default: `3600`.
1297    #[serde(default = "default_evolution_min_interval_secs")]
1298    pub evolution_min_interval_secs: u64,
1299    /// Maximum number of rules to keep in the DB (prune lowest-hit rules above this). Default: `200`.
1300    #[serde(default = "default_compression_max_rules")]
1301    pub max_rules: u32,
1302    /// Timeout in milliseconds for safe regex compilation. Default: `500`.
1303    #[serde(default = "default_regex_compile_timeout_ms")]
1304    pub regex_compile_timeout_ms: u64,
1305}
1306
1307impl Default for ToolCompressionConfig {
1308    fn default() -> Self {
1309        Self {
1310            enabled: false,
1311            min_lines_to_compress: default_compression_min_lines(),
1312            evolution_provider: ProviderName::default(),
1313            evolution_min_interval_secs: default_evolution_min_interval_secs(),
1314            max_rules: default_compression_max_rules(),
1315            regex_compile_timeout_ms: default_regex_compile_timeout_ms(),
1316        }
1317    }
1318}
1319
1320// ── ToolsConfig ───────────────────────────────────────────────────────────────
1321
1322/// Top-level configuration for tool execution.
1323///
1324/// Deserialized from `[tools]` in TOML. The `permission_policy()` method (which constructs
1325/// a runtime `PermissionPolicy`) lives in `zeph-tools` as a free function to avoid
1326/// importing runtime types into this leaf crate.
1327#[derive(Debug, Deserialize, Serialize)]
1328pub struct ToolsConfig {
1329    /// Enable all tools. Default: `true`.
1330    #[serde(default = "default_true")]
1331    pub enabled: bool,
1332    /// Summarize long tool output before injection into context. Default: `true`.
1333    #[serde(default = "default_true")]
1334    pub summarize_output: bool,
1335    /// Shell tool configuration.
1336    #[serde(default)]
1337    pub shell: ShellConfig,
1338    /// Web scrape tool configuration.
1339    #[serde(default)]
1340    pub scrape: ScrapeConfig,
1341    /// Audit log configuration.
1342    #[serde(default)]
1343    pub audit: AuditConfig,
1344    /// Declarative permissions. Overrides legacy `shell.blocked_commands` when set.
1345    #[serde(default)]
1346    pub permissions: Option<PermissionsConfig>,
1347    /// Output filter configuration.
1348    #[serde(default)]
1349    pub filters: FilterConfig,
1350    /// Large response offload configuration.
1351    #[serde(default)]
1352    pub overflow: OverflowConfig,
1353    /// Sliding-window anomaly detector.
1354    #[serde(default)]
1355    pub anomaly: AnomalyConfig,
1356    /// Tool result cache.
1357    #[serde(default)]
1358    pub result_cache: ResultCacheConfig,
1359    /// Think-Augmented Function Calling.
1360    #[serde(default)]
1361    pub tafc: TafcConfig,
1362    /// Tool dependency graph.
1363    #[serde(default)]
1364    pub dependencies: DependencyConfig,
1365    /// Error retry configuration.
1366    #[serde(default)]
1367    pub retry: RetryConfig,
1368    /// Declarative policy compiler for tool call authorization.
1369    #[serde(default)]
1370    pub policy: PolicyConfig,
1371    /// LLM-based adversarial policy agent.
1372    #[serde(default)]
1373    pub adversarial_policy: AdversarialPolicyConfig,
1374    /// Utility-guided tool dispatch gate.
1375    #[serde(default)]
1376    pub utility: UtilityScoringConfig,
1377    /// Per-path read allow/deny sandbox for the file tool.
1378    #[serde(default)]
1379    pub file: FileConfig,
1380    /// OAP declarative pre-action authorization.
1381    #[serde(default)]
1382    pub authorization: AuthorizationConfig,
1383    /// Maximum tool calls allowed per agent session. `None` = unlimited.
1384    #[serde(default)]
1385    pub max_tool_calls_per_session: Option<u32>,
1386    /// Speculative tool execution configuration.
1387    #[serde(default)]
1388    pub speculative: SpeculativeConfig,
1389    /// OS-level subprocess sandbox configuration.
1390    #[serde(default)]
1391    pub sandbox: SandboxConfig,
1392    /// Egress network event logging configuration.
1393    #[serde(default)]
1394    pub egress: EgressConfig,
1395    /// TACO self-evolving tool output compression configuration.
1396    #[serde(default)]
1397    pub compression: ToolCompressionConfig,
1398}
1399
1400impl Default for ToolsConfig {
1401    fn default() -> Self {
1402        Self {
1403            enabled: true,
1404            summarize_output: true,
1405            shell: ShellConfig::default(),
1406            scrape: ScrapeConfig::default(),
1407            audit: AuditConfig::default(),
1408            permissions: None,
1409            filters: FilterConfig::default(),
1410            overflow: OverflowConfig::default(),
1411            anomaly: AnomalyConfig::default(),
1412            result_cache: ResultCacheConfig::default(),
1413            tafc: TafcConfig::default(),
1414            dependencies: DependencyConfig::default(),
1415            retry: RetryConfig::default(),
1416            policy: PolicyConfig::default(),
1417            adversarial_policy: AdversarialPolicyConfig::default(),
1418            utility: UtilityScoringConfig::default(),
1419            file: FileConfig::default(),
1420            authorization: AuthorizationConfig::default(),
1421            max_tool_calls_per_session: None,
1422            speculative: SpeculativeConfig::default(),
1423            sandbox: SandboxConfig::default(),
1424            egress: EgressConfig::default(),
1425            compression: ToolCompressionConfig::default(),
1426        }
1427    }
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432    use super::*;
1433
1434    #[test]
1435    fn deserialize_default_config() {
1436        let toml_str = r#"
1437            enabled = true
1438
1439            [shell]
1440            timeout = 60
1441            blocked_commands = ["rm -rf /", "sudo"]
1442        "#;
1443
1444        let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1445        assert!(config.enabled);
1446        assert_eq!(config.shell.timeout, 60);
1447        assert_eq!(config.shell.blocked_commands.len(), 2);
1448    }
1449
1450    #[test]
1451    fn empty_blocked_commands() {
1452        let config: ToolsConfig = toml::from_str(r"[shell]\ntimeout = 30\n").unwrap_or_default();
1453        assert!(config.enabled);
1454    }
1455
1456    #[test]
1457    fn default_tools_config() {
1458        let config = ToolsConfig::default();
1459        assert!(config.enabled);
1460        assert!(config.summarize_output);
1461        assert_eq!(config.shell.timeout, 30);
1462        assert!(config.shell.blocked_commands.is_empty());
1463        assert!(config.audit.enabled);
1464    }
1465
1466    #[test]
1467    fn audit_destination_serde_roundtrip() {
1468        let cases = [
1469            ("\"stdout\"", AuditDestination::Stdout),
1470            ("\"stderr\"", AuditDestination::Stderr),
1471            (
1472                "\"/var/log/audit.log\"",
1473                AuditDestination::File("/var/log/audit.log".into()),
1474            ),
1475        ];
1476        for (json_str, expected) in cases {
1477            let got: AuditDestination = serde_json::from_str(json_str).unwrap();
1478            assert_eq!(got, expected);
1479            let serialized = serde_json::to_string(&got).unwrap();
1480            let roundtrip: AuditDestination = serde_json::from_str(&serialized).unwrap();
1481            assert_eq!(roundtrip, expected);
1482        }
1483    }
1484
1485    #[test]
1486    fn audit_destination_toml_in_config() {
1487        let cases = [
1488            (
1489                r#"[audit]
1490destination = "stdout""#,
1491                AuditDestination::Stdout,
1492            ),
1493            (
1494                r#"[audit]
1495destination = "stderr""#,
1496                AuditDestination::Stderr,
1497            ),
1498            (
1499                r#"[audit]
1500destination = "/var/log/zeph-audit.log""#,
1501                AuditDestination::File("/var/log/zeph-audit.log".into()),
1502            ),
1503        ];
1504        for (toml_str, expected) in cases {
1505            let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1506            assert_eq!(config.audit.destination, expected);
1507        }
1508    }
1509
1510    #[test]
1511    fn policy_provider_serde_roundtrip() {
1512        let toml_str = r#"
1513            [policy]
1514            enabled = true
1515            policy_provider = "my-llm"
1516        "#;
1517        let config: ToolsConfig = toml::from_str(toml_str).unwrap();
1518        assert_eq!(config.policy.policy_provider.as_str(), "my-llm");
1519
1520        let json = serde_json::to_string(&config.policy).unwrap();
1521        let back: PolicyConfig = serde_json::from_str(&json).unwrap();
1522        assert_eq!(back.policy_provider.as_str(), "my-llm");
1523    }
1524
1525    #[test]
1526    fn policy_provider_default_is_empty() {
1527        let config = PolicyConfig::default();
1528        assert!(config.policy_provider.is_empty());
1529    }
1530
1531    #[test]
1532    fn utility_window_default_is_zero() {
1533        let config = UtilityScoringConfig::default();
1534        assert_eq!(config.utility_window, 0);
1535    }
1536
1537    #[test]
1538    fn utility_window_serde_roundtrip() {
1539        #[derive(serde::Deserialize)]
1540        struct Wrapper {
1541            utility_scoring: UtilityScoringConfig,
1542        }
1543
1544        let toml_str = "[utility_scoring]\nenabled = true\nutility_window = 3\n";
1545        let w: Wrapper = toml::from_str(toml_str).unwrap();
1546        assert_eq!(w.utility_scoring.utility_window, 3);
1547
1548        let json = serde_json::to_string(&w.utility_scoring).unwrap();
1549        let back: UtilityScoringConfig = serde_json::from_str(&json).unwrap();
1550        assert_eq!(back.utility_window, 3);
1551    }
1552
1553    #[test]
1554    fn high_gain_tools_default_is_empty() {
1555        let config = UtilityScoringConfig::default();
1556        assert!(config.high_gain_tools.is_empty());
1557    }
1558
1559    #[test]
1560    fn high_gain_tools_serde_roundtrip() {
1561        #[derive(serde::Deserialize)]
1562        struct Wrapper {
1563            utility_scoring: UtilityScoringConfig,
1564        }
1565
1566        let toml_str =
1567            "[utility_scoring]\nenabled = true\nhigh_gain_tools = [\"github_create_issue\"]\n";
1568        let w: Wrapper = toml::from_str(toml_str).unwrap();
1569        assert_eq!(
1570            w.utility_scoring.high_gain_tools,
1571            vec!["github_create_issue"]
1572        );
1573
1574        let json = serde_json::to_string(&w.utility_scoring).unwrap();
1575        let back: UtilityScoringConfig = serde_json::from_str(&json).unwrap();
1576        assert_eq!(back.high_gain_tools, vec!["github_create_issue"]);
1577    }
1578
1579    #[test]
1580    fn adversarial_policy_provider_default_is_empty() {
1581        let config = AdversarialPolicyConfig::default();
1582        assert!(config.policy_provider.is_empty());
1583    }
1584
1585    #[test]
1586    fn adversarial_policy_provider_serde_roundtrip() {
1587        #[derive(serde::Deserialize)]
1588        struct Wrapper {
1589            adversarial_policy: AdversarialPolicyConfig,
1590        }
1591        let toml_str = r#"
1592            [adversarial_policy]
1593            enabled = true
1594            policy_provider = "fast-llm"
1595        "#;
1596        let w: Wrapper = toml::from_str(toml_str).unwrap();
1597        assert_eq!(w.adversarial_policy.policy_provider.as_str(), "fast-llm");
1598
1599        let json = serde_json::to_string(&w.adversarial_policy).unwrap();
1600        let back: AdversarialPolicyConfig = serde_json::from_str(&json).unwrap();
1601        assert_eq!(back.policy_provider.as_str(), "fast-llm");
1602    }
1603
1604    #[test]
1605    fn adversarial_policy_provider_empty_when_omitted() {
1606        #[derive(serde::Deserialize)]
1607        struct Wrapper {
1608            adversarial_policy: AdversarialPolicyConfig,
1609        }
1610        let toml_str = r"
1611            [adversarial_policy]
1612            enabled = true
1613        ";
1614        let w: Wrapper = toml::from_str(toml_str).unwrap();
1615        assert!(
1616            w.adversarial_policy.policy_provider.is_empty(),
1617            "omitted policy_provider must default to empty (→ primary provider used)"
1618        );
1619    }
1620}