Skip to main content

kimetsu_brain/
trust.rs

1//! v2.6: how much a memory's origin is worth.
2//!
3//! Every memory in the brain has been treated as equally believable regardless
4//! of where it came from — a lesson you typed yourself, one a model distilled
5//! from a transcript, and one that arrived in a pack downloaded from a URL all
6//! rank on relevance and usefulness alone.
7//!
8//! That is the shape of a real attack. Memory poisoning (OWASP ASI06) differs
9//! from prompt injection in exactly the way that matters here: prompt injection
10//! is session-scoped and resets, while a poisoned memory persists and
11//! influences every future session until someone notices. MINJA
12//! (arXiv 2601.05504) reports >95% injection success against memory-backed
13//! agents through ordinary, unprivileged interaction — no elevated access, just
14//! conversation that induces the agent to write something.
15//!
16//! Kimetsu's exposure is narrower than a hosted service's — the brain is a
17//! local file — but it is not zero, and it grows with exactly the features that
18//! make the product good: `brain import` from a URL, `brain sync` across
19//! machines, and Kimetsu Remote's shared org brain.
20//!
21//! ## What this module does
22//!
23//! It scores *origin*, and nothing else. A [`Provenance`] read off the memory's
24//! stored snapshot maps to a [`trust_multiplier`] the broker folds into the
25//! composite score, so a corroborated local lesson outranks an anonymous
26//! imported one at equal relevance.
27//!
28//! Two deliberate limits:
29//!
30//! * **It never blocks retrieval.** Trust is a weight, not a gate. A hard gate
31//!   on provenance would make a bad pack import silently delete a user's
32//!   working knowledge, which is a worse failure than the one it prevents.
33//! * **Corroboration outranks origin.** A memory that has been cited in a
34//!   successful local run has been *tested here*, whatever its origin, and
35//!   carries no penalty at all from then on. Otherwise an imported pack — the
36//!   whole point of which is to share knowledge — would stay second-class
37//!   forever.
38//!
39//! ## Not done here
40//!
41//! Quarantining imports — routing pack memories into the proposal queue instead
42//! of accepting them outright — is the mechanism that actually stops a poisoned
43//! pack from influencing anything before a human looks at it, and it is
44//! deliberately not in this module: it changes what `brain import` *does*, which
45//! belongs in the import path rather than behind a scoring weight. It lives in
46//! [`crate::packs::quarantine_memories`], on by default for `http(s)://`
47//! sources. This module stays the answer for *history* — a pack imported before
48//! quarantine existed is discounted by origin rather than pulled back out of
49//! retrieval, because reaching into a working brain on an upgrade is a worse
50//! failure than the one quarantine prevents.
51
52use serde::{Deserialize, Serialize};
53
54/// Where a memory came from.
55///
56/// Ordered least to most trusted, so the enum's ordering *is* the trust
57/// ordering and the two cannot drift apart.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum Provenance {
61    /// Arrived in a pack (`brain import`), possibly from a URL. The widest
62    /// attack surface Kimetsu has: content authored elsewhere, by someone
63    /// else, landing in the brain wholesale.
64    Pack,
65    /// Replicated from another machine or a shared org brain (`brain sync`,
66    /// Kimetsu Remote). Authored by someone with access, which is a real
67    /// constraint, but not by this user on this machine.
68    Remote,
69    /// Written by the model — the session-end distiller or reflection. The
70    /// content is derived from a real transcript, but no human read it before
71    /// it became durable, and a transcript can contain anything the agent was
72    /// shown.
73    Distilled,
74    /// Consolidation output: a staple or merge of memories already in the
75    /// brain. Inherits its members' standing and adds no new claims.
76    Derived,
77    /// Recorded by the user, or by an agent acting on the user's instruction,
78    /// on this machine.
79    Local,
80}
81
82impl Provenance {
83    pub fn as_str(self) -> &'static str {
84        match self {
85            Provenance::Pack => "pack",
86            Provenance::Remote => "remote",
87            Provenance::Distilled => "distilled",
88            Provenance::Derived => "derived",
89            Provenance::Local => "local",
90        }
91    }
92
93    /// Classify a memory's stored `provenance_snapshot_json`.
94    ///
95    /// Unrecognised or missing provenance reads as [`Provenance::Local`]: every
96    /// memory written before this module existed has a snapshot this does not
97    /// know, and treating an existing brain's entire contents as untrusted on
98    /// upgrade would be a far worse outcome than the attack being defended.
99    pub fn from_snapshot(snapshot_json: &str) -> Self {
100        let Ok(value) = serde_json::from_str::<serde_json::Value>(snapshot_json) else {
101            return Provenance::Local;
102        };
103        let source = value
104            .get("source")
105            .and_then(serde_json::Value::as_str)
106            .unwrap_or("");
107        match source {
108            "pack" => Provenance::Pack,
109            "remote" | "sync" | "org" => Provenance::Remote,
110            "distiller" | "distilled" | "reflection" => Provenance::Distilled,
111            "staple" | "merge" | "consolidation" => Provenance::Derived,
112            _ => Provenance::Local,
113        }
114    }
115}
116
117/// Multiplier applied to a candidate's composite score.
118///
119/// `corroborated` means the memory has been cited in a *successful* run on this
120/// machine — precisely what `memories.last_useful_at` records, and the reason
121/// the signal is a boolean rather than a count: `last_useful_at` is already
122/// selected by every candidate query, so reading it costs nothing, whereas
123/// counting citations would put a per-row aggregate on the hot path.
124///
125/// A corroborated memory carries no origin penalty at all, whatever its
126/// provenance. It has been tested here; where it was written stops being the
127/// most informative thing about it. Without that, an imported pack — the entire
128/// point of which is to share knowledge — would stay second-class forever.
129///
130/// Bounded in `(0, 1]` — trust can only ever hold a memory back, never promote
131/// one above its relevance. Promotion is what `usefulness_score` is for, and
132/// two mechanisms that both boost would make the composite unreadable.
133pub fn trust_multiplier(provenance: Provenance, corroborated: bool) -> f32 {
134    if corroborated {
135        return 1.0;
136    }
137    match provenance {
138        Provenance::Local | Provenance::Derived => 1.0,
139        Provenance::Distilled => 0.95,
140        Provenance::Remote => 0.90,
141        Provenance::Pack => 0.85,
142    }
143}
144
145// ── Audit ───────────────────────────────────────────────────────────────────
146
147/// One provenance class in the audit report.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ProvenanceGroup {
150    pub provenance: String,
151    pub total: usize,
152    /// Cited in a successful run here at least once.
153    pub corroborated: usize,
154    /// Never corroborated *and* from an external origin: the population a
155    /// poisoned memory would be hiding in.
156    pub unvetted: usize,
157}
158
159/// A suspicious burst of writes.
160///
161/// Memory poisoning through ordinary interaction tends to arrive as a cluster —
162/// MINJA's technique is to induce several related writes in a short window. A
163/// human recording lessons does not usually produce thirty memories in a
164/// minute; a pack import or a runaway loop does. This flags the shape without
165/// claiming to know intent.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct WriteBurst {
168    /// RFC 3339 minute the burst falls in.
169    pub minute: String,
170    pub writes: usize,
171}
172
173/// The audit report.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct AuditReport {
176    pub groups: Vec<ProvenanceGroup>,
177    pub bursts: Vec<WriteBurst>,
178    /// Total active memories considered.
179    pub total: usize,
180}
181
182/// Writes in one minute above which a cluster is worth a look.
183pub const BURST_THRESHOLD: usize = 20;
184
185/// Group the active corpus by provenance and flag write bursts.
186///
187/// Read-only and non-destructive by design: this reports, and a human decides.
188/// An automated purge keyed on a heuristic like "many writes in one minute"
189/// would delete a legitimate bulk import, which is a worse outcome than the
190/// attack it is guarding against.
191pub fn audit(conn: &rusqlite::Connection) -> kimetsu_core::KimetsuResult<AuditReport> {
192    use std::collections::BTreeMap;
193
194    let mut stmt = conn.prepare(
195        "SELECT provenance_snapshot_json, last_useful_at, created_at
196         FROM memories
197         WHERE invalidated_at IS NULL AND superseded_by IS NULL",
198    )?;
199    let rows = stmt
200        .query_map([], |row| {
201            Ok((
202                row.get::<_, Option<String>>(0)?,
203                row.get::<_, Option<String>>(1)?,
204                row.get::<_, String>(2)?,
205            ))
206        })?
207        .collect::<Result<Vec<_>, _>>()?;
208
209    let mut by_provenance: BTreeMap<Provenance, (usize, usize)> = BTreeMap::new();
210    let mut by_minute: BTreeMap<String, usize> = BTreeMap::new();
211    for (snapshot, last_useful_at, created_at) in &rows {
212        let provenance = Provenance::from_snapshot(snapshot.as_deref().unwrap_or("{}"));
213        let entry = by_provenance.entry(provenance).or_insert((0, 0));
214        entry.0 += 1;
215        if last_useful_at.is_some() {
216            entry.1 += 1;
217        }
218        // RFC 3339 truncated to the minute: "2026-07-24T20:31".
219        let minute: String = created_at.chars().take(16).collect();
220        *by_minute.entry(minute).or_insert(0) += 1;
221    }
222
223    let groups = by_provenance
224        .into_iter()
225        .map(|(provenance, (total, corroborated))| ProvenanceGroup {
226            provenance: provenance.as_str().to_string(),
227            total,
228            corroborated,
229            unvetted: if provenance >= Provenance::Derived {
230                0 // local and derived memories have no external origin to vet
231            } else {
232                total - corroborated
233            },
234        })
235        .collect();
236
237    let mut bursts: Vec<WriteBurst> = by_minute
238        .into_iter()
239        .filter(|(_, writes)| *writes >= BURST_THRESHOLD)
240        .map(|(minute, writes)| WriteBurst { minute, writes })
241        .collect();
242    bursts.sort_by_key(|b| std::cmp::Reverse(b.writes));
243
244    Ok(AuditReport {
245        groups,
246        bursts,
247        total: rows.len(),
248    })
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn the_enum_ordering_is_the_trust_ordering() {
257        assert!(Provenance::Pack < Provenance::Remote);
258        assert!(Provenance::Remote < Provenance::Distilled);
259        assert!(Provenance::Distilled < Provenance::Derived);
260        assert!(Provenance::Derived < Provenance::Local);
261
262        // …and the multipliers agree with it, so the two cannot drift apart.
263        let m = |p| trust_multiplier(p, false);
264        assert!(m(Provenance::Pack) < m(Provenance::Remote));
265        assert!(m(Provenance::Remote) < m(Provenance::Distilled));
266        assert!(m(Provenance::Distilled) <= m(Provenance::Derived));
267        assert_eq!(m(Provenance::Local), 1.0);
268    }
269
270    /// The upgrade-safety property: an existing brain full of memories written
271    /// before provenance was a concept must not become untrusted overnight.
272    #[test]
273    fn unknown_provenance_reads_as_local() {
274        assert_eq!(Provenance::from_snapshot("{}"), Provenance::Local);
275        assert_eq!(Provenance::from_snapshot("not json"), Provenance::Local);
276        assert_eq!(
277            Provenance::from_snapshot(r#"{"source":"manual_cli"}"#),
278            Provenance::Local
279        );
280        assert_eq!(
281            Provenance::from_snapshot(r#"{"source":"something-from-the-future"}"#),
282            Provenance::Local
283        );
284    }
285
286    #[test]
287    fn known_sources_classify() {
288        for (json, expected) in [
289            (r#"{"source":"pack"}"#, Provenance::Pack),
290            (r#"{"source":"sync"}"#, Provenance::Remote),
291            (r#"{"source":"org"}"#, Provenance::Remote),
292            (r#"{"source":"distiller"}"#, Provenance::Distilled),
293            (r#"{"source":"staple"}"#, Provenance::Derived),
294        ] {
295            assert_eq!(Provenance::from_snapshot(json), expected, "for {json}");
296        }
297    }
298
299    /// A shared pack is the whole point of packs. A memory that has proven
300    /// itself locally must stop being treated as an outsider.
301    #[test]
302    fn corroboration_erases_the_origin_penalty() {
303        let cold = trust_multiplier(Provenance::Pack, false);
304        let proven = trust_multiplier(Provenance::Pack, true);
305        assert!(cold < 1.0, "an uncorroborated pack memory is discounted");
306        assert!(
307            (proven - 1.0).abs() < 1e-6,
308            "once cited in a successful run here, origin stops mattering: {proven}"
309        );
310        for provenance in [
311            Provenance::Pack,
312            Provenance::Remote,
313            Provenance::Distilled,
314            Provenance::Derived,
315            Provenance::Local,
316        ] {
317            assert_eq!(trust_multiplier(provenance, true), 1.0, "{provenance:?}");
318        }
319    }
320
321    /// Trust holds a memory back; it never promotes one. Two mechanisms that
322    /// both boost would make the composite score unreadable.
323    #[test]
324    fn trust_never_exceeds_one() {
325        for provenance in [
326            Provenance::Pack,
327            Provenance::Remote,
328            Provenance::Distilled,
329            Provenance::Derived,
330            Provenance::Local,
331        ] {
332            for corroborated in [false, true] {
333                let m = trust_multiplier(provenance, corroborated);
334                assert!(
335                    m > 0.0 && m <= 1.0,
336                    "{provenance:?} corroborated={corroborated} gave {m}"
337                );
338            }
339        }
340    }
341
342    // ── Audit ────────────────────────────────────────────────────────────
343
344    fn audit_conn() -> rusqlite::Connection {
345        let conn = rusqlite::Connection::open_in_memory().expect("open");
346        crate::schema::initialize(&conn).expect("schema");
347        conn
348    }
349
350    fn insert(conn: &rusqlite::Connection, id: &str, source: &str, corroborated: bool, at: &str) {
351        conn.execute(
352            "INSERT INTO memories
353             (memory_id, scope, kind, text, normalized_text, confidence,
354              provenance_snapshot_json, created_at, last_useful_at)
355             VALUES (?1, 'project', 'fact', ?1, ?1, 0.9, ?2, ?3, ?4)",
356            rusqlite::params![
357                id,
358                format!(r#"{{"source":"{source}"}}"#),
359                at,
360                corroborated.then(|| at.to_string()),
361            ],
362        )
363        .expect("insert");
364    }
365
366    /// The population a poisoned memory hides in: external origin, never
367    /// corroborated. Local memories are not "unvetted" — there is no external
368    /// origin to vet.
369    #[test]
370    fn audit_counts_the_unvetted_external_population() {
371        let conn = audit_conn();
372        insert(
373            &conn,
374            "local-1",
375            "manual_cli",
376            false,
377            "2026-01-01T00:00:00Z",
378        );
379        insert(&conn, "pack-1", "pack", false, "2026-01-01T00:00:00Z");
380        insert(&conn, "pack-2", "pack", true, "2026-01-01T00:00:00Z");
381        insert(
382            &conn,
383            "distilled-1",
384            "distiller",
385            false,
386            "2026-01-01T00:00:00Z",
387        );
388
389        let report = audit(&conn).expect("audit");
390        assert_eq!(report.total, 4);
391
392        let group = |name: &str| {
393            report
394                .groups
395                .iter()
396                .find(|g| g.provenance == name)
397                .unwrap_or_else(|| panic!("missing group {name}"))
398                .clone()
399        };
400        assert_eq!(group("local").unvetted, 0, "nothing external to vet");
401        assert_eq!(group("pack").total, 2);
402        assert_eq!(group("pack").corroborated, 1);
403        assert_eq!(
404            group("pack").unvetted,
405            1,
406            "the corroborated one has been tested here"
407        );
408        assert_eq!(group("distilled").unvetted, 1);
409    }
410
411    #[test]
412    fn audit_flags_a_write_burst_and_ignores_ordinary_writing() {
413        let conn = audit_conn();
414        // A human recording lessons across the day.
415        for i in 0..10 {
416            insert(
417                &conn,
418                &format!("slow-{i}"),
419                "manual_cli",
420                false,
421                &format!("2026-01-01T{:02}:00:00Z", i),
422            );
423        }
424        assert!(
425            audit(&conn).expect("audit").bursts.is_empty(),
426            "ordinary writing is not a burst"
427        );
428
429        // …and a cluster that arrived faster than anyone types.
430        for i in 0..BURST_THRESHOLD {
431            insert(
432                &conn,
433                &format!("burst-{i}"),
434                "pack",
435                false,
436                "2026-02-02T03:04:00Z",
437            );
438        }
439        let bursts = audit(&conn).expect("audit").bursts;
440        assert_eq!(bursts.len(), 1, "got: {bursts:?}");
441        assert_eq!(bursts[0].minute, "2026-02-02T03:04");
442        assert_eq!(bursts[0].writes, BURST_THRESHOLD);
443    }
444
445    #[test]
446    fn audit_of_an_empty_brain_is_empty_not_an_error() {
447        let report = audit(&audit_conn()).expect("audit");
448        assert_eq!(report.total, 0);
449        assert!(report.groups.is_empty());
450        assert!(report.bursts.is_empty());
451    }
452}