From 60452712b2da8c570486771fe61ea5fbe4b2404e Mon Sep 17 00:00:00 2001 From: Lukas Parsons Date: Tue, 7 Jul 2026 21:38:15 -0400 Subject: [PATCH] fix: skip empty untrusted context blocks to prevent model hallucination untrusted_context_message() now returns None when content is empty or whitespace-only. Previously an empty document would produce a blank UNTRUSTED_SOURCE_DATA wrapper that confused Gemma into thinking the user pasted a placeholder template. Callers already use truthiness guards (if _doc_message:) for insertion, so None propagates cleanly. Two _protected setter sites needed explicit guards added (doc + email paths in agent_loop.py). --- src/agent_loop.py | 6 ++++-- src/prompt_security.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/agent_loop.py b/src/agent_loop.py index 2709c99b..fd8e9018 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -1504,7 +1504,8 @@ def _build_system_prompt( "knowing the user's personal style." ) _doc_message = untrusted_context_message("active editor document", doc_ctx) - _doc_message["_protected"] = True + if _doc_message: + _doc_message["_protected"] = True # Auto-detect suggestion mode _last_user_msg = "" @@ -1584,7 +1585,8 @@ def _build_system_prompt( f"open email's sender.\n" ) _email_message = untrusted_context_message("active email reader", email_ctx) - _email_message["_protected"] = True + if _email_message: + _email_message["_protected"] = True # Inject writing style for any email writing path. This is deliberately # broader than read/list: models may compose via send_email, reply_to_email, diff --git a/src/prompt_security.py b/src/prompt_security.py index 3a25c79d..a64ff002 100644 --- a/src/prompt_security.py +++ b/src/prompt_security.py @@ -61,7 +61,7 @@ def _sanitize_label(label: str) -> str: return label -def untrusted_context_message(label: str, content: Any) -> Dict[str, Any]: +def untrusted_context_message(label: str, content: Any) -> Dict[str, Any] | None: """Return an LLM message that keeps retrieved/source text out of system role. The template is structured so that *only* the hardcoded @@ -69,9 +69,15 @@ def untrusted_context_message(label: str, content: Any) -> Dict[str, Any]: caller-derived text is placed in the pre-guard trusted framing zone. The source label and the body content are both placed *inside* the guarded block where the LLM treats them as untrusted data. + + Returns None when content is empty or whitespace-only — avoids + injecting blank wrapper blocks that confuse the model into thinking + the user pasted a placeholder template. """ + text = "" if content is None else str(content).strip() + if not text: + return None safe_label = _sanitize_label(label) - text = "" if content is None else str(content) text = _escape_guard_markers(text) return { "role": "user",