vignette/scripts/capture-g7-runtime-evidence.py
Yun Chan 16e791e044 G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
2026-08-08 01:30:53 +09:00

346 lines
12 KiB
Python

#!/usr/bin/env python3
"""Capture authenticated, metadata-only G7 voice runtime snapshots.
The sampler calls the admin-only ``/admin/voice-runtime`` endpoint. It never
writes the session cookie, audio, transcripts, session IDs, or provider payloads.
Every sample must come from the same process-local worker instance.
"""
from __future__ import annotations
import argparse
import json
import math
import os
import ssl
import time
import urllib.error
import urllib.request
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Callable
from urllib.parse import urlsplit
SCHEMA_VERSION = "vignette.g7-runtime-sampling.v1"
SNAPSHOT_SCHEMA_VERSION = "vignette.voice-runtime.v1"
MAX_SAMPLES = 7_200
MAX_INTERVAL_SECONDS = 60.0
MAX_CAPTURE_WINDOW_SECONDS = 7_200.0
class EvidenceFailure(RuntimeError):
"""A runtime sampling or privacy contract failed."""
def utc_now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def validate_target(url: str) -> tuple[str, str]:
parsed = urlsplit(url)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise EvidenceFailure("runtime_url_invalid")
if parsed.username or parsed.password or parsed.fragment:
raise EvidenceFailure("runtime_url_contains_credentials_or_fragment")
if parsed.path.rstrip("/") != "/admin/voice-runtime" or parsed.query:
raise EvidenceFailure("runtime_url_path_invalid")
return parsed.netloc, parsed.path
def validate_capture_bounds(samples: int, interval_seconds: float) -> None:
if not 1 <= samples <= MAX_SAMPLES:
raise EvidenceFailure("sample_count_out_of_bounds")
if not math.isfinite(interval_seconds) or not 0.05 <= interval_seconds <= 60:
raise EvidenceFailure("sample_interval_out_of_bounds")
if (samples - 1) * interval_seconds > MAX_CAPTURE_WINDOW_SECONDS:
raise EvidenceFailure("capture_window_out_of_bounds")
def _nonnegative_number(value: object, *, code: str) -> int | float:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise EvidenceFailure(code)
if not math.isfinite(float(value)) or value < 0:
raise EvidenceFailure(code)
return value
def validate_snapshot(payload: object) -> dict[str, Any]:
if not isinstance(payload, dict):
raise EvidenceFailure("runtime_snapshot_invalid_shape")
if payload.get("schema_version") != SNAPSHOT_SCHEMA_VERSION:
raise EvidenceFailure("runtime_snapshot_schema_drift")
if payload.get("scope") != "single_api_worker":
raise EvidenceFailure("runtime_snapshot_scope_drift")
if payload.get("privacy_boundary") != (
"metadata_only_no_audio_transcript_or_session_ids"
):
raise EvidenceFailure("runtime_snapshot_privacy_boundary_drift")
if payload.get("reset_supported") is not False:
raise EvidenceFailure("runtime_snapshot_reset_contract_drift")
limits = payload.get("limits")
process = payload.get("process")
counters = payload.get("counters")
if not all(isinstance(item, dict) for item in (limits, process, counters)):
raise EvidenceFailure("runtime_snapshot_sections_missing")
assert isinstance(limits, dict) and isinstance(process, dict)
assert isinstance(counters, dict)
for field in (
"max_utterance_audio_bytes",
"streaming_event_queue_max_items",
"uvicorn_ws_max_queue",
):
if (
_nonnegative_number(
limits.get(field), code=f"runtime_limit_invalid:{field}"
)
<= 0
):
raise EvidenceFailure(f"runtime_limit_invalid:{field}")
worker_id = process.get("worker_instance_id")
started_at = process.get("started_at_utc")
platform = process.get("platform")
if not isinstance(worker_id, str) or len(worker_id) < 16:
raise EvidenceFailure("runtime_worker_identity_invalid")
if not isinstance(started_at, str) or not started_at:
raise EvidenceFailure("runtime_worker_start_invalid")
if not isinstance(platform, str) or not platform:
raise EvidenceFailure("runtime_platform_invalid")
for field in (
"pid",
"uptime_seconds",
"rss_bytes",
"peak_rss_bytes",
"cpu_user_seconds",
"cpu_system_seconds",
"threads",
):
_nonnegative_number(process.get(field), code=f"runtime_process_invalid:{field}")
for field, value in counters.items():
_nonnegative_number(value, code=f"runtime_counter_invalid:{field}")
forbidden = {
"session_id",
"transcript",
"transcript_text",
"raw_audio",
"provider_payload",
}
def walk(value: object) -> None:
if isinstance(value, dict):
if forbidden.intersection(str(key) for key in value):
raise EvidenceFailure("runtime_snapshot_forbidden_field")
for child in value.values():
walk(child)
elif isinstance(value, list):
for child in value:
walk(child)
walk(payload)
return payload
def fetch_snapshot(
*,
url: str,
cookie_name: str,
cookie_value: str,
timeout_seconds: float,
) -> dict[str, Any]:
if not cookie_name or any(char in cookie_name for char in "\r\n;="):
raise EvidenceFailure("cookie_name_invalid")
if not cookie_value or any(char in cookie_value for char in "\r\n;"):
raise EvidenceFailure("admin_session_cookie_invalid")
request = urllib.request.Request(
url,
headers={
"Accept": "application/json",
"Cookie": f"{cookie_name}={cookie_value}",
},
method="GET",
)
try:
with urllib.request.urlopen(
request,
timeout=timeout_seconds,
context=ssl.create_default_context(),
) as response:
if response.status != 200:
raise EvidenceFailure(f"runtime_http_status:{response.status}")
raw = response.read(1_048_577)
except (OSError, urllib.error.URLError) as exc:
raise EvidenceFailure("runtime_request_failed") from exc
if len(raw) > 1_048_576:
raise EvidenceFailure("runtime_response_too_large")
try:
return validate_snapshot(json.loads(raw.decode("utf-8")))
except (UnicodeError, json.JSONDecodeError) as exc:
raise EvidenceFailure("runtime_response_invalid_json") from exc
def base_evidence(
*, url: str, samples: int, interval_seconds: float, cookie_env: str
) -> dict[str, object]:
host, path = validate_target(url)
return {
"schema_version": SCHEMA_VERSION,
"status": "running",
"started_at_utc": utc_now(),
"ended_at_utc": None,
"target_host": host,
"target_path": path,
"cookie_env": cookie_env,
"cookie_present": False,
"cookie_value_logged": False,
"privacy_boundary": "metadata_only_no_audio_transcript_session_or_cookie_values",
"requested_samples": samples,
"interval_seconds": interval_seconds,
"samples_completed": 0,
"worker_instance_id": None,
"worker_pid": None,
"worker_started_at_utc": None,
"samples": [],
"high_water": {},
"failure_type": None,
}
def capture_evidence(
*,
url: str,
cookie_name: str,
cookie_value: str,
cookie_env: str,
samples: int,
interval_seconds: float,
timeout_seconds: float,
fetch: Callable[..., dict[str, Any]] = fetch_snapshot,
sleep: Callable[[float], None] = time.sleep,
) -> dict[str, object]:
validate_capture_bounds(samples, interval_seconds)
evidence = base_evidence(
url=url,
samples=samples,
interval_seconds=interval_seconds,
cookie_env=cookie_env,
)
evidence["cookie_present"] = bool(cookie_value)
snapshots: list[dict[str, Any]] = []
identity: tuple[str, int, str] | None = None
for index in range(samples):
if index:
sleep(interval_seconds)
snapshot = fetch(
url=url,
cookie_name=cookie_name,
cookie_value=cookie_value,
timeout_seconds=timeout_seconds,
)
process = snapshot["process"]
current_identity = (
str(process["worker_instance_id"]),
int(process["pid"]),
str(process["started_at_utc"]),
)
if identity is None:
identity = current_identity
elif current_identity != identity:
raise EvidenceFailure("runtime_worker_drift")
snapshots.append(
{"sequence": index + 1, "observed_at_utc": utc_now(), "snapshot": snapshot}
)
assert identity is not None
(
evidence["worker_instance_id"],
evidence["worker_pid"],
evidence["worker_started_at_utc"],
) = identity
evidence["samples"] = snapshots
evidence["samples_completed"] = len(snapshots)
final_snapshot = snapshots[-1]["snapshot"]
evidence["high_water"] = {
"process_peak_rss_bytes": max(
item["snapshot"]["process"]["peak_rss_bytes"] for item in snapshots
),
"process_threads_max": max(
item["snapshot"]["process"]["threads"] for item in snapshots
),
"websocket_high_water": final_snapshot["counters"]["websocket_high_water"],
"streaming_provider_session_high_water": final_snapshot["counters"][
"streaming_provider_session_high_water"
],
"route_audio_buffer_high_water_bytes": final_snapshot["counters"][
"route_audio_buffer_high_water_bytes"
],
"streaming_event_queue_high_water_items": final_snapshot["counters"][
"streaming_event_queue_high_water_items"
],
"streaming_event_queue_saturation_total": final_snapshot["counters"][
"streaming_event_queue_saturation_total"
],
"streaming_event_queue_wait_seconds_total": final_snapshot["counters"][
"streaming_event_queue_wait_seconds_total"
],
}
evidence["status"] = "passed"
evidence["ended_at_utc"] = utc_now()
return evidence
def emit(evidence: dict[str, object], output: Path | None) -> None:
payload = json.dumps(evidence, ensure_ascii=False, indent=2, sort_keys=True) + "\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 parser() -> argparse.ArgumentParser:
result = argparse.ArgumentParser(description=__doc__)
result.add_argument("--url", required=True)
result.add_argument("--cookie-env", default="VIGNETTE_ADMIN_SESSION_COOKIE")
result.add_argument("--cookie-name", default="__Host-vignette_sid")
result.add_argument("--samples", type=int, default=10)
result.add_argument("--interval-seconds", type=float, default=1.0)
result.add_argument("--timeout-seconds", type=float, default=15.0)
result.add_argument("--evidence-output", type=Path)
return result
def main() -> int:
args = parser().parse_args()
evidence = base_evidence(
url=args.url,
samples=args.samples,
interval_seconds=args.interval_seconds,
cookie_env=args.cookie_env,
)
try:
validate_capture_bounds(args.samples, args.interval_seconds)
if not math.isfinite(args.timeout_seconds) or args.timeout_seconds <= 0:
raise EvidenceFailure("timeout_invalid")
cookie_value = os.getenv(args.cookie_env, "").strip()
evidence = capture_evidence(
url=args.url,
cookie_name=args.cookie_name,
cookie_value=cookie_value,
cookie_env=args.cookie_env,
samples=args.samples,
interval_seconds=args.interval_seconds,
timeout_seconds=args.timeout_seconds,
)
emit(evidence, args.evidence_output)
return 0
except EvidenceFailure as exc:
evidence["status"] = "failed"
evidence["failure_type"] = str(exc)
evidence["ended_at_utc"] = utc_now()
emit(evidence, args.evidence_output)
return 1
if __name__ == "__main__":
raise SystemExit(main())