#!/usr/bin/env python3 """Fail-closed G7 public WSS and physical-microphone soak runner. Unlike ``soak-voice-websocket.py``, this runner has no synthetic authentication, consent, STT, turn-generation, or TTS overrides. Authenticated preflight uses an existing learner session cookie, an existing owned voice session, and the public TLS WebSocket route. Full soak additionally captures a physical microphone through ffmpeg. The runner never writes raw audio, cookie values, session IDs, transcripts, or replies to its evidence. Its production soak duration is at least 3,120 seconds so the concurrent proof window can retain a full 3,000-second intersection. Use ``--preflight-only`` for a route/auth/provider-readiness audit that never enumerates or opens a microphone. Full soak cannot enumerate or capture a microphone unless the operator passes ``--confirm-physical-capture`` explicitly. Exit codes: 0 = preflight or full soak passed 1 = a live contract failed 2 = invalid CLI usage (argparse) 3 = an external prerequisite is absent or microphone speech is too quiet """ from __future__ import annotations import argparse import asyncio import hashlib import json import math import os import shutil import struct import subprocess import time from dataclasses import asdict, dataclass, field from datetime import UTC, datetime from pathlib import Path from typing import Any from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from websockets.asyncio.client import ClientConnection, connect from websockets.exceptions import ConnectionClosed DEFAULT_WSS_URL = "wss://api-vignette.chanpaca.net/voice/ws" DEFAULT_ORIGIN = "https://vignette.chanpaca.net" DEFAULT_COOKIE_ENV = "VIGNETTE_PUBLIC_SESSION_COOKIE" DEFAULT_SESSION_ENV = "VIGNETTE_PUBLIC_VOICE_SESSION_ID" DEFAULT_COOKIE_NAME = "__Host-vignette_sid" MIN_PRODUCTION_DURATION_SECONDS = 3_120.0 DEFAULT_DURATION_SECONDS = MIN_PRODUCTION_DURATION_SECONDS DEFAULT_TURN_INTERVAL_SECONDS = 60.0 DEFAULT_HEARTBEAT_SECONDS = 20.0 DEFAULT_CAPTURE_SECONDS = 3.0 DEFAULT_MIN_RMS_DBFS = -65.0 DEFAULT_FRAME_TIMEOUT_SECONDS = 120.0 DEFAULT_AUDIO_CHUNK_BYTES = 16_384 class GateFailure(RuntimeError): """The public route violated a required contract.""" class GateBlocked(RuntimeError): """A required external prerequisite isn't available.""" def __init__(self, blockers: list[str]) -> None: self.blockers = sorted(set(blockers)) super().__init__(",".join(self.blockers)) @dataclass(frozen=True, slots=True) class PcmMetrics: bytes_captured: int sample_rate_hz: int channels: int sample_width_bytes: int duration_seconds: float rms_dbfs: float peak_dbfs: float @dataclass(frozen=True, slots=True) class TurnTranscriptMetrics: turn_number: int interim_transcript_frames: int speech_final_transcript_frames: int first_interim_latency_ms: float | None speech_final_latency_ms: float | None @dataclass(slots=True) class ExternalSoakEvidence: schema_version: str = "vignette.g7-public-voice-soak.v4" mode: str = "preflight" status: str = "running" started_at_utc: str = field(default_factory=lambda: utc_now()) ended_at_utc: str | None = None target_host: str = "" target_path: str = "" origin: str = "" contractual_duration_seconds: float = DEFAULT_DURATION_SECONDS requested_duration_seconds: float = 0.0 elapsed_seconds: float = 0.0 microphone_device_enumerated: bool = False physical_capture_confirmed: bool = False physical_microphone_used: bool = False microphone_device: str = "" raw_audio_retained: bool = False public_wss_used: bool = False authenticated_public_wss_ready: bool = False real_database_session_binding_required: bool = True provider_overrides_used: bool = False expected_stt_provider: str = "" expected_stt_model: str = "" expected_tts_provider: str = "" expected_tts_model: str = "" ready_stt_provider: str | None = None ready_stt_model: str | None = None ready_tts_provider: str | None = None ready_tts_model: str | None = None ready_provider_metadata_validated: bool = False cookie_env: str = "" cookie_present: bool = False cookie_value_logged: bool = False session_id_present: bool = False session_id_sha256: str | None = None tls_version: str | None = None tls_cipher: str | None = None peer_certificate_not_after: str | None = None response_server: str | None = None cloudflare_ray_present: bool = False unauthenticated_handshake_accepted: bool = False unauthenticated_close_code: int | None = None connections_opened: int = 0 application_pongs: int = 0 turns_attempted: int = 0 turns_succeeded: int = 0 transcript_frames: int = 0 interim_transcript_frames: int = 0 speech_final_transcript_frames: int = 0 transcript_latency_origin: str = "audio_start_sent_to_client_frame_observed" first_interim_latency_ms_min: float | None = None first_interim_latency_ms_max: float | None = None speech_final_latency_ms_min: float | None = None speech_final_latency_ms_max: float | None = None turn_transcript_metrics: list[TurnTranscriptMetrics] = field(default_factory=list) reply_frames: int = 0 tts_end_frames: int = 0 tts_binary_frames: int = 0 tts_binary_bytes: int = 0 captured_pcm_bytes: int = 0 microphone_rms_dbfs_min: float | None = None microphone_rms_dbfs_max: float | None = None microphone_peak_dbfs_max: float | None = None close_code: int | None = None blockers: list[str] = field(default_factory=list) failure_type: str | None = None def utc_now() -> str: return datetime.now(UTC).isoformat().replace("+00:00", "Z") def sha256_text(value: str) -> str: return hashlib.sha256(value.encode("utf-8")).hexdigest() def target_metadata(wss_url: str) -> tuple[str, str]: parsed = urlsplit(wss_url) return parsed.netloc, parsed.path def with_session_id(wss_url: str, session_id: str) -> str: parsed = urlsplit(wss_url) if parsed.scheme != "wss": raise ValueError("--wss-url must use wss://") query = dict(parse_qsl(parsed.query, keep_blank_values=True)) query["session_id"] = session_id return urlunsplit( (parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment) ) def pcm_metrics( audio: bytes, *, sample_rate_hz: int = 16_000, channels: int = 1, sample_width_bytes: int = 2, ) -> PcmMetrics: if not audio or len(audio) % sample_width_bytes: raise GateFailure("microphone_capture_invalid_pcm") samples = struct.unpack(f"<{len(audio) // 2}h", audio) squares = sum(sample * sample for sample in samples) rms = math.sqrt(squares / len(samples)) peak = max(abs(sample) for sample in samples) def dbfs(value: float) -> float: if value <= 0: return -120.0 return round(20 * math.log10(value / 32_768), 3) frames = len(audio) / (channels * sample_width_bytes) return PcmMetrics( bytes_captured=len(audio), sample_rate_hz=sample_rate_hz, channels=channels, sample_width_bytes=sample_width_bytes, duration_seconds=round(frames / sample_rate_hz, 3), rms_dbfs=dbfs(rms), peak_dbfs=dbfs(peak), ) def capture_microphone_pcm( *, ffmpeg: str, microphone_device: str, capture_seconds: float, ) -> tuple[bytes, PcmMetrics]: command = [ ffmpeg, "-hide_banner", "-loglevel", "error", "-f", "dshow", "-i", f"audio={microphone_device}", "-t", str(capture_seconds), "-ac", "1", "-ar", "16000", "-f", "s16le", "pipe:1", ] try: completed = subprocess.run( command, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=capture_seconds + 15, shell=False, ) except (OSError, subprocess.TimeoutExpired) as exc: raise GateBlocked(["physical_microphone_capture_unavailable"]) from exc if completed.returncode != 0 or not completed.stdout: raise GateBlocked(["physical_microphone_capture_unavailable"]) metrics = pcm_metrics(completed.stdout) return completed.stdout, metrics def dshow_audio_devices(*, ffmpeg: str) -> list[str]: """Enumerate DirectShow audio names without opening a capture stream.""" command = [ ffmpeg, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy", ] try: completed = subprocess.run( command, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15, shell=False, ) except (OSError, subprocess.TimeoutExpired) as exc: raise GateBlocked(["physical_microphone_enumeration_unavailable"]) from exc output = completed.stderr.decode("utf-8", errors="replace") devices: list[str] = [] for line in output.splitlines(): marker = '" (audio)' if marker not in line: continue prefix = line.split(marker, 1)[0] if '"' not in prefix: continue devices.append(prefix.rsplit('"', 1)[-1]) if not devices: raise GateBlocked(["physical_microphone_enumeration_unavailable"]) return devices def record_pcm_metrics( evidence: ExternalSoakEvidence, metrics: PcmMetrics, ) -> None: evidence.physical_microphone_used = True evidence.captured_pcm_bytes += metrics.bytes_captured if evidence.microphone_rms_dbfs_min is None: evidence.microphone_rms_dbfs_min = metrics.rms_dbfs else: evidence.microphone_rms_dbfs_min = min( evidence.microphone_rms_dbfs_min, metrics.rms_dbfs, ) if evidence.microphone_rms_dbfs_max is None: evidence.microphone_rms_dbfs_max = metrics.rms_dbfs else: evidence.microphone_rms_dbfs_max = max( evidence.microphone_rms_dbfs_max, metrics.rms_dbfs, ) if evidence.microphone_peak_dbfs_max is None: evidence.microphone_peak_dbfs_max = metrics.peak_dbfs else: evidence.microphone_peak_dbfs_max = max( evidence.microphone_peak_dbfs_max, metrics.peak_dbfs, ) def json_frame(raw: str | bytes) -> dict[str, Any] | None: if isinstance(raw, bytes): return None try: payload = json.loads(raw) except json.JSONDecodeError as exc: raise GateFailure("public_wss_non_json_frame") from exc if not isinstance(payload, dict): raise GateFailure("public_wss_non_object_frame") return payload def _ready_metadata_value(ready: dict[str, Any], field_name: str) -> str: value = ready.get(field_name) if not isinstance(value, str) or not value.strip(): raise GateFailure( f"authenticated_public_wss_ready_metadata_missing:{field_name}" ) normalized = value.strip() if len(normalized) > 160 or any( ord(character) < 32 or ord(character) == 127 for character in normalized ): raise GateFailure( f"authenticated_public_wss_ready_metadata_invalid:{field_name}" ) return normalized def validate_ready_provider_metadata( ready: dict[str, Any], evidence: ExternalSoakEvidence, *, expected_stt_provider: str, expected_stt_model: str, expected_tts_provider: str, expected_tts_model: str, ) -> None: """Record only allowlisted provider metadata and enforce the expected route.""" actual = { "stt_provider": _ready_metadata_value(ready, "stt_provider"), "stt_model": _ready_metadata_value(ready, "stt_model"), "tts_provider": _ready_metadata_value(ready, "tts_provider"), "tts_model": _ready_metadata_value(ready, "tts_model"), } expected = { "stt_provider": expected_stt_provider, "stt_model": expected_stt_model, "tts_provider": expected_tts_provider, "tts_model": expected_tts_model, } evidence.expected_stt_provider = expected_stt_provider evidence.expected_stt_model = expected_stt_model evidence.expected_tts_provider = expected_tts_provider evidence.expected_tts_model = expected_tts_model evidence.ready_stt_provider = actual["stt_provider"] evidence.ready_stt_model = actual["stt_model"] evidence.ready_tts_provider = actual["tts_provider"] evidence.ready_tts_model = actual["tts_model"] for field_name, expected_value in expected.items(): if actual[field_name] != expected_value: raise GateFailure(f"authenticated_public_wss_{field_name}_mismatch") evidence.ready_provider_metadata_validated = True async def receive_until( websocket: ClientConnection, predicate, *, timeout_seconds: float, observed_at: list[float] | None = None, ) -> list[str | bytes]: deadline = time.monotonic() + timeout_seconds frames: list[str | bytes] = [] while time.monotonic() < deadline: remaining = max(0.01, deadline - time.monotonic()) try: raw = await asyncio.wait_for(websocket.recv(), timeout=remaining) except TimeoutError as exc: raise GateFailure("public_wss_frame_timeout") from exc frames.append(raw) if observed_at is not None: observed_at.append(time.monotonic()) payload = json_frame(raw) if payload is not None and payload.get("type") == "error": code = str(payload.get("code") or "unspecified") raise GateFailure(f"public_wss_error_frame:{code}") if payload is not None and payload.get("type") == "degraded": raise GateFailure("public_wss_degraded_frame") if predicate(raw, payload): return frames raise GateFailure("public_wss_predicate_timeout") def _merge_latency_range( current_min: float | None, current_max: float | None, value: float | None, ) -> tuple[float | None, float | None]: if value is None: return current_min, current_max return ( value if current_min is None else min(current_min, value), value if current_max is None else max(current_max, value), ) def record_turn_transcript_metrics( evidence: ExternalSoakEvidence, *, payloads_with_observed_at: list[tuple[dict[str, Any], float]], turn_started_at: float, ) -> TurnTranscriptMetrics: transcript_frames = [ (payload, observed_at) for payload, observed_at in payloads_with_observed_at if payload.get("type") == "transcript" ] interim_frames = [ (payload, observed_at) for payload, observed_at in transcript_frames if payload.get("final") is False and payload.get("speech_final") is False ] speech_final_frames = [ (payload, observed_at) for payload, observed_at in transcript_frames if payload.get("final") is True and payload.get("speech_final") is True ] def latency_ms(observed_at: float) -> float: return round(max(0.0, observed_at - turn_started_at) * 1_000, 3) first_interim_latency_ms = ( latency_ms(interim_frames[0][1]) if interim_frames else None ) speech_final_latency_ms = ( latency_ms(speech_final_frames[0][1]) if len(speech_final_frames) == 1 else None ) metrics = TurnTranscriptMetrics( turn_number=evidence.turns_attempted, interim_transcript_frames=len(interim_frames), speech_final_transcript_frames=len(speech_final_frames), first_interim_latency_ms=first_interim_latency_ms, speech_final_latency_ms=speech_final_latency_ms, ) evidence.transcript_frames += len(transcript_frames) evidence.interim_transcript_frames += metrics.interim_transcript_frames evidence.speech_final_transcript_frames += metrics.speech_final_transcript_frames ( evidence.first_interim_latency_ms_min, evidence.first_interim_latency_ms_max, ) = _merge_latency_range( evidence.first_interim_latency_ms_min, evidence.first_interim_latency_ms_max, metrics.first_interim_latency_ms, ) ( evidence.speech_final_latency_ms_min, evidence.speech_final_latency_ms_max, ) = _merge_latency_range( evidence.speech_final_latency_ms_min, evidence.speech_final_latency_ms_max, metrics.speech_final_latency_ms, ) evidence.turn_transcript_metrics.append(metrics) return metrics async def receive_type( websocket: ClientConnection, expected_type: str, *, timeout_seconds: float, ) -> tuple[dict[str, Any], list[str | bytes]]: frames = await receive_until( websocket, lambda _raw, payload: ( payload is not None and payload.get("type") == expected_type ), timeout_seconds=timeout_seconds, ) payload = json_frame(frames[-1]) assert payload is not None return payload, frames def record_network_metadata( websocket: ClientConnection, evidence: ExternalSoakEvidence, ) -> None: evidence.public_wss_used = True evidence.connections_opened += 1 headers = getattr(getattr(websocket, "response", None), "headers", None) if headers is not None: evidence.response_server = headers.get("server") evidence.cloudflare_ray_present = bool(headers.get("cf-ray")) transport = getattr(websocket, "transport", None) ssl_object = transport.get_extra_info("ssl_object") if transport else None if ssl_object is None: return evidence.tls_version = ssl_object.version() cipher = ssl_object.cipher() evidence.tls_cipher = cipher[0] if cipher else None certificate = ssl_object.getpeercert() or {} evidence.peer_certificate_not_after = certificate.get("notAfter") async def expect_close_code( websocket: ClientConnection, *, timeout_seconds: float, ) -> int: try: while True: await asyncio.wait_for(websocket.recv(), timeout=timeout_seconds) except ConnectionClosed as exc: return exc.code except TimeoutError as exc: raise GateFailure("public_wss_close_timeout") from exc async def unauthenticated_route_probe( *, wss_url: str, origin: str, timeout_seconds: float, evidence: ExternalSoakEvidence, ) -> None: probe_url = with_session_id(wss_url, "g7-external-readonly-probe") websocket = await connect( probe_url, origin=origin, open_timeout=min(timeout_seconds, 15), close_timeout=5, ping_interval=None, max_size=1_048_576, ) record_network_metadata(websocket, evidence) raw = await asyncio.wait_for(websocket.recv(), timeout=min(timeout_seconds, 15)) payload = json_frame(raw) if payload != {"type": "error", "detail": "not authenticated"}: raise GateFailure("public_wss_unauthenticated_contract_drift") evidence.unauthenticated_handshake_accepted = True evidence.unauthenticated_close_code = await expect_close_code( websocket, timeout_seconds=min(timeout_seconds, 15), ) if evidence.unauthenticated_close_code != 1008: raise GateFailure("public_wss_unauthenticated_close_drift") async def open_authenticated( *, wss_url: str, origin: str, session_id: str, cookie_name: str, cookie_value: str, timeout_seconds: float, evidence: ExternalSoakEvidence, expected_stt_provider: str, expected_stt_model: str, expected_tts_provider: str, expected_tts_model: str, ) -> ClientConnection: if any(character in cookie_value for character in "\r\n;"): raise GateBlocked(["authenticated_session_cookie_invalid"]) websocket = await connect( with_session_id(wss_url, session_id), origin=origin, additional_headers={"Cookie": f"{cookie_name}={cookie_value}"}, open_timeout=min(timeout_seconds, 15), close_timeout=10, ping_interval=20, ping_timeout=20, max_size=32 * 1024 * 1024, max_queue=16, ) record_network_metadata(websocket, evidence) try: ready, _ = await receive_type( websocket, "ready", timeout_seconds=min(timeout_seconds, 30), ) if ready.get("session_id") != session_id or ready.get("state") != "idle": raise GateFailure("authenticated_public_wss_ready_drift") validate_ready_provider_metadata( ready, evidence, expected_stt_provider=expected_stt_provider, expected_stt_model=expected_stt_model, expected_tts_provider=expected_tts_provider, expected_tts_model=expected_tts_model, ) except Exception: await websocket.close() raise evidence.authenticated_public_wss_ready = True return websocket async def application_ping( websocket: ClientConnection, evidence: ExternalSoakEvidence, *, timeout_seconds: float, ) -> None: await websocket.send(json.dumps({"type": "ping"})) await receive_type( websocket, "pong", timeout_seconds=min(timeout_seconds, 30), ) evidence.application_pongs += 1 async def run_audio_turn( websocket: ClientConnection, audio: bytes, evidence: ExternalSoakEvidence, *, timeout_seconds: float, chunk_bytes: int = DEFAULT_AUDIO_CHUNK_BYTES, ) -> None: evidence.turns_attempted += 1 turn_started_at = time.monotonic() await websocket.send( json.dumps( { "type": "audio_start", "format": "pcm", "sample_rate": 16_000, "channels": 1, "sample_width": 2, } ) ) listening, _ = await receive_type( websocket, "state", timeout_seconds=min(timeout_seconds, 30), ) if listening.get("state") != "listening": raise GateFailure("public_wss_listening_state_drift") for start in range(0, len(audio), chunk_bytes): await websocket.send(audio[start : start + chunk_bytes]) await websocket.send( json.dumps( { "type": "audio_end", "format": "pcm", "sample_rate": 16_000, "channels": 1, "sample_width": 2, } ) ) observed_at: list[float] = [] frames = await receive_until( websocket, lambda _raw, payload: ( payload is not None and payload.get("type") == "state" and payload.get("state") == "idle" ), timeout_seconds=timeout_seconds, observed_at=observed_at, ) payloads_with_observed_at = [ (payload, frame_observed_at) for raw, frame_observed_at in zip(frames, observed_at, strict=True) if (payload := json_frame(raw)) is not None ] payloads = [payload for payload, _observed_at in payloads_with_observed_at] types = [str(payload.get("type")) for payload in payloads] transcript_metrics = record_turn_transcript_metrics( evidence, payloads_with_observed_at=payloads_with_observed_at, turn_started_at=turn_started_at, ) if transcript_metrics.interim_transcript_frames < 1: raise GateFailure("public_wss_interim_transcript_missing") if transcript_metrics.speech_final_transcript_frames < 1: raise GateFailure("public_wss_speech_final_transcript_missing") if transcript_metrics.speech_final_transcript_frames > 1: raise GateFailure("public_wss_speech_final_transcript_duplicate") required = {"reply", "tts_end"} if not required.issubset(types): raise GateFailure("public_wss_turn_contract_incomplete") binary_frames = [raw for raw in frames if isinstance(raw, bytes)] if not binary_frames: raise GateFailure("public_wss_tts_binary_missing") evidence.reply_frames += types.count("reply") evidence.tts_end_frames += types.count("tts_end") evidence.tts_binary_frames += len(binary_frames) evidence.tts_binary_bytes += sum(len(raw) for raw in binary_frames) evidence.turns_succeeded += 1 async def wait_with_heartbeats( websocket: ClientConnection, evidence: ExternalSoakEvidence, *, seconds: float, heartbeat_seconds: float, timeout_seconds: float, ) -> None: deadline = time.monotonic() + max(0.0, seconds) while time.monotonic() < deadline: remaining = deadline - time.monotonic() await asyncio.sleep(min(remaining, heartbeat_seconds)) if time.monotonic() < deadline or seconds >= heartbeat_seconds: await application_ping( websocket, evidence, timeout_seconds=timeout_seconds, ) def external_blockers( *, cookie_present: bool, session_id_present: bool, device_enumerated: bool, capture_confirmed: bool, microphone_rms_dbfs: float | None, minimum_rms_dbfs: float, ) -> list[str]: blockers: list[str] = [] if not cookie_present: blockers.append("authenticated_session_cookie_missing") if not session_id_present: blockers.append("owned_voice_session_id_missing") if not device_enumerated: blockers.append("physical_microphone_device_not_enumerated") if not capture_confirmed: blockers.append("physical_microphone_capture_not_confirmed") elif microphone_rms_dbfs is not None and microphone_rms_dbfs < minimum_rms_dbfs: blockers.append("physical_microphone_signal_below_threshold") return blockers async def run_preflight( args: argparse.Namespace, evidence: ExternalSoakEvidence, *, cookie_value: str, session_id: str, ) -> None: await unauthenticated_route_probe( wss_url=args.wss_url, origin=args.origin, timeout_seconds=args.frame_timeout_seconds, evidence=evidence, ) blockers: list[str] = [] if not cookie_value: blockers.append("authenticated_session_cookie_missing") if not session_id: blockers.append("owned_voice_session_id_missing") if blockers: raise GateBlocked(blockers) websocket = await open_authenticated( wss_url=args.wss_url, origin=args.origin, session_id=session_id, cookie_name=args.cookie_name, cookie_value=cookie_value, timeout_seconds=args.frame_timeout_seconds, evidence=evidence, expected_stt_provider=args.expected_stt_provider, expected_stt_model=args.expected_stt_model, expected_tts_provider=args.expected_tts_provider, expected_tts_model=args.expected_tts_model, ) try: await application_ping( websocket, evidence, timeout_seconds=args.frame_timeout_seconds, ) await websocket.send(json.dumps({"type": "close"})) evidence.close_code = await expect_close_code( websocket, timeout_seconds=min(args.frame_timeout_seconds, 30), ) if evidence.close_code != 1000: raise GateFailure("authenticated_public_wss_close_drift") finally: await websocket.close() async def run_soak( args: argparse.Namespace, evidence: ExternalSoakEvidence, *, cookie_value: str, session_id: str, ) -> None: blockers = [] if not cookie_value: blockers.append("authenticated_session_cookie_missing") if not session_id: blockers.append("owned_voice_session_id_missing") if not args.confirm_physical_capture: blockers.append("physical_microphone_capture_not_confirmed") if blockers: raise GateBlocked(blockers) evidence.physical_capture_confirmed = True devices = await asyncio.to_thread(dshow_audio_devices, ffmpeg=args.ffmpeg) evidence.microphone_device_enumerated = args.microphone_device in devices if not evidence.microphone_device_enumerated: raise GateBlocked(["physical_microphone_device_not_enumerated"]) websocket = await open_authenticated( wss_url=args.wss_url, origin=args.origin, session_id=session_id, cookie_name=args.cookie_name, cookie_value=cookie_value, timeout_seconds=args.frame_timeout_seconds, evidence=evidence, expected_stt_provider=args.expected_stt_provider, expected_stt_model=args.expected_stt_model, expected_tts_provider=args.expected_tts_provider, expected_tts_model=args.expected_tts_model, ) started = time.monotonic() deadline = started + args.duration_seconds try: await application_ping( websocket, evidence, timeout_seconds=args.frame_timeout_seconds, ) while time.monotonic() < deadline or evidence.turns_attempted == 0: audio, metrics = await asyncio.to_thread( capture_microphone_pcm, ffmpeg=args.ffmpeg, microphone_device=args.microphone_device, capture_seconds=args.capture_seconds, ) record_pcm_metrics(evidence, metrics) if metrics.rms_dbfs < args.min_rms_dbfs: raise GateBlocked(["physical_microphone_signal_below_threshold"]) await run_audio_turn( websocket, audio, evidence, timeout_seconds=args.frame_timeout_seconds, ) remaining = deadline - time.monotonic() if remaining > 0: await wait_with_heartbeats( websocket, evidence, seconds=min(remaining, args.turn_interval_seconds), heartbeat_seconds=args.heartbeat_seconds, timeout_seconds=args.frame_timeout_seconds, ) evidence.elapsed_seconds = round(time.monotonic() - started, 3) if evidence.elapsed_seconds < args.duration_seconds: raise GateFailure("public_wss_soak_ended_early") await websocket.send(json.dumps({"type": "close"})) evidence.close_code = await expect_close_code( websocket, timeout_seconds=min(args.frame_timeout_seconds, 30), ) if evidence.close_code != 1000: raise GateFailure("authenticated_public_wss_close_drift") finally: await websocket.close() def parser() -> argparse.ArgumentParser: result = argparse.ArgumentParser( description=( "Physical-microphone and authenticated public /voice/ws soak. " "Production requires at least 3,120 seconds so the concurrent " "proof retains a 3,000-second common window." ) ) result.add_argument("--preflight-only", action="store_true") result.add_argument("--wss-url", default=DEFAULT_WSS_URL) result.add_argument("--origin", default=DEFAULT_ORIGIN) result.add_argument("--cookie-env", default=DEFAULT_COOKIE_ENV) result.add_argument("--session-env", default=DEFAULT_SESSION_ENV) result.add_argument("--cookie-name", default=DEFAULT_COOKIE_NAME) result.add_argument("--session-id") result.add_argument("--expected-stt-provider", required=True) result.add_argument("--expected-stt-model", required=True) result.add_argument("--expected-tts-provider", required=True) result.add_argument("--expected-tts-model", required=True) result.add_argument( "--microphone-device", help="required for full soak; ignored by --preflight-only", ) result.add_argument( "--confirm-physical-capture", action="store_true", help="explicitly authorize opening the named microphone for this run", ) result.add_argument("--ffmpeg", default=shutil.which("ffmpeg") or "ffmpeg") result.add_argument( "--duration-seconds", type=float, default=DEFAULT_DURATION_SECONDS ) result.add_argument( "--turn-interval-seconds", type=float, default=DEFAULT_TURN_INTERVAL_SECONDS, ) result.add_argument( "--heartbeat-seconds", type=float, default=DEFAULT_HEARTBEAT_SECONDS, ) result.add_argument( "--capture-seconds", type=float, default=DEFAULT_CAPTURE_SECONDS, ) result.add_argument("--min-rms-dbfs", type=float, default=DEFAULT_MIN_RMS_DBFS) result.add_argument( "--frame-timeout-seconds", type=float, default=DEFAULT_FRAME_TIMEOUT_SECONDS, ) result.add_argument("--evidence-output", type=Path) return result def validate_args(args: argparse.Namespace, cli: argparse.ArgumentParser) -> None: try: with_session_id(args.wss_url, "validation") except ValueError as exc: cli.error(str(exc)) if args.duration_seconds <= 0: cli.error("--duration-seconds must be > 0") if ( not args.preflight_only and args.duration_seconds < MIN_PRODUCTION_DURATION_SECONDS ): cli.error( "--duration-seconds must be >= 3120 for a production physical soak" ) if args.turn_interval_seconds <= 0: cli.error("--turn-interval-seconds must be > 0") if args.heartbeat_seconds <= 0: cli.error("--heartbeat-seconds must be > 0") if args.capture_seconds <= 0: cli.error("--capture-seconds must be > 0") if not -120 <= args.min_rms_dbfs <= 0: cli.error("--min-rms-dbfs must be between -120 and 0") if args.frame_timeout_seconds <= 0: cli.error("--frame-timeout-seconds must be > 0") if not args.cookie_env or not args.session_env or not args.cookie_name: cli.error("cookie/session environment names and cookie name must be non-empty") for field_name in ( "expected_stt_provider", "expected_stt_model", "expected_tts_provider", "expected_tts_model", ): value = getattr(args, field_name) if ( not value.strip() or len(value.strip()) > 160 or any( ord(character) < 32 or ord(character) == 127 for character in value.strip() ) ): option_name = "--" + field_name.replace("_", "-") cli.error(f"{option_name} must be a non-empty provider/model identifier") setattr(args, field_name, value.strip()) if not args.preflight_only and not (args.microphone_device or "").strip(): cli.error("--microphone-device is required for full soak") def evidence_json(evidence: ExternalSoakEvidence) -> str: return json.dumps(asdict(evidence), ensure_ascii=False, indent=2, sort_keys=True) def emit_evidence( evidence: ExternalSoakEvidence, output: Path | None, ) -> None: payload = evidence_json(evidence) + "\n" if output is not None: output.parent.mkdir(parents=True, exist_ok=True) output.write_text(payload, encoding="utf-8") print(payload, end="") def main() -> int: cli = parser() args = cli.parse_args() validate_args(args, cli) session_id = (args.session_id or os.getenv(args.session_env, "")).strip() cookie_value = os.getenv(args.cookie_env, "").strip() host, path = target_metadata(args.wss_url) evidence = ExternalSoakEvidence( mode="preflight" if args.preflight_only else "public_soak", target_host=host, target_path=path, origin=args.origin, requested_duration_seconds=( 0.0 if args.preflight_only else args.duration_seconds ), microphone_device=("" if args.preflight_only else args.microphone_device), expected_stt_provider=args.expected_stt_provider, expected_stt_model=args.expected_stt_model, expected_tts_provider=args.expected_tts_provider, expected_tts_model=args.expected_tts_model, cookie_env=args.cookie_env, cookie_present=bool(cookie_value), session_id_present=bool(session_id), session_id_sha256=sha256_text(session_id) if session_id else None, ) try: if args.preflight_only: asyncio.run( run_preflight( args, evidence, cookie_value=cookie_value, session_id=session_id, ) ) else: asyncio.run( run_soak( args, evidence, cookie_value=cookie_value, session_id=session_id, ) ) evidence.status = "passed" evidence.ended_at_utc = utc_now() emit_evidence(evidence, args.evidence_output) return 0 except GateBlocked as exc: evidence.status = "blocked" evidence.blockers = exc.blockers evidence.ended_at_utc = utc_now() emit_evidence(evidence, args.evidence_output) return 3 except Exception as exc: evidence.status = "failed" evidence.failure_type = ( str(exc) if isinstance(exc, GateFailure) else type(exc).__name__ ) evidence.ended_at_utc = utc_now() emit_evidence(evidence, args.evidence_output) return 1 if __name__ == "__main__": raise SystemExit(main())