fix(env_loader): warn when non-ASCII stripped from credential env vars (#13300)
Load-time sanitizer silently removed non-ASCII codepoints from any env var ending in _API_KEY / _TOKEN / _SECRET / _KEY, turning copy-paste artifacts (Unicode lookalikes, ZWSP, NBSP) into opaque provider-side API_KEY_INVALID errors. Warn once per key to stderr with the offending codepoints (U+XXXX) and guidance to re-copy from the provider dashboard.
This commit is contained in:
@@ -54,15 +54,17 @@ class TestEnvLoaderSanitization:
|
||||
"""Tests for _sanitize_loaded_credentials in env_loader."""
|
||||
|
||||
def test_strips_non_ascii_from_api_key(self, monkeypatch):
|
||||
from hermes_cli.env_loader import _sanitize_loaded_credentials
|
||||
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
|
||||
|
||||
_WARNED_KEYS.discard("OPENROUTER_API_KEY")
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-proj-abcʋdef")
|
||||
_sanitize_loaded_credentials()
|
||||
assert os.environ["OPENROUTER_API_KEY"] == "sk-proj-abcdef"
|
||||
|
||||
def test_strips_non_ascii_from_token(self, monkeypatch):
|
||||
from hermes_cli.env_loader import _sanitize_loaded_credentials
|
||||
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
|
||||
|
||||
_WARNED_KEYS.discard("DISCORD_BOT_TOKEN")
|
||||
monkeypatch.setenv("DISCORD_BOT_TOKEN", "tokénvalue")
|
||||
_sanitize_loaded_credentials()
|
||||
assert os.environ["DISCORD_BOT_TOKEN"] == "toknvalue"
|
||||
@@ -81,3 +83,51 @@ class TestEnvLoaderSanitization:
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-proj-allascii123")
|
||||
_sanitize_loaded_credentials()
|
||||
assert os.environ["OPENAI_API_KEY"] == "sk-proj-allascii123"
|
||||
|
||||
def test_warns_to_stderr_when_stripping(self, monkeypatch, capsys):
|
||||
"""Silent stripping masks bad keys as opaque provider 400s (see #6843 fallout).
|
||||
|
||||
Users must be told when a copy-paste artifact was removed so they
|
||||
can re-copy the key if authentication fails.
|
||||
"""
|
||||
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
|
||||
|
||||
_WARNED_KEYS.discard("GOOGLE_API_KEY")
|
||||
monkeypatch.setenv("GOOGLE_API_KEY", "AIzaSy\u200babcdef") # ZWSP mid-key
|
||||
_sanitize_loaded_credentials()
|
||||
assert os.environ["GOOGLE_API_KEY"] == "AIzaSyabcdef"
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "GOOGLE_API_KEY" in captured.err
|
||||
assert "U+200B" in captured.err
|
||||
assert "re-copy" in captured.err.lower()
|
||||
|
||||
def test_warning_fires_only_once_per_key(self, monkeypatch, capsys):
|
||||
"""Repeated loads (user env + project env) must not double-warn."""
|
||||
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
|
||||
|
||||
_WARNED_KEYS.discard("GEMINI_API_KEY")
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "AIza\u028bbad")
|
||||
_sanitize_loaded_credentials()
|
||||
first = capsys.readouterr().err
|
||||
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "AIza\u028bbad2")
|
||||
_sanitize_loaded_credentials()
|
||||
second = capsys.readouterr().err
|
||||
|
||||
assert "GEMINI_API_KEY" in first
|
||||
assert second == "" # no repeat warning
|
||||
|
||||
def test_ascii_control_chars_not_stripped(self, monkeypatch, capsys):
|
||||
"""ASCII control bytes (e.g. ESC 0x1B from terminal paste) are NOT non-ASCII.
|
||||
|
||||
This is intentional — they're valid ASCII for HTTP headers even if the
|
||||
provider rejects them. Documents the scope of the sanitizer.
|
||||
"""
|
||||
from hermes_cli.env_loader import _sanitize_loaded_credentials, _WARNED_KEYS
|
||||
|
||||
_WARNED_KEYS.clear()
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant\x1bapi-key")
|
||||
_sanitize_loaded_credentials()
|
||||
assert os.environ["ANTHROPIC_API_KEY"] == "sk-ant\x1bapi-key"
|
||||
assert capsys.readouterr().err == ""
|
||||
|
||||
Reference in New Issue
Block a user