fix(whatsapp-cloud): review follow-ups for #43921

- nous_subscription: gate the STT managed-default flip on openai-audio
  entitlement and skip when a local backend (faster-whisper or custom
  command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
  GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
  normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
  wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
  delete transient .ogg after voice upload, FIFO-cap interactive-button
  state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
This commit is contained in:
teknium1 2026-06-11 07:51:01 -07:00
parent 2ecb4e62bb
commit 52c7976f40
No known key found for this signature in database
8 changed files with 319 additions and 35 deletions

View file

@ -226,6 +226,24 @@ def _stt_label(current_provider: str) -> str:
return mapping.get(current_provider or "local", current_provider or "Local faster-whisper")
def _local_stt_backend_available() -> bool:
"""Whether a local STT backend could serve transcription right now.
True when faster-whisper is importable or a custom local STT command
is configured. Used both for feature detection and to stop
``apply_nous_managed_defaults`` from flipping a working local setup
to the managed gateway.
"""
if get_env_value("HERMES_LOCAL_STT_COMMAND"):
return True
try:
from tools.transcription_tools import _HAS_FASTER_WHISPER
return bool(_HAS_FASTER_WHISPER)
except Exception:
return False
def _resolve_browser_feature_state(
*,
browser_tool_enabled: bool,
@ -772,10 +790,22 @@ def apply_nous_managed_defaults(
# (requires `pip install faster-whisper`); for Nous subscribers we
# flip it to "openai" so the managed audio gateway handles transcription
# via the same auth as TTS. Skipped when the user has explicitly
# configured STT or has direct credentials for a non-managed provider.
if not features.stt.explicit_configured and not (
get_env_value("GROQ_API_KEY")
or get_env_value("MISTRAL_API_KEY")
# configured STT, has direct credentials for a non-managed provider,
# has a working local backend (faster-whisper installed or a custom
# local command — strong intent signal that "local" was a choice, not
# just the DEFAULT_CONFIG seed), or isn't entitled to the managed
# "openai-audio" category (flipping would point at a gateway that
# refuses them, silently breaking voice transcription).
if (
not features.stt.explicit_configured
and not _local_stt_backend_available()
and not (
resolve_openai_audio_api_key()
or get_env_value("GROQ_API_KEY")
or get_env_value("MISTRAL_API_KEY")
)
and features.account_info is not None
and features.account_info.tool_gateway_entitled_for("openai-audio")
):
stt_cfg["provider"] = "openai"
changed.add("stt")

View file

@ -162,17 +162,25 @@ def _validate_access_token(value: str) -> tuple[bool, Optional[str]]:
# ---------------------------------------------------------------------------
def _prompt(message: str, default: Optional[str] = None) -> str:
def _prompt(message: str, default: Optional[str] = None, secret: bool = False) -> str:
"""Read one line of input. Returns "" on EOF / Ctrl+C / empty input.
The ``default`` parameter is shown to the user but NOT auto-applied
on empty input callers handle the "user kept existing" case
explicitly so they can distinguish between a real value and a
display preview (e.g. ``"abc12345..."`` for masked secrets).
``secret=True`` reads via ``getpass`` so credentials are not echoed
to the terminal (or left in scrollback).
"""
try:
suffix = f" [{default}]" if default else ""
raw = input(f"{message}{suffix}: ").strip()
if secret and sys.stdin.isatty():
import getpass
raw = getpass.getpass(f"{message}{suffix} (input hidden): ").strip()
else:
raw = input(f"{message}{suffix}: ").strip()
except (EOFError, KeyboardInterrupt):
print()
return ""
@ -185,6 +193,7 @@ def _prompt_validated(
*,
current: Optional[str] = None,
help_text: Optional[str] = None,
secret: bool = False,
) -> Optional[str]:
"""Repeat the prompt until the user enters a valid value or aborts.
@ -198,7 +207,7 @@ def _prompt_validated(
attempts = 0
while True:
attempts += 1
value = _prompt(f"{message}", default=current)
value = _prompt(f"{message}", default=current, secret=secret)
if not value:
return None
ok, reason = validator(value)
@ -295,6 +304,7 @@ def run_whatsapp_cloud_setup() -> int:
"Access Token",
_validate_access_token,
current=current_display,
secret=True,
help_text=(
"Two options for getting one:\n\n"
" (a) TEMP — App Dashboard → WhatsApp → API Setup →\n"
@ -335,6 +345,7 @@ def run_whatsapp_cloud_setup() -> int:
"App Secret",
_validate_app_secret,
current=current_secret_display,
secret=True,
help_text=(
"Found in: App Dashboard → Settings → Basic →\n"
"'App secret' field (click 'Show', enter your Facebook password).\n\n"