memoryd: refuse and flag memory writes through bxcore::sanitize
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
+103
-17
@@ -1,12 +1,16 @@
|
||||
//! Applying a `memory` call (M5a spec, section 5). `note` records experience in any session;
|
||||
//! the identity actions change `memory.md` or `user.md`, and only a session that has not read
|
||||
//! untrusted content may use them. Every refusal is a fixed sentence: it reaches the model.
|
||||
//!
|
||||
//! Every text a write would store goes through the sanitizer (M5b spec, section 7): a text that
|
||||
//! breaks a refuse rule is refused; one that matches a flag rule is kept, one tier lower, as a
|
||||
//! note marked untrusted (section 6.3).
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use bxcore::sanitize::{Refusal, flag, refuse};
|
||||
use bxcore::{DataClass, MemoryArgs, MemoryFile, Timestamp};
|
||||
|
||||
use crate::check::check;
|
||||
use crate::entries;
|
||||
use crate::files::{Store, grouped};
|
||||
use crate::notes::{Kind, Note, minute};
|
||||
@@ -37,6 +41,8 @@ pub struct Outcome {
|
||||
pub action: &'static str,
|
||||
pub answer: String,
|
||||
pub change: Change,
|
||||
/// The flag rule a text of the write matched, which made it an untrusted note.
|
||||
pub flag: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// The sentence for a write over a file's limit.
|
||||
@@ -80,9 +86,42 @@ fn stale(file: MemoryFile) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn checked(text: &str) -> Result<String, String> {
|
||||
check(text).map_err(|refused| refused.sentence().to_string())?;
|
||||
Ok(text.trim().to_string())
|
||||
/// The answer when an identity write matched a flag rule and became notes (M5b spec, section 6.3).
|
||||
pub fn flagged_as_note(file: MemoryFile, rule: &str) -> String {
|
||||
format!(
|
||||
"not added to {}: it matches the rule {rule}, so it was saved in notes.md as an untrusted note",
|
||||
file.name()
|
||||
)
|
||||
}
|
||||
|
||||
/// The answer to a `note` that matched a flag rule, from a session that has not read untrusted
|
||||
/// content.
|
||||
pub fn noted_flagged(rule: &str) -> String {
|
||||
format!("saved in notes.md, marked untrusted: it matches the rule {rule}")
|
||||
}
|
||||
|
||||
/// The texts `args` would store: the `text` of an add, a replace or a note, and every `text` of a
|
||||
/// consolidate (its rewrites and new entries). An `old` is not here: it names what is stored.
|
||||
fn texts(args: &MemoryArgs) -> Vec<&str> {
|
||||
match args {
|
||||
MemoryArgs::Add { text, .. }
|
||||
| MemoryArgs::Replace { text, .. }
|
||||
| MemoryArgs::Note { text } => vec![text.as_str()],
|
||||
MemoryArgs::Remove { .. } => Vec::new(),
|
||||
MemoryArgs::Consolidate { entries, .. } => {
|
||||
entries.iter().filter_map(|e| e.text.as_deref()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The refusal of the first text `args` would store that breaks a refuse rule.
|
||||
pub fn refused(args: &MemoryArgs) -> Option<Refusal> {
|
||||
texts(args).into_iter().find_map(refuse)
|
||||
}
|
||||
|
||||
/// The rule of the first text `args` would store that matches a flag rule.
|
||||
pub fn flagged(args: &MemoryArgs) -> Option<&'static str> {
|
||||
texts(args).into_iter().find_map(flag).map(|f| f.rule)
|
||||
}
|
||||
|
||||
fn note(ctx: &Context, kind: Kind, text: &str) -> Note {
|
||||
@@ -98,6 +137,10 @@ fn note(ctx: &Context, kind: Kind, text: &str) -> Note {
|
||||
pub fn apply(store: &Store, args: &MemoryArgs, ctx: &Context) -> Result<Outcome, String> {
|
||||
// 1. `args.check()?`.
|
||||
args.check()?;
|
||||
// 1a. A text that breaks a refuse rule: Err(refusal.sentence), before anything is written.
|
||||
if let Some(refusal) = refused(args) {
|
||||
return Err(refusal.sentence.to_string());
|
||||
}
|
||||
// 2. A Note: return `noted(store, ctx, text, "note")`. Otherwise take the file.
|
||||
let file = match args {
|
||||
MemoryArgs::Note { text } => {
|
||||
@@ -126,6 +169,11 @@ pub fn apply(store: &Store, args: &MemoryArgs, ctx: &Context) -> Result<Outcome,
|
||||
// &before, args, ctx)?;`.
|
||||
let before = store.entries(file)?;
|
||||
let (action, after, moved) = edited(file, &before, args, ctx)?;
|
||||
// 4a. A text that matches a flag rule: `as_notes` saves every text of the write as an
|
||||
// untrusted note instead, and the file is not changed.
|
||||
if let Some(rule) = flagged(args) {
|
||||
return as_notes(store, ctx, file, action, &texts(args), rule);
|
||||
}
|
||||
// 5. When `after` holds the same entry twice (collect into a HashSet and compare lengths):
|
||||
// Err(format!("{} would hold the same entry twice", file.name())).
|
||||
let unique: HashSet<&String> = after.iter().collect();
|
||||
@@ -160,6 +208,7 @@ pub fn apply(store: &Store, args: &MemoryArgs, ctx: &Context) -> Result<Outcome,
|
||||
after,
|
||||
notes: moved,
|
||||
},
|
||||
flag: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,7 +220,7 @@ fn edited(
|
||||
) -> Result<(&'static str, Vec<String>, Vec<Note>), String> {
|
||||
match args {
|
||||
MemoryArgs::Add { text, .. } => {
|
||||
let text = checked(text)?;
|
||||
let text = text.trim().to_string();
|
||||
if before.contains(&text) {
|
||||
return Err(format!("{} already has this entry", file.name()));
|
||||
}
|
||||
@@ -181,7 +230,7 @@ fn edited(
|
||||
}
|
||||
MemoryArgs::Replace { old, text, .. } => {
|
||||
let pos = position(before, old).ok_or_else(|| stale(file))?;
|
||||
let text = checked(text)?;
|
||||
let text = text.trim().to_string();
|
||||
if before
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -221,30 +270,67 @@ fn noted(
|
||||
text: &str,
|
||||
action: &'static str,
|
||||
) -> Result<Outcome, String> {
|
||||
let text = checked(text)?;
|
||||
let rule = flag(text).map(|f| f.rule);
|
||||
let entry = note(
|
||||
ctx,
|
||||
Kind::Noted {
|
||||
class: ctx.taint,
|
||||
untrusted: ctx.untrusted,
|
||||
untrusted: ctx.untrusted || rule.is_some(),
|
||||
},
|
||||
&text,
|
||||
text.trim(),
|
||||
);
|
||||
store.append("notes.md", &entry.render())?;
|
||||
let answer = match (ctx.untrusted, rule) {
|
||||
(true, _) => NOTED_UNTRUSTED.to_string(),
|
||||
(false, Some(rule)) => noted_flagged(rule),
|
||||
(false, None) => NOTED.to_string(),
|
||||
};
|
||||
Ok(Outcome {
|
||||
action,
|
||||
answer: if ctx.untrusted {
|
||||
NOTED_UNTRUSTED
|
||||
} else {
|
||||
NOTED
|
||||
}
|
||||
.to_string(),
|
||||
answer,
|
||||
change: Change {
|
||||
file: None,
|
||||
before: Vec::new(),
|
||||
after: Vec::new(),
|
||||
notes: vec![entry],
|
||||
},
|
||||
flag: rule,
|
||||
})
|
||||
}
|
||||
|
||||
/// An identity write that matched the flag rule `rule`: each of `texts`, trimmed, is appended to
|
||||
/// `notes.md` as a note marked untrusted, in order, and the identity file is not changed.
|
||||
fn as_notes(
|
||||
store: &Store,
|
||||
ctx: &Context,
|
||||
file: MemoryFile,
|
||||
action: &'static str,
|
||||
texts: &[&str],
|
||||
rule: &'static str,
|
||||
) -> Result<Outcome, String> {
|
||||
let mut notes = Vec::new();
|
||||
for text in texts {
|
||||
let entry = note(
|
||||
ctx,
|
||||
Kind::Noted {
|
||||
class: ctx.taint,
|
||||
untrusted: true,
|
||||
},
|
||||
text.trim(),
|
||||
);
|
||||
store.append("notes.md", &entry.render())?;
|
||||
notes.push(entry);
|
||||
}
|
||||
Ok(Outcome {
|
||||
action,
|
||||
answer: flagged_as_note(file, rule),
|
||||
change: Change {
|
||||
file: None,
|
||||
before: Vec::new(),
|
||||
after: Vec::new(),
|
||||
notes,
|
||||
},
|
||||
flag: Some(rule),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -285,7 +371,7 @@ fn consolidate(
|
||||
}
|
||||
bxcore::EntryAction::Rewrite { old, text } => {
|
||||
claim(file, before, &mut named, &old)?;
|
||||
after.push(checked(&text)?);
|
||||
after.push(text.trim().to_string());
|
||||
}
|
||||
bxcore::EntryAction::Move(old) => {
|
||||
claim(file, before, &mut named, &old)?;
|
||||
@@ -296,7 +382,7 @@ fn consolidate(
|
||||
out.push(note(ctx, Kind::Dropped(file), old.trim()));
|
||||
}
|
||||
bxcore::EntryAction::New(text) => {
|
||||
after.push(checked(&text)?);
|
||||
after.push(text.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user