Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 44 additions & 21 deletions crates/zeph-core/src/agent/context/assembly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use zeph_skills::loader::SkillMeta;
use zeph_skills::prompt::{format_skills_catalog, format_skills_prompt_compact};

use crate::redact::scrub_content;
use zeph_sanitizer::{ContentSource, ContentSourceKind};
use zeph_sanitizer::{ContentSource, ContentSourceKind, MemorySourceHint};

#[cfg(feature = "lsp-context")]
use super::super::LSP_NOTE_PREFIX;
Expand Down Expand Up @@ -664,38 +664,54 @@ impl<C: Channel> Agent<C> {

// Insert fetched messages (order: doc_rag, corrections, recall, cross-session, summaries at position 1)
// All memory-sourced messages are sanitized before insertion (CRIT-02: memory poisoning defense).
// Each path carries a MemorySourceHint that modulates injection detection sensitivity:
// ExternalContent — full detection (graph facts, document RAG may hold adversarial content)
// ConversationHistory — detection skipped (user's own prior turns, false-positive suppression)
// LlmSummary — detection skipped (generated by our model from already-sanitized content)
if let Some(msg) = graph_facts_msg.filter(|_| self.msg.messages.len() > 1) {
self.msg
.messages
.insert(1, self.sanitize_memory_message(msg).await); // lgtm[rust/cleartext-logging]
self.msg.messages.insert(
1,
self.sanitize_memory_message(msg, MemorySourceHint::ExternalContent)
.await,
); // lgtm[rust/cleartext-logging]
tracing::debug!("injected knowledge graph facts into context");
}
if let Some(msg) = doc_rag_msg.filter(|_| self.msg.messages.len() > 1) {
self.msg
.messages
.insert(1, self.sanitize_memory_message(msg).await); // lgtm[rust/cleartext-logging]
self.msg.messages.insert(
1,
self.sanitize_memory_message(msg, MemorySourceHint::ExternalContent)
.await,
); // lgtm[rust/cleartext-logging]
tracing::debug!("injected document RAG context");
}
if let Some(msg) = corrections_msg.filter(|_| self.msg.messages.len() > 1) {
self.msg
.messages
.insert(1, self.sanitize_memory_message(msg).await); // lgtm[rust/cleartext-logging]
self.msg.messages.insert(
1,
self.sanitize_memory_message(msg, MemorySourceHint::ConversationHistory)
.await,
); // lgtm[rust/cleartext-logging]
tracing::debug!("injected past corrections into context");
}
if let Some(msg) = recall_msg.filter(|_| self.msg.messages.len() > 1) {
self.msg
.messages
.insert(1, self.sanitize_memory_message(msg).await); // lgtm[rust/cleartext-logging]
self.msg.messages.insert(
1,
self.sanitize_memory_message(msg, MemorySourceHint::ConversationHistory)
.await,
); // lgtm[rust/cleartext-logging]
}
if let Some(msg) = cross_session_msg.filter(|_| self.msg.messages.len() > 1) {
self.msg
.messages
.insert(1, self.sanitize_memory_message(msg).await); // lgtm[rust/cleartext-logging]
self.msg.messages.insert(
1,
self.sanitize_memory_message(msg, MemorySourceHint::LlmSummary)
.await,
); // lgtm[rust/cleartext-logging]
}
if let Some(msg) = summaries_msg.filter(|_| self.msg.messages.len() > 1) {
self.msg
.messages
.insert(1, self.sanitize_memory_message(msg).await); // lgtm[rust/cleartext-logging]
self.msg.messages.insert(
1,
self.sanitize_memory_message(msg, MemorySourceHint::LlmSummary)
.await,
); // lgtm[rust/cleartext-logging]
tracing::debug!("injected summaries into context");
}

Expand Down Expand Up @@ -765,8 +781,15 @@ impl<C: Channel> Agent<C> {
/// This is the SOLE sanitization point for the 6 memory retrieval paths (`doc_rag`,
/// corrections, recall, `cross_session`, summaries, `graph_facts`). Do not add redundant
/// sanitization in zeph-memory or at other call sites.
async fn sanitize_memory_message(&self, mut msg: Message) -> Message {
let source = ContentSource::new(ContentSourceKind::MemoryRetrieval);
///
/// The `hint` parameter modulates injection detection sensitivity:
/// - `ConversationHistory` / `LlmSummary`: detection skipped (false-positive suppression).
/// - `ExternalContent`: full detection (document RAG, graph facts).
///
/// Truncation, control-char stripping, delimiter escaping, and spotlighting remain active
/// for all hints (defense-in-depth invariant).
async fn sanitize_memory_message(&self, mut msg: Message, hint: MemorySourceHint) -> Message {
let source = ContentSource::new(ContentSourceKind::MemoryRetrieval).with_memory_hint(hint);
let sanitized = self.security.sanitizer.sanitize(&msg.content, source);
self.update_metrics(|m| m.sanitizer_runs += 1);
if !sanitized.injection_flags.is_empty() {
Expand Down
Loading
Loading