#!/usr/bin/env python3 """Fail-closed validator for the complete G7 external evidence gate. All four artifacts are required: authenticated public physical-microphone soak, process-local voice runtime sampling, pinned Linux topology sampling, and an independent human-held-out voice-gain pack. No artifact may contain raw audio or transcripts, and synthetic evidence cannot satisfy this gate. """ from __future__ import annotations import argparse import hashlib import json import math import sys from datetime import UTC, datetime from pathlib import Path from typing import Any REPO_ROOT = Path(__file__).resolve().parents[1] API_ROOT = REPO_ROOT / "apps/api" if str(API_ROOT) not in sys.path: sys.path.insert(0, str(API_ROOT)) from app.contracts.g7_external_evidence import G7HumanVoiceGainEvidencePack # noqa: E402 from app.services.g7_voice_gain_evidence import evaluate_human_voice_gain # noqa: E402 MINIMUM_SOAK_SECONDS = 3_000.0 # 이 게이트는 특정 벤더가 아니라 **운영하기로 결정한 provider** 를 강제한다. # 2026-08-08 소유자 결정: STT 는 노트북 상주 faster-whisper(`local_whisper`), # TTS 는 노트북 Higgs(`higgs`). 이전 값은 벤더 하나(`deepgram`/`openai`)가 # 하드코딩돼 있었을 뿐 결정 기록이 아니었다. # # 목록은 닫혀 있다. 배치 STT(`openai`)는 interim/final 계약을 만족할 수 없어 # 여기 들어오지 못한다. 어떤 경우에도 `expected_* == ready_*` 결속은 유지되므로 # 선언한 provider 와 실제로 돈 provider 가 다르면 계속 실패한다. ALLOWED_STT_PROVIDERS = ("local_whisper", "deepgram") ALLOWED_TTS_PROVIDERS = ("higgs", "openai") def load_json(path: Path, errors: list[str], label: str) -> dict[str, Any]: try: payload = json.loads(path.read_text(encoding="utf-8")) except FileNotFoundError: errors.append(f"{label}:missing") return {} except (OSError, UnicodeError, json.JSONDecodeError): errors.append(f"{label}:unreadable") return {} if not isinstance(payload, dict): errors.append(f"{label}:invalid_shape") return {} return payload def _number(value: object) -> float | None: if isinstance(value, bool) or not isinstance(value, (int, float)): return None result = float(value) return result if math.isfinite(result) else None def _require(errors: list[str], condition: bool, code: str) -> None: if not condition: errors.append(code) def _iso(value: object) -> datetime | None: if not isinstance(value, str) or not value: return None try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return None if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=UTC) return parsed.astimezone(UTC) def validate_public_soak(payload: dict[str, Any], errors: list[str]) -> None: prefix = "voice_soak" _require( errors, payload.get("schema_version") == "vignette.g7-public-voice-soak.v4", f"{prefix}:schema", ) _require(errors, payload.get("mode") == "public_soak", f"{prefix}:mode") _require(errors, payload.get("status") == "passed", f"{prefix}:status") requested = _number(payload.get("requested_duration_seconds")) elapsed = _number(payload.get("elapsed_seconds")) _require( errors, requested is not None and requested >= MINIMUM_SOAK_SECONDS, f"{prefix}:duration_requested", ) _require( errors, elapsed is not None and requested is not None and elapsed >= requested, f"{prefix}:duration_elapsed", ) for field in ( "microphone_device_enumerated", "physical_capture_confirmed", "physical_microphone_used", "public_wss_used", "authenticated_public_wss_ready", "real_database_session_binding_required", "ready_provider_metadata_validated", "cookie_present", "session_id_present", "cloudflare_ray_present", "unauthenticated_handshake_accepted", ): _require(errors, payload.get(field) is True, f"{prefix}:{field}") for field in ( "raw_audio_retained", "provider_overrides_used", "cookie_value_logged", ): _require(errors, payload.get(field) is False, f"{prefix}:{field}") _require(errors, payload.get("tls_version") == "TLSv1.3", f"{prefix}:tls") _require( errors, payload.get("unauthenticated_close_code") == 1008, f"{prefix}:unauth_close", ) _require(errors, payload.get("close_code") == 1000, f"{prefix}:auth_close") _require( errors, payload.get("expected_stt_provider") in ALLOWED_STT_PROVIDERS, f"{prefix}:stt_provider_not_operated", ) _require( errors, payload.get("expected_tts_provider") in ALLOWED_TTS_PROVIDERS, f"{prefix}:tts_provider_not_operated", ) for expected, ready in ( ("expected_stt_provider", "ready_stt_provider"), ("expected_stt_model", "ready_stt_model"), ("expected_tts_provider", "ready_tts_provider"), ("expected_tts_model", "ready_tts_model"), ): _require( errors, bool(payload.get(expected)) and payload.get(expected) == payload.get(ready), f"{prefix}:{ready}_mismatch", ) attempted = payload.get("turns_attempted") succeeded = payload.get("turns_succeeded") metrics = payload.get("turn_transcript_metrics") _require( errors, isinstance(attempted, int) and attempted >= 10, f"{prefix}:turns_attempted", ) _require(errors, succeeded == attempted, f"{prefix}:turns_succeeded") _require( errors, isinstance(metrics, list) and len(metrics) == succeeded, f"{prefix}:turn_metrics_count", ) if isinstance(metrics, list): interim_total = 0 final_total = 0 seen_turns: set[int] = set() for item in metrics: if not isinstance(item, dict): errors.append(f"{prefix}:turn_metric_shape") continue turn = item.get("turn_number") interim = item.get("interim_transcript_frames") final = item.get("speech_final_transcript_frames") _require( errors, isinstance(turn, int) and turn > 0 and turn not in seen_turns, f"{prefix}:turn_number", ) if isinstance(turn, int): seen_turns.add(turn) _require( errors, isinstance(interim, int) and interim >= 1, f"{prefix}:interim_missing", ) _require(errors, final == 1, f"{prefix}:speech_final_not_exact") _require( errors, _number(item.get("first_interim_latency_ms")) is not None, f"{prefix}:interim_latency", ) _require( errors, _number(item.get("speech_final_latency_ms")) is not None, f"{prefix}:final_latency", ) interim_total += interim if isinstance(interim, int) else 0 final_total += final if isinstance(final, int) else 0 _require( errors, payload.get("interim_transcript_frames") == interim_total, f"{prefix}:interim_total", ) _require( errors, payload.get("speech_final_transcript_frames") == final_total, f"{prefix}:final_total", ) _require( errors, isinstance(succeeded, int) and payload.get("reply_frames", 0) >= succeeded, f"{prefix}:reply_frames", ) _require( errors, isinstance(succeeded, int) and payload.get("tts_end_frames", 0) >= succeeded, f"{prefix}:tts_end_frames", ) _require(errors, payload.get("tts_binary_bytes", 0) > 0, f"{prefix}:tts_audio") _require(errors, payload.get("captured_pcm_bytes", 0) > 0, f"{prefix}:captured_pcm") _require(errors, not payload.get("blockers"), f"{prefix}:blockers") _require(errors, payload.get("failure_type") is None, f"{prefix}:failure") def validate_runtime(payload: dict[str, Any], errors: list[str]) -> None: prefix = "runtime" _require( errors, payload.get("schema_version") == "vignette.g7-runtime-sampling.v1", f"{prefix}:schema", ) _require(errors, payload.get("status") == "passed", f"{prefix}:status") _require( errors, payload.get("privacy_boundary") == "metadata_only_no_audio_transcript_session_or_cookie_values", f"{prefix}:privacy", ) _require( errors, payload.get("cookie_present") is True and payload.get("cookie_value_logged") is False, f"{prefix}:cookie_boundary", ) requested = payload.get("requested_samples") completed = payload.get("samples_completed") interval = _number(payload.get("interval_seconds")) _require( errors, isinstance(requested, int) and requested == completed and requested > 1, f"{prefix}:sample_count", ) _require( errors, interval is not None and isinstance(completed, int) and (completed - 1) * interval >= MINIMUM_SOAK_SECONDS, f"{prefix}:coverage", ) samples = payload.get("samples") _require( errors, isinstance(samples, list) and len(samples) == completed, f"{prefix}:samples", ) high = payload.get("high_water") _require(errors, isinstance(high, dict), f"{prefix}:high_water") if isinstance(high, dict): for field in ( "process_peak_rss_bytes", "process_threads_max", "websocket_high_water", "streaming_provider_session_high_water", "route_audio_buffer_high_water_bytes", "streaming_event_queue_high_water_items", ): _require(errors, (_number(high.get(field)) or 0) > 0, f"{prefix}:{field}") capture_started = _iso(payload.get("started_at_utc")) worker_started = _iso(payload.get("worker_started_at_utc")) _require( errors, capture_started is not None and worker_started is not None and 0 <= (capture_started - worker_started).total_seconds() <= 120, f"{prefix}:fresh_worker_required", ) if isinstance(samples, list) and samples: final = samples[-1].get("snapshot") if isinstance(samples[-1], dict) else None counters = final.get("counters") if isinstance(final, dict) else None limits = final.get("limits") if isinstance(final, dict) else None _require( errors, isinstance(final, dict) and final.get("schema_version") == "vignette.voice-runtime.v1", f"{prefix}:snapshot_schema", ) _require( errors, isinstance(final, dict) and final.get("scope") == "single_api_worker", f"{prefix}:snapshot_scope", ) _require( errors, isinstance(final, dict) and final.get("privacy_boundary") == "metadata_only_no_audio_transcript_or_session_ids", f"{prefix}:snapshot_privacy", ) _require(errors, isinstance(counters, dict), f"{prefix}:final_counters") _require( errors, isinstance(limits, dict) and limits.get("uvicorn_ws_max_queue") == 4, f"{prefix}:uvicorn_queue_cap", ) if isinstance(counters, dict): for field in ( "provider_fallback_total", "websocket_error_total", "audio_overflow_rejections_total", ): _require(errors, counters.get(field) == 0, f"{prefix}:{field}") def validate_topology(payload: dict[str, Any], errors: list[str]) -> None: prefix = "topology" _require( errors, payload.get("schema_version") == "vignette.g7-topology-evidence.v1", f"{prefix}:schema", ) _require(errors, payload.get("status") == "passed", f"{prefix}:status") scope = payload.get("scope") _require( errors, isinstance(scope, dict) and scope.get("metadata_only") is True, f"{prefix}:privacy", ) if isinstance(scope, dict): for field in ( "raw_command_output_retained", "socket_endpoints_retained", "request_payloads_retained", "audio_retained", "transcripts_retained", ): _require(errors, scope.get(field) is False, f"{prefix}:{field}") edge = scope.get("cloudflare_edge") _require( errors, isinstance(edge, dict) and edge.get("internal_queue_measured") is False and edge.get("evidence_boundary") == "separate_external_artifact_required", f"{prefix}:edge_boundary", ) requested = payload.get("requested") completed = payload.get("samples_completed") _require(errors, isinstance(requested, dict), f"{prefix}:requested") if isinstance(requested, dict): sample_count = requested.get("samples") interval = _number(requested.get("interval_seconds")) _require( errors, isinstance(sample_count, int) and sample_count == completed and sample_count > 1, f"{prefix}:sample_count", ) _require( errors, interval is not None and isinstance(completed, int) and (completed - 1) * interval >= MINIMUM_SOAK_SECONDS, f"{prefix}:coverage", ) targets = payload.get("targets") _require( errors, isinstance(targets, dict) and set(targets) == {"api", "caddy"}, f"{prefix}:targets", ) if isinstance(targets, dict): capture_started = _iso(payload.get("started_at_utc")) for role in ("api", "caddy"): target = targets.get(role) _require(errors, isinstance(target, dict), f"{prefix}:{role}_target") if isinstance(target, dict): _require( errors, isinstance(target.get("container_id"), str) and len(target["container_id"]) == 64, f"{prefix}:{role}_container", ) digest = target.get("image_digest") _require( errors, isinstance(digest, str) and digest.startswith("sha256:") and len(digest) == 71, f"{prefix}:{role}_image", ) target_started = _iso(target.get("started_at")) _require( errors, capture_started is not None and target_started is not None and 0 <= (capture_started - target_started).total_seconds() <= 120, f"{prefix}:{role}_fresh_container_required", ) summary = payload.get("summary") _require(errors, isinstance(summary, dict), f"{prefix}:summary") if isinstance(summary, dict): containers = summary.get("containers") host_tcp = summary.get("host_tcp") _require(errors, isinstance(containers, dict), f"{prefix}:container_summary") if isinstance(containers, dict): for role in ("api", "caddy"): values = containers.get(role) _require(errors, isinstance(values, dict), f"{prefix}:{role}_summary") if isinstance(values, dict): for field in ( "cgroup_memory_peak_bytes_max", "cgroup_cpu_usage_usec_max", "cgroup_pids_current_max", "proc_vm_hwm_bytes_max", "proc_threads_max", "proc_fd_count_max", ): _require( errors, (_number(values.get(field)) or 0) > 0, f"{prefix}:{role}_{field}", ) _require( errors, isinstance(host_tcp, dict) and (_number(host_tcp.get("connections_max")) or 0) > 0, f"{prefix}:host_tcp", ) _require(errors, payload.get("failure_type") is None, f"{prefix}:failure") def validate_human_gain(payload: dict[str, Any], errors: list[str]) -> dict[str, Any]: try: pack = G7HumanVoiceGainEvidencePack.model_validate(payload) result = evaluate_human_voice_gain(pack) except Exception as exc: errors.append(f"human_gain:invalid:{type(exc).__name__}") return {} if not result.passed: errors.extend(f"human_gain:{reason}" for reason in result.failure_reasons) return result.model_dump(mode="json") def validate_binding( voice: dict[str, Any], runtime: dict[str, Any], topology: dict[str, Any], errors: list[str], ) -> None: voice_host = voice.get("target_host") runtime_host = runtime.get("target_host") requested = topology.get("requested") topology_host = ( requested.get("public_host") if isinstance(requested, dict) else None ) _require( errors, bool(voice_host) and voice_host == runtime_host == topology_host, "binding:public_host", ) windows = [] for label, payload in ( ("voice", voice), ("runtime", runtime), ("topology", topology), ): start, end = ( _iso(payload.get("started_at_utc")), _iso(payload.get("ended_at_utc")), ) if start is None or end is None or end < start: errors.append(f"binding:{label}_time") else: windows.append((start, end)) if len(windows) == 3: _require( errors, max(start for start, _ in windows) <= min(end for _, end in windows), "binding:no_concurrent_overlap", ) def artifact_sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def parser() -> argparse.ArgumentParser: result = argparse.ArgumentParser(description=__doc__) result.add_argument("--voice-soak", type=Path, required=True) result.add_argument("--runtime", type=Path, required=True) result.add_argument("--topology", type=Path, required=True) result.add_argument("--human-voice-gain", type=Path, required=True) result.add_argument("--json", action="store_true") return result def main() -> int: args = parser().parse_args() errors: list[str] = [] paths = { "voice_soak": args.voice_soak, "runtime": args.runtime, "topology": args.topology, "human_voice_gain": args.human_voice_gain, } payloads = {label: load_json(path, errors, label) for label, path in paths.items()} if payloads["voice_soak"]: validate_public_soak(payloads["voice_soak"], errors) if payloads["runtime"]: validate_runtime(payloads["runtime"], errors) if payloads["topology"]: validate_topology(payloads["topology"], errors) gain_result = ( validate_human_gain(payloads["human_voice_gain"], errors) if payloads["human_voice_gain"] else {} ) if all(payloads[label] for label in ("voice_soak", "runtime", "topology")): validate_binding( payloads["voice_soak"], payloads["runtime"], payloads["topology"], errors ) report = { "schema_version": "vignette.g7-external-proof-result.v1", "passed": not errors, "clinical_claim_allowed": False, "artifact_sha256": { label: artifact_sha256(path) for label, path in paths.items() if path.is_file() }, "human_voice_gain_result": gain_result, "errors": errors, } print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)) return 0 if not errors else 1 if __name__ == "__main__": raise SystemExit(main())