Skip to main content

agentzero_config/
model.rs

1use agentzero_core::common::local_providers::is_local_provider;
2use anyhow::anyhow;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use tracing::warn;
6use url::Url;
7
8#[derive(Debug, Clone, Deserialize, Serialize, Default)]
9#[serde(default)]
10pub struct AgentZeroConfig {
11    pub provider: ProviderConfig,
12    pub memory: MemoryConfig,
13    pub agent: AgentSettings,
14    pub security: SecurityConfig,
15    pub autonomy: AutonomyConfig,
16    pub observability: ObservabilityConfig,
17    pub research: ResearchConfig,
18    pub runtime: RuntimeConfig,
19    pub browser: BrowserConfig,
20    pub http_request: HttpRequestConfig,
21    pub web_fetch: WebFetchConfig,
22    pub web_search: WebSearchConfig,
23    pub composio: ComposioConfig,
24    pub pushover: PushoverConfig,
25    pub cost: CostConfig,
26    pub identity: IdentityConfig,
27    pub multimodal: MultimodalConfig,
28    pub skills: SkillsConfig,
29    #[serde(alias = "provider_settings")]
30    pub provider_options: ProviderOptionsConfig,
31    pub gateway: GatewayConfig,
32    pub channels_config: ChannelsGlobalConfig,
33    pub query_classification: QueryClassificationConfig,
34    pub model_providers: HashMap<String, ModelProviderProfile>,
35    pub model_routes: Vec<ModelRoute>,
36    pub embedding_routes: Vec<EmbeddingRoute>,
37    pub agents: HashMap<String, DelegateAgentConfig>,
38    pub privacy: PrivacyConfig,
39}
40
41impl AgentZeroConfig {
42    pub fn validate(&self) -> anyhow::Result<()> {
43        if self.provider.kind.trim().is_empty() {
44            return Err(anyhow!("provider.kind must not be empty"));
45        }
46        if self.provider.base_url.trim().is_empty() {
47            return Err(anyhow!("provider.base_url must not be empty"));
48        }
49        let provider_url = Url::parse(&self.provider.base_url)
50            .map_err(|_| anyhow!("provider.base_url must be a valid URL"))?;
51        if !matches!(provider_url.scheme(), "http" | "https") {
52            return Err(anyhow!("provider.base_url scheme must be http or https"));
53        }
54        if is_local_provider(&self.provider.kind) {
55            let is_localhost = matches!(
56                provider_url.host_str(),
57                Some("localhost") | Some("127.0.0.1") | Some("0.0.0.0") | Some("::1")
58            );
59            if !is_localhost {
60                if self.privacy.mode == "local_only" || self.privacy.enforce_local_provider {
61                    return Err(anyhow!(
62                        "privacy mode '{}' requires localhost base_url for local provider '{}', \
63                         but got '{}'. Use http://localhost:<port> or change your provider.",
64                        self.privacy.mode,
65                        self.provider.kind,
66                        self.provider.base_url,
67                    ));
68                }
69                tracing::warn!(
70                    "provider '{}' is a local provider but base_url '{}' is not localhost \
71                     — did you mean to use a different provider?",
72                    self.provider.kind,
73                    self.provider.base_url,
74                );
75            }
76        }
77        if self.provider.model.trim().is_empty() {
78            return Err(anyhow!("provider.model must not be empty"));
79        }
80        if !(0.0..=2.0).contains(&self.provider.default_temperature) {
81            return Err(anyhow!(
82                "provider.default_temperature must be between 0.0 and 2.0"
83            ));
84        }
85        if let Some(api) = &self.provider.provider_api {
86            if !matches!(api.as_str(), "openai-chat-completions" | "openai-responses") {
87                return Err(anyhow!(
88                    "provider.provider_api must be 'openai-chat-completions' or 'openai-responses'"
89                ));
90            }
91        }
92
93        if self.memory.backend.trim().is_empty() {
94            return Err(anyhow!("memory.backend must not be empty"));
95        }
96        match self.memory.backend.as_str() {
97            "sqlite" | "turso" => {}
98            other => {
99                return Err(anyhow!(
100                    "unsupported memory.backend `{other}`; expected `sqlite` or `turso`"
101                ));
102            }
103        }
104        if self.memory.sqlite_path.trim().is_empty() {
105            return Err(anyhow!("memory.sqlite_path must not be empty"));
106        }
107
108        if self.agent.max_tool_iterations == 0 {
109            return Err(anyhow!("agent.max_tool_iterations must be > 0"));
110        }
111        if self.agent.request_timeout_ms == 0 {
112            return Err(anyhow!("agent.request_timeout_ms must be > 0"));
113        }
114        if self.agent.memory_window_size == 0 {
115            return Err(anyhow!("agent.memory_window_size must be > 0"));
116        }
117        if self.agent.max_prompt_chars == 0 {
118            return Err(anyhow!("agent.max_prompt_chars must be > 0"));
119        }
120        if self.agent.hooks.timeout_ms == 0 {
121            return Err(anyhow!("agent.hooks.timeout_ms must be > 0"));
122        }
123        if !is_valid_hook_error_mode(&self.agent.hooks.on_error_default) {
124            return Err(anyhow!(
125                "agent.hooks.on_error_default must be one of: block, warn, ignore"
126            ));
127        }
128        if let Some(value) = &self.agent.hooks.on_error_low {
129            if !is_valid_hook_error_mode(value) {
130                return Err(anyhow!(
131                    "agent.hooks.on_error_low must be one of: block, warn, ignore"
132                ));
133            }
134        }
135        if let Some(value) = &self.agent.hooks.on_error_medium {
136            if !is_valid_hook_error_mode(value) {
137                return Err(anyhow!(
138                    "agent.hooks.on_error_medium must be one of: block, warn, ignore"
139                ));
140            }
141        }
142        if let Some(value) = &self.agent.hooks.on_error_high {
143            if !is_valid_hook_error_mode(value) {
144                return Err(anyhow!(
145                    "agent.hooks.on_error_high must be one of: block, warn, ignore"
146                ));
147            }
148        }
149
150        if self.security.allowed_root.trim().is_empty() {
151            return Err(anyhow!("security.allowed_root must not be empty"));
152        }
153        if self.security.allowed_commands.is_empty() && !self.agent.is_dev_mode() {
154            return Err(anyhow!("security.allowed_commands must not be empty"));
155        }
156
157        if self.security.read_file.max_read_bytes == 0 {
158            return Err(anyhow!("security.read_file.max_read_bytes must be > 0"));
159        }
160        if self.security.write_file.max_write_bytes == 0 {
161            return Err(anyhow!("security.write_file.max_write_bytes must be > 0"));
162        }
163
164        if self.security.shell.max_args == 0 {
165            return Err(anyhow!("security.shell.max_args must be > 0"));
166        }
167        if self.security.shell.max_arg_length == 0 {
168            return Err(anyhow!("security.shell.max_arg_length must be > 0"));
169        }
170        if self.security.shell.max_output_bytes == 0 {
171            return Err(anyhow!("security.shell.max_output_bytes must be > 0"));
172        }
173        if self.security.shell.forbidden_chars.is_empty() {
174            return Err(anyhow!("security.shell.forbidden_chars must not be empty"));
175        }
176
177        if self.security.mcp.enabled && self.security.mcp.allowed_servers.is_empty() {
178            return Err(anyhow!(
179                "security.mcp.allowed_servers must not be empty when MCP is enabled"
180            ));
181        }
182
183        if self.security.audit.enabled && self.security.audit.path.trim().is_empty() {
184            return Err(anyhow!(
185                "security.audit.path must not be empty when audit is enabled"
186            ));
187        }
188
189        // Gateway validation
190        if self.gateway.host.trim().is_empty() {
191            return Err(anyhow!("gateway.host must not be empty"));
192        }
193        if self.gateway.port == 0 {
194            return Err(anyhow!("gateway.port must be > 0"));
195        }
196        if !self.gateway.allow_public_bind
197            && self.gateway.host != "127.0.0.1"
198            && self.gateway.host != "::1"
199            && self.gateway.host != "localhost"
200        {
201            return Err(anyhow!(
202                "gateway.host `{}` binds publicly but gateway.allow_public_bind is false",
203                self.gateway.host
204            ));
205        }
206
207        // Autonomy validation
208        match self.autonomy.level.trim() {
209            "supervised" | "autonomous" | "semi" | "locked" => {}
210            other => {
211                return Err(anyhow!(
212                    "autonomy.level must be one of: supervised, autonomous, semi, locked; got `{other}`"
213                ));
214            }
215        }
216        if self.autonomy.max_actions_per_hour == 0 {
217            return Err(anyhow!("autonomy.max_actions_per_hour must be > 0"));
218        }
219        if self.autonomy.max_cost_per_day_cents == 0 {
220            return Err(anyhow!("autonomy.max_cost_per_day_cents must be > 0"));
221        }
222
223        // Privacy validation
224        match self.privacy.mode.as_str() {
225            "off" | "local_only" | "encrypted" | "full" => {}
226            other => {
227                return Err(anyhow!(
228                    "privacy.mode must be one of: off, local_only, encrypted, full; got `{other}`"
229                ));
230            }
231        }
232        if (self.privacy.mode == "local_only" || self.privacy.enforce_local_provider)
233            && !is_local_provider(&self.provider.kind)
234        {
235            return Err(anyhow!(
236                "privacy mode '{}' requires a local provider, but '{}' is a cloud provider; \
237                 use ollama, llamacpp, lmstudio, vllm, sglang, or another local provider",
238                self.privacy.mode,
239                self.provider.kind
240            ));
241        }
242        if self.privacy.noise.session_timeout_secs == 0 {
243            return Err(anyhow!("privacy.noise.session_timeout_secs must be > 0"));
244        }
245        if self.privacy.noise.max_sessions == 0 {
246            return Err(anyhow!("privacy.noise.max_sessions must be > 0"));
247        }
248        if self.privacy.sealed_envelopes.max_envelope_bytes == 0 {
249            return Err(anyhow!(
250                "privacy.sealed_envelopes.max_envelope_bytes must be > 0"
251            ));
252        }
253        match self.privacy.noise.handshake_pattern.as_str() {
254            "XX" | "IK" => {}
255            other => {
256                return Err(anyhow!(
257                    "privacy.noise.handshake_pattern must be XX or IK; got `{other}`"
258                ));
259            }
260        }
261        // Encrypted mode requires Noise to be enabled — without an encrypted
262        // transport, there is no mechanism to enforce the "encrypted" promise.
263        if self.privacy.mode == "encrypted" && !self.privacy.noise.enabled {
264            return Err(anyhow!(
265                "privacy.mode 'encrypted' requires privacy.noise.enabled = true; \
266                 either enable Noise or change the privacy mode"
267            ));
268        }
269
270        // Per-agent privacy boundary validation.
271        let valid_boundaries = ["", "inherit", "local_only", "encrypted_only", "any"];
272        for (name, agent) in &self.agents {
273            if !agent.privacy_boundary.is_empty()
274                && !valid_boundaries.contains(&agent.privacy_boundary.as_str())
275            {
276                return Err(anyhow!(
277                    "agents.{name}.privacy_boundary must be one of: inherit, local_only, \
278                     encrypted_only, any; got '{}'",
279                    agent.privacy_boundary
280                ));
281            }
282            // Agent boundary can't be more permissive than global privacy mode.
283            // Map global mode → boundary string for comparison.
284            let global_boundary = match self.privacy.mode.as_str() {
285                "local_only" => "local_only",
286                "encrypted" | "full" => "encrypted_only",
287                _ => "any",
288            };
289            if !agent.privacy_boundary.is_empty()
290                && agent.privacy_boundary != "inherit"
291                && global_boundary == "local_only"
292                && agent.privacy_boundary != "local_only"
293            {
294                return Err(anyhow!(
295                    "agents.{name}.privacy_boundary '{}' is more permissive than \
296                     global privacy mode '{}' (local_only)",
297                    agent.privacy_boundary,
298                    self.privacy.mode
299                ));
300            }
301        }
302
303        // Per-tool privacy boundary validation.
304        for (tool_name, boundary) in &self.security.tool_boundaries {
305            if !valid_boundaries.contains(&boundary.as_str()) {
306                return Err(anyhow!(
307                    "security.tool_boundaries.{tool_name} must be one of: inherit, local_only, \
308                     encrypted_only, any; got '{boundary}'"
309                ));
310            }
311        }
312
313        // Non-fatal validation warnings for routing config.
314        if self.query_classification.enabled && self.query_classification.rules.is_empty() {
315            warn!(
316                "query_classification is enabled but has no rules — classification will be a no-op"
317            );
318        }
319        for route in &self.embedding_routes {
320            if route.provider.trim().is_empty() {
321                warn!(hint = %route.hint, "embedding route has an empty provider field");
322            }
323            if route.model.trim().is_empty() {
324                warn!(hint = %route.hint, "embedding route has an empty model field");
325            }
326        }
327
328        Ok(())
329    }
330
331    /// Return a copy with secret fields masked for safe display.
332    pub fn masked(&self) -> Self {
333        let mut copy = self.clone();
334        let mask = |opt: &mut Option<String>| {
335            if opt.as_ref().is_some_and(|v| !v.is_empty()) {
336                *opt = Some("****".to_string());
337            }
338        };
339        mask(&mut copy.browser.computer_use.api_key);
340        mask(&mut copy.web_fetch.api_key);
341        mask(&mut copy.web_search.api_key);
342        mask(&mut copy.web_search.brave_api_key);
343        mask(&mut copy.web_search.perplexity_api_key);
344        mask(&mut copy.web_search.exa_api_key);
345        mask(&mut copy.web_search.jina_api_key);
346        mask(&mut copy.composio.api_key);
347        mask(&mut copy.skills.clawhub_token);
348        mask(&mut copy.gateway.node_control.auth_token);
349        for profile in copy.model_providers.values_mut() {
350            mask(&mut profile.api_key);
351        }
352        for route in &mut copy.model_routes {
353            mask(&mut route.api_key);
354        }
355        for route in &mut copy.embedding_routes {
356            mask(&mut route.api_key);
357        }
358        for agent in copy.agents.values_mut() {
359            mask(&mut agent.api_key);
360        }
361        copy
362    }
363}
364
365#[derive(Debug, Clone, Deserialize, Serialize)]
366#[serde(default)]
367pub struct ProviderConfig {
368    #[serde(alias = "name", alias = "default_provider")]
369    pub kind: String,
370    pub base_url: String,
371    pub model: String,
372    pub default_temperature: f64,
373    pub provider_api: Option<String>,
374    pub model_support_vision: Option<bool>,
375    #[serde(default)]
376    pub transport: TransportSettings,
377}
378
379/// Transport-level settings loaded from `[provider.transport]` in TOML.
380#[derive(Debug, Clone, Deserialize, Serialize)]
381#[serde(default)]
382pub struct TransportSettings {
383    pub timeout_ms: u64,
384    pub max_retries: usize,
385    pub circuit_breaker_threshold: u32,
386    pub circuit_breaker_reset_ms: u64,
387}
388
389impl Default for TransportSettings {
390    fn default() -> Self {
391        Self {
392            timeout_ms: 30_000,
393            max_retries: 3,
394            circuit_breaker_threshold: 5,
395            circuit_breaker_reset_ms: 30_000,
396        }
397    }
398}
399
400impl Default for ProviderConfig {
401    fn default() -> Self {
402        Self {
403            kind: "openrouter".to_string(),
404            base_url: "https://openrouter.ai/api".to_string(),
405            model: "anthropic/claude-sonnet-4-6".to_string(),
406            default_temperature: 0.7,
407            provider_api: None,
408            model_support_vision: None,
409            transport: TransportSettings::default(),
410        }
411    }
412}
413
414#[derive(Debug, Clone, Deserialize, Serialize)]
415#[serde(default)]
416pub struct MemoryConfig {
417    pub backend: String,
418    #[serde(alias = "path")]
419    pub sqlite_path: String,
420}
421
422impl Default for MemoryConfig {
423    fn default() -> Self {
424        Self {
425            backend: "sqlite".to_string(),
426            sqlite_path: default_sqlite_path(),
427        }
428    }
429}
430
431fn default_sqlite_path() -> String {
432    agentzero_core::common::paths::default_sqlite_path()
433        .map(|path| path.to_string_lossy().to_string())
434        .unwrap_or_else(|| "./agentzero.db".to_string())
435}
436
437fn is_valid_hook_error_mode(value: &str) -> bool {
438    matches!(value.trim(), "block" | "warn" | "ignore")
439}
440
441#[derive(Debug, Clone, Deserialize, Serialize)]
442#[serde(default)]
443pub struct AgentSettings {
444    pub max_tool_iterations: usize,
445    pub request_timeout_ms: u64,
446    #[serde(alias = "max_history_messages")]
447    pub memory_window_size: usize,
448    pub max_prompt_chars: usize,
449    pub mode: String,
450    pub hooks: HookSettings,
451    pub parallel_tools: bool,
452    pub tool_dispatcher: String,
453    pub compact_context: bool,
454    pub loop_detection_no_progress_threshold: usize,
455    pub loop_detection_ping_pong_cycles: usize,
456    pub loop_detection_failure_streak: usize,
457    /// Optional system prompt sent to the LLM at the start of each conversation.
458    pub system_prompt: Option<String>,
459}
460
461impl Default for AgentSettings {
462    fn default() -> Self {
463        Self {
464            max_tool_iterations: 20,
465            request_timeout_ms: 30_000,
466            memory_window_size: 50,
467            max_prompt_chars: 8_000,
468            mode: "development".to_string(),
469            hooks: HookSettings::default(),
470            parallel_tools: false,
471            tool_dispatcher: "auto".to_string(),
472            compact_context: true,
473            loop_detection_no_progress_threshold: 3,
474            loop_detection_ping_pong_cycles: 2,
475            loop_detection_failure_streak: 3,
476            system_prompt: None,
477        }
478    }
479}
480
481impl AgentSettings {
482    pub fn is_dev_mode(&self) -> bool {
483        matches!(self.mode.trim(), "dev" | "development")
484    }
485}
486
487#[derive(Debug, Clone, Deserialize, Serialize)]
488#[serde(default)]
489pub struct HookSettings {
490    pub enabled: bool,
491    pub timeout_ms: u64,
492    pub fail_closed: bool,
493    pub on_error_default: String,
494    pub on_error_low: Option<String>,
495    pub on_error_medium: Option<String>,
496    pub on_error_high: Option<String>,
497}
498
499impl Default for HookSettings {
500    fn default() -> Self {
501        Self {
502            enabled: false,
503            timeout_ms: 250,
504            fail_closed: false,
505            on_error_default: "warn".to_string(),
506            on_error_low: None,
507            on_error_medium: None,
508            on_error_high: None,
509        }
510    }
511}
512
513#[derive(Debug, Clone, Deserialize, Serialize)]
514#[serde(default)]
515pub struct SecurityConfig {
516    pub allowed_root: String,
517    pub allowed_commands: Vec<String>,
518    pub read_file: ReadFileConfig,
519    pub write_file: WriteFileConfig,
520    pub shell: ShellConfig,
521    pub mcp: McpConfig,
522    pub plugin: PluginConfig,
523    pub audit: AuditConfig,
524    pub url_access: UrlAccessConfig,
525    pub otp: OtpConfig,
526    pub estop: EstopConfig,
527    pub outbound_leak_guard: OutboundLeakGuardConfig,
528    pub perplexity_filter: PerplexityFilterConfig,
529    pub syscall_anomaly: SyscallAnomalyConfig,
530    /// Per-tool privacy boundaries. Keys are tool names, values are boundary
531    /// strings: "inherit", "local_only", "encrypted_only", "any".
532    #[serde(default)]
533    pub tool_boundaries: std::collections::HashMap<String, String>,
534}
535
536impl Default for SecurityConfig {
537    fn default() -> Self {
538        Self {
539            allowed_root: ".".to_string(),
540            allowed_commands: vec![
541                "ls".to_string(),
542                "pwd".to_string(),
543                "cat".to_string(),
544                "echo".to_string(),
545                "grep".to_string(),
546                "find".to_string(),
547                "head".to_string(),
548                "tail".to_string(),
549                "wc".to_string(),
550                "sort".to_string(),
551                "uniq".to_string(),
552                "diff".to_string(),
553                "file".to_string(),
554                "which".to_string(),
555                "basename".to_string(),
556                "dirname".to_string(),
557                "mkdir".to_string(),
558                "cp".to_string(),
559                "mv".to_string(),
560                "rm".to_string(),
561                "touch".to_string(),
562                "date".to_string(),
563                "env".to_string(),
564                "test".to_string(),
565                "tr".to_string(),
566                "cut".to_string(),
567                "xargs".to_string(),
568                "sed".to_string(),
569                "awk".to_string(),
570                "git".to_string(),
571                "cargo".to_string(),
572                "rustc".to_string(),
573                "npm".to_string(),
574                "node".to_string(),
575                "python3".to_string(),
576            ],
577            read_file: ReadFileConfig::default(),
578            write_file: WriteFileConfig::default(),
579            shell: ShellConfig::default(),
580            mcp: McpConfig::default(),
581            plugin: PluginConfig::default(),
582            audit: AuditConfig::default(),
583            url_access: UrlAccessConfig::default(),
584            otp: OtpConfig::default(),
585            estop: EstopConfig::default(),
586            outbound_leak_guard: OutboundLeakGuardConfig::default(),
587            perplexity_filter: PerplexityFilterConfig::default(),
588            syscall_anomaly: SyscallAnomalyConfig::default(),
589            tool_boundaries: std::collections::HashMap::new(),
590        }
591    }
592}
593
594#[derive(Debug, Clone, Deserialize, Serialize)]
595#[serde(default)]
596pub struct ReadFileConfig {
597    pub max_read_bytes: u64,
598    pub allow_binary: bool,
599}
600
601impl Default for ReadFileConfig {
602    fn default() -> Self {
603        Self {
604            max_read_bytes: 256 * 1024,
605            allow_binary: false,
606        }
607    }
608}
609
610#[derive(Debug, Clone, Deserialize, Serialize)]
611#[serde(default)]
612pub struct WriteFileConfig {
613    pub enabled: bool,
614    pub max_write_bytes: u64,
615}
616
617impl Default for WriteFileConfig {
618    fn default() -> Self {
619        Self {
620            enabled: false,
621            max_write_bytes: 64 * 1024,
622        }
623    }
624}
625
626#[derive(Debug, Clone, Deserialize, Serialize)]
627#[serde(default)]
628pub struct ShellConfig {
629    pub max_args: usize,
630    pub max_arg_length: usize,
631    pub max_output_bytes: usize,
632    pub forbidden_chars: String,
633    pub context_aware_parsing: bool,
634}
635
636impl Default for ShellConfig {
637    fn default() -> Self {
638        Self {
639            max_args: 32,
640            max_arg_length: 4096,
641            max_output_bytes: 65536,
642            forbidden_chars: ";&|><$`\n\r".to_string(),
643            context_aware_parsing: true,
644        }
645    }
646}
647
648#[derive(Debug, Clone, Deserialize, Serialize, Default)]
649#[serde(default)]
650pub struct McpConfig {
651    pub enabled: bool,
652    pub allowed_servers: Vec<String>,
653}
654
655#[derive(Debug, Clone, Deserialize, Serialize, Default)]
656#[serde(default)]
657pub struct PluginConfig {
658    /// Enable the process-based plugin tool (legacy).
659    pub enabled: bool,
660    /// Enable WASM plugin discovery and loading.
661    pub wasm_enabled: bool,
662    /// Override for the global plugin install directory.
663    /// Defaults to `{data_dir}/plugins/`.
664    pub global_plugin_dir: Option<String>,
665    /// Override for the project-level plugin directory.
666    /// Defaults to `{workspace}/.agentzero/plugins/`.
667    pub project_plugin_dir: Option<String>,
668    /// Override for the development plugin directory (CWD hot-reload).
669    /// Defaults to `{cwd}/plugins/`.
670    pub dev_plugin_dir: Option<String>,
671}
672
673#[derive(Debug, Clone, Deserialize, Serialize)]
674#[serde(default)]
675pub struct AuditConfig {
676    pub enabled: bool,
677    pub path: String,
678}
679
680impl Default for AuditConfig {
681    fn default() -> Self {
682        Self {
683            enabled: false,
684            path: "./agentzero-audit.log".to_string(),
685        }
686    }
687}
688
689// --- Phase A3: New config sections ---
690
691#[derive(Debug, Clone, Deserialize, Serialize)]
692#[serde(default)]
693pub struct AutonomyConfig {
694    pub level: String,
695    pub workspace_only: bool,
696    pub forbidden_paths: Vec<String>,
697    pub allowed_roots: Vec<String>,
698    pub auto_approve: Vec<String>,
699    pub always_ask: Vec<String>,
700    pub allow_sensitive_file_reads: bool,
701    pub allow_sensitive_file_writes: bool,
702    pub non_cli_excluded_tools: Vec<String>,
703    pub non_cli_approval_approvers: Vec<String>,
704    pub non_cli_natural_language_approval_mode: String,
705    pub non_cli_natural_language_approval_mode_by_channel: HashMap<String, String>,
706    pub max_actions_per_hour: u32,
707    pub max_cost_per_day_cents: u32,
708    pub require_approval_for_medium_risk: bool,
709    pub block_high_risk_commands: bool,
710}
711
712impl Default for AutonomyConfig {
713    fn default() -> Self {
714        Self {
715            level: "supervised".to_string(),
716            workspace_only: true,
717            forbidden_paths: vec![
718                "/etc".to_string(),
719                "/root".to_string(),
720                "/proc".to_string(),
721                "/sys".to_string(),
722                "~/.ssh".to_string(),
723                "~/.gnupg".to_string(),
724                "~/.aws".to_string(),
725            ],
726            allowed_roots: Vec::new(),
727            auto_approve: Vec::new(),
728            always_ask: Vec::new(),
729            allow_sensitive_file_reads: false,
730            allow_sensitive_file_writes: false,
731            non_cli_excluded_tools: Vec::new(),
732            non_cli_approval_approvers: Vec::new(),
733            non_cli_natural_language_approval_mode: "direct".to_string(),
734            non_cli_natural_language_approval_mode_by_channel: HashMap::new(),
735            max_actions_per_hour: 200,
736            max_cost_per_day_cents: 2000,
737            require_approval_for_medium_risk: true,
738            block_high_risk_commands: true,
739        }
740    }
741}
742
743#[derive(Debug, Clone, Deserialize, Serialize)]
744#[serde(default)]
745pub struct ObservabilityConfig {
746    pub backend: String,
747    pub otel_endpoint: String,
748    pub otel_service_name: String,
749    pub runtime_trace_mode: String,
750    pub runtime_trace_path: String,
751    pub runtime_trace_max_entries: usize,
752}
753
754impl Default for ObservabilityConfig {
755    fn default() -> Self {
756        Self {
757            backend: "none".to_string(),
758            otel_endpoint: "http://localhost:4318".to_string(),
759            otel_service_name: "agentzero".to_string(),
760            runtime_trace_mode: "none".to_string(),
761            runtime_trace_path: "state/runtime-trace.jsonl".to_string(),
762            runtime_trace_max_entries: 200,
763        }
764    }
765}
766
767#[derive(Debug, Clone, Deserialize, Serialize)]
768#[serde(default)]
769pub struct ResearchConfig {
770    pub enabled: bool,
771    pub trigger: String,
772    pub keywords: Vec<String>,
773    pub min_message_length: usize,
774    pub max_iterations: usize,
775    pub show_progress: bool,
776}
777
778impl Default for ResearchConfig {
779    fn default() -> Self {
780        Self {
781            enabled: false,
782            trigger: "never".to_string(),
783            keywords: vec![
784                "find".to_string(),
785                "search".to_string(),
786                "check".to_string(),
787                "investigate".to_string(),
788            ],
789            min_message_length: 50,
790            max_iterations: 5,
791            show_progress: true,
792        }
793    }
794}
795
796#[derive(Debug, Clone, Deserialize, Serialize)]
797#[serde(default)]
798pub struct RuntimeConfig {
799    pub kind: String,
800    pub reasoning_enabled: Option<bool>,
801    pub wasm: WasmRuntimeConfig,
802}
803
804impl Default for RuntimeConfig {
805    fn default() -> Self {
806        Self {
807            kind: "native".to_string(),
808            reasoning_enabled: None,
809            wasm: WasmRuntimeConfig::default(),
810        }
811    }
812}
813
814#[derive(Debug, Clone, Deserialize, Serialize)]
815#[serde(default)]
816pub struct WasmRuntimeConfig {
817    pub tools_dir: String,
818    pub fuel_limit: u64,
819    pub memory_limit_mb: u64,
820    pub max_module_size_mb: u64,
821    pub allow_workspace_read: bool,
822    pub allow_workspace_write: bool,
823    pub allowed_hosts: Vec<String>,
824    pub security: WasmSecurityConfig,
825}
826
827impl Default for WasmRuntimeConfig {
828    fn default() -> Self {
829        Self {
830            tools_dir: "tools/wasm".to_string(),
831            fuel_limit: 1_000_000,
832            memory_limit_mb: 64,
833            max_module_size_mb: 50,
834            allow_workspace_read: false,
835            allow_workspace_write: false,
836            allowed_hosts: Vec::new(),
837            security: WasmSecurityConfig::default(),
838        }
839    }
840}
841
842#[derive(Debug, Clone, Deserialize, Serialize)]
843#[serde(default)]
844pub struct WasmSecurityConfig {
845    pub require_workspace_relative_tools_dir: bool,
846    pub reject_symlink_modules: bool,
847    pub reject_symlink_tools_dir: bool,
848    pub strict_host_validation: bool,
849    pub capability_escalation_mode: String,
850    pub module_hash_policy: String,
851    pub module_sha256: HashMap<String, String>,
852}
853
854impl Default for WasmSecurityConfig {
855    fn default() -> Self {
856        Self {
857            require_workspace_relative_tools_dir: true,
858            reject_symlink_modules: true,
859            reject_symlink_tools_dir: true,
860            strict_host_validation: true,
861            capability_escalation_mode: "deny".to_string(),
862            module_hash_policy: "warn".to_string(),
863            module_sha256: HashMap::new(),
864        }
865    }
866}
867
868#[derive(Debug, Clone, Deserialize, Serialize)]
869#[serde(default)]
870pub struct BrowserConfig {
871    pub enabled: bool,
872    pub allowed_domains: Vec<String>,
873    pub browser_open: String,
874    pub session_name: Option<String>,
875    pub backend: String,
876    pub auto_backend_priority: Vec<String>,
877    pub agent_browser_command: String,
878    pub agent_browser_extra_args: Vec<String>,
879    pub agent_browser_timeout_ms: u64,
880    pub native_headless: bool,
881    pub native_webdriver_url: String,
882    pub native_chrome_path: Option<String>,
883    pub computer_use: ComputerUseConfig,
884}
885
886impl Default for BrowserConfig {
887    fn default() -> Self {
888        Self {
889            enabled: false,
890            allowed_domains: Vec::new(),
891            browser_open: "default".to_string(),
892            session_name: None,
893            backend: "agent_browser".to_string(),
894            auto_backend_priority: Vec::new(),
895            agent_browser_command: "agent-browser".to_string(),
896            agent_browser_extra_args: Vec::new(),
897            agent_browser_timeout_ms: 30_000,
898            native_headless: true,
899            native_webdriver_url: "http://127.0.0.1:9515".to_string(),
900            native_chrome_path: None,
901            computer_use: ComputerUseConfig::default(),
902        }
903    }
904}
905
906#[derive(Debug, Clone, Deserialize, Serialize)]
907#[serde(default)]
908pub struct ComputerUseConfig {
909    pub endpoint: String,
910    pub api_key: Option<String>,
911    pub timeout_ms: u64,
912    pub allow_remote_endpoint: bool,
913    pub window_allowlist: Vec<String>,
914    pub max_coordinate_x: Option<u32>,
915    pub max_coordinate_y: Option<u32>,
916}
917
918impl Default for ComputerUseConfig {
919    fn default() -> Self {
920        Self {
921            endpoint: "http://127.0.0.1:8787/v1/actions".to_string(),
922            api_key: None,
923            timeout_ms: 15_000,
924            allow_remote_endpoint: false,
925            window_allowlist: Vec::new(),
926            max_coordinate_x: None,
927            max_coordinate_y: None,
928        }
929    }
930}
931
932#[derive(Debug, Clone, Deserialize, Serialize)]
933#[serde(default)]
934pub struct HttpRequestConfig {
935    pub enabled: bool,
936    pub allowed_domains: Vec<String>,
937    pub max_response_size: usize,
938    pub timeout_secs: u64,
939    pub user_agent: String,
940    pub credential_profiles: HashMap<String, CredentialProfile>,
941}
942
943impl Default for HttpRequestConfig {
944    fn default() -> Self {
945        Self {
946            enabled: false,
947            allowed_domains: Vec::new(),
948            max_response_size: 1_000_000,
949            timeout_secs: 30,
950            user_agent: "AgentZero/1.0".to_string(),
951            credential_profiles: HashMap::new(),
952        }
953    }
954}
955
956#[derive(Debug, Clone, Deserialize, Serialize, Default)]
957#[serde(default)]
958pub struct CredentialProfile {
959    pub header_name: String,
960    pub env_var: String,
961    #[serde(default)]
962    pub value_prefix: String,
963}
964
965#[derive(Debug, Clone, Deserialize, Serialize)]
966#[serde(default)]
967pub struct WebFetchConfig {
968    pub enabled: bool,
969    pub provider: String,
970    pub api_key: Option<String>,
971    pub api_url: Option<String>,
972    pub allowed_domains: Vec<String>,
973    pub blocked_domains: Vec<String>,
974    pub max_response_size: usize,
975    pub timeout_secs: u64,
976    pub user_agent: String,
977}
978
979impl Default for WebFetchConfig {
980    fn default() -> Self {
981        Self {
982            enabled: false,
983            provider: "fast_html2md".to_string(),
984            api_key: None,
985            api_url: None,
986            allowed_domains: vec!["*".to_string()],
987            blocked_domains: Vec::new(),
988            max_response_size: 500_000,
989            timeout_secs: 30,
990            user_agent: "AgentZero/1.0".to_string(),
991        }
992    }
993}
994
995#[derive(Debug, Clone, Deserialize, Serialize)]
996#[serde(default)]
997pub struct WebSearchConfig {
998    pub enabled: bool,
999    pub provider: String,
1000    pub fallback_providers: Vec<String>,
1001    pub retries_per_provider: u32,
1002    pub retry_backoff_ms: u64,
1003    pub api_key: Option<String>,
1004    pub api_url: Option<String>,
1005    pub brave_api_key: Option<String>,
1006    pub perplexity_api_key: Option<String>,
1007    pub exa_api_key: Option<String>,
1008    pub jina_api_key: Option<String>,
1009    pub max_results: usize,
1010    pub timeout_secs: u64,
1011    pub user_agent: String,
1012    pub domain_filter: Vec<String>,
1013    pub language_filter: Vec<String>,
1014    pub country: Option<String>,
1015    pub recency_filter: Option<String>,
1016}
1017
1018impl Default for WebSearchConfig {
1019    fn default() -> Self {
1020        Self {
1021            enabled: false,
1022            provider: "duckduckgo".to_string(),
1023            fallback_providers: Vec::new(),
1024            retries_per_provider: 0,
1025            retry_backoff_ms: 250,
1026            api_key: None,
1027            api_url: None,
1028            brave_api_key: None,
1029            perplexity_api_key: None,
1030            exa_api_key: None,
1031            jina_api_key: None,
1032            max_results: 5,
1033            timeout_secs: 15,
1034            user_agent: "AgentZero/1.0".to_string(),
1035            domain_filter: Vec::new(),
1036            language_filter: Vec::new(),
1037            country: None,
1038            recency_filter: None,
1039        }
1040    }
1041}
1042
1043#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1044#[serde(default)]
1045pub struct ComposioConfig {
1046    pub enabled: bool,
1047    pub api_key: Option<String>,
1048    pub entity_id: String,
1049}
1050
1051#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1052#[serde(default)]
1053pub struct PushoverConfig {
1054    pub enabled: bool,
1055}
1056
1057#[derive(Debug, Clone, Deserialize, Serialize)]
1058#[serde(default)]
1059pub struct CostConfig {
1060    pub enabled: bool,
1061    pub daily_limit_usd: f64,
1062    pub monthly_limit_usd: f64,
1063    pub warn_at_percent: u32,
1064    pub allow_override: bool,
1065}
1066
1067impl Default for CostConfig {
1068    fn default() -> Self {
1069        Self {
1070            enabled: false,
1071            daily_limit_usd: 10.0,
1072            monthly_limit_usd: 100.0,
1073            warn_at_percent: 80,
1074            allow_override: false,
1075        }
1076    }
1077}
1078
1079#[derive(Debug, Clone, Deserialize, Serialize)]
1080#[serde(default)]
1081pub struct IdentityConfig {
1082    pub format: String,
1083    pub aieos_path: Option<String>,
1084    pub aieos_inline: Option<String>,
1085}
1086
1087impl Default for IdentityConfig {
1088    fn default() -> Self {
1089        Self {
1090            format: "openclaw".to_string(),
1091            aieos_path: None,
1092            aieos_inline: None,
1093        }
1094    }
1095}
1096
1097#[derive(Debug, Clone, Deserialize, Serialize)]
1098#[serde(default)]
1099pub struct MultimodalConfig {
1100    pub max_images: usize,
1101    pub max_image_size_mb: usize,
1102    pub allow_remote_fetch: bool,
1103}
1104
1105impl Default for MultimodalConfig {
1106    fn default() -> Self {
1107        Self {
1108            max_images: 4,
1109            max_image_size_mb: 5,
1110            allow_remote_fetch: false,
1111        }
1112    }
1113}
1114
1115#[derive(Debug, Clone, Deserialize, Serialize)]
1116#[serde(default)]
1117pub struct SkillsConfig {
1118    pub open_skills_enabled: bool,
1119    pub open_skills_dir: Option<String>,
1120    pub prompt_injection_mode: String,
1121    pub clawhub_token: Option<String>,
1122}
1123
1124impl Default for SkillsConfig {
1125    fn default() -> Self {
1126        Self {
1127            open_skills_enabled: false,
1128            open_skills_dir: None,
1129            prompt_injection_mode: "full".to_string(),
1130            clawhub_token: None,
1131        }
1132    }
1133}
1134
1135#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1136#[serde(default)]
1137pub struct ProviderOptionsConfig {
1138    pub reasoning_level: Option<String>,
1139    pub transport: Option<String>,
1140}
1141
1142#[derive(Debug, Clone, Deserialize, Serialize)]
1143#[serde(default)]
1144pub struct GatewayConfig {
1145    pub host: String,
1146    pub port: u16,
1147    pub require_pairing: bool,
1148    pub allow_public_bind: bool,
1149    pub node_control: NodeControlConfig,
1150    /// When true, the gateway operates as a privacy relay only.
1151    /// Normal agent endpoints return 503; only relay routes are active.
1152    pub relay_mode: bool,
1153    /// Relay-specific configuration.
1154    pub relay: RelayConfig,
1155}
1156
1157impl Default for GatewayConfig {
1158    fn default() -> Self {
1159        Self {
1160            host: "127.0.0.1".to_string(),
1161            port: 42617,
1162            require_pairing: true,
1163            allow_public_bind: false,
1164            node_control: NodeControlConfig::default(),
1165            relay_mode: false,
1166            relay: RelayConfig::default(),
1167        }
1168    }
1169}
1170
1171#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1172#[serde(default)]
1173pub struct NodeControlConfig {
1174    pub enabled: bool,
1175    pub auth_token: Option<String>,
1176    pub allowed_node_ids: Vec<String>,
1177}
1178
1179#[derive(Debug, Clone, Deserialize, Serialize)]
1180#[serde(default)]
1181pub struct ChannelsGlobalConfig {
1182    pub message_timeout_secs: u64,
1183    pub group_reply: HashMap<String, GroupReplyConfig>,
1184    pub ack_reaction: HashMap<String, AckReactionConfig>,
1185    pub stream_mode: String,
1186    pub draft_update_interval_ms: u64,
1187    pub interrupt_on_new_message: bool,
1188    /// Default privacy boundary applied to all channels unless overridden.
1189    /// Empty string means inherit the global `privacy.mode`.
1190    pub default_privacy_boundary: String,
1191}
1192
1193impl Default for ChannelsGlobalConfig {
1194    fn default() -> Self {
1195        Self {
1196            message_timeout_secs: 300,
1197            group_reply: HashMap::new(),
1198            ack_reaction: HashMap::new(),
1199            stream_mode: "off".to_string(),
1200            draft_update_interval_ms: 500,
1201            interrupt_on_new_message: false,
1202            default_privacy_boundary: String::new(),
1203        }
1204    }
1205}
1206
1207#[derive(Debug, Clone, Deserialize, Serialize)]
1208#[serde(default)]
1209pub struct GroupReplyConfig {
1210    pub mode: String,
1211    pub allowed_sender_ids: Vec<String>,
1212    pub bot_name: Option<String>,
1213}
1214
1215impl Default for GroupReplyConfig {
1216    fn default() -> Self {
1217        Self {
1218            mode: "all_messages".to_string(),
1219            allowed_sender_ids: Vec::new(),
1220            bot_name: None,
1221        }
1222    }
1223}
1224
1225#[derive(Debug, Clone, Deserialize, Serialize)]
1226#[serde(default)]
1227pub struct AckReactionConfig {
1228    pub enabled: bool,
1229    pub emoji_pool: Vec<String>,
1230    pub strategy: String,
1231    pub sample_rate: f64,
1232    pub rules: Vec<AckReactionRule>,
1233}
1234
1235impl Default for AckReactionConfig {
1236    fn default() -> Self {
1237        Self {
1238            enabled: false,
1239            emoji_pool: vec!["👍".to_string(), "👀".to_string(), "🤔".to_string()],
1240            strategy: "random".to_string(),
1241            sample_rate: 1.0,
1242            rules: Vec::new(),
1243        }
1244    }
1245}
1246
1247#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1248#[serde(default)]
1249pub struct AckReactionRule {
1250    pub contains_any: Vec<String>,
1251    pub contains_all: Vec<String>,
1252    pub contains_none: Vec<String>,
1253    pub regex: Option<String>,
1254    pub sender_ids: Vec<String>,
1255    pub chat_ids: Vec<String>,
1256    pub emoji_override: Vec<String>,
1257}
1258
1259// --- Phase A4: Model provider profiles ---
1260
1261#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1262#[serde(default)]
1263pub struct ModelProviderProfile {
1264    pub name: Option<String>,
1265    pub base_url: Option<String>,
1266    pub wire_api: Option<String>,
1267    pub model: Option<String>,
1268    pub api_key: Option<String>,
1269    pub requires_openai_auth: bool,
1270}
1271
1272// --- Phase A5: Model/embedding routes and query classification ---
1273
1274#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1275#[serde(default)]
1276pub struct ModelRoute {
1277    pub hint: String,
1278    pub provider: String,
1279    pub model: String,
1280    pub max_tokens: Option<usize>,
1281    pub api_key: Option<String>,
1282    pub transport: Option<String>,
1283}
1284
1285#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1286#[serde(default)]
1287pub struct EmbeddingRoute {
1288    pub hint: String,
1289    pub provider: String,
1290    pub model: String,
1291    pub dimensions: Option<usize>,
1292    pub api_key: Option<String>,
1293}
1294
1295#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1296#[serde(default)]
1297pub struct QueryClassificationConfig {
1298    pub enabled: bool,
1299    pub rules: Vec<QueryClassificationRule>,
1300}
1301
1302#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1303#[serde(default)]
1304pub struct QueryClassificationRule {
1305    pub hint: String,
1306    #[serde(default)]
1307    pub keywords: Vec<String>,
1308    #[serde(default)]
1309    pub patterns: Vec<String>,
1310    pub min_length: Option<usize>,
1311    pub max_length: Option<usize>,
1312    #[serde(default)]
1313    pub priority: i32,
1314}
1315
1316// --- Phase A6: Delegate sub-agent config ---
1317
1318#[derive(Debug, Clone, Deserialize, Serialize)]
1319#[serde(default)]
1320pub struct DelegateAgentConfig {
1321    pub provider: String,
1322    pub model: String,
1323    pub system_prompt: Option<String>,
1324    pub api_key: Option<String>,
1325    pub temperature: Option<f64>,
1326    pub max_depth: usize,
1327    pub agentic: bool,
1328    pub allowed_tools: Vec<String>,
1329    pub max_iterations: usize,
1330    /// Per-agent privacy boundary: "inherit", "local_only", "encrypted_only", "any".
1331    #[serde(default)]
1332    pub privacy_boundary: String,
1333    /// Restrict this agent to only use these provider kinds.
1334    #[serde(default)]
1335    pub allowed_providers: Vec<String>,
1336    /// Block this agent from using these provider kinds.
1337    #[serde(default)]
1338    pub blocked_providers: Vec<String>,
1339}
1340
1341impl Default for DelegateAgentConfig {
1342    fn default() -> Self {
1343        Self {
1344            provider: String::new(),
1345            model: String::new(),
1346            system_prompt: None,
1347            api_key: None,
1348            temperature: None,
1349            max_depth: 3,
1350            agentic: false,
1351            allowed_tools: Vec::new(),
1352            max_iterations: 10,
1353            privacy_boundary: String::new(),
1354            allowed_providers: Vec::new(),
1355            blocked_providers: Vec::new(),
1356        }
1357    }
1358}
1359
1360// --- Phase B2-B6: Security config extensions ---
1361
1362#[derive(Debug, Clone, Deserialize, Serialize)]
1363#[serde(default)]
1364pub struct UrlAccessConfig {
1365    pub block_private_ip: bool,
1366    pub allow_cidrs: Vec<String>,
1367    pub allow_domains: Vec<String>,
1368    pub allow_loopback: bool,
1369    pub require_first_visit_approval: bool,
1370    pub enforce_domain_allowlist: bool,
1371    pub domain_allowlist: Vec<String>,
1372    pub domain_blocklist: Vec<String>,
1373    pub approved_domains: Vec<String>,
1374}
1375
1376impl Default for UrlAccessConfig {
1377    fn default() -> Self {
1378        Self {
1379            block_private_ip: true,
1380            allow_cidrs: Vec::new(),
1381            allow_domains: Vec::new(),
1382            allow_loopback: false,
1383            require_first_visit_approval: false,
1384            enforce_domain_allowlist: false,
1385            domain_allowlist: Vec::new(),
1386            domain_blocklist: Vec::new(),
1387            approved_domains: Vec::new(),
1388        }
1389    }
1390}
1391
1392#[derive(Debug, Clone, Deserialize, Serialize)]
1393#[serde(default)]
1394pub struct OtpConfig {
1395    pub enabled: bool,
1396    pub method: String,
1397    pub token_ttl_secs: u64,
1398    pub cache_valid_secs: u64,
1399    pub gated_actions: Vec<String>,
1400    pub gated_domains: Vec<String>,
1401    pub gated_domain_categories: Vec<String>,
1402}
1403
1404impl Default for OtpConfig {
1405    fn default() -> Self {
1406        Self {
1407            enabled: false,
1408            method: "totp".to_string(),
1409            token_ttl_secs: 30,
1410            cache_valid_secs: 300,
1411            gated_actions: vec![
1412                "shell".to_string(),
1413                "file_write".to_string(),
1414                "browser_open".to_string(),
1415                "browser".to_string(),
1416                "memory_forget".to_string(),
1417            ],
1418            gated_domains: Vec::new(),
1419            gated_domain_categories: Vec::new(),
1420        }
1421    }
1422}
1423
1424#[derive(Debug, Clone, Deserialize, Serialize)]
1425#[serde(default)]
1426pub struct EstopConfig {
1427    pub enabled: bool,
1428    pub state_file: String,
1429    pub require_otp_to_resume: bool,
1430}
1431
1432impl Default for EstopConfig {
1433    fn default() -> Self {
1434        Self {
1435            enabled: false,
1436            state_file: "~/.agentzero/estop-state.json".to_string(),
1437            require_otp_to_resume: true,
1438        }
1439    }
1440}
1441
1442#[derive(Debug, Clone, Deserialize, Serialize)]
1443#[serde(default)]
1444pub struct OutboundLeakGuardConfig {
1445    pub enabled: bool,
1446    pub action: String,
1447    pub sensitivity: f64,
1448}
1449
1450impl Default for OutboundLeakGuardConfig {
1451    fn default() -> Self {
1452        Self {
1453            enabled: true,
1454            action: "redact".to_string(),
1455            sensitivity: 0.7,
1456        }
1457    }
1458}
1459
1460#[derive(Debug, Clone, Deserialize, Serialize)]
1461#[serde(default)]
1462pub struct PerplexityFilterConfig {
1463    pub enable_perplexity_filter: bool,
1464    pub perplexity_threshold: f64,
1465    pub suffix_window_chars: usize,
1466    pub min_prompt_chars: usize,
1467    pub symbol_ratio_threshold: f64,
1468}
1469
1470impl Default for PerplexityFilterConfig {
1471    fn default() -> Self {
1472        Self {
1473            enable_perplexity_filter: false,
1474            perplexity_threshold: 18.0,
1475            suffix_window_chars: 64,
1476            min_prompt_chars: 32,
1477            symbol_ratio_threshold: 0.20,
1478        }
1479    }
1480}
1481
1482#[derive(Debug, Clone, Deserialize, Serialize)]
1483#[serde(default)]
1484pub struct SyscallAnomalyConfig {
1485    pub enabled: bool,
1486    pub strict_mode: bool,
1487    pub alert_on_unknown_syscall: bool,
1488    pub max_denied_events_per_minute: u32,
1489    pub max_total_events_per_minute: u32,
1490    pub max_alerts_per_minute: u32,
1491    pub alert_cooldown_secs: u64,
1492    pub log_path: String,
1493    pub baseline_syscalls: Vec<String>,
1494}
1495
1496impl Default for SyscallAnomalyConfig {
1497    fn default() -> Self {
1498        Self {
1499            enabled: true,
1500            strict_mode: false,
1501            alert_on_unknown_syscall: true,
1502            max_denied_events_per_minute: 5,
1503            max_total_events_per_minute: 120,
1504            max_alerts_per_minute: 30,
1505            alert_cooldown_secs: 20,
1506            log_path: "syscall-anomalies.log".to_string(),
1507            baseline_syscalls: vec![
1508                "read".to_string(),
1509                "write".to_string(),
1510                "openat".to_string(),
1511                "close".to_string(),
1512                "execve".to_string(),
1513                "futex".to_string(),
1514            ],
1515        }
1516    }
1517}
1518
1519// --- Privacy AI configuration ---
1520
1521#[derive(Debug, Clone, Deserialize, Serialize)]
1522#[serde(default)]
1523pub struct PrivacyConfig {
1524    /// Privacy mode:
1525    /// - `"off"` — no privacy features.
1526    /// - `"local_only"` — all traffic stays on-device; cloud providers rejected.
1527    /// - `"encrypted"` — cloud providers allowed through Noise-encrypted transport.
1528    /// - `"full"` — all privacy features auto-enabled (noise, sealed envelopes,
1529    ///   key rotation); cloud providers allowed through encrypted transport.
1530    ///
1531    /// Simple: just set `"encrypted"` or `"full"` and everything auto-configures.
1532    /// Advanced: set individual `noise`, `sealed_envelopes`, `key_rotation` options.
1533    pub mode: String,
1534    /// When true, reject cloud providers — only local providers allowed.
1535    pub enforce_local_provider: bool,
1536    /// When true, block all outbound network calls to non-loopback destinations
1537    /// during agent execution (strict local-only mode).
1538    pub block_cloud_providers: bool,
1539    /// Noise Protocol settings for E2E encrypted gateway communication.
1540    pub noise: NoiseConfig,
1541    /// Sealed envelope settings for zero-knowledge packet routing.
1542    pub sealed_envelopes: SealedEnvelopeConfig,
1543    /// Automatic key rotation settings.
1544    pub key_rotation: KeyRotationConfig,
1545}
1546
1547impl Default for PrivacyConfig {
1548    fn default() -> Self {
1549        Self {
1550            mode: "off".to_string(),
1551            enforce_local_provider: false,
1552            block_cloud_providers: false,
1553            noise: NoiseConfig::default(),
1554            sealed_envelopes: SealedEnvelopeConfig::default(),
1555            key_rotation: KeyRotationConfig::default(),
1556        }
1557    }
1558}
1559
1560#[derive(Debug, Clone, Deserialize, Serialize)]
1561#[serde(default)]
1562pub struct NoiseConfig {
1563    pub enabled: bool,
1564    /// Noise handshake pattern: "XX" (mutual auth) or "IK" (known server key).
1565    pub handshake_pattern: String,
1566    /// Session timeout in seconds.
1567    pub session_timeout_secs: u64,
1568    /// Maximum concurrent Noise sessions.
1569    pub max_sessions: usize,
1570}
1571
1572impl Default for NoiseConfig {
1573    fn default() -> Self {
1574        Self {
1575            enabled: false,
1576            handshake_pattern: "XX".to_string(),
1577            session_timeout_secs: 3600,
1578            max_sessions: 256,
1579        }
1580    }
1581}
1582
1583#[derive(Debug, Clone, Deserialize, Serialize)]
1584#[serde(default)]
1585pub struct SealedEnvelopeConfig {
1586    pub enabled: bool,
1587    /// Default TTL for sealed envelopes in seconds.
1588    pub default_ttl_secs: u32,
1589    /// Maximum envelope size in bytes.
1590    pub max_envelope_bytes: usize,
1591    /// Enable randomized timing jitter on relay submit/poll responses
1592    /// to mitigate traffic-analysis side-channels.
1593    pub timing_jitter_enabled: bool,
1594    /// Minimum jitter delay on submit responses (milliseconds).
1595    pub submit_jitter_min_ms: u32,
1596    /// Maximum jitter delay on submit responses (milliseconds).
1597    pub submit_jitter_max_ms: u32,
1598    /// Minimum jitter delay on poll responses (milliseconds).
1599    pub poll_jitter_min_ms: u32,
1600    /// Maximum jitter delay on poll responses (milliseconds).
1601    pub poll_jitter_max_ms: u32,
1602}
1603
1604impl Default for SealedEnvelopeConfig {
1605    fn default() -> Self {
1606        Self {
1607            enabled: false,
1608            default_ttl_secs: 86400,
1609            max_envelope_bytes: 1_048_576,
1610            timing_jitter_enabled: false,
1611            submit_jitter_min_ms: 10,
1612            submit_jitter_max_ms: 100,
1613            poll_jitter_min_ms: 20,
1614            poll_jitter_max_ms: 200,
1615        }
1616    }
1617}
1618
1619#[derive(Debug, Clone, Deserialize, Serialize)]
1620#[serde(default)]
1621pub struct KeyRotationConfig {
1622    pub enabled: bool,
1623    /// Rotation interval in seconds (default: 7 days).
1624    pub rotation_interval_secs: u64,
1625    /// Overlap period where both old and new keys are valid.
1626    pub overlap_secs: u64,
1627    /// Path to the key store directory. Empty = default data dir.
1628    pub key_store_path: String,
1629}
1630
1631impl Default for KeyRotationConfig {
1632    fn default() -> Self {
1633        Self {
1634            enabled: false,
1635            rotation_interval_secs: 604_800,
1636            overlap_secs: 86_400,
1637            key_store_path: String::new(),
1638        }
1639    }
1640}
1641
1642#[derive(Debug, Clone, Deserialize, Serialize)]
1643#[serde(default)]
1644pub struct RelayConfig {
1645    /// Random timing jitter range in milliseconds (0-N) added to relay responses
1646    /// to prevent timing analysis.
1647    pub timing_jitter_ms: u64,
1648    /// Maximum number of envelopes per routing_id mailbox.
1649    pub max_mailbox_size: usize,
1650    /// Garbage collection interval in seconds for expired envelopes.
1651    pub gc_interval_secs: u64,
1652}
1653
1654impl Default for RelayConfig {
1655    fn default() -> Self {
1656        Self {
1657            timing_jitter_ms: 500,
1658            max_mailbox_size: 1000,
1659            gc_interval_secs: 60,
1660        }
1661    }
1662}