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 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -10,7 +10,7 @@ from functools import lru_cache
from typing import Literal
from urllib.parse import urlsplit
from pydantic import Field, model_validator
from pydantic import Field, SecretStr, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from .contracts.engine_gateway import EngineProvider
@ -78,6 +78,10 @@ class Settings(BaseSettings):
default="http://engine:8100",
validation_alias="ENGINE_URL",
)
engine_gateway_shared_secret: SecretStr = Field(
default=SecretStr(""),
validation_alias="ENGINE_GATEWAY_SHARED_SECRET",
)
engine_mode: EngineMode = Field(
default="claude_api",
validation_alias="ENGINE_MODE",
@ -138,6 +142,86 @@ class Settings(BaseSettings):
default="https://api.openai.com/v1",
validation_alias="OPENAI_BASE_URL",
)
# `local_whisper`는 노트북 상주 faster-whisper 사이드카(scripts/local-whisper-stt-server.py)를
# 쓴다. 오디오가 호스트를 벗어나지 않고 외부 STT 키가 필요 없다.
voice_stt_provider: Literal["openai", "deepgram", "local_whisper"] = Field(
default="openai",
validation_alias="VIGNETTE_VOICE_STT_PROVIDER",
)
local_whisper_stt_url: str = Field(
default="ws://127.0.0.1:9882/v1/listen",
validation_alias="VIGNETTE_LOCAL_WHISPER_STT_URL",
)
local_whisper_stt_model: str = Field(
default="large-v3",
validation_alias="VIGNETTE_LOCAL_WHISPER_STT_MODEL",
)
local_whisper_stt_language: str = Field(
default="ko",
validation_alias="VIGNETTE_LOCAL_WHISPER_STT_LANGUAGE",
)
local_whisper_endpointing_ms: int = Field(
default=300,
ge=10,
le=5000,
validation_alias="VIGNETTE_LOCAL_WHISPER_ENDPOINTING_MS",
)
local_whisper_utterance_end_ms: int = Field(
default=1200,
ge=1000,
le=10000,
validation_alias="VIGNETTE_LOCAL_WHISPER_UTTERANCE_END_MS",
)
local_whisper_finalize_timeout_seconds: float = Field(
default=20.0,
ge=1.0,
le=120.0,
validation_alias="VIGNETTE_LOCAL_WHISPER_FINALIZE_TIMEOUT_SECONDS",
)
deepgram_api_key: SecretStr = Field(
default=SecretStr(""),
validation_alias="DEEPGRAM_API_KEY",
)
deepgram_stt_url: str = Field(
default="wss://api.deepgram.com/v1/listen",
validation_alias="DEEPGRAM_STT_URL",
)
deepgram_stt_model: str = Field(
default="nova-3",
validation_alias="DEEPGRAM_STT_MODEL",
)
deepgram_stt_language: str = Field(
default="ko",
validation_alias="DEEPGRAM_STT_LANGUAGE",
)
deepgram_endpointing_ms: int = Field(
default=300,
ge=10,
le=5000,
validation_alias="DEEPGRAM_ENDPOINTING_MS",
)
deepgram_utterance_end_ms: int = Field(
default=1200,
ge=1000,
le=10000,
validation_alias="DEEPGRAM_UTTERANCE_END_MS",
)
deepgram_keepalive_seconds: float = Field(
default=4.0,
ge=1.0,
le=8.0,
validation_alias="DEEPGRAM_KEEPALIVE_SECONDS",
)
deepgram_finalize_timeout_seconds: float = Field(
default=15.0,
ge=1.0,
le=60.0,
validation_alias="DEEPGRAM_FINALIZE_TIMEOUT_SECONDS",
)
deepgram_mip_opt_out: bool = Field(
default=True,
validation_alias="DEEPGRAM_MIP_OPT_OUT",
)
voice_poc_sample_tts_enabled: bool = Field(
default=False,
validation_alias="VIGNETTE_VOICE_POC_SAMPLE_TTS",
@ -164,6 +248,125 @@ class Settings(BaseSettings):
default="dev-insecure-change-me",
validation_alias="SESSION_SECRET",
)
# G3 evaluator ingestion is a machine-to-machine boundary. An empty value
# does not fall back to a development secret; the endpoints fail closed.
rupture_internal_token: SecretStr = Field(
default=SecretStr(""),
validation_alias="VIGNETTE_RUPTURE_INTERNAL_TOKEN",
)
# G4 evaluator prescription ingestion is independently revocable from G3.
# Missing or short configuration disables the endpoint instead of falling
# back to a shared development secret.
practice_internal_token: SecretStr = Field(
default=SecretStr(""),
validation_alias="VIGNETTE_PRACTICE_INTERNAL_TOKEN",
)
# G5 evaluator calibration/transfer ingestion has its own independently
# revocable credential and never receives an implicit development fallback.
calibration_transfer_internal_token: SecretStr = Field(
default=SecretStr(""),
validation_alias="VIGNETTE_CALIBRATION_TRANSFER_INTERNAL_TOKEN",
)
# G6 supervision/research ingestion separates supervisor and research AI
# views behind one independently revocable machine credential.
supervision_research_internal_token: SecretStr = Field(
default=SecretStr(""),
validation_alias="VIGNETTE_SUPERVISION_RESEARCH_INTERNAL_TOKEN",
)
supervision_research_producer_enabled: bool = Field(
default=True,
validation_alias="VIGNETTE_SUPERVISION_RESEARCH_PRODUCER_ENABLED",
)
supervision_research_producer_interval_seconds: float = Field(
default=3600.0,
ge=60.0,
validation_alias="VIGNETTE_SUPERVISION_RESEARCH_PRODUCER_INTERVAL_SECONDS",
)
supervision_research_producer_startup_delay_seconds: float = Field(
default=15.0,
ge=0.0,
le=300.0,
validation_alias="VIGNETTE_SUPERVISION_RESEARCH_PRODUCER_STARTUP_DELAY_SECONDS",
)
# G8 automation qualification is research-only and independently revocable;
# no development token is synthesized when the setting is absent.
continuous_improvement_internal_token: SecretStr = Field(
default=SecretStr(""),
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN",
)
# G8 scheduled generation is opt-in because each queued item performs
# multiple model calls. Schema readiness is checked separately in lifespan.
continuous_improvement_producer_enabled: bool = Field(
default=False,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED",
)
continuous_improvement_producer_interval_seconds: float = Field(
default=3600.0,
ge=30.0,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_INTERVAL_SECONDS",
)
continuous_improvement_producer_startup_delay_seconds: float = Field(
default=30.0,
ge=0.0,
le=300.0,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_STARTUP_DELAY_SECONDS",
)
continuous_improvement_producer_retry_delay_seconds: float = Field(
default=300.0,
ge=30.0,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_RETRY_DELAY_SECONDS",
)
continuous_improvement_producer_lease_timeout_seconds: float = Field(
default=1800.0,
ge=60.0,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_LEASE_TIMEOUT_SECONDS",
)
continuous_improvement_producer_engine_timeout_seconds: float = Field(
default=300.0,
ge=60.0,
le=600.0,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENGINE_TIMEOUT_SECONDS",
)
continuous_improvement_producer_batch_size: int = Field(
default=1,
ge=1,
le=10,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_BATCH_SIZE",
)
# Operational measurement drift is a separate fail-closed ingestion gate.
# Enabling the producer alone must not start consuming benchmark ledgers.
continuous_improvement_drift_trigger_enabled: bool = Field(
default=False,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED",
)
# Human authorization and rollback execution are separate boundaries. NAS
# deployments must opt in explicitly and point at an authenticated control plane.
continuous_improvement_rollback_executor_enabled: bool = Field(
default=False,
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_ENABLED",
)
continuous_improvement_rollback_executor_endpoint: str = Field(
default="",
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_ENDPOINT",
)
continuous_improvement_rollback_executor_token: SecretStr = Field(
default=SecretStr(""),
validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_TOKEN",
)
continuous_improvement_rollback_executor_timeout_seconds: float = Field(
default=30.0,
ge=2.0,
le=120.0,
validation_alias=(
"VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_TIMEOUT_SECONDS"
),
)
# G7 consent-bound evaluator ingestion is isolated from every other
# machine credential and fails closed when unconfigured.
multimodal_alliance_internal_token: SecretStr = Field(
default=SecretStr(""),
validation_alias="VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN",
)
# __Host- 쿠키 정책: prod 에선 secure=True 강제
cookie_name: str = "__Host-vignette_sid"
session_ttl_seconds: int = 60 * 60 * 8 # 8h
@ -322,6 +525,52 @@ class Settings(BaseSettings):
@model_validator(mode="after")
def validate_non_dev_runtime_flags(self) -> "Settings":
gateway_secret = self.engine_gateway_shared_secret.get_secret_value().strip()
if gateway_secret and (
len(gateway_secret) < 32
or gateway_secret.lower().startswith(
("change-me", "replace-with", "dummy", "example")
)
):
raise ValueError(
"ENGINE_GATEWAY_SHARED_SECRET must be a non-placeholder value "
"containing at least 32 characters"
)
if self.continuous_improvement_rollback_executor_enabled:
endpoint = self.continuous_improvement_rollback_executor_endpoint.strip()
parsed_endpoint = urlsplit(endpoint)
rollback_token = (
self.continuous_improvement_rollback_executor_token.get_secret_value().strip()
)
local_dev_http = (
self.environment == "dev"
and parsed_endpoint.scheme == "http"
and _is_local_url(endpoint)
)
if not endpoint or not parsed_endpoint.hostname:
raise ValueError(
"rollback executor endpoint is required when executor is enabled"
)
if parsed_endpoint.scheme != "https" and not local_dev_http:
raise ValueError(
"rollback executor endpoint must use HTTPS outside local development"
)
if (
parsed_endpoint.username
or parsed_endpoint.password
or parsed_endpoint.query
or parsed_endpoint.fragment
):
raise ValueError(
"rollback executor endpoint cannot contain credentials, query, or fragment"
)
if len(rollback_token) < 32 or rollback_token.lower().startswith(
("change-me", "replace-with", "dummy", "example")
):
raise ValueError(
"rollback executor token must be a non-placeholder value "
"containing at least 32 characters"
)
if self.environment != "dev":
forbidden: list[str] = []
if self.auth_dev_login_enabled: