Task 04's second session stopped too. It checked its code against the markup test cases in its reasoning, and the tag it spelled out ended its reasoning. Building tags from pieces keeps them out of files, not out of the model's thoughts. check.rs is now given whole, with the tags from pieces, and task 04 fills only entries.rs. The three later tests that used a tag only as something the check refuses now use an instruction phrase or an invisible character. They pass against the reference. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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(())
|
|
}
|