Skip to content
Open
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
5 changes: 5 additions & 0 deletions plugins/platforms/discord/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3819,6 +3819,10 @@ def _probe_audio_duration_seconds(self, audio_path: str) -> Optional[float]:
pass

try:
# OS media helper — scrub credentials, same rule as the voice
# TTS/STT and playback subprocesses (#56332 / #70342).
from tools.environments.local import hermes_subprocess_env

proc = subprocess.run(
[
"ffprobe",
Expand All @@ -3832,6 +3836,7 @@ def _probe_audio_duration_seconds(self, audio_path: str) -> Optional[float]:
text=True,
timeout=5,
stdin=subprocess.DEVNULL,
env=hermes_subprocess_env(inherit_credentials=False),
)
if proc.returncode == 0:
raw = (proc.stdout or "").strip()
Expand Down
5 changes: 5 additions & 0 deletions plugins/platforms/telegram/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,15 @@ def _probe_voice_duration_seconds(path: str) -> Optional[int]:
import subprocess

if shutil.which("ffprobe"):
# OS media helper — scrub credentials, same rule as the voice
# TTS/STT and playback subprocesses (#56332 / #70342).
from tools.environments.local import hermes_subprocess_env

proc = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", path],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
env=hermes_subprocess_env(inherit_credentials=False),
)
if proc.returncode == 0:
return _coerce_duration_seconds(proc.stdout.strip())
Expand Down
107 changes: 107 additions & 0 deletions tests/tools/test_media_helper_env_scrub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""OS media helpers must not inherit Hermes credentials.

Established by the TTS/STT command scrub (#56332 / #70342) and extended to
voice-mode playback: ``ffplay``/``afplay``/``aplay`` are spawned with
``hermes_subprocess_env(inherit_credentials=False)`` so provider API keys and
gateway tokens never reach an OS media helper.

``ffmpeg`` / ``ffprobe`` are the same kind of process — third-party binaries
Hermes shells out to for transcoding and duration probes — and they run on the
same voice/attachment paths.
"""

from unittest.mock import MagicMock, patch


_SECRETS = {
"OPENAI_API_KEY": "sk-test",
"TELEGRAM_BOT_TOKEN": "secret-token",
"ANTHROPIC_API_KEY": "sk-ant-test",
}


def _seed_secrets(monkeypatch):
for key, value in _SECRETS.items():
monkeypatch.setenv(key, value)


def _assert_scrubbed(env):
assert env is not None, "media helper inherited the full process environment"
for key in _SECRETS:
assert key not in env, f"{key} leaked into the media helper env"


def test_tts_ffmpeg_transcode_scrubs_credentials(tmp_path, monkeypatch):
"""The voice-note OGG transcode on the TTS path."""
_seed_secrets(monkeypatch)
src = tmp_path / "in.wav"
src.write_bytes(b"RIFFfake")
out = tmp_path / "out.ogg"

captured = {}

def fake_run(cmd, **kwargs):
captured["env"] = kwargs.get("env")
out.write_bytes(b"OggSfake")
result = MagicMock()
result.returncode = 0
result.stderr = b""
return result

import tools.tts_tool as tts

with patch.object(tts, "_has_ffmpeg", return_value=True), patch.object(
tts.subprocess, "run", side_effect=fake_run
):
tts._ffmpeg_transcode_to_opus(str(src), str(out))

_assert_scrubbed(captured.get("env"))


def test_telegram_ffprobe_duration_scrubs_credentials(tmp_path, monkeypatch):
"""Inbound voice-note duration probe."""
_seed_secrets(monkeypatch)
media = tmp_path / "voice.ogg"
media.write_bytes(b"OggSfake")

captured = {}

def fake_run(cmd, **kwargs):
captured["env"] = kwargs.get("env")
result = MagicMock()
result.returncode = 0
result.stdout = "3.5"
return result

from plugins.platforms.telegram import adapter as tg

with patch("shutil.which", return_value="/usr/bin/ffprobe"), patch(
"subprocess.run", side_effect=fake_run
):
tg._probe_voice_duration_seconds(str(media))

_assert_scrubbed(captured.get("env"))


def test_discord_ffprobe_duration_scrubs_credentials(tmp_path, monkeypatch):
"""Discord's audio duration probe — same helper, same exposure."""
_seed_secrets(monkeypatch)
media = tmp_path / "clip.mp3"
media.write_bytes(b"ID3fake")

captured = {}

def fake_run(cmd, **kwargs):
captured["env"] = kwargs.get("env")
result = MagicMock()
result.returncode = 0
result.stdout = "2.0"
return result

from plugins.platforms.discord import adapter as dc

adapter = dc.DiscordAdapter.__new__(dc.DiscordAdapter)
with patch.object(dc.subprocess, "run", side_effect=fake_run):
adapter._probe_audio_duration_seconds(str(media))

_assert_scrubbed(captured.get("env"))
6 changes: 6 additions & 0 deletions tools/tts_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1269,13 +1269,19 @@ def _ffmpeg_transcode_to_opus(input_path: str, ogg_path: str) -> Optional[str]:
in_place = os.path.abspath(input_path) == os.path.abspath(ogg_path)
work_path = ogg_path + ".tmp.ogg" if in_place else ogg_path
try:
# Sibling of the TTS/STT command scrub (#56332 / #70342) and the
# voice-mode playback scrub: ffmpeg is an OS media helper and has no
# business seeing provider API keys or gateway tokens.
from tools.environments.local import hermes_subprocess_env

result = subprocess.run(
["ffmpeg", "-i", input_path, "-acodec", "libopus",
"-ac", "1", "-b:a", "48k", "-vbr", "on",
"-application", "voip", "-compression_level", "10", "-f", "ogg",
work_path, "-y"],
capture_output=True, timeout=30,
stdin=subprocess.DEVNULL,
env=hermes_subprocess_env(inherit_credentials=False),
creationflags=windows_hide_flags(),
)
if result.returncode != 0:
Expand Down
Loading