diff --git a/tests/agent/test_redact.py b/tests/agent/test_redact.py new file mode 100644 index 000000000..52e015ca9 --- /dev/null +++ b/tests/agent/test_redact.py @@ -0,0 +1,173 @@ +"""Tests for agent.redact -- secret masking in logs and output.""" + +import logging + +import pytest + +from agent.redact import redact_sensitive_text, RedactingFormatter + + +class TestKnownPrefixes: + def test_openai_sk_key(self): + text = "Using key sk-proj-abc123def456ghi789jkl012" + result = redact_sensitive_text(text) + assert "sk-pro" in result + assert "abc123def456" not in result + assert "..." in result + + def test_openrouter_sk_key(self): + text = "OPENROUTER_API_KEY=sk-or-v1-abcdefghijklmnopqrstuvwxyz1234567890" + result = redact_sensitive_text(text) + assert "abcdefghijklmnop" not in result + + def test_github_pat_classic(self): + result = redact_sensitive_text("token: ghp_abc123def456ghi789jkl") + assert "abc123def456" not in result + + def test_github_pat_fine_grained(self): + result = redact_sensitive_text("github_pat_abc123def456ghi789jklmno") + assert "abc123def456" not in result + + def test_slack_token(self): + token = "xoxb-" + "0" * 12 + "-" + "a" * 14 + result = redact_sensitive_text(token) + assert "a" * 14 not in result + + def test_google_api_key(self): + result = redact_sensitive_text("AIzaSyB-abc123def456ghi789jklmno012345") + assert "abc123def456" not in result + + def test_perplexity_key(self): + result = redact_sensitive_text("pplx-abcdef123456789012345") + assert "abcdef12345" not in result + + def test_fal_key(self): + result = redact_sensitive_text("fal_abc123def456ghi789jkl") + assert "abc123def456" not in result + + def test_short_token_fully_masked(self): + result = redact_sensitive_text("key=sk-short1234567") + assert "***" in result + + +class TestEnvAssignments: + def test_export_api_key(self): + text = "export OPENAI_API_KEY=sk-proj-abc123def456ghi789jkl012" + result = redact_sensitive_text(text) + assert "OPENAI_API_KEY=" in result + assert "abc123def456" not in result + + def test_quoted_value(self): + text = 'MY_SECRET_TOKEN="supersecretvalue123456789"' + result = redact_sensitive_text(text) + assert "MY_SECRET_TOKEN=" in result + assert "supersecretvalue" not in result + + def test_non_secret_env_unchanged(self): + text = "HOME=/home/user" + result = redact_sensitive_text(text) + assert result == text + + def test_path_unchanged(self): + text = "PATH=/usr/local/bin:/usr/bin" + result = redact_sensitive_text(text) + assert result == text + + +class TestJsonFields: + def test_json_api_key(self): + text = '{"apiKey": "sk-proj-abc123def456ghi789jkl012"}' + result = redact_sensitive_text(text) + assert "abc123def456" not in result + + def test_json_token(self): + text = '{"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.longtoken.here"}' + result = redact_sensitive_text(text) + assert "eyJhbGciOiJSUzI1NiIs" not in result + + def test_json_non_secret_unchanged(self): + text = '{"name": "John", "model": "gpt-4"}' + result = redact_sensitive_text(text) + assert result == text + + +class TestAuthHeaders: + def test_bearer_token(self): + text = "Authorization: Bearer sk-proj-abc123def456ghi789jkl012" + result = redact_sensitive_text(text) + assert "Authorization: Bearer" in result + assert "abc123def456" not in result + + def test_case_insensitive(self): + text = "authorization: bearer mytoken123456789012345678" + result = redact_sensitive_text(text) + assert "mytoken12345" not in result + + +class TestTelegramTokens: + def test_bot_token(self): + text = "bot123456789:ABCDEfghij-KLMNopqrst_UVWXyz12345" + result = redact_sensitive_text(text) + assert "ABCDEfghij" not in result + assert "123456789:***" in result + + def test_raw_token(self): + text = "12345678901:ABCDEfghijKLMNopqrstUVWXyz1234567890" + result = redact_sensitive_text(text) + assert "ABCDEfghij" not in result + + +class TestPassthrough: + def test_empty_string(self): + assert redact_sensitive_text("") == "" + + def test_none_returns_none(self): + assert redact_sensitive_text(None) is None + + def test_normal_text_unchanged(self): + text = "Hello world, this is a normal log message with no secrets." + assert redact_sensitive_text(text) == text + + def test_code_unchanged(self): + text = "def main():\n print('hello')\n return 42" + assert redact_sensitive_text(text) == text + + def test_url_without_key_unchanged(self): + text = "Connecting to https://api.openai.com/v1/chat/completions" + assert redact_sensitive_text(text) == text + + +class TestRedactingFormatter: + def test_formats_and_redacts(self): + formatter = RedactingFormatter("%(message)s") + record = logging.LogRecord( + name="test", level=logging.INFO, pathname="", lineno=0, + msg="Key is sk-proj-abc123def456ghi789jkl012", + args=(), exc_info=None, + ) + result = formatter.format(record) + assert "abc123def456" not in result + assert "sk-pro" in result + + +class TestPrintenvSimulation: + """Simulate what happens when the agent runs `env` or `printenv`.""" + + def test_full_env_dump(self): + env_dump = """HOME=/home/user +PATH=/usr/local/bin:/usr/bin +OPENAI_API_KEY=sk-proj-abc123def456ghi789jkl012mno345 +OPENROUTER_API_KEY=sk-or-v1-reallyLongSecretKeyValue12345678 +FIRECRAWL_API_KEY=fc-shortkey123456789012 +TELEGRAM_BOT_TOKEN=bot987654321:ABCDEfghij-KLMNopqrst_UVWXyz12345 +SHELL=/bin/bash +USER=teknium""" + result = redact_sensitive_text(env_dump) + # Secrets should be masked + assert "abc123def456" not in result + assert "reallyLongSecretKey" not in result + assert "ABCDEfghij" not in result + # Non-secrets should survive + assert "HOME=/home/user" in result + assert "SHELL=/bin/bash" in result + assert "USER=teknium" in result diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index cb0d9cd4e..f758768eb 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -1037,8 +1037,12 @@ def terminal_tool( ) output = output[:head_chars] + truncated_notice + output[-tail_chars:] + # Redact secrets from command output (catches env/printenv leaking keys) + from agent.redact import redact_sensitive_text + output = redact_sensitive_text(output.strip()) if output else "" + return json.dumps({ - "output": output.strip() if output else "", + "output": output, "exit_code": returncode, "error": None }, ensure_ascii=False)