From 38bb193f381fa0f3fd42ac7f1e48e44b50179386 Mon Sep 17 00:00:00 2001 From: Frowtek Date: Sat, 4 Jul 2026 06:21:25 +0300 Subject: [PATCH] fix(stt): route transcription inputs through the shared read guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `transcribe_audio` reads a local file and hands it to the configured STT provider — for the hosted providers (Groq, OpenAI, Mistral, xAI, ElevenLabs) that ships the file's bytes to a third-party API. The same local-input read guard was added to image-gen (587be5b5b) and xAI video-gen (104232979) to keep the agent from feeding credential/secret stores to a provider, but STT was missed. Call `get_read_block_error(file_path)` at the top of `transcribe_audio`, before validation/dispatch, so a `.env`, `auth.json`, `.anthropic_oauth.json`, `mcp-tokens/`, etc. is refused up front instead of being transcribed (and, for hosted providers, exfiltrated). This is defense-in-depth, not a security boundary — the guard's own message says so — but it restores parity with the image/video-gen tools. Regression test: a `.env` file is refused with the shared read-guard message before any provider dispatch (mutation-verified). --- tests/tools/test_transcription_tools.py | 28 ++++++++++++++++++++++++- tools/transcription_tools.py | 9 ++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_transcription_tools.py b/tests/tools/test_transcription_tools.py index 581b6ccbec50..6fdf9dd6cff4 100644 --- a/tests/tools/test_transcription_tools.py +++ b/tests/tools/test_transcription_tools.py @@ -2317,7 +2317,9 @@ class TestLocalBaseUrlNoApiKey: assert not _is_local_or_private_url("") -# ============================================================================ +# ===================================================================== + + # CAF (iMessage voice note) conversion tests # ============================================================================ @@ -2456,3 +2458,27 @@ class TestCafConversion: assert result["success"] is True mock_convert.assert_not_called() +======= +class TestTranscribeCredentialReadGuard: + """transcribe_audio must refuse credential/secret stores before dispatch.""" + + def test_transcribe_audio_blocks_credential_read(self, tmp_path): + """A ``.env`` (secret-bearing) file is refused up front, so its + plaintext is never shipped to an external STT provider — mirroring the + read guard added to image-gen (587be5b5b) and xAI video-gen + (104232979).""" + from tools.transcription_tools import transcribe_audio + from agent.file_safety import get_read_block_error + + env_file = tmp_path / ".env" + env_file.write_text("OPENAI_API_KEY=sk-secret\n") + + expected = get_read_block_error(str(env_file)) + assert expected, "test setup: a .env file should be read-blocked" + + result = transcribe_audio(str(env_file)) + + assert result["success"] is False + # The error is the shared read-guard message, not an audio-validation + # or provider error — proving the guard fired before dispatch. + assert result["error"] == expected diff --git a/tools/transcription_tools.py b/tools/transcription_tools.py index 4f228f470117..877f799621c0 100644 --- a/tools/transcription_tools.py +++ b/tools/transcription_tools.py @@ -2144,6 +2144,15 @@ def _transcribe_prepared_audio(file_path: str, model: Optional[str] = None) -> D - "error" (str, optional): Error message if success is False - "provider" (str, optional): Which provider was used """ + # Refuse to feed a credential / secret store (auth.json, .env, OAuth + # tokens, mcp-tokens/, ...) to an STT provider: an external provider would + # ship its plaintext contents to a third-party API. Mirrors the local-input + # read guard added to image-gen (587be5b5b) and xAI video-gen (104232979). + from agent.file_safety import get_read_block_error + blocked = get_read_block_error(file_path) + if blocked: + return {"success": False, "transcript": "", "error": blocked} + # Apply common path validation before provider resolution so invalid files # cannot trigger provider setup or lazy installation. The remote-upload # size cap is enforced separately below, only for non-local providers.