diff --git a/agent/redact.py b/agent/redact.py index 7727e816fe..5d0f9f2c6e 100644 --- a/agent/redact.py +++ b/agent/redact.py @@ -798,7 +798,10 @@ def is_env_dump_command(command: str | None) -> bool: REDACTION_UNAVAILABLE = "[redaction-unavailable]" -_BEARER_RESIDUE_RE = re.compile(r"\bBearer\s+(?:\[[^\]]+\]|[A-Za-z0-9._~+/-]+=*)", re.IGNORECASE) +# The opaque branch needs a 20-char floor (the floor the gateway/A2A sweeps always had): without it the +# English word "bearer" turns "the bearer of bad news" into "Bearer [redacted] bad news" on every chat +# reply. The bracket branch folds an already-masked residue ("Bearer [redacted-jwt]") to one marker. +_BEARER_RESIDUE_RE = re.compile(r"\bBearer\s+(?:\[[^\]]+\]|[A-Za-z0-9._~+/-]{20,}=*)", re.IGNORECASE) def redact_for_egress(text: str) -> str: diff --git a/tests/agent/test_redact.py b/tests/agent/test_redact.py index 427fd184fc..16a4e132cd 100644 --- a/tests/agent/test_redact.py +++ b/tests/agent/test_redact.py @@ -1197,6 +1197,12 @@ class TestRedactForEgress: assert "opaque0123456789abcdef" not in out assert "https://x.example" in out + def test_bearer_sweep_masks_real_tokens_not_the_english_word(self): + from agent.redact import redact_for_egress + prose = "I'm the bearer of bad news: the deploy failed" + assert redact_for_egress(prose) == prose + assert "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" not in redact_for_egress("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9") + def test_fails_closed_when_the_redactor_raises(self, monkeypatch): from agent import redact as R monkeypatch.setattr(R, "redact_sensitive_text", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom"))) diff --git a/tests/monitoring/test_gateway_health_export.py b/tests/monitoring/test_gateway_health_export.py index 5e24671e5d..e38397e698 100644 --- a/tests/monitoring/test_gateway_health_export.py +++ b/tests/monitoring/test_gateway_health_export.py @@ -32,11 +32,11 @@ def test_otlp_attrs_redact_strings_and_never_export_profile(): "event": "gateway_health", "name": "gateway.lifecycle", "profile": "user@example.com", - "exit_reason": "Bearer top-secret-token for user@example.com", + "exit_reason": "Bearer top-secret-token-0123456789 for user@example.com", }) assert "hermes.profile" not in attrs - assert "top-secret-token" not in str(attrs) + assert "top-secret-token-0123456789" not in str(attrs) assert "user@example.com" not in str(attrs) @@ -48,7 +48,7 @@ def test_resource_attributes_are_allowlisted_and_sanitized(): "service.instance.id": "install-1", "deployment.environment.name": "staging", "user.email": "user@example.com", - "authorization": "Bearer top-secret-token", + "authorization": "Bearer top-secret-token-0123456789", "custom.request.id": "unbounded", }) @@ -73,13 +73,13 @@ def test_diagnostic_log_attributes_are_allowlisted_redacted_and_profile_free(): "name": "platform.fatal", "subsystem": "platform.slack", "profile": "user@example.com", - "error_code": "Bearer top-secret-token", + "error_code": "Bearer top-secret-token-0123456789", "custom": "must-not-egress", }) assert "hermes.profile" not in attrs assert "hermes.custom" not in attrs - assert "top-secret-token" not in str(attrs) + assert "top-secret-token-0123456789" not in str(attrs)