103 lines
3.1 KiB
Rust
103 lines
3.1 KiB
Rust
//! 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::<Vec<_>>().join(" ");
|
|
if PHRASES.iter().any(|p| collapsed.contains(p)) {
|
|
return Err(Refused::Phrase);
|
|
}
|
|
Ok(())
|
|
}
|