use serde::{Deserialize, Serialize};
use crate::Error;
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct GuardrailsConfig {
#[serde(default)]
pub injection: Option<InjectionConfig>,
#[serde(default)]
pub pii: Option<PiiConfig>,
#[serde(default)]
pub tool_policy: Option<ToolPolicyConfig>,
#[serde(default)]
pub llm_judge: Option<LlmJudgeConfig>,
#[serde(default)]
pub secret_scan: Option<SecretScanConfig>,
#[serde(default)]
pub behavioral: Option<BehavioralConfig>,
#[serde(default)]
pub action_budget: Option<ActionBudgetConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct InjectionConfig {
#[serde(default = "default_injection_threshold")]
pub threshold: f32,
#[serde(default = "default_injection_mode")]
pub mode: String,
}
fn default_injection_threshold() -> f32 {
0.5
}
fn default_injection_mode() -> String {
"deny".into()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PiiConfig {
#[serde(default = "default_pii_action")]
pub action: String,
#[serde(default = "default_pii_detectors")]
pub detectors: Vec<String>,
}
fn default_pii_action() -> String {
"redact".into()
}
pub(super) fn default_pii_detectors() -> Vec<String> {
vec![
"email".into(),
"phone".into(),
"ssn".into(),
"credit_card".into(),
]
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SecretScanConfig {
#[serde(default = "default_secret_action")]
pub action: String,
#[serde(default)]
pub custom_patterns: Vec<SecretPatternConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SecretPatternConfig {
pub label: String,
pub pattern: String,
}
fn default_secret_action() -> String {
"redact".into()
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct BehavioralRuleConfig {
#[serde(rename = "type")]
pub rule_type: String,
#[serde(default)]
pub tool_pattern: Option<String>,
#[serde(default)]
pub max_count: Option<usize>,
#[serde(default)]
pub window_seconds: Option<u64>,
#[serde(default)]
pub first: Option<String>,
#[serde(default)]
pub then: Option<String>,
#[serde(default)]
pub within_turns: Option<usize>,
#[serde(default)]
pub max_denied: Option<usize>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
pub struct BehavioralConfig {
#[serde(default = "default_behavioral_window_size")]
pub window_size: usize,
#[serde(default = "default_behavioral_window_ttl")]
pub window_ttl_seconds: u64,
#[serde(default)]
pub rules: Vec<BehavioralRuleConfig>,
}
fn default_behavioral_window_size() -> usize {
200
}
fn default_behavioral_window_ttl() -> u64 {
1800
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ActionBudgetRuleConfig {
pub tool_pattern: String,
pub max_calls: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
pub struct ActionBudgetConfig {
#[serde(default)]
pub default_budget: Option<usize>,
#[serde(default)]
pub rules: Vec<ActionBudgetRuleConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LlmJudgeConfig {
pub criteria: Vec<String>,
#[serde(default)]
pub evaluate_tool_inputs: bool,
#[serde(default = "default_llm_judge_timeout")]
pub timeout_seconds: u64,
#[serde(default = "default_llm_judge_max_tokens")]
pub max_judge_tokens: u32,
}
fn default_llm_judge_timeout() -> u64 {
10
}
fn default_llm_judge_max_tokens() -> u32 {
256
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolPolicyConfig {
#[serde(default = "default_tool_policy_action")]
pub default_action: String,
#[serde(default)]
pub rules: Vec<ToolPolicyRuleConfig>,
}
fn default_tool_policy_action() -> String {
"allow".into()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolPolicyRuleConfig {
pub tool: String,
pub action: String,
#[serde(default)]
pub input_constraints: Vec<InputConstraintConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct InputConstraintConfig {
pub path: String,
#[serde(default)]
pub deny_pattern: Option<String>,
#[serde(default)]
pub max_length: Option<usize>,
}
impl GuardrailsConfig {
pub fn is_empty(&self) -> bool {
self.injection.is_none()
&& self.pii.is_none()
&& self.tool_policy.is_none()
&& self.llm_judge.is_none()
&& self.secret_scan.is_none()
&& self.behavioral.is_none()
&& self.action_budget.is_none()
}
pub fn build(
&self,
) -> Result<Vec<std::sync::Arc<dyn crate::agent::guardrail::Guardrail>>, Error> {
self.build_with_judge(None)
}
pub fn build_with_judge(
&self,
judge_provider: Option<std::sync::Arc<crate::llm::BoxedProvider>>,
) -> Result<Vec<std::sync::Arc<dyn crate::agent::guardrail::Guardrail>>, Error> {
use std::sync::Arc;
use crate::agent::guardrail::Guardrail;
use crate::agent::guardrails::injection::{GuardrailMode, InjectionClassifierGuardrail};
use crate::agent::guardrails::pii::{PiiAction, PiiDetector, PiiGuardrail};
use crate::agent::guardrails::tool_policy::{
InputConstraint, ToolPolicyGuardrail, ToolRule,
};
let mut guardrails: Vec<Arc<dyn Guardrail>> = Vec::new();
if let Some(cfg) = &self.injection {
let mode = match cfg.mode.as_str() {
"warn" => GuardrailMode::Warn,
"deny" => GuardrailMode::Deny,
other => {
return Err(Error::Config(format!(
"invalid injection mode: `{other}` (expected \"warn\" or \"deny\")"
)));
}
};
guardrails.push(Arc::new(InjectionClassifierGuardrail::new(
cfg.threshold,
mode,
)));
}
if let Some(cfg) = &self.pii {
let action = match cfg.action.as_str() {
"redact" => PiiAction::Redact,
"warn" => PiiAction::Warn,
"deny" => PiiAction::Deny,
other => {
return Err(Error::Config(format!(
"invalid PII action: `{other}` (expected \"redact\", \"warn\", or \"deny\")"
)));
}
};
let detectors: Vec<PiiDetector> = cfg
.detectors
.iter()
.map(|name| match name.as_str() {
"email" => Ok(PiiDetector::Email),
"phone" => Ok(PiiDetector::Phone),
"ssn" => Ok(PiiDetector::Ssn),
"credit_card" => Ok(PiiDetector::CreditCard),
other => Err(Error::Config(format!(
"unknown PII detector: `{other}` (expected email, phone, ssn, or credit_card)"
))),
})
.collect::<Result<_, _>>()?;
guardrails.push(Arc::new(PiiGuardrail::new(detectors, action)));
}
if let Some(cfg) = &self.tool_policy {
let default_action = parse_guard_action(&cfg.default_action)?;
let mut rules = Vec::with_capacity(cfg.rules.len());
for rule_cfg in &cfg.rules {
let action = parse_guard_action(&rule_cfg.action)?;
let mut constraints = Vec::new();
for ic in &rule_cfg.input_constraints {
if let Some(pattern_str) = &ic.deny_pattern {
let pattern = regex::Regex::new(pattern_str).map_err(|e| {
Error::Config(format!("invalid deny_pattern `{pattern_str}`: {e}"))
})?;
constraints.push(InputConstraint::FieldDenied {
path: ic.path.clone(),
pattern,
});
}
if let Some(max) = ic.max_length {
constraints.push(InputConstraint::MaxFieldLength {
path: ic.path.clone(),
max_bytes: max,
});
}
}
rules.push(ToolRule {
tool_pattern: rule_cfg.tool.clone(),
action,
input_constraints: constraints,
});
}
guardrails.push(Arc::new(ToolPolicyGuardrail::new(rules, default_action)));
}
if let Some(cfg) = &self.llm_judge {
if let Some(provider) = judge_provider {
let mut builder =
crate::agent::guardrails::llm_judge::LlmJudgeGuardrail::builder(provider)
.criteria(cfg.criteria.clone())
.timeout(std::time::Duration::from_secs(cfg.timeout_seconds))
.max_judge_tokens(cfg.max_judge_tokens);
if cfg.evaluate_tool_inputs {
builder = builder.evaluate_tool_inputs(true);
}
let judge = builder
.build()
.map_err(|e| Error::Config(format!("llm_judge guardrail build failed: {e}")))?;
guardrails.push(Arc::new(judge));
} else {
tracing::warn!(
"[guardrails.llm_judge] is configured but no judge provider was supplied — \
LLM judge guardrail will NOT be active. Use build_with_judge(Some(provider))."
);
}
}
if let Some(cfg) = &self.secret_scan {
use crate::agent::guardrails::secret_scanner::{SecretAction, SecretScannerGuardrail};
let action = match cfg.action.as_str() {
"redact" => SecretAction::Redact,
"deny" => SecretAction::Deny,
other => {
return Err(Error::Config(format!(
"invalid secret_scan action: `{other}` (expected \"redact\" or \"deny\")"
)));
}
};
let mut builder = SecretScannerGuardrail::builder().action(action);
for cp in &cfg.custom_patterns {
let re = regex::Regex::new(&cp.pattern).map_err(|e| {
Error::Config(format!(
"invalid secret_scan custom pattern `{}`: {e}",
cp.label
))
})?;
builder = builder.custom_pattern(&cp.label, re);
}
guardrails.push(Arc::new(builder.build()));
}
if let Some(cfg) = &self.behavioral {
use crate::agent::guardrails::behavioral::{BehaviorRule, BehavioralMonitorGuardrail};
let mut builder = BehavioralMonitorGuardrail::builder()
.window_size(cfg.window_size)
.window_ttl(std::time::Duration::from_secs(cfg.window_ttl_seconds));
for rule_cfg in &cfg.rules {
let rule = match rule_cfg.rule_type.as_str() {
"frequency_limit" => BehaviorRule::FrequencyLimit {
tool_pattern: rule_cfg.tool_pattern.clone().unwrap_or_else(|| "*".into()),
max_count: rule_cfg.max_count.unwrap_or(10),
window: std::time::Duration::from_secs(
rule_cfg.window_seconds.unwrap_or(60),
),
},
"suspicious_sequence" => BehaviorRule::SuspiciousSequence {
first: rule_cfg.first.clone().unwrap_or_default(),
then: rule_cfg.then.clone().unwrap_or_default(),
within_turns: rule_cfg.within_turns.unwrap_or(3),
},
"denial_spike" => BehaviorRule::DenialSpike {
max_denied: rule_cfg.max_denied.unwrap_or(5),
window: std::time::Duration::from_secs(
rule_cfg.window_seconds.unwrap_or(60),
),
},
other => {
return Err(Error::Config(format!(
"unknown behavioral rule type: `{other}` \
(expected \"frequency_limit\", \"suspicious_sequence\", or \"denial_spike\")"
)));
}
};
builder = builder.rule(rule);
}
guardrails.push(Arc::new(builder.build()));
}
if let Some(cfg) = &self.action_budget {
use crate::agent::guardrails::action_budget::ActionBudgetGuardrail;
let mut builder = ActionBudgetGuardrail::builder();
if let Some(default) = cfg.default_budget {
builder = builder.default_budget(default);
}
for rule in &cfg.rules {
builder = builder.rule(&rule.tool_pattern, rule.max_calls);
}
guardrails.push(Arc::new(builder.build()));
}
Ok(guardrails)
}
}
fn parse_guard_action(s: &str) -> Result<crate::agent::guardrail::GuardAction, Error> {
match s {
"allow" => Ok(crate::agent::guardrail::GuardAction::Allow),
"warn" => Ok(crate::agent::guardrail::GuardAction::warn(String::new())),
"deny" => Ok(crate::agent::guardrail::GuardAction::deny(String::new())),
other => Err(Error::Config(format!(
"invalid action: `{other}` (expected \"allow\", \"warn\", or \"deny\")"
))),
}
}