Skip to content
Closed
Show file tree
Hide file tree
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
9 changes: 9 additions & 0 deletions agent/redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
45 changes: 45 additions & 0 deletions tests/agent/test_redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}'
Expand Down
Loading