vignette/scripts/probe-nas-preview-g8-rollback.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

1063 lines
38 KiB
Python

#!/usr/bin/env python3
"""Prepare and verify a G8 NAS-preview runtime rollback control-plane probe.
This harness has two phases:
``prepare``
Build a deterministic, secret-free release-gate request, human rollback
approval, expected executor command, and executor manifest. The manifest
uses ``vignette.nas-preview-rollback-manifest.v1`` (executor schema d31b0d).
``execute``
Send the prepared requests to a separately deployed control-plane API and
verify the durable ``executed`` lifecycle receipt through the read model.
The main preview API must never authorize its own rollback. ``execute``
therefore requires both control-plane and preview base URLs and rejects equal
origins. A distinct control-plane API, with its own process lifecycle and an
enabled rollback executor, is a hard prerequisite. Tokens, cookies, login
identity, and raw HTTP response bodies are never printed.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass
from http.cookiejar import CookieJar
from pathlib import Path
from typing import Any, Iterable
from urllib.parse import urlsplit
from uuid import UUID, uuid5
PLAN_SCHEMA = "vignette.g8-rollback-control-plane-probe-plan.v1"
RESULT_SCHEMA = "vignette.g8-rollback-control-plane-probe-result.v1"
EXECUTOR_REQUEST_SCHEMA = "oas.rollback-executor.v1"
EXECUTOR_MANIFEST_SCHEMA = "vignette.nas-preview-rollback-manifest.v1"
DATA_CLASSIFICATION = "synthetic_replay_red_team_coverage_drift"
INTERNAL_TOKEN_HEADER = "X-Vignette-Continuous-Improvement-Token"
TARGET_PROJECT = "vignette-preview-20260807"
TARGET_ROOT = "/volume1/docker/vignette-preview-20260807"
TARGET_SERVICES = ["api", "web"]
TARGET_HEALTH_URL = "http://127.0.0.1:8088/api/health"
RELEASE_GATE_PATH = "/internal/continuous-improvement/release-gates"
APPROVAL_PATH = "/continuous-improvement/approvals"
READ_MODEL_PATH = "/internal/continuous-improvement"
DEV_LOGIN_PATH = "/auth/dev-login"
UUID_NAMESPACE = UUID("b9ecf1e8-8d40-5e3d-9ab2-c0d4bb842496")
SUFFIX_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
IMAGE_RE = re.compile(r"^sha256:[a-f0-9]{64}$")
ARTIFACT_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,179}$")
RELEASE_ID_RE = re.compile(r"^oas-g8-release-[a-z0-9-]+$")
PROVENANCE_RE = re.compile(r"^(repo|db|audit)://[a-zA-Z0-9_./:-]+$")
SAFE_REF_PREFIXES = ("https://", "audit://", "db://", "repo://")
SAFE_EVIDENCE_RE = re.compile(r"^(?:https|audit|db|repo)://[A-Za-z0-9._:/-]+$")
MAX_RESPONSE_BYTES = 1_048_576
PLAN_FIELDS = frozenset(
{
"schema_version",
"probe_id",
"suffix",
"plan_sha256",
"release_gate",
"approval",
"expected_executor_request",
"executor_manifest",
"safety",
}
)
RESULT_FIELDS = frozenset(
{
"schema_version",
"probe_id",
"plan_sha256",
"release_gate_id",
"approval_event_id",
"lifecycle_event_id",
"lifecycle_status",
"artifact_record_id",
"artifact_id",
"artifact_sha256",
"executor_receipt_id",
"executor_evidence_refs",
"release_gate_idempotent_replay",
"approval_idempotent_replay",
"binding_verified",
"control_plane_separation_verified",
"contains_secrets",
"contains_pii",
}
)
class ProbeError(RuntimeError):
"""Fail-closed error with a non-sensitive stable code."""
class _NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(
self,
req: urllib.request.Request,
fp: Any,
code: int,
msg: str,
headers: Any,
newurl: str,
) -> None:
return None
@dataclass(frozen=True)
class ApiResponse:
status: int
body: Any
def _canonical_bytes(value: Any) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
def _sha256_json(value: Any) -> str:
return hashlib.sha256(_canonical_bytes(value)).hexdigest()
def _manifest_bytes(value: Any) -> bytes:
return _canonical_bytes(value) + b"\n"
def _stable_uuid(suffix: str, label: str) -> str:
return str(uuid5(UUID_NAMESPACE, f"{suffix}:{label}"))
def _require_exact_dict(value: Any, fields: Iterable[str], code: str) -> dict[str, Any]:
if not isinstance(value, dict) or set(value) != set(fields):
raise ProbeError(code)
return value
def _require_string(value: Any, code: str, *, maximum: int = 500) -> str:
if not isinstance(value, str) or not value or len(value) > maximum:
raise ProbeError(code)
return value
def _require_uuid(value: Any, code: str) -> str:
text = _require_string(value, code, maximum=36)
try:
parsed = UUID(text)
except ValueError as exc:
raise ProbeError(code) from exc
if str(parsed) != text:
raise ProbeError(code)
return text
def _require_sha256(value: Any, code: str) -> str:
if not isinstance(value, str) or SHA256_RE.fullmatch(value) is None:
raise ProbeError(code)
return value
def _require_image(value: Any, code: str) -> str:
if not isinstance(value, str) or IMAGE_RE.fullmatch(value) is None:
raise ProbeError(code)
return value
def _require_artifact_id(value: Any) -> str:
text = _require_string(value, "invalid_artifact_id", maximum=180)
if ARTIFACT_ID_RE.fullmatch(text) is None:
raise ProbeError("invalid_artifact_id")
return text
def _require_provenance(value: Any) -> str:
text = _require_string(value, "invalid_artifact_provenance_uri")
if PROVENANCE_RE.fullmatch(text) is None:
raise ProbeError("invalid_artifact_provenance_uri")
return text
def _require_evidence_refs(value: Any) -> list[str]:
if not isinstance(value, list) or not 1 <= len(value) <= 100:
raise ProbeError("invalid_evidence_refs")
refs: list[str] = []
for item in value:
ref = _require_string(item, "invalid_evidence_ref")
if (
not ref.startswith(SAFE_REF_PREFIXES)
or SAFE_EVIDENCE_RE.fullmatch(ref) is None
):
raise ProbeError("invalid_evidence_ref")
refs.append(ref)
if len(refs) != len(set(refs)):
raise ProbeError("duplicate_evidence_refs")
return refs
def _artifact(
*,
suffix: str,
kind: str,
artifact_id: str | None = None,
artifact_sha256: str | None = None,
provenance_uri: str | None = None,
) -> dict[str, str]:
return {
"artifact_record_id": _stable_uuid(suffix, f"artifact:{kind}"),
"artifact_id": artifact_id or f"g8-nas-probe-{kind}-{suffix}",
"content_sha256": artifact_sha256
or hashlib.sha256(f"{suffix}:{kind}".encode("ascii")).hexdigest(),
"provenance_uri": provenance_uri
or f"audit://vignette-nas-preview/g8-rollback-probe/{suffix}/{kind}",
}
def build_plan(
*,
suffix: str,
artifact_id: str,
artifact_sha256: str,
artifact_provenance_uri: str,
evidence_refs: list[str],
compose_sha256: str,
env_sha256: str,
resolved_config_sha256: str,
api_image: str,
web_image: str,
release_id: str | None = None,
) -> dict[str, Any]:
if SUFFIX_RE.fullmatch(suffix) is None:
raise ProbeError("invalid_suffix")
artifact_id = _require_artifact_id(artifact_id)
artifact_sha256 = _require_sha256(artifact_sha256, "invalid_artifact_sha256")
artifact_provenance_uri = _require_provenance(artifact_provenance_uri)
evidence_refs = _require_evidence_refs(evidence_refs)
compose_sha256 = _require_sha256(compose_sha256, "invalid_compose_sha256")
env_sha256 = _require_sha256(env_sha256, "invalid_env_sha256")
resolved_config_sha256 = _require_sha256(
resolved_config_sha256, "invalid_resolved_config_sha256"
)
api_image = _require_image(api_image, "invalid_api_image")
web_image = _require_image(web_image, "invalid_web_image")
release_id = release_id or f"oas-g8-release-nas-rollback-{suffix}"
if (
not isinstance(release_id, str)
or len(release_id) > 180
or RELEASE_ID_RE.fullmatch(release_id) is None
):
raise ProbeError("invalid_release_id")
release_submission_id = _stable_uuid(suffix, "release-submission")
gate_id = _stable_uuid(suffix, "release-gate")
approval_submission_id = _stable_uuid(suffix, "approval-submission")
approval_event_id = _stable_uuid(suffix, "approval-event")
effect_record_id = _stable_uuid(suffix, "effect-record")
rollback_artifact = _artifact(
suffix=suffix,
kind="rollback",
artifact_id=artifact_id,
artifact_sha256=artifact_sha256,
provenance_uri=artifact_provenance_uri,
)
artifacts = {
"baseline": _artifact(suffix=suffix, kind="baseline"),
"threshold": _artifact(suffix=suffix, kind="threshold"),
"provenance": [_artifact(suffix=suffix, kind="provenance")],
"rollback": rollback_artifact,
}
release_payload = {
"submission_id": release_submission_id,
"gate_id": gate_id,
"data_classification": DATA_CLASSIFICATION,
"manifest": {
"release_id": release_id,
"red_green_passed": True,
"contract_passed": True,
"e2e_passed": True,
"runtime_proof_passed": True,
"public_proof_passed": True,
"ssot_synced": True,
"evidence_refs": evidence_refs,
},
"artifacts": artifacts,
}
approval_payload = {
"submission_id": approval_submission_id,
"approval_event_id": approval_event_id,
"effect_record_id": effect_record_id,
"target_kind": "release_gate",
"target_id": gate_id,
"decision": "authorize_rollback",
"reason_code": "nas_preview_runtime_rollback_probe",
"evidence_refs": evidence_refs,
}
expected_executor_request = {
"schema_version": EXECUTOR_REQUEST_SCHEMA,
"idempotency_key": effect_record_id,
"approval_event_id": approval_event_id,
"rollback_scope": "runtime",
"target_kind": "release_gate",
"target_id": gate_id,
"subject_id": release_id,
"rollback_target_id": artifact_id,
"artifact_record_id": rollback_artifact["artifact_record_id"],
"artifact_id": artifact_id,
"artifact_sha256": artifact_sha256,
"artifact_provenance_uri": artifact_provenance_uri,
"authorization_evidence_refs": evidence_refs,
}
runtime_root = f"{TARGET_ROOT}/.rollback-executor/runtime/{artifact_id}"
manifest_artifact = {
"idempotency_key": effect_record_id,
"approval_event_id": approval_event_id,
"authorization_evidence_refs": evidence_refs,
"artifact_id": artifact_id,
"artifact_sha256": artifact_sha256,
"artifact_record_id": rollback_artifact["artifact_record_id"],
"artifact_provenance_uri": artifact_provenance_uri,
"target_id": gate_id,
"subject_id": release_id,
"rollback_target_id": artifact_id,
"compose_file": f"{runtime_root}/docker-compose.yml",
"compose_sha256": compose_sha256,
"env_file": f"{runtime_root}/.env",
"env_sha256": env_sha256,
"resolved_config_sha256": resolved_config_sha256,
"api_image": api_image,
"web_image": web_image,
}
manifest_payload = {
"schema_version": EXECUTOR_MANIFEST_SCHEMA,
"target": {
"compose_project": TARGET_PROJECT,
"remote_root": TARGET_ROOT,
"services": TARGET_SERVICES,
"health_url": TARGET_HEALTH_URL,
},
"artifacts": [manifest_artifact],
}
core: dict[str, Any] = {
"schema_version": PLAN_SCHEMA,
"probe_id": f"nas-g8-probe-{hashlib.sha256(suffix.encode('ascii')).hexdigest()[:24]}",
"suffix": suffix,
"release_gate": {"path": RELEASE_GATE_PATH, "payload": release_payload},
"approval": {"path": APPROVAL_PATH, "payload": approval_payload},
"expected_executor_request": expected_executor_request,
"executor_manifest": {
"file_sha256": hashlib.sha256(
_manifest_bytes(manifest_payload)
).hexdigest(),
"payload": manifest_payload,
},
"safety": {
"control_plane_required": True,
"main_preview_self_rollback_forbidden": True,
"contains_secrets": False,
"contains_pii": False,
},
}
plan = {**core, "plan_sha256": _sha256_json(core)}
return validate_plan(plan)
def _validate_gate_artifact(value: Any, code: str) -> dict[str, Any]:
artifact = _require_exact_dict(
value,
{"artifact_record_id", "artifact_id", "content_sha256", "provenance_uri"},
code,
)
_require_uuid(artifact["artifact_record_id"], code)
_require_artifact_id(artifact["artifact_id"])
_require_sha256(artifact["content_sha256"], code)
_require_provenance(artifact["provenance_uri"])
return artifact
def validate_plan(value: Any) -> dict[str, Any]:
plan = _require_exact_dict(value, PLAN_FIELDS, "invalid_plan_shape")
if plan["schema_version"] != PLAN_SCHEMA:
raise ProbeError("invalid_plan_schema")
suffix = _require_string(plan["suffix"], "invalid_suffix", maximum=64)
if SUFFIX_RE.fullmatch(suffix) is None:
raise ProbeError("invalid_suffix")
expected_probe_id = (
f"nas-g8-probe-{hashlib.sha256(suffix.encode('ascii')).hexdigest()[:24]}"
)
if plan["probe_id"] != expected_probe_id:
raise ProbeError("probe_id_mismatch")
supplied_digest = _require_sha256(plan["plan_sha256"], "invalid_plan_sha256")
core = {key: item for key, item in plan.items() if key != "plan_sha256"}
if supplied_digest != _sha256_json(core):
raise ProbeError("plan_sha256_mismatch")
release = _require_exact_dict(
plan["release_gate"], {"path", "payload"}, "invalid_release_gate_shape"
)
approval = _require_exact_dict(
plan["approval"], {"path", "payload"}, "invalid_approval_shape"
)
if release["path"] != RELEASE_GATE_PATH or approval["path"] != APPROVAL_PATH:
raise ProbeError("invalid_api_path")
release_payload = _require_exact_dict(
release["payload"],
{"submission_id", "gate_id", "data_classification", "manifest", "artifacts"},
"invalid_release_payload_shape",
)
_require_uuid(release_payload["submission_id"], "invalid_release_submission_id")
gate_id = _require_uuid(release_payload["gate_id"], "invalid_gate_id")
if release_payload["data_classification"] != DATA_CLASSIFICATION:
raise ProbeError("invalid_data_classification")
manifest = _require_exact_dict(
release_payload["manifest"],
{
"release_id",
"red_green_passed",
"contract_passed",
"e2e_passed",
"runtime_proof_passed",
"public_proof_passed",
"ssot_synced",
"evidence_refs",
},
"invalid_release_manifest_shape",
)
release_id = _require_string(
manifest["release_id"], "invalid_release_id", maximum=180
)
if RELEASE_ID_RE.fullmatch(release_id) is None:
raise ProbeError("invalid_release_id")
for field in (
"red_green_passed",
"contract_passed",
"e2e_passed",
"runtime_proof_passed",
"public_proof_passed",
"ssot_synced",
):
if manifest[field] is not True:
raise ProbeError("release_gate_not_qualified")
release_evidence = _require_evidence_refs(manifest["evidence_refs"])
artifacts = _require_exact_dict(
release_payload["artifacts"],
{"baseline", "threshold", "provenance", "rollback"},
"invalid_gate_artifacts_shape",
)
_validate_gate_artifact(artifacts["baseline"], "invalid_baseline_artifact")
_validate_gate_artifact(artifacts["threshold"], "invalid_threshold_artifact")
if (
not isinstance(artifacts["provenance"], list)
or len(artifacts["provenance"]) != 1
):
raise ProbeError("invalid_provenance_artifacts")
_validate_gate_artifact(artifacts["provenance"][0], "invalid_provenance_artifact")
rollback_artifact = _validate_gate_artifact(
artifacts["rollback"], "invalid_rollback_artifact"
)
record_ids = [
artifacts["baseline"]["artifact_record_id"],
artifacts["threshold"]["artifact_record_id"],
artifacts["provenance"][0]["artifact_record_id"],
rollback_artifact["artifact_record_id"],
]
if len(record_ids) != len(set(record_ids)):
raise ProbeError("duplicate_artifact_record_ids")
approval_payload = _require_exact_dict(
approval["payload"],
{
"submission_id",
"approval_event_id",
"effect_record_id",
"target_kind",
"target_id",
"decision",
"reason_code",
"evidence_refs",
},
"invalid_approval_payload_shape",
)
_require_uuid(approval_payload["submission_id"], "invalid_approval_submission_id")
approval_event_id = _require_uuid(
approval_payload["approval_event_id"], "invalid_approval_event_id"
)
effect_record_id = _require_uuid(
approval_payload["effect_record_id"], "invalid_effect_record_id"
)
if (
approval_payload["target_kind"] != "release_gate"
or approval_payload["target_id"] != gate_id
or approval_payload["decision"] != "authorize_rollback"
or approval_payload["reason_code"] != "nas_preview_runtime_rollback_probe"
):
raise ProbeError("invalid_approval_binding")
approval_evidence = _require_evidence_refs(approval_payload["evidence_refs"])
if approval_evidence != release_evidence:
raise ProbeError("release_approval_evidence_mismatch")
expected_request = _require_exact_dict(
plan["expected_executor_request"],
{
"schema_version",
"idempotency_key",
"approval_event_id",
"rollback_scope",
"target_kind",
"target_id",
"subject_id",
"rollback_target_id",
"artifact_record_id",
"artifact_id",
"artifact_sha256",
"artifact_provenance_uri",
"authorization_evidence_refs",
},
"invalid_executor_request_shape",
)
expected_binding = {
"schema_version": EXECUTOR_REQUEST_SCHEMA,
"idempotency_key": effect_record_id,
"approval_event_id": approval_event_id,
"rollback_scope": "runtime",
"target_kind": "release_gate",
"target_id": gate_id,
"subject_id": release_id,
"rollback_target_id": rollback_artifact["artifact_id"],
"artifact_record_id": rollback_artifact["artifact_record_id"],
"artifact_id": rollback_artifact["artifact_id"],
"artifact_sha256": rollback_artifact["content_sha256"],
"artifact_provenance_uri": rollback_artifact["provenance_uri"],
"authorization_evidence_refs": approval_evidence,
}
if expected_request != expected_binding:
raise ProbeError("executor_request_binding_mismatch")
executor_manifest = _require_exact_dict(
plan["executor_manifest"],
{"file_sha256", "payload"},
"invalid_executor_manifest_envelope",
)
executor_payload = _require_exact_dict(
executor_manifest["payload"],
{"schema_version", "target", "artifacts"},
"invalid_executor_manifest_shape",
)
if executor_payload["schema_version"] != EXECUTOR_MANIFEST_SCHEMA:
raise ProbeError("invalid_executor_manifest_schema")
if executor_payload["target"] != {
"compose_project": TARGET_PROJECT,
"remote_root": TARGET_ROOT,
"services": TARGET_SERVICES,
"health_url": TARGET_HEALTH_URL,
}:
raise ProbeError("invalid_executor_manifest_target")
if (
not isinstance(executor_payload["artifacts"], list)
or len(executor_payload["artifacts"]) != 1
):
raise ProbeError("invalid_executor_manifest_artifacts")
manifest_artifact = _require_exact_dict(
executor_payload["artifacts"][0],
{
"idempotency_key",
"approval_event_id",
"authorization_evidence_refs",
"artifact_id",
"artifact_sha256",
"artifact_record_id",
"artifact_provenance_uri",
"target_id",
"subject_id",
"rollback_target_id",
"compose_file",
"compose_sha256",
"env_file",
"env_sha256",
"resolved_config_sha256",
"api_image",
"web_image",
},
"invalid_executor_manifest_artifact_shape",
)
for field in expected_binding:
if field in {"schema_version", "rollback_scope", "target_kind"}:
continue
manifest_field = (
"authorization_evidence_refs"
if field == "authorization_evidence_refs"
else field
)
if manifest_artifact.get(manifest_field) != expected_binding[field]:
raise ProbeError("executor_manifest_binding_mismatch")
expected_root = (
f"{TARGET_ROOT}/.rollback-executor/runtime/{rollback_artifact['artifact_id']}"
)
if manifest_artifact["compose_file"] != f"{expected_root}/docker-compose.yml":
raise ProbeError("invalid_compose_file")
if manifest_artifact["env_file"] != f"{expected_root}/.env":
raise ProbeError("invalid_env_file")
_require_sha256(manifest_artifact["compose_sha256"], "invalid_compose_sha256")
_require_sha256(manifest_artifact["env_sha256"], "invalid_env_sha256")
_require_sha256(
manifest_artifact["resolved_config_sha256"],
"invalid_resolved_config_sha256",
)
_require_image(manifest_artifact["api_image"], "invalid_api_image")
_require_image(manifest_artifact["web_image"], "invalid_web_image")
manifest_digest = _require_sha256(
executor_manifest["file_sha256"], "invalid_executor_manifest_sha256"
)
if manifest_digest != hashlib.sha256(_manifest_bytes(executor_payload)).hexdigest():
raise ProbeError("executor_manifest_sha256_mismatch")
if plan["safety"] != {
"control_plane_required": True,
"main_preview_self_rollback_forbidden": True,
"contains_secrets": False,
"contains_pii": False,
}:
raise ProbeError("invalid_safety_contract")
return plan
def load_plan(path: Path) -> dict[str, Any]:
try:
raw = path.read_bytes()
except OSError as exc:
raise ProbeError("plan_unreadable") from exc
if not raw or len(raw) > MAX_RESPONSE_BYTES:
raise ProbeError("plan_size_rejected")
try:
value = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ProbeError("plan_json_invalid") from exc
return validate_plan(value)
def _normalize_base_url(
value: str, *, label: str, require_secure_transport: bool
) -> tuple[str, str]:
parsed = urlsplit(value)
hostname = (parsed.hostname or "").lower()
local_http = parsed.scheme == "http" and hostname in {
"localhost",
"127.0.0.1",
"::1",
}
if parsed.scheme not in {"http", "https"}:
raise ProbeError(f"invalid_{label}_url")
if require_secure_transport and parsed.scheme != "https" and not local_http:
raise ProbeError(f"{label}_must_use_https")
if (
not parsed.netloc
or parsed.username
or parsed.password
or parsed.query
or parsed.fragment
):
raise ProbeError(f"invalid_{label}_url")
normalized = value.rstrip("/")
origin = f"{parsed.scheme.lower()}://{parsed.netloc.lower()}"
return normalized, origin
class ApiClient:
def __init__(self, base_url: str, timeout: float) -> None:
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self._opener = urllib.request.build_opener(
urllib.request.ProxyHandler({}),
_NoRedirect(),
urllib.request.HTTPCookieProcessor(CookieJar()),
)
def request(
self,
method: str,
path: str,
payload: dict[str, Any] | None = None,
*,
headers: dict[str, str] | None = None,
expected: set[int] | None = None,
) -> ApiResponse:
data = None
request_headers = {"Accept": "application/json", **(headers or {})}
if payload is not None:
data = _canonical_bytes(payload)
request_headers["Content-Type"] = "application/json"
request = urllib.request.Request(
f"{self.base_url}{path}",
data=data,
headers=request_headers,
method=method,
)
try:
with self._opener.open(request, timeout=self.timeout) as response:
raw = response.read(MAX_RESPONSE_BYTES + 1)
status = response.status
except urllib.error.HTTPError as exc:
try:
exc.read(MAX_RESPONSE_BYTES + 1)
finally:
exc.close()
raise ProbeError(f"http_{exc.code}_{method.lower()}") from None
except (urllib.error.URLError, TimeoutError, OSError):
raise ProbeError(f"transport_failed_{method.lower()}") from None
if status not in (expected or {200}):
raise ProbeError(f"unexpected_http_{status}_{method.lower()}")
if len(raw) > MAX_RESPONSE_BYTES:
raise ProbeError("response_size_rejected")
try:
body = json.loads(raw) if raw else {}
except (UnicodeDecodeError, json.JSONDecodeError):
raise ProbeError("response_json_invalid") from None
return ApiResponse(status=status, body=body)
def _require_response_dict(value: Any, code: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ProbeError(code)
return value
def _single(items: Any, predicate: Any, code: str) -> dict[str, Any]:
if not isinstance(items, list):
raise ProbeError(code)
matches = [item for item in items if isinstance(item, dict) and predicate(item)]
if len(matches) != 1:
raise ProbeError(code)
return matches[0]
def execute_plan(
*,
plan: dict[str, Any],
control_plane_base_url: str,
preview_base_url: str,
internal_token: str,
admin_email: str,
timeout: float,
) -> dict[str, Any]:
plan = validate_plan(plan)
control_url, control_origin = _normalize_base_url(
control_plane_base_url,
label="control_plane",
require_secure_transport=True,
)
_, preview_origin = _normalize_base_url(
preview_base_url,
label="preview",
require_secure_transport=False,
)
if control_origin == preview_origin:
raise ProbeError("main_preview_self_rollback_forbidden")
if not isinstance(internal_token, str) or len(internal_token) < 32:
raise ProbeError("internal_token_unavailable")
if (
not isinstance(admin_email, str)
or "@" not in admin_email
or len(admin_email) > 320
):
raise ProbeError("admin_identity_unavailable")
if timeout <= 0 or timeout > 300:
raise ProbeError("invalid_timeout")
client = ApiClient(control_url, timeout)
internal_headers = {INTERNAL_TOKEN_HEADER: internal_token}
release = plan["release_gate"]
release_payload = release["payload"]
created = client.request(
"POST",
release["path"],
release_payload,
headers=internal_headers,
expected={201},
).body
replayed = client.request(
"POST",
release["path"],
release_payload,
headers=internal_headers,
expected={201},
).body
for response in (created, replayed):
response = _require_response_dict(response, "release_response_invalid")
if (
response.get("gate_id") != release_payload["gate_id"]
or response.get("qualified") is not True
or response.get("state") != "pending_human_approval"
or response.get("promotion_executed") is not False
):
raise ProbeError("release_response_binding_mismatch")
if replayed.get("idempotent_replay") is not True:
raise ProbeError("release_gate_not_idempotent")
client.request(
"POST",
DEV_LOGIN_PATH,
{
"email": admin_email,
"role": "admin",
"display_name": "G8 Rollback Control Plane Probe",
"cohort_ids": [],
},
headers={"Origin": control_origin},
expected={200},
)
approval = plan["approval"]
approval_payload = approval["payload"]
approved = client.request(
"POST", approval["path"], approval_payload, expected={201}
).body
approval_replay = client.request(
"POST", approval["path"], approval_payload, expected={201}
).body
for response in (approved, approval_replay):
response = _require_response_dict(response, "approval_response_invalid")
if (
response.get("approval_event_id") != approval_payload["approval_event_id"]
or response.get("effect_record_id") != approval_payload["effect_record_id"]
or response.get("target_kind") != "release_gate"
or response.get("target_id") != release_payload["gate_id"]
or response.get("decision") != "authorize_rollback"
):
raise ProbeError("approval_response_binding_mismatch")
if approval_replay.get("idempotent_replay") is not True:
raise ProbeError("approval_not_idempotent")
view = client.request(
"GET", READ_MODEL_PATH, headers=internal_headers, expected={200}
).body
view = _require_response_dict(view, "read_model_invalid")
for flag in (
"silent_auto_promotion_allowed",
"raw_transcript_included",
"pii_included",
"clinical_claim_allowed",
):
if view.get(flag) is not False:
raise ProbeError("read_model_safety_boundary_failed")
gate_id = release_payload["gate_id"]
release_gate = _single(
view.get("release_gates"),
lambda item: item.get("gate_id") == gate_id,
"release_gate_read_model_mismatch",
)
if (
release_gate.get("release_id") != release_payload["manifest"]["release_id"]
or release_gate.get("qualified") is not True
or release_gate.get("state") != "pending_human_approval"
):
raise ProbeError("release_gate_read_model_mismatch")
expected_request = plan["expected_executor_request"]
artifact = _single(
view.get("gate_artifacts"),
lambda item: (
item.get("artifact_record_id") == expected_request["artifact_record_id"]
),
"rollback_artifact_read_model_mismatch",
)
if (
artifact.get("owner_kind") != "release_gate"
or artifact.get("owner_id") != gate_id
or artifact.get("artifact_kind") != "rollback"
or artifact.get("artifact_id") != expected_request["artifact_id"]
or artifact.get("content_sha256") != expected_request["artifact_sha256"]
or artifact.get("provenance_uri") != expected_request["artifact_provenance_uri"]
):
raise ProbeError("rollback_artifact_read_model_mismatch")
approval_event = _single(
view.get("approvals"),
lambda item: (
item.get("approval_event_id") == approval_payload["approval_event_id"]
),
"approval_read_model_mismatch",
)
if (
approval_event.get("target_kind") != "release_gate"
or approval_event.get("target_id") != gate_id
or approval_event.get("decision") != "authorize_rollback"
or approval_event.get("evidence_refs") != approval_payload["evidence_refs"]
):
raise ProbeError("approval_read_model_mismatch")
lifecycle = _single(
view.get("lifecycle_events"),
lambda item: (
item.get("lifecycle_event_id") == approval_payload["effect_record_id"]
),
"rollback_lifecycle_read_model_mismatch",
)
executor_refs = lifecycle.get("executor_evidence_refs")
lifecycle_refs = lifecycle.get("evidence_refs")
receipt_id = lifecycle.get("executor_receipt_id")
if (
lifecycle.get("target_kind") != "release_gate"
or lifecycle.get("target_id") != gate_id
or lifecycle.get("event_type") != "rollback"
or lifecycle.get("event_status") != "executed"
or lifecycle.get("approval_event_id") != approval_payload["approval_event_id"]
or lifecycle.get("artifact_record_id") != expected_request["artifact_record_id"]
or not isinstance(receipt_id, str)
or not receipt_id
or not isinstance(executor_refs, list)
or not executor_refs
or not isinstance(lifecycle_refs, list)
or not set(approval_payload["evidence_refs"]).issubset(lifecycle_refs)
or not set(executor_refs).issubset(lifecycle_refs)
):
raise ProbeError("rollback_executed_receipt_binding_mismatch")
result = {
"schema_version": RESULT_SCHEMA,
"probe_id": plan["probe_id"],
"plan_sha256": plan["plan_sha256"],
"release_gate_id": gate_id,
"approval_event_id": approval_payload["approval_event_id"],
"lifecycle_event_id": approval_payload["effect_record_id"],
"lifecycle_status": "executed",
"artifact_record_id": expected_request["artifact_record_id"],
"artifact_id": expected_request["artifact_id"],
"artifact_sha256": expected_request["artifact_sha256"],
"executor_receipt_id": receipt_id,
"executor_evidence_refs": executor_refs,
"release_gate_idempotent_replay": True,
"approval_idempotent_replay": True,
"binding_verified": True,
"control_plane_separation_verified": True,
"contains_secrets": False,
"contains_pii": False,
}
if set(result) != RESULT_FIELDS:
raise ProbeError("result_shape_invalid")
return result
def _write_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=__doc__,
epilog=(
"SAFETY: the main preview API must not self-authorize rollback. "
"Execute only against a separately deployed control-plane API whose "
"rollback executor and immutable d31b0d manifest are provisioned."
),
)
subparsers = parser.add_subparsers(dest="command", required=True)
prepare = subparsers.add_parser(
"prepare", help="emit deterministic request and executor-manifest bindings"
)
prepare.add_argument("--suffix", required=True)
prepare.add_argument("--release-id")
prepare.add_argument("--artifact-id", required=True)
prepare.add_argument("--artifact-sha256", required=True)
prepare.add_argument("--artifact-provenance-uri", required=True)
prepare.add_argument("--evidence-ref", action="append", required=True)
prepare.add_argument("--compose-sha256", required=True)
prepare.add_argument("--env-sha256", required=True)
prepare.add_argument("--resolved-config-sha256", required=True)
prepare.add_argument("--api-image", required=True)
prepare.add_argument("--web-image", required=True)
prepare.add_argument("--output", type=Path)
prepare.add_argument("--executor-manifest-output", type=Path)
execute = subparsers.add_parser(
"execute",
help="exercise a separate control-plane API and verify executed receipt binding",
)
execute.add_argument("--plan", type=Path, required=True)
execute.add_argument("--control-plane-base-url", required=True)
execute.add_argument("--preview-base-url", required=True)
execute.add_argument(
"--internal-token-env",
default="VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN",
)
execute.add_argument(
"--admin-email-env", default="VIGNETTE_G8_ROLLBACK_PROBE_ADMIN_EMAIL"
)
execute.add_argument("--timeout", type=float, default=30.0)
execute.add_argument("--output", type=Path)
return parser
def main(argv: list[str] | None = None) -> int:
args = _build_parser().parse_args(argv)
try:
if args.command == "prepare":
plan = build_plan(
suffix=args.suffix,
release_id=args.release_id,
artifact_id=args.artifact_id,
artifact_sha256=args.artifact_sha256,
artifact_provenance_uri=args.artifact_provenance_uri,
evidence_refs=args.evidence_ref,
compose_sha256=args.compose_sha256,
env_sha256=args.env_sha256,
resolved_config_sha256=args.resolved_config_sha256,
api_image=args.api_image,
web_image=args.web_image,
)
if args.output:
_write_json(args.output, plan)
if args.executor_manifest_output:
args.executor_manifest_output.parent.mkdir(parents=True, exist_ok=True)
args.executor_manifest_output.write_bytes(
_manifest_bytes(plan["executor_manifest"]["payload"])
)
result = plan
else:
plan = load_plan(args.plan)
result = execute_plan(
plan=plan,
control_plane_base_url=args.control_plane_base_url,
preview_base_url=args.preview_base_url,
internal_token=os.environ.get(args.internal_token_env, ""),
admin_email=os.environ.get(args.admin_email_env, ""),
timeout=args.timeout,
)
if args.output:
_write_json(args.output, result)
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
except ProbeError as exc:
print(
json.dumps(
{
"schema_version": "vignette.g8-rollback-control-plane-probe-error.v1",
"ok": False,
"error": str(exc),
},
ensure_ascii=False,
sort_keys=True,
),
file=sys.stderr,
)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())