use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use zeph_common::SkillTrustLevel;
use crate::tools::{AutonomyLevel, PreExecutionVerifierConfig};
use crate::defaults::default_true;
use crate::vigil::VigilConfig;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScannerConfig {
#[serde(default = "default_true")]
pub injection_patterns: bool,
#[serde(default)]
pub capability_escalation_check: bool,
}
impl Default for ScannerConfig {
fn default() -> Self {
Self {
injection_patterns: true,
capability_escalation_check: false,
}
}
}
use crate::rate_limit::RateLimitConfig;
use crate::sanitizer::GuardrailConfig;
use crate::sanitizer::{
CausalIpiConfig, ContentIsolationConfig, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
PiiFilterConfig, ResponseVerificationConfig,
};
fn default_trust_default_level() -> SkillTrustLevel {
SkillTrustLevel::Quarantined
}
fn default_trust_local_level() -> SkillTrustLevel {
SkillTrustLevel::Trusted
}
fn default_trust_hash_mismatch_level() -> SkillTrustLevel {
SkillTrustLevel::Quarantined
}
fn default_trust_bundled_level() -> SkillTrustLevel {
SkillTrustLevel::Trusted
}
fn default_llm_timeout() -> u64 {
120
}
fn default_embedding_timeout() -> u64 {
30
}
fn default_a2a_timeout() -> u64 {
30
}
fn default_max_parallel_tools() -> usize {
8
}
fn default_llm_request_timeout() -> u64 {
600
}
fn default_context_prep_timeout() -> u64 {
30
}
fn default_no_providers_backoff_secs() -> u64 {
2
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TrustConfig {
#[serde(default = "default_trust_default_level")]
pub default_level: SkillTrustLevel,
#[serde(default = "default_trust_local_level")]
pub local_level: SkillTrustLevel,
#[serde(default = "default_trust_hash_mismatch_level")]
pub hash_mismatch_level: SkillTrustLevel,
#[serde(default = "default_trust_bundled_level")]
pub bundled_level: SkillTrustLevel,
#[serde(default = "default_true")]
pub scan_on_load: bool,
#[serde(default)]
pub scanner: ScannerConfig,
}
impl Default for TrustConfig {
fn default() -> Self {
Self {
default_level: default_trust_default_level(),
local_level: default_trust_local_level(),
hash_mismatch_level: default_trust_hash_mismatch_level(),
bundled_level: default_trust_bundled_level(),
scan_on_load: true,
scanner: ScannerConfig::default(),
}
}
}
fn default_decay_per_turn() -> f32 {
0.85
}
fn default_window_turns() -> u32 {
8
}
fn default_elevated_at() -> f32 {
2.0
}
fn default_high_at() -> f32 {
4.0
}
fn default_critical_at() -> f32 {
8.0
}
fn default_alert_threshold() -> f32 {
4.0
}
fn default_auto_recover_after_turns() -> u32 {
16
}
fn default_subagent_inheritance_factor() -> f32 {
0.5
}
fn default_high_call_rate_threshold() -> u32 {
12
}
fn default_unusual_read_threshold() -> u32 {
24
}
fn default_auto_recover_floor() -> u32 {
4
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TrajectorySentinelConfig {
#[serde(default = "default_decay_per_turn")]
pub decay_per_turn: f32,
#[serde(default = "default_window_turns")]
pub window_turns: u32,
#[serde(default = "default_elevated_at")]
pub elevated_at: f32,
#[serde(default = "default_high_at")]
pub high_at: f32,
#[serde(default = "default_critical_at")]
pub critical_at: f32,
#[serde(default = "default_alert_threshold")]
pub alert_threshold: f32,
#[serde(default = "default_auto_recover_after_turns")]
pub auto_recover_after_turns: u32,
#[serde(default = "default_subagent_inheritance_factor")]
pub subagent_inheritance_factor: f32,
#[serde(default = "default_high_call_rate_threshold")]
pub high_call_rate_threshold: u32,
#[serde(default = "default_unusual_read_threshold")]
pub unusual_read_threshold: u32,
}
impl Default for TrajectorySentinelConfig {
fn default() -> Self {
Self {
decay_per_turn: default_decay_per_turn(),
window_turns: default_window_turns(),
elevated_at: default_elevated_at(),
high_at: default_high_at(),
critical_at: default_critical_at(),
alert_threshold: default_alert_threshold(),
auto_recover_after_turns: default_auto_recover_after_turns(),
subagent_inheritance_factor: default_subagent_inheritance_factor(),
high_call_rate_threshold: default_high_call_rate_threshold(),
unusual_read_threshold: default_unusual_read_threshold(),
}
}
}
impl TrajectorySentinelConfig {
pub fn validate(&self) -> Result<(), String> {
if self.decay_per_turn <= 0.0 || self.decay_per_turn > 1.0 {
return Err(format!(
"trajectory.decay_per_turn must be in (0.0, 1.0]; got {}",
self.decay_per_turn
));
}
if self.elevated_at >= self.high_at {
return Err(format!(
"trajectory: elevated_at ({}) must be < high_at ({})",
self.elevated_at, self.high_at
));
}
if self.high_at >= self.critical_at {
return Err(format!(
"trajectory: high_at ({}) must be < critical_at ({})",
self.high_at, self.critical_at
));
}
if self.auto_recover_after_turns < default_auto_recover_floor() {
return Err(format!(
"trajectory.auto_recover_after_turns must be >= {}; got {}",
default_auto_recover_floor(),
self.auto_recover_after_turns
));
}
if self.decay_per_turn < 1.0 {
let ideal = self
.decay_per_turn
.powf(0.5_f32.ln() / self.decay_per_turn.ln());
if (self.subagent_inheritance_factor - ideal).abs() > 0.1 {
tracing::warn!(
configured = self.subagent_inheritance_factor,
ideal = ideal,
decay = self.decay_per_turn,
"trajectory.subagent_inheritance_factor deviates from calibrated value by more than 0.1"
);
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PatternStrictness {
Strict,
Permissive,
#[default]
ProvisionalForDynamicNamespaces,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScopeConfig {
#[serde(default)]
pub patterns: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct CapabilityScopesConfig {
#[serde(default = "default_scope_name")]
pub default_scope: String,
#[serde(default)]
pub strict: bool,
#[serde(default)]
pub pattern_strictness: PatternStrictness,
#[serde(default, flatten)]
pub scopes: HashMap<String, ScopeConfig>,
}
fn default_scope_name() -> String {
"general".to_owned()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SecurityConfig {
#[serde(default = "default_true")]
pub redact_secrets: bool,
#[serde(default)]
pub autonomy_level: AutonomyLevel,
#[serde(default)]
pub content_isolation: ContentIsolationConfig,
#[serde(default)]
pub exfiltration_guard: ExfiltrationGuardConfig,
#[serde(default)]
pub memory_validation: MemoryWriteValidationConfig,
#[serde(default)]
pub pii_filter: PiiFilterConfig,
#[serde(default)]
pub rate_limit: RateLimitConfig,
#[serde(default)]
pub pre_execution_verify: PreExecutionVerifierConfig,
#[serde(default)]
pub guardrail: GuardrailConfig,
#[serde(default)]
pub response_verification: ResponseVerificationConfig,
#[serde(default)]
pub causal_ipi: CausalIpiConfig,
#[serde(default)]
pub vigil: VigilConfig,
#[serde(default)]
pub trajectory: TrajectorySentinelConfig,
#[serde(default)]
pub capability_scopes: CapabilityScopesConfig,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
redact_secrets: true,
autonomy_level: AutonomyLevel::default(),
content_isolation: ContentIsolationConfig::default(),
exfiltration_guard: ExfiltrationGuardConfig::default(),
memory_validation: MemoryWriteValidationConfig::default(),
pii_filter: PiiFilterConfig::default(),
rate_limit: RateLimitConfig::default(),
pre_execution_verify: PreExecutionVerifierConfig::default(),
guardrail: GuardrailConfig::default(),
response_verification: ResponseVerificationConfig::default(),
causal_ipi: CausalIpiConfig::default(),
vigil: VigilConfig::default(),
trajectory: TrajectorySentinelConfig::default(),
capability_scopes: CapabilityScopesConfig::default(),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub struct TimeoutConfig {
#[serde(default = "default_llm_timeout")]
pub llm_seconds: u64,
#[serde(default = "default_llm_request_timeout")]
pub llm_request_timeout_secs: u64,
#[serde(default = "default_embedding_timeout")]
pub embedding_seconds: u64,
#[serde(default = "default_a2a_timeout")]
pub a2a_seconds: u64,
#[serde(default = "default_max_parallel_tools")]
pub max_parallel_tools: usize,
#[serde(default = "default_context_prep_timeout")]
pub context_prep_timeout_secs: u64,
#[serde(default = "default_no_providers_backoff_secs")]
pub no_providers_backoff_secs: u64,
}
impl Default for TimeoutConfig {
fn default() -> Self {
Self {
llm_seconds: default_llm_timeout(),
llm_request_timeout_secs: default_llm_request_timeout(),
embedding_seconds: default_embedding_timeout(),
a2a_seconds: default_a2a_timeout(),
max_parallel_tools: default_max_parallel_tools(),
context_prep_timeout_secs: default_context_prep_timeout(),
no_providers_backoff_secs: default_no_providers_backoff_secs(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trust_config_default_has_scan_on_load_true() {
let config = TrustConfig::default();
assert!(config.scan_on_load);
}
#[test]
fn trust_config_serde_roundtrip_with_scan_on_load() {
let config = TrustConfig {
default_level: SkillTrustLevel::Quarantined,
local_level: SkillTrustLevel::Trusted,
hash_mismatch_level: SkillTrustLevel::Quarantined,
bundled_level: SkillTrustLevel::Trusted,
scan_on_load: false,
scanner: ScannerConfig::default(),
};
let toml = toml::to_string(&config).expect("serialize");
let deserialized: TrustConfig = toml::from_str(&toml).expect("deserialize");
assert!(!deserialized.scan_on_load);
assert_eq!(deserialized.bundled_level, SkillTrustLevel::Trusted);
}
#[test]
fn trust_config_missing_scan_on_load_defaults_to_true() {
let toml = r#"
default_level = "quarantined"
local_level = "trusted"
hash_mismatch_level = "quarantined"
"#;
let config: TrustConfig = toml::from_str(toml).expect("deserialize");
assert!(
config.scan_on_load,
"missing scan_on_load must default to true"
);
}
#[test]
fn trust_config_default_has_bundled_level_trusted() {
let config = TrustConfig::default();
assert_eq!(config.bundled_level, SkillTrustLevel::Trusted);
}
#[test]
fn trust_config_missing_bundled_level_defaults_to_trusted() {
let toml = r#"
default_level = "quarantined"
local_level = "trusted"
hash_mismatch_level = "quarantined"
"#;
let config: TrustConfig = toml::from_str(toml).expect("deserialize");
assert_eq!(
config.bundled_level,
SkillTrustLevel::Trusted,
"missing bundled_level must default to trusted"
);
}
#[test]
fn scanner_config_defaults() {
let cfg = ScannerConfig::default();
assert!(cfg.injection_patterns);
assert!(!cfg.capability_escalation_check);
}
#[test]
fn scanner_config_serde_roundtrip() {
let cfg = ScannerConfig {
injection_patterns: false,
capability_escalation_check: true,
};
let toml = toml::to_string(&cfg).expect("serialize");
let back: ScannerConfig = toml::from_str(&toml).expect("deserialize");
assert!(!back.injection_patterns);
assert!(back.capability_escalation_check);
}
#[test]
fn trust_config_scanner_defaults_when_missing() {
let toml = r#"
default_level = "quarantined"
local_level = "trusted"
hash_mismatch_level = "quarantined"
"#;
let config: TrustConfig = toml::from_str(toml).expect("deserialize");
assert!(config.scanner.injection_patterns);
assert!(!config.scanner.capability_escalation_check);
}
#[test]
fn timeout_config_context_prep_timeout_default() {
let cfg = TimeoutConfig::default();
assert_eq!(
cfg.context_prep_timeout_secs, 30,
"context_prep_timeout_secs default must be 30s (#3357)"
);
}
#[test]
fn timeout_config_no_providers_backoff_default() {
let cfg = TimeoutConfig::default();
assert_eq!(
cfg.no_providers_backoff_secs, 2,
"no_providers_backoff_secs default must be 2s (#3357)"
);
}
#[test]
fn timeout_config_new_fields_deserialize_from_toml() {
let toml = r"
context_prep_timeout_secs = 60
no_providers_backoff_secs = 10
";
let cfg: TimeoutConfig = toml::from_str(toml).expect("deserialize");
assert_eq!(cfg.context_prep_timeout_secs, 60);
assert_eq!(cfg.no_providers_backoff_secs, 10);
}
#[test]
fn timeout_config_new_fields_default_when_missing_from_toml() {
let cfg: TimeoutConfig = toml::from_str("").expect("deserialize empty");
assert_eq!(cfg.context_prep_timeout_secs, 30);
assert_eq!(cfg.no_providers_backoff_secs, 2);
}
}