security(agent): gate elevated MemorySource construction behind a distinct API
Two related trust-boundary gaps, both closed: 1. ConsolidationEngine::add_memory took a plain `source: MemorySource` parameter, so any caller could claim MemorySource::System/Correction — which get elevated importance weighting in score_correction — for content whose actual origin the caller doesn't control or hasn't verified. Split into add_memory(UntrustedSource) for ordinary caller-supplied content (User/Tool/Retrieval only, no elevated variant exists to claim) and add_trusted_memory(TrustedSource) for content whose elevated trust the caller has independently verified (System/ Correction). Updated the one production consumer outside this crate (clawhdf5-bench's consolidation_efficiency benchmark) and all tests. 2. The provenance/anomaly wiring added in the previous commit introduced the same pattern: infer_memory_source mapped source_channel == "system" or "correction" straight to the elevated MemorySource variants. Since MemoryEntry.source_channel is unvalidated caller-supplied text, this let a write dodge check_source_anomaly's User-flood detection by simply self-labeling source_channel = "system". infer_memory_source now never returns System/Correction — only Tool/Retrieval (recognized channel names) or User (everything else, the conservative default). INT-05
This commit is contained in:
@@ -16,6 +16,55 @@ pub enum MemorySource {
|
||||
Correction,
|
||||
}
|
||||
|
||||
/// Source classification for content whose true origin is *not*
|
||||
/// independently verified by the caller of [`ConsolidationEngine::add_memory`]
|
||||
/// — arbitrary text forwarded from a user, a tool's output, or a retrieval
|
||||
/// pipeline. This is the only source set `add_memory` accepts; it cannot
|
||||
/// claim the `System`/`Correction` importance boost (see [`TrustedSource`]
|
||||
/// and [`ConsolidationEngine::add_trusted_memory`]) — a caller passing
|
||||
/// through untrusted content has no way to self-report an elevated trust
|
||||
/// level through this entry point.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum UntrustedSource {
|
||||
User,
|
||||
Tool,
|
||||
Retrieval,
|
||||
}
|
||||
|
||||
impl From<UntrustedSource> for MemorySource {
|
||||
fn from(s: UntrustedSource) -> Self {
|
||||
match s {
|
||||
UntrustedSource::User => MemorySource::User,
|
||||
UntrustedSource::Tool => MemorySource::Tool,
|
||||
UntrustedSource::Retrieval => MemorySource::Retrieval,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Source classification for content whose elevated trust level has been
|
||||
/// independently verified by the caller — e.g. the library's own
|
||||
/// system-generated text, or a caller that ran its own correction-cue
|
||||
/// detection (as `memory_strategy::SaveOnUserCorrection` does) rather than
|
||||
/// forwarding a caller-supplied label verbatim. `MemorySource::System`/
|
||||
/// `Correction` get elevated importance weighting in
|
||||
/// [`ImportanceScorer::score_correction`]; only reachable through
|
||||
/// [`ConsolidationEngine::add_trusted_memory`], a distinct entry point from
|
||||
/// the one untrusted content is passed through.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum TrustedSource {
|
||||
System,
|
||||
Correction,
|
||||
}
|
||||
|
||||
impl From<TrustedSource> for MemorySource {
|
||||
fn from(s: TrustedSource) -> Self {
|
||||
match s {
|
||||
TrustedSource::System => MemorySource::System,
|
||||
TrustedSource::Correction => MemorySource::Correction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum MemoryTier {
|
||||
Working,
|
||||
@@ -199,10 +248,41 @@ impl ConsolidationEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new memory to the Working tier.
|
||||
/// Add a new memory to the Working tier from an untrusted/ordinary origin
|
||||
/// (User, Tool, or Retrieval). This is the entry point for arbitrary
|
||||
/// caller-supplied content — it cannot claim the elevated System/
|
||||
/// Correction importance boost. Use [`Self::add_trusted_memory`] for
|
||||
/// content whose elevated trust level the caller has independently
|
||||
/// verified.
|
||||
///
|
||||
/// Importance is scored against existing Working-tier records only.
|
||||
pub fn add_memory(
|
||||
&mut self,
|
||||
chunk: String,
|
||||
embedding: Vec<f32>,
|
||||
source: UntrustedSource,
|
||||
now: f64,
|
||||
) -> u64 {
|
||||
self.add_memory_with_source(chunk, embedding, source.into(), now)
|
||||
}
|
||||
|
||||
/// Add a new memory tagged System or Correction, which get elevated
|
||||
/// importance weighting in [`ImportanceScorer::score_correction`]. Only
|
||||
/// call this from code that has independently verified the origin (the
|
||||
/// library's own system-generated text, or a caller that ran its own
|
||||
/// correction-cue detection) — never from a path that forwards a
|
||||
/// caller-supplied trust label verbatim.
|
||||
pub fn add_trusted_memory(
|
||||
&mut self,
|
||||
chunk: String,
|
||||
embedding: Vec<f32>,
|
||||
source: TrustedSource,
|
||||
now: f64,
|
||||
) -> u64 {
|
||||
self.add_memory_with_source(chunk, embedding, source.into(), now)
|
||||
}
|
||||
|
||||
fn add_memory_with_source(
|
||||
&mut self,
|
||||
chunk: String,
|
||||
embedding: Vec<f32>,
|
||||
@@ -418,13 +498,44 @@ mod tests {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Add memory — basic
|
||||
// ---------------------------------------------------------------------------
|
||||
/// add_trusted_memory(TrustedSource::Correction) must actually produce a
|
||||
/// MemorySource::Correction record — the only way to reach that elevated
|
||||
/// classification, since add_memory's UntrustedSource has no such variant.
|
||||
#[test]
|
||||
fn test_add_trusted_memory_sets_correction_source() {
|
||||
let mut engine = ConsolidationEngine::new(ConsolidationConfig::default());
|
||||
let id = engine.add_trusted_memory(
|
||||
"verified correction".to_string(),
|
||||
unit_vec(4, 0),
|
||||
TrustedSource::Correction,
|
||||
0.0,
|
||||
);
|
||||
let rec = engine.get_by_id(id).unwrap();
|
||||
assert_eq!(rec.source, MemorySource::Correction);
|
||||
}
|
||||
|
||||
/// add_trusted_memory(TrustedSource::System) must produce a
|
||||
/// MemorySource::System record.
|
||||
#[test]
|
||||
fn test_add_trusted_memory_sets_system_source() {
|
||||
let mut engine = ConsolidationEngine::new(ConsolidationConfig::default());
|
||||
let id = engine.add_trusted_memory(
|
||||
"bootstrap text".to_string(),
|
||||
unit_vec(4, 0),
|
||||
TrustedSource::System,
|
||||
0.0,
|
||||
);
|
||||
let rec = engine.get_by_id(id).unwrap();
|
||||
assert_eq!(rec.source, MemorySource::System);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_memory_basic() {
|
||||
let mut engine = ConsolidationEngine::new(ConsolidationConfig::default());
|
||||
let id = engine.add_memory(
|
||||
"Hello world".to_string(),
|
||||
unit_vec(4, 0),
|
||||
MemorySource::User,
|
||||
UntrustedSource::User,
|
||||
1_000_000.0,
|
||||
);
|
||||
assert_eq!(id, 0);
|
||||
@@ -592,7 +703,7 @@ mod tests {
|
||||
let id = engine.add_memory(
|
||||
"x".to_string(),
|
||||
unit_vec(4, i as usize),
|
||||
MemorySource::User,
|
||||
UntrustedSource::User,
|
||||
i as f64,
|
||||
);
|
||||
// Force low importance so promotion threshold is not crossed.
|
||||
@@ -625,10 +736,10 @@ mod tests {
|
||||
let cfg = ConsolidationConfig::default();
|
||||
let mut engine = ConsolidationEngine::new(cfg);
|
||||
|
||||
let id = engine.add_memory(
|
||||
let id = engine.add_trusted_memory(
|
||||
"important memory".to_string(),
|
||||
unit_vec(4, 0),
|
||||
MemorySource::Correction,
|
||||
TrustedSource::Correction,
|
||||
0.0,
|
||||
);
|
||||
// Force importance above threshold.
|
||||
@@ -661,7 +772,7 @@ mod tests {
|
||||
let id = engine.add_memory(
|
||||
"frequently accessed".to_string(),
|
||||
unit_vec(4, 0),
|
||||
MemorySource::User,
|
||||
UntrustedSource::User,
|
||||
0.0,
|
||||
);
|
||||
|
||||
@@ -689,7 +800,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_access_memory_reactivation() {
|
||||
let mut engine = ConsolidationEngine::new(ConsolidationConfig::default());
|
||||
let id = engine.add_memory("chunk".to_string(), unit_vec(4, 0), MemorySource::User, 0.0);
|
||||
let id = engine.add_memory("chunk".to_string(), unit_vec(4, 0), UntrustedSource::User, 0.0);
|
||||
|
||||
engine.access_memory(id, 5000.0);
|
||||
let rec = engine.get_by_id(id).unwrap();
|
||||
@@ -710,11 +821,11 @@ mod tests {
|
||||
let mut engine = ConsolidationEngine::new(ConsolidationConfig::default());
|
||||
|
||||
// 2 Working
|
||||
engine.add_memory("w1".to_string(), unit_vec(4, 0), MemorySource::User, 0.0);
|
||||
engine.add_memory("w2".to_string(), unit_vec(4, 1), MemorySource::User, 0.0);
|
||||
engine.add_memory("w1".to_string(), unit_vec(4, 0), UntrustedSource::User, 0.0);
|
||||
engine.add_memory("w2".to_string(), unit_vec(4, 1), UntrustedSource::User, 0.0);
|
||||
|
||||
// 1 Episodic (manually set)
|
||||
let id_e = engine.add_memory("e1".to_string(), unit_vec(4, 2), MemorySource::User, 0.0);
|
||||
let id_e = engine.add_memory("e1".to_string(), unit_vec(4, 2), UntrustedSource::User, 0.0);
|
||||
engine
|
||||
.records
|
||||
.iter_mut()
|
||||
@@ -723,7 +834,7 @@ mod tests {
|
||||
.tier = MemoryTier::Episodic;
|
||||
|
||||
// 1 Semantic (manually set)
|
||||
let id_s = engine.add_memory("s1".to_string(), unit_vec(4, 3), MemorySource::User, 0.0);
|
||||
let id_s = engine.add_memory("s1".to_string(), unit_vec(4, 3), UntrustedSource::User, 0.0);
|
||||
engine
|
||||
.records
|
||||
.iter_mut()
|
||||
@@ -752,7 +863,7 @@ mod tests {
|
||||
let id = engine.add_memory(
|
||||
"episodic chunk".to_string(),
|
||||
unit_vec(4, i as usize),
|
||||
MemorySource::User,
|
||||
UntrustedSource::User,
|
||||
i as f64,
|
||||
);
|
||||
let rec = engine.records.iter_mut().find(|r| r.id == id).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user