From 5613dfea938ab26bc759cebba7184931a1b18d94 Mon Sep 17 00:00:00 2001 From: flamiinngo Date: Sun, 17 May 2026 09:14:27 +0100 Subject: [PATCH] fix(security): redact xAI (Grok) API keys in logs xAI is a first-class provider in hermes-agent with its own credential pool entry (XAI_API_KEY / xai-oauth). API keys follow the format xai-<60+ alphanumeric chars> and were absent from _PREFIX_PATTERNS in agent/redact.py. When a key appears raw in log output, tool results, or error messages, it passed through completely unmasked. The ENV-assignment and Bearer header patterns catch the most common cases, but a raw token in a stack trace or debug print had no protection. Verified before fix: redact_sensitive_text("using key xai-ABCD...rstu to call xAI", force=True) # "using key xai-ABCD...rstu to call xAI" <- exposed After fix: # "using key xai-AB...rstu to call xAI" <- masked Five unit tests added to TestXaiToken covering bare token masking, env assignment, short-prefix false positive, company name false positive, and visible prefix in masked output. --- agent/redact.py | 1 + tests/agent/test_redact.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/agent/redact.py b/agent/redact.py index c6643304a9d..4cafbaef7a2 100644 --- a/agent/redact.py +++ b/agent/redact.py @@ -103,6 +103,7 @@ _PREFIX_PATTERNS = [ r"hsk-[A-Za-z0-9]{10,}", # Hindsight API key r"mem0_[A-Za-z0-9]{10,}", # Mem0 Platform API key r"brv_[A-Za-z0-9]{10,}", # ByteRover API key + r"xai-[A-Za-z0-9]{30,}", # xAI (Grok) API key ] # ENV assignment patterns: KEY=value where KEY contains a secret-like name diff --git a/tests/agent/test_redact.py b/tests/agent/test_redact.py index a2c6b60b276..928eb1ff357 100644 --- a/tests/agent/test_redact.py +++ b/tests/agent/test_redact.py @@ -511,3 +511,29 @@ class TestFormBodyRedaction: text = "first=1\nsecond=2" # Should pass through (still subject to other redactors) assert "first=1" in redact_sensitive_text(text) + + +class TestXaiToken: + KEY = "xai-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstu" + + def test_bare_token_masked(self): + result = redact_sensitive_text(f"using key {self.KEY}", force=True) + assert self.KEY not in result + assert "xai-AB" in result + + def test_env_assignment_masked(self): + result = redact_sensitive_text(f"XAI_API_KEY={self.KEY}", force=True) + assert self.KEY not in result + + def test_too_short_not_masked(self): + short = "xai-tooshort" + result = redact_sensitive_text(f"text {short} here", force=True) + assert short in result + + def test_company_name_not_masked(self): + result = redact_sensitive_text("xai is a company", force=True) + assert result == "xai is a company" + + def test_prefix_visible_in_masked_output(self): + result = redact_sensitive_text(self.KEY, force=True) + assert result.startswith("xai-AB")