Skip to content
Closed
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
27 changes: 27 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def __repr__(self):
from agent.memory_manager import StreamingContextScrubber, build_memory_context_block, sanitize_context
from agent.think_scrubber import StreamingThinkScrubber
from agent.retry_utils import jittered_backoff
from agent.redact import redact_sensitive_text
from agent.error_classifier import classify_api_error, FailoverReason
from agent.prompt_builder import (
DEFAULT_AGENT_IDENTITY, PLATFORM_HINTS,
Expand Down Expand Up @@ -5150,6 +5151,15 @@ def _save_session_log(self, messages: List[Dict[str, Any]] = None):
if msg.get("role") == "assistant" and msg.get("content"):
msg = dict(msg)
msg["content"] = self._clean_session_content(msg["content"])
# Redact credentials from assistant responses — defence-in-depth
# for any content that may have bypassed _build_assistant_message.
msg["content"] = redact_sensitive_text(msg["content"])
elif msg.get("role") == "tool" and isinstance(msg.get("content"), str):
# Redact credentials from tool output — terminal commands
# that echo API keys, tokens, or PATs in their stdout/stderr
# should not persist to session logs.
msg = dict(msg)
msg["content"] = redact_sensitive_text(msg["content"])
cleaned.append(msg)

# Guard: never overwrite a larger session log with fewer messages.
Expand Down Expand Up @@ -9795,6 +9805,15 @@ def _build_assistant_message(self, assistant_message, finish_reason: str) -> dic
if isinstance(_san_content, str) and _san_content:
_san_content = self._strip_think_blocks(_san_content).strip()

# Redact credentials (GitHub PATs, API keys, Bearer tokens, etc.)
# from assistant content so they never enter conversation history.
# This is a defence-in-depth addition to the tool-argument redaction
# below: if the LLM accidentally includes a secret in its natural
# language response, it gets caught here before persistence.
# Ref #19798 — PAT leaked via inline mention in assistant response.
if isinstance(_san_content, str) and _san_content:
_san_content = redact_sensitive_text(_san_content)

msg = {
"role": "assistant",
"content": _san_content,
Expand Down Expand Up @@ -9916,6 +9935,14 @@ def _build_assistant_message(self, assistant_message, finish_reason: str) -> dic
"arguments": tool_call.function.arguments
},
}
# Redact credentials (GitHub PATs, API keys, Bearer tokens, etc.)
# from tool call arguments so they never enter conversation history.
# Tool execution uses the raw API response object, not this dict,
# so redacting here is safe and only affects persistence/storage.
if isinstance(tc_dict["function"]["arguments"], str):
tc_dict["function"]["arguments"] = redact_sensitive_text(
tc_dict["function"]["arguments"]
)
# Preserve extra_content (e.g. Gemini thought_signature) so it
# is sent back on subsequent API calls. Without this, Gemini 3
# thinking models reject the request with a 400 error.
Expand Down