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 산출물은 커밋에서 제외했다.
137 lines
5 KiB
Python
137 lines
5 KiB
Python
"""Serve a local, synthetic G8 rollback receipt control plane for contract smoke.
|
|
|
|
This harness never mutates a model or deployment. It only proves that the API
|
|
binds a separately authenticated HTTP executor receipt to the pinned command.
|
|
It must not be used as production rollback evidence.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import re
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from typing import Any
|
|
|
|
|
|
TOKEN_HEADER = "X-Vignette-Rollback-Executor-Token"
|
|
MAX_BODY_BYTES = 65_536
|
|
SHA256_PATTERN = re.compile(r"^[a-f0-9]{64}$")
|
|
REQUIRED_FIELDS = (
|
|
"idempotency_key",
|
|
"rollback_scope",
|
|
"target_kind",
|
|
"target_id",
|
|
"artifact_record_id",
|
|
"artifact_sha256",
|
|
)
|
|
|
|
|
|
class HarnessError(ValueError):
|
|
pass
|
|
|
|
|
|
def build_synthetic_receipt(payload: Any) -> dict[str, Any]:
|
|
if not isinstance(payload, dict):
|
|
raise HarnessError("request body must be an object")
|
|
if payload.get("schema_version") != "oas.rollback-executor.v1":
|
|
raise HarnessError("unsupported rollback executor schema")
|
|
missing = [key for key in REQUIRED_FIELDS if not payload.get(key)]
|
|
if missing:
|
|
raise HarnessError(f"missing required fields: {','.join(missing)}")
|
|
if payload["rollback_scope"] not in {"model", "runtime"}:
|
|
raise HarnessError("invalid rollback scope")
|
|
if payload["target_kind"] not in {"model_change_gate", "release_gate"}:
|
|
raise HarnessError("invalid target kind")
|
|
if not SHA256_PATTERN.fullmatch(str(payload["artifact_sha256"])):
|
|
raise HarnessError("invalid artifact sha256")
|
|
binding = "|".join(str(payload[key]) for key in REQUIRED_FIELDS)
|
|
execution_id = f"synthetic-g8-{hashlib.sha256(binding.encode()).hexdigest()[:24]}"
|
|
return {
|
|
"schema_version": "oas.rollback-executor.v1",
|
|
"status": "executed",
|
|
"execution_id": execution_id,
|
|
"idempotency_key": payload["idempotency_key"],
|
|
"rollback_scope": payload["rollback_scope"],
|
|
"target_kind": payload["target_kind"],
|
|
"target_id": payload["target_id"],
|
|
"artifact_record_id": payload["artifact_record_id"],
|
|
"artifact_sha256": payload["artifact_sha256"],
|
|
"evidence_refs": [f"audit://synthetic-g8-control-plane/{execution_id}"],
|
|
}
|
|
|
|
|
|
def _handler(*, token: str, mode: str) -> type[BaseHTTPRequestHandler]:
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, format: str, *args: object) -> None:
|
|
return None
|
|
|
|
def _json(self, status: int, payload: dict[str, Any]) -> None:
|
|
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def do_POST(self) -> None:
|
|
if self.path != "/rollback":
|
|
self._json(404, {"detail": "not found"})
|
|
return
|
|
presented = self.headers.get(TOKEN_HEADER, "")
|
|
if not hmac.compare_digest(presented, token):
|
|
self._json(403, {"detail": "forbidden"})
|
|
return
|
|
try:
|
|
content_length = int(self.headers.get("Content-Length", "0"))
|
|
except ValueError:
|
|
self._json(400, {"detail": "invalid content length"})
|
|
return
|
|
if content_length < 1 or content_length > MAX_BODY_BYTES:
|
|
self._json(413, {"detail": "request body size rejected"})
|
|
return
|
|
try:
|
|
payload = json.loads(self.rfile.read(content_length))
|
|
receipt = build_synthetic_receipt(payload)
|
|
except (HarnessError, json.JSONDecodeError) as exc:
|
|
self._json(422, {"detail": str(exc)})
|
|
return
|
|
if mode == "failed":
|
|
self._json(503, {"detail": "synthetic executor failure"})
|
|
return
|
|
self._json(200, receipt)
|
|
|
|
return Handler
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--host", default="127.0.0.1")
|
|
parser.add_argument("--port", type=int, default=18148)
|
|
parser.add_argument("--token", required=True)
|
|
parser.add_argument("--mode", choices=("executed", "failed"), default="executed")
|
|
args = parser.parse_args()
|
|
if len(args.token) < 32:
|
|
raise SystemExit("--token must contain at least 32 characters")
|
|
server = ThreadingHTTPServer(
|
|
(args.host, args.port), _handler(token=args.token, mode=args.mode)
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"ready": True,
|
|
"endpoint": f"http://{args.host}:{args.port}/rollback",
|
|
"mode": args.mode,
|
|
"synthetic_only": True,
|
|
},
|
|
separators=(",", ":"),
|
|
),
|
|
flush=True,
|
|
)
|
|
server.serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|