diff --git a/agent/redact.py b/agent/redact.py index 1beb10450fdf..1b08591d9daa 100644 --- a/agent/redact.py +++ b/agent/redact.py @@ -344,6 +344,15 @@ def redact_sensitive_text(text: str, *, force: bool = False, code_file: bool = F if _has_known_prefix_substring(text): text = _PREFIX_RE.sub(lambda m: _mask_token(m.group(1)), text) + # ENV assignments: OPENAI_API_KEY=sk-abc... + def _redact_env(m): + name, quote, value = m.group(1), m.group(2), m.group(3) + # Skip programmatic env lookups — these reference variable *names*, + # not actual secret values (fixes #2852). + if re.match(r"os\.(?:getenv|environ)", value): + return m.group(0) + return f"{name}={quote}{_mask_token(value)}{quote}" + text = _ENV_ASSIGN_RE.sub(_redact_env, text) # ENV assignments: OPENAI_API_KEY=*** (skip for code files — false positives) if not code_file: if "=" in text: diff --git a/tests/agent/test_redact.py b/tests/agent/test_redact.py index 928eb1ff357e..b05279d013cb 100644 --- a/tests/agent/test_redact.py +++ b/tests/agent/test_redact.py @@ -115,6 +115,51 @@ def test_export_whitespace_preserved(self): assert "mypassword" not in result +class TestEnvLookupPreserved: + """Programmatic env var lookups must not be corrupted (issue #2852).""" + + def test_os_getenv_single_quote(self): + text = "ha_token=os.getenv('HOMEASSISTANT_TOKEN')" + result = redact_sensitive_text(text) + assert result == text + + def test_os_getenv_double_quote(self): + text = 'api_token=os.getenv("MY_API_TOKEN")' + result = redact_sensitive_text(text) + assert result == text + + def test_os_environ_get(self): + text = "self.token=os.environ.get('HOMEASSISTANT_TOKEN')" + result = redact_sensitive_text(text) + assert result == text + + def test_os_environ_bracket(self): + text = "secret=os.environ['MY_SECRET']" + result = redact_sensitive_text(text) + assert result == text + + def test_spaced_assignment(self): + text = "ha_token = os.getenv('HOMEASSISTANT_TOKEN')" + result = redact_sensitive_text(text) + assert result == text + + def test_real_env_value_still_redacted(self): + text = "HOMEASSISTANT_TOKEN=eyJhbGciOiJIUzI1NiJ9.abc123.xyz" + result = redact_sensitive_text(text) + assert "eyJhbGciOiJIUzI1NiJ9" not in result + + def test_multiline_skill_file(self): + text = """def _get_credentials(): + ha_url = os.getenv('HOMEASSISTANT_URL') + ha_token=os.getenv('HOMEASSISTANT_TOKEN') + if not ha_url or not ha_token: + raise ValueError('Missing credentials') + return ha_url, ha_token""" + result = redact_sensitive_text(text) + assert "os.getenv('HOMEASSISTANT_TOKEN')" in result + assert "os.getenv('HOMEASSISTANT_URL')" in result + + class TestJsonFields: def test_json_api_key(self): text = '{"apiKey": "sk-proj-abc123def456ghi789jkl012"}'