use std::str::FromStr;
use crate::adapter::LlmError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum MaxSensitivity {
Low,
Medium,
High,
}
impl FromStr for MaxSensitivity {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"low" => Ok(Self::Low),
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
other => Err(format!(
"unrecognised sensitivity level {other:?}; expected one of: low, medium, high"
)),
}
}
}
impl MaxSensitivity {
#[must_use]
pub fn allows(&self, level: &str) -> bool {
let candidate = match level.to_ascii_lowercase().as_str() {
"low" => Self::Low,
"medium" => Self::Medium,
"high" => Self::High,
_ => Self::High,
};
candidate <= *self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SensitivityGateResult {
pub max_memory_sensitivity: String,
pub configured_max: MaxSensitivity,
pub allowed: bool,
}
impl SensitivityGateResult {
#[must_use]
pub fn evaluate(memory_max_str: &str, configured_max: MaxSensitivity) -> Self {
let allowed = configured_max.allows(memory_max_str);
Self {
max_memory_sensitivity: memory_max_str.to_string(),
configured_max,
allowed,
}
}
}
pub fn check_remote_prompt_sensitivity(prompt: &str, max: MaxSensitivity) -> Result<(), LlmError> {
tracing::info!(
max = ?max,
prompt_len = prompt.len(),
"remote prompt sensitivity gate: evaluating"
);
if max == MaxSensitivity::High {
tracing::debug!("remote prompt sensitivity gate: max=High, unconditional pass");
return Ok(());
}
let lower = prompt.to_ascii_lowercase();
let has_high_marker = lower.contains("[sensitivity:high]") || lower.contains("[sens:high]");
if has_high_marker {
tracing::info!(
max = ?max,
"remote prompt sensitivity gate: high-sensitivity marker found; excluding prompt"
);
return Err(LlmError::InvalidRequest(
"sensitivity_exceeds_remote_threshold: prompt contains high-sensitivity content \
above the configured max_sensitivity level; memory excluded from remote dispatch"
.to_string(),
));
}
tracing::debug!(max = ?max, "remote prompt sensitivity gate: pass (no high-sensitivity markers)");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_str_parses_all_variants() {
assert_eq!(
"low".parse::<MaxSensitivity>().unwrap(),
MaxSensitivity::Low
);
assert_eq!(
"medium".parse::<MaxSensitivity>().unwrap(),
MaxSensitivity::Medium
);
assert_eq!(
"high".parse::<MaxSensitivity>().unwrap(),
MaxSensitivity::High
);
}
#[test]
fn from_str_is_case_insensitive() {
assert_eq!(
"LOW".parse::<MaxSensitivity>().unwrap(),
MaxSensitivity::Low
);
assert_eq!(
"Medium".parse::<MaxSensitivity>().unwrap(),
MaxSensitivity::Medium
);
assert_eq!(
"HIGH".parse::<MaxSensitivity>().unwrap(),
MaxSensitivity::High
);
}
#[test]
fn from_str_rejects_unknown() {
assert!("critical".parse::<MaxSensitivity>().is_err());
assert!("".parse::<MaxSensitivity>().is_err());
}
#[test]
fn allows_low_gate_permits_only_low() {
let gate = MaxSensitivity::Low;
assert!(gate.allows("low"));
assert!(!gate.allows("medium"));
assert!(!gate.allows("high"));
}
#[test]
fn allows_medium_gate_permits_low_and_medium() {
let gate = MaxSensitivity::Medium;
assert!(gate.allows("low"));
assert!(gate.allows("medium"));
assert!(!gate.allows("high"));
}
#[test]
fn allows_high_gate_permits_all() {
let gate = MaxSensitivity::High;
assert!(gate.allows("low"));
assert!(gate.allows("medium"));
assert!(gate.allows("high"));
}
#[test]
fn allows_unknown_level_treated_as_high_sensitivity() {
assert!(!MaxSensitivity::Low.allows("classified"));
assert!(!MaxSensitivity::Medium.allows("classified"));
assert!(MaxSensitivity::High.allows("classified"));
}
#[test]
fn check_remote_prompt_sensitivity_passes_unmarked_prompt_at_all_gates() {
assert!(check_remote_prompt_sensitivity("some prompt text", MaxSensitivity::Low).is_ok());
assert!(
check_remote_prompt_sensitivity("some prompt text", MaxSensitivity::Medium).is_ok()
);
assert!(check_remote_prompt_sensitivity("some prompt text", MaxSensitivity::High).is_ok());
}
#[test]
fn check_remote_prompt_sensitivity_high_gate_allows_marked_prompt() {
let marked = "Context: [SENSITIVITY:HIGH] — user medical history.";
assert!(
check_remote_prompt_sensitivity(marked, MaxSensitivity::High).is_ok(),
"High gate must pass even when high-sensitivity marker is present"
);
}
#[test]
fn check_remote_prompt_sensitivity_medium_gate_rejects_marked_prompt() {
let marked = "Context: [SENSITIVITY:HIGH] — confidential data.";
let err = check_remote_prompt_sensitivity(marked, MaxSensitivity::Medium)
.expect_err("Medium gate must reject a prompt with a high-sensitivity marker");
assert!(
matches!(err, LlmError::InvalidRequest(ref msg) if msg.contains("sensitivity_exceeds_remote_threshold")),
"error must name the stable invariant: {err:?}"
);
}
#[test]
fn check_remote_prompt_sensitivity_low_gate_rejects_sens_high_marker() {
let marked = "User profile: [sens:high] present.";
let err = check_remote_prompt_sensitivity(marked, MaxSensitivity::Low)
.expect_err("Low gate must reject [sens:high] marker");
assert!(
matches!(err, LlmError::InvalidRequest(_)),
"expected InvalidRequest: {err:?}"
);
}
#[test]
fn check_remote_prompt_sensitivity_marker_matching_is_case_insensitive() {
for marker in &["[Sensitivity:High]", "[SENSITIVITY:HIGH]", "[sens:HIGH]"] {
let prompt = format!("data: {marker} info");
let err = check_remote_prompt_sensitivity(&prompt, MaxSensitivity::Medium)
.expect_err("case-variant marker must be rejected");
assert!(
matches!(err, LlmError::InvalidRequest(_)),
"marker {marker} not caught: {err:?}"
);
}
}
#[test]
fn max_sensitivity_ordering() {
assert!(MaxSensitivity::Low < MaxSensitivity::Medium);
assert!(MaxSensitivity::Medium < MaxSensitivity::High);
assert!(MaxSensitivity::Low < MaxSensitivity::High);
}
}