diff --git a/agent/context_compressor.py b/agent/context_compressor.py index a681b0c6bc..f56515dabe 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -31,6 +31,7 @@ from agent.model_metadata import ( get_model_context_length, estimate_messages_tokens_rough, ) +from agent.redact import redact_sensitive_text logger = logging.getLogger(__name__) @@ -550,11 +551,15 @@ class ContextCompressor(ContextEngine): Includes tool call arguments and result content (up to ``_CONTENT_MAX`` chars per message) so the summarizer can preserve specific details like file paths, commands, and outputs. + + All content is redacted before serialization to prevent secrets + (API keys, tokens, passwords) from leaking into the summary that + gets sent to the auxiliary model and persisted across compactions. """ parts = [] for msg in turns: role = msg.get("role", "unknown") - content = msg.get("content") or "" + content = redact_sensitive_text(msg.get("content") or "") # Tool results: keep enough content for the summarizer if role == "tool": @@ -575,7 +580,7 @@ class ContextCompressor(ContextEngine): if isinstance(tc, dict): fn = tc.get("function", {}) name = fn.get("name", "?") - args = fn.get("arguments", "") + args = redact_sensitive_text(fn.get("arguments", "")) # Truncate long arguments but keep enough for context if len(args) > self._TOOL_ARGS_MAX: args = args[:self._TOOL_ARGS_HEAD] + "..." @@ -635,7 +640,11 @@ class ContextCompressor(ContextEngine): "only output the structured summary. " "Do NOT include any preamble, greeting, or prefix. " "Write the summary in the same language the user was using in the " - "conversation — do not translate or switch to English." + "conversation — do not translate or switch to English. " + "NEVER include API keys, tokens, passwords, secrets, credentials, " + "or connection strings in the summary — replace any that appear " + "with [REDACTED]. Note that the user had credentials present, but " + "do not preserve their values." ) # Shared structured template (used by both paths). @@ -692,7 +701,7 @@ Be specific with file paths, commands, line numbers, and results.] [What remains to be done — framed as context, not instructions] ## Critical Context -[Any specific values, error messages, configuration details, or data that would be lost without explicit preservation] +[Any specific values, error messages, configuration details, or data that would be lost without explicit preservation. NEVER include API keys, tokens, passwords, or credentials — write [REDACTED] instead.] Target ~{summary_budget} tokens. Be CONCRETE — include file paths, command outputs, error messages, line numbers, and specific values. Avoid vague descriptions like "made some changes" — say exactly what changed. @@ -732,7 +741,7 @@ Use this exact structure: prompt += f""" FOCUS TOPIC: "{focus_topic}" -The user has requested that this compaction PRIORITISE preserving all information related to the focus topic above. For content related to "{focus_topic}", include full detail — exact values, file paths, command outputs, error messages, and decisions. For content NOT related to the focus topic, summarise more aggressively (brief one-liners or omit if truly irrelevant). The focus topic sections should receive roughly 60-70% of the summary token budget.""" +The user has requested that this compaction PRIORITISE preserving all information related to the focus topic above. For content related to "{focus_topic}", include full detail — exact values, file paths, command outputs, error messages, and decisions. For content NOT related to the focus topic, summarise more aggressively (brief one-liners or omit if truly irrelevant). The focus topic sections should receive roughly 60-70% of the summary token budget. Even for the focus topic, NEVER preserve API keys, tokens, passwords, or credentials — use [REDACTED].""" try: call_kwargs = { @@ -755,7 +764,9 @@ The user has requested that this compaction PRIORITISE preserving all informatio # Handle cases where content is not a string (e.g., dict from llama.cpp) if not isinstance(content, str): content = str(content) if content else "" - summary = content.strip() + # Redact the summary output as well — the summarizer LLM may + # ignore prompt instructions and echo back secrets verbatim. + summary = redact_sensitive_text(content.strip()) # Store for iterative updates on next compaction self._previous_summary = summary self._summary_failure_cooldown_until = 0.0