//! The check on every entry the agent writes, and again on every identity entry when the block is //! built (M5a spec, section 5, "The check"). Short and fixed on purpose: defence in depth. The //! real defence is that a session which read untrusted content can only take notes. /// Which rule an entry broke. The sentences are fixed: they reach the model. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Refused { Control, Invisible, Markup, Phrase, } impl Refused { pub fn sentence(self) -> &'static str { match self { Refused::Control => "memory refused this entry: it contains a control character", Refused::Invisible => { "memory refused this entry: it contains an invisible or direction-changing character" } Refused::Markup => "memory refused this entry: it contains chat-template markup", Refused::Phrase => "memory refused this entry: it contains an instruction phrase", } } } /// Unicode 16's `Default_Ignorable_Code_Point` (DerivedCoreProperties.txt), inclusive ranges. const DEFAULT_IGNORABLE: &[(u32, u32)] = &[ (0x00AD, 0x00AD), (0x034F, 0x034F), (0x061C, 0x061C), (0x115F, 0x1160), (0x17B4, 0x17B5), (0x180B, 0x180F), (0x200B, 0x200F), (0x202A, 0x202E), (0x2060, 0x206F), (0x3164, 0x3164), (0xFE00, 0xFE0F), (0xFEFF, 0xFEFF), (0xFFA0, 0xFFA0), (0xFFF0, 0xFFF8), (0x1BCA0, 0x1BCA3), (0x1D173, 0x1D17A), (0xE0000, 0xE0FFF), ]; /// Invisible without being default-ignorable: the Braille blank, and the line and paragraph /// separators. const ALSO_INVISIBLE: &[u32] = &[0x2800, 0x2028, 0x2029]; // Built with `concat!` so this file never holds the markup itself: a model reading it could take // the markup for its own. const MARKUP: &[&str] = &[ concat!("<", "|"), concat!("|", ">"), concat!("<", "think"), concat!("<", "/think"), concat!("<", "tool_call"), concat!("<", "/tool_call"), ]; const PHRASES: &[&str] = &[ "ignore previous instructions", "ignore all previous", "disregard previous instructions", "disregard all previous", "new instructions:", "system prompt:", ]; /// Whether `c` is refused by rule 2. pub fn invisible(c: char) -> bool { let code = u32::from(c); ALSO_INVISIBLE.contains(&code) || DEFAULT_IGNORABLE .iter() .any(|&(low, high)| (low..=high).contains(&code)) } /// The first rule `text` breaks, in the order of the spec, or `Ok`. pub fn check(text: &str) -> Result<(), Refused> { if text .chars() .any(|c| c.is_control() && c != '\n' && c != '\t') { return Err(Refused::Control); } if text.chars().any(invisible) { return Err(Refused::Invisible); } let lower = text.to_lowercase(); if MARKUP.iter().any(|m| lower.contains(m)) { return Err(Refused::Markup); } // Runs of spaces, tabs and newlines count as one space. let collapsed = lower.split_whitespace().collect::>().join(" "); if PHRASES.iter().any(|p| collapsed.contains(p)) { return Err(Refused::Phrase); } Ok(()) }