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