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:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
|
|
@ -20,7 +20,17 @@ from typing import Any
|
|||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
REQUIRED_ENV_KEYS = (
|
||||
INTERNAL_TOKEN_ENV_KEYS = (
|
||||
"VIGNETTE_RUPTURE_INTERNAL_TOKEN",
|
||||
"VIGNETTE_PRACTICE_INTERNAL_TOKEN",
|
||||
"VIGNETTE_CALIBRATION_TRANSFER_INTERNAL_TOKEN",
|
||||
"VIGNETTE_SUPERVISION_RESEARCH_INTERNAL_TOKEN",
|
||||
"VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN",
|
||||
"VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN",
|
||||
)
|
||||
MIN_INTERNAL_TOKEN_LENGTH = 32
|
||||
ENGINE_GATEWAY_SECRET_ENV_KEY = "ENGINE_GATEWAY_SHARED_SECRET"
|
||||
COMPOSE_REQUIRED_ENV_KEYS = (
|
||||
"ENVIRONMENT",
|
||||
"POSTGRES_PASSWORD",
|
||||
"APP_DB_PASSWORD",
|
||||
|
|
@ -28,7 +38,15 @@ REQUIRED_ENV_KEYS = (
|
|||
"SESSION_SECRET",
|
||||
"OAUTH_GOOGLE_CLIENT_ID",
|
||||
"OAUTH_GOOGLE_CLIENT_SECRET",
|
||||
)
|
||||
) + INTERNAL_TOKEN_ENV_KEYS
|
||||
DIRECT_RUNTIME_REQUIRED_ENV_KEYS = (
|
||||
"ENVIRONMENT",
|
||||
"DATABASE_URL",
|
||||
"OPENAI_API_KEY",
|
||||
"SESSION_SECRET",
|
||||
"OAUTH_GOOGLE_CLIENT_ID",
|
||||
"OAUTH_GOOGLE_CLIENT_SECRET",
|
||||
) + INTERNAL_TOKEN_ENV_KEYS
|
||||
PLACEHOLDER_VALUES = {
|
||||
"",
|
||||
"change-me",
|
||||
|
|
@ -78,6 +96,7 @@ def check_pinned_requirements() -> Check:
|
|||
paths = [
|
||||
REPO_ROOT / "apps" / "api" / "requirements.txt",
|
||||
REPO_ROOT / "apps" / "api" / "requirements-rag.txt",
|
||||
REPO_ROOT / "apps" / "api" / "engine_gateway" / "requirements.txt",
|
||||
]
|
||||
offenders: list[str] = []
|
||||
for path in paths:
|
||||
|
|
@ -162,17 +181,59 @@ def _parse_env_file(path: Path) -> dict[str, str]:
|
|||
return values
|
||||
|
||||
|
||||
def check_env_file(path: Path, *, allow_placeholder_secrets: bool) -> Check:
|
||||
def check_env_file(
|
||||
path: Path,
|
||||
*,
|
||||
allow_placeholder_secrets: bool,
|
||||
deployment_profile: str = "compose",
|
||||
) -> Check:
|
||||
if not path.exists():
|
||||
return Check("env_file", False, f"{_rel(path)} missing")
|
||||
values = _parse_env_file(path)
|
||||
errors: list[str] = []
|
||||
for key in REQUIRED_ENV_KEYS:
|
||||
required_keys = (
|
||||
DIRECT_RUNTIME_REQUIRED_ENV_KEYS
|
||||
if deployment_profile == "direct-runtime"
|
||||
else COMPOSE_REQUIRED_ENV_KEYS
|
||||
)
|
||||
for key in required_keys:
|
||||
value = values.get(key, "")
|
||||
if key not in values:
|
||||
errors.append(f"{key} missing")
|
||||
elif not allow_placeholder_secrets and value in PLACEHOLDER_VALUES:
|
||||
errors.append(f"{key} is placeholder or empty")
|
||||
if not allow_placeholder_secrets:
|
||||
gateway_secret = values.get(ENGINE_GATEWAY_SECRET_ENV_KEY, "")
|
||||
if gateway_secret:
|
||||
if len(gateway_secret) < MIN_INTERNAL_TOKEN_LENGTH:
|
||||
errors.append(
|
||||
f"{ENGINE_GATEWAY_SECRET_ENV_KEY} must contain at least "
|
||||
f"{MIN_INTERNAL_TOKEN_LENGTH} characters"
|
||||
)
|
||||
if gateway_secret.lower().startswith(
|
||||
("change-me", "replace-with", "dummy", "example")
|
||||
):
|
||||
errors.append(
|
||||
f"{ENGINE_GATEWAY_SECRET_ENV_KEY} is placeholder or empty"
|
||||
)
|
||||
token_values: dict[str, list[str]] = {}
|
||||
for key in INTERNAL_TOKEN_ENV_KEYS:
|
||||
value = values.get(key, "")
|
||||
if value and len(value) < MIN_INTERNAL_TOKEN_LENGTH:
|
||||
errors.append(
|
||||
f"{key} must contain at least {MIN_INTERNAL_TOKEN_LENGTH} characters"
|
||||
)
|
||||
if value.lower().startswith(("change-me", "replace-with", "dummy", "example")):
|
||||
errors.append(f"{key} is placeholder or empty")
|
||||
if value:
|
||||
token_values.setdefault(value, []).append(key)
|
||||
for duplicate_keys in token_values.values():
|
||||
if len(duplicate_keys) > 1:
|
||||
errors.append(
|
||||
"internal tokens must be independently revocable: "
|
||||
+ ", ".join(duplicate_keys)
|
||||
+ " share one value"
|
||||
)
|
||||
engine_mode = values.get("ENGINE_MODE", "claude_cli")
|
||||
if engine_mode not in VALID_ENGINE_MODES:
|
||||
errors.append(f"ENGINE_MODE invalid: {engine_mode}")
|
||||
|
|
@ -182,7 +243,11 @@ def check_env_file(path: Path, *, allow_placeholder_secrets: bool) -> Check:
|
|||
errors.append(f"{key}=true is forbidden outside dev")
|
||||
if errors:
|
||||
return Check("env_file", False, "; ".join(errors))
|
||||
return Check("env_file", True, f"{_rel(path)} has required deployment keys")
|
||||
return Check(
|
||||
"env_file",
|
||||
True,
|
||||
f"{_rel(path)} has required {deployment_profile} deployment keys",
|
||||
)
|
||||
|
||||
|
||||
async def check_database(database_url: str, *, require_app_role: bool) -> Check:
|
||||
|
|
@ -212,6 +277,14 @@ async def check_database(database_url: str, *, require_app_role: bool) -> Check:
|
|||
to_regclass('app.sessions') IS NOT NULL AS has_sessions,
|
||||
to_regclass('app.turns') IS NOT NULL AS has_turns,
|
||||
to_regclass('app.session_review_status') IS NOT NULL AS has_session_review_status,
|
||||
to_regclass('app.measurement_event') IS NOT NULL AS has_measurement_event,
|
||||
to_regclass('app.outcome_trajectory_revision') IS NOT NULL AS has_outcome_trajectory,
|
||||
to_regclass('app.rupture_episode') IS NOT NULL AS has_rupture_episode,
|
||||
to_regclass('app.practice_prescription') IS NOT NULL AS has_practice_prescription,
|
||||
to_regclass('app.calibration_prediction_history') IS NOT NULL AS has_calibration_history,
|
||||
to_regclass('app.supervision_attention_snapshot') IS NOT NULL AS has_supervision_snapshot,
|
||||
to_regclass('app.multimodal_consent_snapshot') IS NOT NULL AS has_multimodal_consent,
|
||||
to_regclass('app.ci_content_pipeline') IS NOT NULL AS has_ci_content_pipeline,
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'app'
|
||||
|
|
@ -287,6 +360,14 @@ async def check_database(database_url: str, *, require_app_role: bool) -> Check:
|
|||
"has_sessions",
|
||||
"has_turns",
|
||||
"has_session_review_status",
|
||||
"has_measurement_event",
|
||||
"has_outcome_trajectory",
|
||||
"has_rupture_episode",
|
||||
"has_practice_prescription",
|
||||
"has_calibration_history",
|
||||
"has_supervision_snapshot",
|
||||
"has_multimodal_consent",
|
||||
"has_ci_content_pipeline",
|
||||
"has_persona_triggers",
|
||||
"has_turn_voice_metadata_columns",
|
||||
"has_session_review_worksheet_columns",
|
||||
|
|
@ -332,6 +413,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
action="store_true",
|
||||
help="Allow example placeholder secrets when validating .env.example.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deployment-profile",
|
||||
choices=("compose", "direct-runtime"),
|
||||
default="compose",
|
||||
help=(
|
||||
"Validate compose packaging secrets or the direct Windows public "
|
||||
"runtime's DATABASE_URL-based configuration."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database-url",
|
||||
default=os.getenv("DATABASE_URL", ""),
|
||||
|
|
@ -357,12 +447,21 @@ async def main_async(argv: list[str]) -> int:
|
|||
check_live_coach_kb(),
|
||||
]
|
||||
if args.env_file:
|
||||
env_path = Path(args.env_file)
|
||||
results.append(
|
||||
check_env_file(
|
||||
Path(args.env_file),
|
||||
env_path,
|
||||
allow_placeholder_secrets=args.allow_placeholder_secrets,
|
||||
deployment_profile=args.deployment_profile,
|
||||
)
|
||||
)
|
||||
if (
|
||||
args.deployment_profile == "direct-runtime"
|
||||
and not args.database_url
|
||||
and not args.skip_db
|
||||
and env_path.exists()
|
||||
):
|
||||
args.database_url = _parse_env_file(env_path).get("DATABASE_URL", "")
|
||||
if args.database_url and not args.skip_db:
|
||||
results.append(
|
||||
await check_database(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue