G8 실제 rollback 증명 종료와 비-secure origin 회기 리뷰 크래시 수정

G8 마지막 게이트인 receipt-bound 실제 image rollback을 격리 NAS
vignette-preview-20260807 에서 실행해 종료했다.

Gate6 계약 정정:
감사 대상 current API 이미지가 com.docker.compose.project/service/version
image label 을 갖고 있어 "helper 의 compose label 0개" 계약은 감사되지 않은
다른 이미지를 쓰지 않는 한 성립하지 않는다. 계약을 key 부재가 아니라
소속(membership) 으로 바꿔 launch-nas-preview-g8-helpers.py 에 구현했다.
image 상속 label 을 baseline 으로 읽고 container 의 모든 compose label 이
baseline 과 같거나 선언된 격리 override 인지 검사하며, 최종 project 는
target 이 아니고 service 는 api/web/db/proxy 가 아니어야 한다. docker run
argv 에 target label 을 주입하면 fake-runner 테스트가 먼저 깨진다 (37/37).

실행 결과:
- rollback-old  receipt nas-g8-723eeef22eab05e63e3fafb0 -> 79ec../c530..
- restore-current receipt nas-g8-2738846cf2cf4fbe8ce0fc26 -> 52e0../6fdb..
- release gate/approval 각 2회 멱등, audit.ci_lifecycle_event rollback/executed 2,
  audit.ci_human_approval_event authorize_rollback 2, silent auto-promotion 0
- HMAC journal 6-record 체인 검증, health 3/3, OpenAPI 126, auth 401, Web 200
- helper 0, listener 0, 비밀 env 파기. down/volume rm/prune 미실행, 공개 런타임 미접촉
- 계획했던 Windows SSH 터널은 NAS sshd 가 direct-tcpip 를 거부해 사용할 수 없어
  sshd 설정 변경 대신 같은 격리 계약의 NAS-side probe 컨테이너로 실행했다

비-secure origin 크래시 수정:
배포된 NAS 프리뷰(평문 HTTP, 비-localhost)에 회기 스펙을 돌려 24건 실패를 확인했고
원인은 하나였다. crypto.randomUUID 는 secure context 전용인데 제품 코드 18곳이
fallback 없이 호출했고 RuptureRepairCard 는 렌더 시점 호출이라 회기 리뷰 라우트
전체가 error boundary 로 떨어졌다. 릴리스 게이트 108/108 은 localhost 후보 스택에서만
돌아 이 경로를 밟은 적이 없다. src/lib/uuid.ts 의 randomUuid() 로 통일하고 fallback 도
crypto.getRandomValues 를 우선 사용해 idempotency key 의 예측 불가능성을 유지했다.
회귀는 insecure-context-uuid.spec.ts 6/6 으로 고정했다(직접 호출 0건 검사 포함).
이 수정은 아직 NAS 에 배포하지 않았다.

검증:
API 898, gateway 58, executor 28, probe 11, helper launcher 37, release agent 21,
ruff clean, web api-types/typecheck/build, SSOT FAIL 0, SSOT unit 5/5,
dashboard E2E 10/10, 학생 폐루프 실 DB 브라우저 4/4(일회용 클론),
crypto 수정 후 기존 스펙 회귀 70/70, 복원된 NAS 실제 브라우저 SSE->DB 리뷰 PASS.

부수 발견(열린 항목):
공개 API 가 engine=false 로 degraded 인데 워치독이 이를 감지하지 못한다. engine 판정이
게이트웨이 /health 의 ok 만 보고 claude readiness probe 를 돌리지 않기 때문이다.
같은 .env 와 같은 CLI 로 새 게이트웨이를 다른 포트에 띄우면 즉시 ready 이므로 상주
프로세스의 세션만 죽은 형태다. TODO A절과 대시보드에 기록했다.

이 커밋은 파일 단위로 담겼다. 위 파일들에는 이전 세션의 미커밋 G0~G8 작업이 함께
들어 있으며, hunk 를 쪼개면 대시보드/체커/TODO 정합성이 깨져 SSOT 체커가 실패한다.
This commit is contained in:
Yun Chan 2026-08-07 22:17:20 +09:00
parent 76d0b9ae9b
commit 93dd8f82d7
22 changed files with 10057 additions and 473 deletions

View file

@ -11,6 +11,11 @@
> **B0. 원천문서 갭** — 한신대 산학협력 원천문서 갭(critical/high/medium)은 SSOT 대시보드 "원천문서 갭 분석"
> 섹션과 상세 근거 `docs/ops/source-docs-gap-analysis-2026-06-26.md`에서 추적한다. C1~C3/H·M 구조는 코드로
> 선제 구축했고, 임상 문안·평가기준·골든셋 콘텐츠는 임상팀(구훈정·어유경) 외부 정의로 받는다.
>
> **Outcome & Alliance OS** — 2026-08-06 정식 전략 실행 트랙으로 승격했다. G0 Measurement Truth, G1
> 현재 소스·실행 증거 재감사에서는 G0~G6이 internal DONE이다. G1 승격 prompt 1.2+read-skew/JSON 복구는 24/24 ready·방향 9/9·오류 0을 재확인했고, G0 census 29/29·위반 0, G4/G5 실제 API/DB/브라우저 폐루프, G6 safety metadata-only 최우선 runtime을 disposable clone에서 확인했다. G8은 2026-08-07 격리 NAS에서 실제 receipt-bound image rollback 2회(`nas-g8-723eeef2…`/`nas-g8-2738846c…`)를 executed lifecycle receipt로 실행해 DONE이다. 과거 clean release/NAS의 route·asset·auth·browser 증거는 배포 존재 이력으로 보존하지만 현재 소스의 완료 증거로 재사용하지 않는다. G7 Multimodal Alliance는 내부 구현 DONE과 외부 proof GATE를 분리하며 Deepgram key·quota live, expected provider/model authenticated public WSS, 명시 동의 물리 마이크·50분 soak, 독립 라벨 voice-gain benchmark와 운영 topology resource high-water를 추적한다. 운영 TTS는 OpenAI API `gpt-4o-mini-tts`로 고정하고 공식 앱 통합·출력 소유 근거와 AI 생성 음성 고지를 반영했다. G0~G8과
> AOS-001~012는 `docs/TODO.md` I절에서 전건 추적하고, 상태는 SSOT 대시보드의 9개 계획 카드가 소유한다.
> 이 얇은 백로그에는 그중 외부·환경 증거가 필요한 항목만 기존 B2/B4/Phase 3 게이트와 합쳐 유지한다.
---
@ -20,7 +25,7 @@
스레드 생성, 처리 결과 audit trail 확장은 아직 설계/승인 필요. 담당 그룹 자동 배정·우선순위 escalation·raw/rollup
보존기간 같은 운영 정책은 B3에서 이미 결정됨(수동 승인·미도입 고정). 자동 수정은 운영자 승인 전까지 실행하지 않는다.
> B1 완료 항목(셸 구분선, 세션 종료 UX/다크테마, 아바타 SVG 리그 복귀와 래스터 비활성화, SEO/공유 카드, 레이아웃 정렬, 권한 위임,
> B1 완료 항목(학생 리뷰 1~5 척도 320px 반응형 재배치, 셸 구분선, 세션 종료 UX/다크테마, 아바타 SVG 리그 복귀와 래스터 비활성화, SEO/공유 카드, 레이아웃 정렬, 권한 위임,
> 메일링, 아카이브 API, TTS voice map, 빈상태 레이아웃, 평가 실패 복구 UX, SSE 저장, live-coach 표면화,
> 음성 메타, 위기 게이트 UI, 운영 티켓/헬스 샘플러 등)의 상세·검증 로그는 아카이브 full-history 참조.
@ -28,20 +33,46 @@
## B2. 환경 제약 — 이 워크스테이션에서 증거 생산 불가
- [ ] **공개 DB 계정·회기 복구 안정화** — 2026-08-07 18:26 KST owner 승인으로 recovered named volume을
`vignette-dev-db:55432`에 전환했다. public owner 집계 users 84, sessions 30, turns 705, Google 계정 16,
Google 소유 회기 30, orphan 0이며 health·engine·OAuth·watchdog가 정상이다. 기존 인증 세션은 복사하지
않아 사용자는 Google 재로그인이 필요하다. current dump·old-original dump·전환 직전 rollback container를
모두 보존하며, 실제 사용자의 소유 회기 확인과 자동 백업 운영화 전에는 제거하지 않는다.
상세: `docs/ops/public-db-recovery-rehearsal-2026-08-07.md`.
- [ ] **DB 백업 운영화** — 검증형 one-shot custom dump와 실제 일회용 restore drill은 통과했다. 예약 실행,
실패 알림, NAS/off-host 암호화 복제, restore drill 주기와 보존 정책을 운영 게이트로 남긴다.
- [ ] **`vnet.18ka.net` 공개 live 전환** — 코드/스크립트는 `vnet.18ka.net`·`api-vnet.18ka.net` 허용까지 갱신됨.
남은 블로커: (1) `vnet.18ka.net`/`api-vnet.18ka.net` DNS A record 부재, (2) cloudflared 현재 인증 컨텍스트가
`18ka.net` zone이 아니라 `chanpaca.net` zone에 route dns 생성, (3) Google Console redirect URI
`https://api-vnet.18ka.net/auth/callback` 등록 필요. 현재 prod API 8001은 `db:true`/`engine:true`로 건강.
상세: `docs/ops/tailscale-vnet-runtime-2026-06-27.md`.
- [ ] **실배포 `infra/.env` owner-secret fill-in + real preflight** — 필수 소유자 비밀값: `APP_DB_PASSWORD`,
`OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET`, `OPENAI_API_KEY`, `SESSION_SECRET`, production-safe
engine/voice flags. 로컬 stray 값(`ENGINE_MODE=claude_cli`, prod sample TTS flag 등)은 운영값으로 복사 금지.
실제 secret 주입 후 `scripts/check-deploy-preflight.py` DB 포함 모드 + 배포지 health를 별도 증거로 닫는다.
- [ ] **Compose 패키징용 `infra/.env` owner-secret fill-in** — 현재 production direct-runtime은 서로 다른 G3~G8
내부 토큰 6개, 필수 운영키, DB `current_user=vignette` non-owner app-role preflight와 공개 health를 통과했다.
향후 Compose 패키징 전환 시 `APP_DB_PASSWORD` 등 패키징 비밀과 prod-safe engine/voice flags를 별도 주입하고
compose profile preflight를 다시 닫는다.
- [ ] **공개 Google OAuth 실제 `/turn` proof** — 로그인 가능한 계정으로 `storageState` 캡처 후 `E2E_PUBLIC_AUTH=1`
+ `chromium-public-auth` 1회 통과 필요. (소유자 지시로 보류 중.) 실행 명령은 대시보드 "다음 실행 명령" 참조.
- [ ] **음성 캐스케이드 live** — 로컬 API 실제 OpenAI STT/TTS와 P1 Higgs 실제 문장 WAV 합성 경로는 live smoke 통과.
Higgs는 무참조 synthetic seed·dev-only로 유지한다. 남은 범위: 실제 Deepgram interim/final WSS, 물리 마이크,
공개 WSS, 50분 양방향 장시간 실측과 상업 라이선스가 명확한 운영 TTS provider 통합.
- [ ] **음성 캐스케이드 live** — 공개 G7 API route와 TLS 1.3 WSS handshake, 비인증 1008 차단, 로컬
synthetic short soak는 통과했다. Deepgram streaming adapter/interim/final/word timestamp, HMAC word pseudonym,
streaming 중 1초 동의 재검사, 공통 consent transaction lock, bounded queue/10MiB cap, 텍스트 보존·음성 재연결
UX, `ready` provider/model 계약은 code/internal 완료다. actual-Postgres 동시 철회는 writer 대기→커밋 뒤 차단과
timeline row 0, synthetic gold benchmark는 text-only 1-MAE `0.9533`→multimodal `0.9673`을 확인했다.
public runner의 preflight는 auth cookie·소유 회기로 WSS ready/ping과 기대 STT/TTS provider/model 네 값을
마이크 열거·캡처 없이 검사한다. full soak는 `--confirm-physical-capture` 없이는 장치를 열기 전에 exit 3이다.
Session 첫 음성 사용은 30일·원음 미보존 서버 동의 원장을 먼저 기록하며, 성공 전 `getUserMedia`와 voice
WebSocket은 0회다. public runner v4의 turn별 interim/speech-final/latency, 관리자 single-worker runtime
high-water, exact image/container/cgroup/proc/Docker/TCP topology sampler, 30명/50회기/150축 독립 blind
human-held-out voice-gain evaluator와 이를 같은 public host·동시 50분 시간창으로 묶는 canonical checker까지
code/internal 준비를 마쳤다.
운영 TTS는 OpenAI API `gpt-4o-mini-tts`와 공식 Services Agreement 근거, 회기 전·중 AI 생성 음성 고지로
닫았다. 남은 범위: 실제 Deepgram 운영 key·quota의 live interim/final, 기대값과 일치하는 authenticated public
ready, 실행 직전 명시 동의 물리 마이크와 50분 양방향 soak, 독립 라벨 held-out/pilot voice-gain benchmark,
같은 실행의 실제 worker·Uvicorn·OS·Cloudflare edge RSS/CPU/queue high-water. 네 artifact가
`scripts/check-g7-external-proof.py` exit 0을 만들기 전에는 닫지 않는다.
격리 NAS 프리뷰 `http://100.116.83.60:8088`은 전용 Compose 프로젝트·포트·네트워크·볼륨에 배포했고, 실제 브라우저 회기와
review API 저장 축어록 2턴을 확인했다. 기존 프로젝트 중단·재생성 명령은 실행하지 않았다. 매일 04:30 KST `Vignette 회기 E2E 정기 검증`
(automation id `vignette-e2e`)은 ACTIVE이며 material milestone+release-only patch 결정성·manifest·clean-index·NAS preflight+배포 SHA 변경 때만 프리뷰를 갱신한다. current exact SHA `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`은 API/Web 새 이미지, 전체 browser E2E 108/108, OpenAPI 126, auth 401, G0~G8, postdeploy browser SSE→DB review와 health 3/3을 통과했다. 실제 receipt-bound rollback은 같은 프리뷰에서 별도 helper로 두 번 실행해 종료했다([런북](./nas-preview-g8-rollback-proof-runbook.md), [기계 판독 증거](./evidence/nas-preview-g8-actual-rollback-2026-08-07.json)). 첫 예약 실행 이력은 대기 상태다. 증거: [배포 증거](./nas-preview-deployment-evidence-2026-08-07.md),
[브라우저 증거](./evidence/nas-preview-live-turn-2026-08-07.png). 물리 마이크 캡처 미승인과 Deepgram 운영 키 부재는 해소되지 않았으므로 G7은 BUILD로 유지한다.
- [ ] **claude_cli ↔ Anthropic API live 동일성** — provider 라우팅·Anthropic `/v1/models` 탐색·지원 추론 강도·관리자 fail-closed 저장 경로는 구현 완료. 남은 범위는 연구팀/기관 `ANTHROPIC_API_KEY`를 게이트웨이 호스트에 주입한 live 응답·계량·오류 표면화 비교다. Claude CLI·Codex CLI(Terra/Medium)·Agy CLI(Gemini 3.6 Flash/High)는 로컬 live probe를 통과했다.
- [ ] **재부팅 후 watchdog smoke**`watch-public-runtime.ps1` + Scheduled Task가 재부팅 후 엔진/API/터널을 복구하고
public `/turn`이 통과하는지 실측. 재부팅 불가로 미실행(parser/check-only 경로는 확인). DNS 개통 후

View file

@ -0,0 +1,238 @@
{
"schema_version": "vignette.nas-preview-g8-actual-rollback.v1",
"recorded_at_kst": "2026-08-07T21:15:00+09:00",
"run_id": "20260807g8a",
"scope": "isolated NAS preview only; public runtime untouched",
"target": {
"compose_project": "vignette-preview-20260807",
"remote_root": "/volume1/docker/vignette-preview-20260807",
"preview_url": "http://100.116.83.60:8088",
"health_url": "http://127.0.0.1:8088/api/health",
"db_ip": "172.22.0.2",
"db_role": "vignette_app",
"db_name": "vignette_preview"
},
"gate1_preconditions": {
"current_api_image": "sha256:52e0e816c47b1152368113b186d5cdb971123bc0961122541a7becf430b88b2d",
"current_web_image": "sha256:6fdbb6460a7617540fa3ceff7eba96a2b9d83678318f0e27f99ac4bd7d93f215",
"previous_api_image": "sha256:79ec731f6bcdb79850507f5b711930a710e2d8e82276e3ea61dc8e4a60e04450",
"previous_web_image": "sha256:c530213f4f8b23f228b2999df0c3653173cfa6939b6abb476f17df7488462f28",
"manifest_sha256": "eeb8edc060e920195e083399814b9594c4d88e3d534daa39facd6c895f9e8ef2",
"rollback_old_plan_sha256": "1ba0af237b83ace5572b49d699b2fd81bc6b46752cb86317c4d013f8a8f6aa6f",
"restore_current_plan_sha256": "9ee8ecca9f712bce96592e9b88f30b24fc2c71c220022efcba95a85316cd3309",
"executor_script_sha256": "07ea4ff348b5b96959c9411123c0c8519f22cc972a7c78ecb5f5e40ae88093d7",
"probe_script_sha256": "74d4b80b031106c620f36dffc7d7a99667f1eda23333dbdb9e7e75e3ec5bab68",
"preproof_dump_sha256": "92ed4736c1b21081e7ce1dc16aa0d026456d7683fd550077136d3de736c1570f",
"preproof_dump_bytes": 960970,
"listeners_18149_8018": 0,
"helper_containers": 0,
"health": "ok/db=true/engine=true"
},
"gate6_helper_isolation": {
"contract": "membership, not compose-label absence",
"rationale": "the audited current API image carries com.docker.compose.* image labels, so a zero-key rule can never hold while the audited runtime image is reused",
"implementation": "scripts/launch-nas-preview-g8-helpers.py",
"tests": "scripts/test_launch_nas_preview_g8_helpers.py 37/37 PASS",
"image_baseline_compose_labels": {
"com.docker.compose.project": "vignette-preview-20260807",
"com.docker.compose.service": "api",
"com.docker.compose.version": "2.20.1"
},
"inherited_compose_labels": {
"com.docker.compose.version": "2.20.1"
},
"isolated_project": "vignette-g8-helper-20260807g8a",
"target_project_members_during_proof": [
"vignette-preview-20260807-api-1",
"vignette-preview-20260807-db-1",
"vignette-preview-20260807-proxy-1",
"vignette-preview-20260807-web-1"
],
"helpers": [
{
"role": "executor",
"container_name": "g8-rollback-executor-20260807g8a",
"container_id": "5f5f3295f57fe9e8ccb61b719d31d0cdfca54730510ab811d95fcc9163b2785c",
"isolated_service": "g8-rollback-executor",
"hardening": "network=host, readonly rootfs, cap-drop ALL, no-new-privileges, user 1028:100, group-add 101, pids-limit 256, restart=no",
"bind": "127.0.0.1:18149",
"unauth_healthz_status": 403,
"token_healthz_status": 200,
"unauth_rollback_post_status": 403
},
{
"role": "control-plane",
"container_name": "g8-rollback-control-plane-20260807g8a",
"container_id": "ba545c633dd2074b0a31889019b6d10fbe60776de8881b161bfb084f835025df",
"isolated_service": "g8-rollback-control-plane",
"bind": "127.0.0.1:8018",
"health_status": 200,
"unauth_auth_me_status": 401,
"producer_enabled": false,
"distinct_lifecycle_from_preview_api": true
},
{
"role": "probe",
"plan": "rollback-old",
"container_name": "g8-rollback-probe-20260807g8a",
"container_id": "15c385c86db05461a61001ecdf38d693f31e4cdd1d8f0e5e2c7f62933f2fa56e",
"isolated_service": "g8-rollback-probe",
"docker_socket_mounted": false,
"exit_code": 0
},
{
"role": "probe",
"plan": "restore-current",
"container_name": "g8-rollback-probe-20260807g8a",
"container_id": "af079155c9b217f6c271bda03ebfe49ac99a5d8febc769d9cb088ada42ad289f",
"isolated_service": "g8-rollback-probe",
"docker_socket_mounted": false,
"exit_code": 0
}
]
},
"planned_step_deviation": {
"planned": "Windows SSH tunnel 127.0.0.1:18018 to NAS 127.0.0.1:8018, probe executed from Windows",
"observed": "NAS sshd refused direct-tcpip with 'administratively prohibited: open failed'",
"action": "probe executed from an isolated NAS-side helper container against NAS loopback 127.0.0.1:8018",
"contract_preserved": "control-plane origin http://127.0.0.1:8018 differs from preview origin http://127.0.0.1:8088; the probe still enforces main_preview_self_rollback_forbidden",
"nas_sshd_config_changed": false
},
"executions": [
{
"plan": "rollback-old",
"probe_id": "nas-g8-probe-51c203e09baf9957a571e915",
"plan_sha256": "3cdbe07193ae17f96ead11f930db7bb085648de2b75aa19b595733650389bf27",
"executor_receipt_id": "nas-g8-723eeef22eab05e63e3fafb0",
"artifact_id": "vignette-preview-release-a8c27b0aadcb9dac",
"artifact_sha256": "a8c27b0aadcb9dacea86c4be5e62e19277b9cc2678ebca51aa02998d2c2d1a0d",
"lifecycle_status": "executed",
"release_gate_idempotent_replay": true,
"approval_idempotent_replay": true,
"binding_verified": true,
"control_plane_separation_verified": true,
"started_at_utc": "2026-08-07T12:04:31+00:00",
"executed_at_utc": "2026-08-07T12:05:11+00:00",
"images_before": {
"api": "sha256:52e0e816c47b1152368113b186d5cdb971123bc0961122541a7becf430b88b2d",
"web": "sha256:6fdbb6460a7617540fa3ceff7eba96a2b9d83678318f0e27f99ac4bd7d93f215"
},
"images_after": {
"api": "sha256:79ec731f6bcdb79850507f5b711930a710e2d8e82276e3ea61dc8e4a60e04450",
"web": "sha256:c530213f4f8b23f228b2999df0c3653173cfa6939b6abb476f17df7488462f28"
},
"health_checks_after": 3,
"health_ok_after": 3,
"result_file_sha256": "54e156fee3fb2b37fe92e7fba845cc02db1887d29e5c3cfd2ca0045942ddf95e"
},
{
"plan": "restore-current",
"probe_id": "nas-g8-probe-3fc9052ae1f4577a316db010",
"plan_sha256": "050d2c72d176bf5e6426de837e0c6a98a85c08789411159d7681b4c4e5c07ca5",
"executor_receipt_id": "nas-g8-2738846cf2cf4fbe8ce0fc26",
"artifact_id": "vignette-preview-release-6030a677af7e87cb",
"artifact_sha256": "6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611",
"lifecycle_status": "executed",
"release_gate_idempotent_replay": true,
"approval_idempotent_replay": true,
"binding_verified": true,
"control_plane_separation_verified": true,
"started_at_utc": "2026-08-07T12:06:06+00:00",
"executed_at_utc": "2026-08-07T12:06:42+00:00",
"images_before": {
"api": "sha256:79ec731f6bcdb79850507f5b711930a710e2d8e82276e3ea61dc8e4a60e04450",
"web": "sha256:c530213f4f8b23f228b2999df0c3653173cfa6939b6abb476f17df7488462f28"
},
"images_after": {
"api": "sha256:52e0e816c47b1152368113b186d5cdb971123bc0961122541a7becf430b88b2d",
"web": "sha256:6fdbb6460a7617540fa3ceff7eba96a2b9d83678318f0e27f99ac4bd7d93f215"
},
"health_checks_after": 3,
"health_ok_after": 3,
"result_file_sha256": "c2f535aa3bfb459ddd1395e21b8ebbea0c3ac6252bbb5cd4217c845cab443825"
}
],
"journal": {
"path": "/volume1/docker/vignette-preview-20260807/.rollback-executor/journal.jsonl",
"schema_version": "vignette.nas-preview-rollback-journal.v1",
"records": 6,
"events": ["started", "prepared", "executed", "started", "prepared", "executed"],
"hash_chain_verified": true,
"sha256": "a5594feb848d33e8ebfe52e55dc2802ba03d3161690b71724ec50fb779470690",
"integrity_key_id": "6d2f7a5169406e2a",
"record_hashes": [
"3a694e813d98811ef6132e159e65f6d980ff16f6495696b165079672734187f4",
"6748e27bbae655386ef4f73c8bea18cae30d6b9ccc0385a73e9b8df59acb4fab",
"7142488d0935b755af33f21675e938f5dda93bb1d268a99feb46bbf4f53ccea4",
"dba76366e9e544f8edfc3a5c532484d0de2b9d4d0b8f98b13e335c980401cd12",
"cd585370680b0882c8ac775e6cc870c70b005b5c146c17f16a283902fff39878",
"34ca44ba32dd9b03b630fc43301addb14bc1a711bbb5551a7081f721d417ac5c"
]
},
"db_lifecycle_aggregate": {
"measured_as": "table owner role, RLS bypassed for counting only",
"before": {
"app_user": 11,
"sessions": 10,
"turns": 14,
"auth_session": 12,
"g8_lifecycle_tables_nonzero": 0
},
"after": {
"app_user": 12,
"sessions": 10,
"turns": 14,
"auth_session": 14,
"app.ci_release_gate": 2,
"app.ci_gate_artifact": 8,
"app.ci_ingestion_submission": 4,
"audit.ci_human_approval_event": 2,
"audit.ci_lifecycle_event": 2,
"other_ci_tables": 0
},
"lifecycle_breakdown": {"rollback/executed": 2},
"approval_breakdown": {"authorize_rollback/release_gate": 2},
"release_gate_state": {"pending_human_approval/qualified": 2},
"silent_auto_promotion": 0,
"executor_receipt_ids_bound_in_db": [
"nas-g8-2738846cf2cf4fbe8ce0fc26",
"nas-g8-723eeef22eab05e63e3fafb0"
]
},
"post_state": {
"helper_containers": 0,
"listeners_18149_8018_18018": 0,
"target_project_members": 4,
"api_image": "sha256:52e0e816c47b1152368113b186d5cdb971123bc0961122541a7becf430b88b2d",
"web_image": "sha256:6fdbb6460a7617540fa3ceff7eba96a2b9d83678318f0e27f99ac4bd7d93f215",
"health_checks": 3,
"health_ok": 3,
"openapi_paths": 126,
"auth_me_status": 401,
"web_root_status": 200,
"override_temp_files": 0,
"helper_env_files_remaining": 0,
"compose_down_executed": false,
"volume_rm_executed": false,
"prune_executed": false,
"public_runtime_mutated": false,
"public_runtime_note": "the public stack (vignette.chanpaca.net, api origin 127.0.0.1:8001, container vignette-dev-db, volume vignette_recovered_prod_20260807) was only read, never restarted, reconfigured or written to"
},
"post_rollback_runtime_verification": {
"test": "session-persistence: persists the browser SSE stream into DB-backed review",
"project": "chromium-single-run",
"target": "http://100.116.83.60:8088",
"result": "passed",
"duration_seconds": 35.0,
"physical_microphone_used": false,
"route_mocks": 0,
"note": "run after restore-current, so the restored runtime is proven to serve a real browser session end to end"
},
"safety_scan": {
"secrets": 0,
"emails": 0,
"cookies": 0,
"non_deterministic_uuids": 0,
"note": "every UUID in the preserved probe results is a uuid5 value derived from the repo-fixed probe namespace and the plan suffix; no account, session or personal identifier is recorded"
}
}

View file

@ -0,0 +1,166 @@
# NAS 격리 프리뷰 배포 증거 — 2026-08-07
## 결과
- Tailnet 프리뷰: `http://100.116.83.60:8088`
- Compose 프로젝트: `vignette-preview-20260807`
- 원격 루트: `/volume1/docker/vignette-preview-20260807`
- 실제 health: `status=ok`, `db=true`, `engine=true`, `engine_mode=claude_cli`
- 배포 SHA: `6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`
- 이전 배포 SHA: `a8c27b0aadcb9dacea86c4be5e62e19277b9cc2678ebca51aa02998d2c2d1a0d`
- API 이미지: `sha256:52e0e816c47b1152368113b186d5cdb971123bc0961122541a7becf430b88b2d`
- Web 이미지: `sha256:6fdbb6460a7617540fa3ceff7eba96a2b9d83678318f0e27f99ac4bd7d93f215`
- OpenAPI: 126 paths, G0~G8 Outcome/Alliance·Practice·Continuous Improvement 경로 포함
- 비인증 `/api/auth/me`: 401
- DB 계약: `vignette_app` 역할과 최종 `app.ci_regression_dag_node` 스키마 접근 gate 통과
기존 NAS 서비스와 포트·네트워크·볼륨을 공유하지 않는 별도 프리뷰다. 배포 작업은 이 전용 Compose
프로젝트만 대상으로 했으며 기존 프로젝트를 대상으로 한 중단·재생성 명령은 실행하지 않았다. 이 기록은
격리 구성과 실행 범위를 증명하지만 기존 서비스의 배포 전후 uptime 비교 증거로 확대 해석하지 않는다.
## 실제 브라우저 증거
- 학습자 경로: dev-login → 온보딩 → DB 페르소나 선택 → 새 회기 → Alliance pre 기준 잠금 → 세션 진입
- Playwright: `1 passed (13.1s)`
- 실제 엔진 1턴: 텍스트 발화 → SSE 내담자 응답 → 프로토콜 종료 → 후속 발화 가능 → DB-backed review
- Playwright: `1 passed (24.4s)`, 테스트 본문 `23.4s`
- 증거 세션: `c96922ab-0334-4864-ae48-df41a24aae14`
- review: 학습자 1턴 + 내담자 1턴, 총 2턴
- 2026-08-07 08:29 KST 실제 NAS review API 재조회: HTTP 200, 같은 session id, 저장 축어록 2턴
- 물리 마이크는 사용하지 않았다. 자동 TTS 재생만 제품 동작으로 확인했다.
- 최종 release-agent 브라우저 증거: 실제 NAS에서
`session-persistence: browser SSE stream -> DB-backed review` 통과,
stdout SHA-256 `b45111ce9f47d30e4c848362813940f3efd5464660b5046159b88befd9a59840`.
시각 증거:
- [학습 홈](./evidence/nas-preview-learner-home-2026-08-07.png)
- [Alliance pre 완료 후 세션](./evidence/nas-preview-learner-session-2026-08-07.png)
- [실제 엔진 1턴과 후속 입력 가능 상태](./evidence/nas-preview-live-turn-2026-08-07.png)
- [기계 판독 배포 증거](./evidence/nas-preview-deployment-2026-08-07.json)
## 실배포에서 닫은 재발 방지 gate
1. Synology BuildKit이 거부하던 루트 `.dockerignore` 비ASCII 활성 패턴을 제거하고 ASCII 회귀 테스트를 추가했다.
2. Compose 2.20이 JSON 기본값을 `{}}`로 조립하던 `FRONTEND_ORIGIN_MAP`을 필수 환경값으로 바꿨다.
3. DB health가 초기 SQL 실행 중 너무 일찍 통과하지 않도록 app role과 최종 G8 스키마 접근을 요구한다.
4. API health는 HTTP 200이 아니라 `status=ok`, `db=true`, `engine=true`를 모두 요구한다.
5. 새 DB 전체 초기화에 5분 `start_period`를 둔다.
6. 프리뷰 dev-login은 `hs.ac.kr`, `twentyoz.kr` 명시 allowlist만 허용한다.
7. 브라우저 SSE는 전송 계층 EOF가 아니라 애플리케이션 `done` 이벤트에서 reader를 종료한다.
8. Windows clean candidate의 생성 API 계약과 Linux `*.sh`를 LF로 정규화해 `bash\r` DB init 실패를 차단한다.
9. 승격·롤백은 active snapshot의 canonical env 경로를 공유하고, 업로드 전에 존재를 검증한다.
10. API/Web 교체 직후 502/503/504·연결 대기·초기 semantic health만 180초 안에서 재확인하고,
OpenAPI/auth 계약 위반은 즉시 실패한다. 최종 승격은 readiness 3회째 통과했다.
## 최신 G7 hardening release-agent 승격
- material milestone과 release-only patch를 두 번 동일하게 생성하고 clean HEAD·canonical index 적용을 확인했다.
- 후보 API 전체, Web API 계약·typecheck·build, 격리 Compose와 회기 E2E를 통과한 뒤에만 NAS를 갱신했다.
- 첫 실제 교체는 즉시 502를 정상 실패로 판단해 이전 이미지로 자동 롤백했고, 이미지 ID·semantic health·
OpenAPI 122·auth 401을 다시 확인했다. 이후 readiness window를 추가한 최종 실행은 3회째 health를 통과했다.
- 첫 실행은 기존 DB-backed mic fixture가 새 동의 모달을 거치지 않아 session E2E 23/24에서 fail-closed로
중단했고 NAS는 변경하지 않았다. 실제 사용자 순서대로 마이크→서버 동의 원장→synthetic capture로 고친 뒤
focused 1/1과 clean candidate session E2E 24/24를 통과했다.
- 최종 런타임은 OpenAPI 124, auth 401, G0~G8 계약과 실제 브라우저 SSE→DB review를 통과한 뒤
active-state를 위 exact SHA와 새 API/Web 이미지로 원자 커밋했다.
- 전체 후보 API 테스트, 최종 session E2E `24/24`·`505.578s`, postdeploy browser review
`25.328s`가 통과했다. 물리 마이크는 사용하지 않았다.
## 현재 G0~G8 통합 승격
- current source를 분류한 release-only patch는 세 번 동일 SHA로 생성됐고 manifest checker와 canonical
clean-index 적용을 통과했다. 모바일 learner 4열 navigation을 소유한 `components/shell/shell.css`
runtime-critical related whole-file로 고정해 패키징 누락을 회귀 테스트로 막았다.
- 후보 전체 browser E2E `108/108``634.578s`에 통과했다. 실제 NAS 교체 뒤 일시적 502는 bounded
readiness 두 번째 시도에서 회복됐고, 이후 독립 health `3/3`, OpenAPI 126 paths, auth 401,
G0~G8 필수 route와 Web/CSS/JS 200을 확인했다.
- postdeploy 실제 브라우저 SSE→DB-backed review는 `32.437s`에 통과했다. preview DB named volume은
그대로이며 browser smoke 뒤 users 11·sessions 10·turns 14·auth sessions 12다. 배포 전 custom dump
SHA는 `47c8156e…958d`이고 삭제·volume 교체·rollback은 없었다.
- 기계 판독 요약: [current 통합 배포 증거](./evidence/nas-preview-current-deploy-2026-08-07.json).
- 이 승격은 current 소스의 실제 런타임 증거지만 G8 receipt-bound image rollback 자체는 아니다. 그 증명은
아래 절에서 별도 loopback executor와 독립 control-plane API로 닫았다.
## G8 실제 receipt-bound image rollback (2026-08-07 21:04~21:12 KST)
실행 절차와 Gate6 계약 정정은 [G8 rollback 증명 런북](./nas-preview-g8-rollback-proof-runbook.md),
기계 판독 증거는 [`evidence/nas-preview-g8-actual-rollback-2026-08-07.json`](./evidence/nas-preview-g8-actual-rollback-2026-08-07.json)이다.
### helper 격리 (Gate6 정정)
감사 대상 current API 이미지 `sha256:52e0e816…8b2d` 자체가 `com.docker.compose.project=vignette-preview-20260807`,
`service=api`, `version=2.20.1` image label을 갖는다. 따라서 helper의 compose label 개수를 0으로 요구하는
이전 계약은 감사되지 않은 다른 이미지를 쓰지 않는 한 성립하지 않는다. 계약을 **key 부재가 아니라 소속**으로
정정해 `scripts/launch-nas-preview-g8-helpers.py`에 구현하고 `scripts/test_launch_nas_preview_g8_helpers.py`
`37/37`로 고정했다.
- image 상속 label을 baseline으로 읽고 container의 모든 `com.docker.compose.*`가 baseline과 같거나 선언된
격리 override인지 검증한다. 실제 결과: 상속 `version=2.20.1`, override `project=vignette-g8-helper-20260807g8a`,
`service=g8-rollback-executor` / `g8-rollback-control-plane` / `g8-rollback-probe`.
- 최종 project는 target이 아니고, 최종 service는 `api`/`web`/`db`/`proxy`가 아니며,
`docker ps --filter label=com.docker.compose.project=vignette-preview-20260807` 멤버는 프리뷰 4개뿐이었다.
- `docker run` argv에 target project/service label을 주입하면 fake-runner 테스트가 먼저 실패한다.
| helper | container | ID |
|---|---|---|
| executor `127.0.0.1:18149` | `g8-rollback-executor-20260807g8a` | `5f5f3295f57f…785c` |
| control-plane `127.0.0.1:8018` | `g8-rollback-control-plane-20260807g8a` | `ba545c633dd2…25df` |
| probe (rollback-old) | `g8-rollback-probe-20260807g8a` | `15c385c86db0…a56e` |
| probe (restore-current) | `g8-rollback-probe-20260807g8a` | `af079155c9b2…289f` |
executor는 host network·read-only rootfs·`cap-drop ALL`·`no-new-privileges`·uid1028/gid100·supplemental
group101·`pids-limit 256`·`restart no`로 실행했고, unauth `/healthz` 403 · token `/healthz` 200 ·
unauth `POST /rollback` 403을 확인했다. control-plane은 producer disabled, preview DB `172.22.0.2`
`vignette_app` role, executor endpoint `http://127.0.0.1:18149/rollback`, unauth `/auth/me` 401이다.
### 계획 대비 이탈 — SSH 터널
계획했던 Windows SSH 터널 `127.0.0.1:18018 → NAS 127.0.0.1:8018`은 NAS sshd가 direct-tcpip를
`administratively prohibited: open failed`로 거부해 사용할 수 없었다. **sshd 설정은 바꾸지 않았고**,
같은 격리 계약의 NAS-side probe 컨테이너에서 loopback으로 실행했다. control-plane origin
`http://127.0.0.1:8018`과 preview origin `http://127.0.0.1:8088`이 다르므로 probe의
`main_preview_self_rollback_forbidden` 계약은 그대로 강제된다.
### 두 번의 executed receipt
| plan | receipt | artifact | 활성화된 API/Web 이미지 |
|---|---|---|---|
| `rollback-old` | `nas-g8-723eeef22eab05e63e3fafb0` | `vignette-preview-release-a8c27b0aadcb9dac` | `79ec…4450` / `c530…2f28` |
| `restore-current` | `nas-g8-2738846cf2cf4fbe8ce0fc26` | `vignette-preview-release-6030a677af7e87cb` | `52e0…8b2d` / `6fdb…f215` |
두 실행 모두 release gate 2회 멱등, approval 2회 멱등, lifecycle `executed`, artifact/approval/receipt
binding 검증, control-plane 분리 검증, 실행 뒤 health `3/3`을 통과했다. 최종 상태는 OpenAPI 126 paths,
`/api/auth/me` 401, Web root 200이다.
### 원장·무결성
- HMAC journal 6 records — `started`/`prepared`/`executed` × 2. `previous_hash` 체인 전수 검증 PASS.
SHA256 `a5594feb848d33e8ebfe52e55dc2802ba03d3161690b71724ec50fb779470690`.
- DB(owner role 집계): `app.ci_release_gate` 2, `app.ci_gate_artifact` 8, `app.ci_ingestion_submission` 4,
`audit.ci_human_approval_event` 2(전부 `authorize_rollback`/`release_gate`),
`audit.ci_lifecycle_event` 2(전부 `rollback`/`executed`), 나머지 `ci_*` 0.
두 executor receipt id가 DB lifecycle에 그대로 결속돼 있다. release gate는 둘 다
`pending_human_approval`·qualified로 남아 silent auto-promotion은 0이다.
- core 집계 변화: app_user 11→12, auth_session 12→14(probe dev-login 2회), sessions 10·turns 14 불변.
- probe 결과 파일 SHA256 — `rollback-old` `54e156fe…f95e`, `restore-current` `c2f535aa…3825`.
- manifest `eeb8edc0…f8ef2`, plan `1ba0af23…aa6f` / `9ee8ecca…3309`, executor script `07ea4ff3…93d7`,
probe script `74d4b80b…ab68`는 실행 전후 동일하다.
### 정리와 보존
helper 3개 제거, listener 18149/8018/18018 = 0, override 임시 파일 0, helper env 3개는 `shred -u`로 파기했다.
journal·manifest·plan·evidence는 보존한다. `docker compose down`, `docker volume rm`, `docker system prune`
실행하지 않았고 공개 런타임(`vignette.chanpaca.net` / `vignette-dev-db`)은 건드리지 않았다.
증거 안전 스캔은 이메일 0·비밀 힌트 0·plan 밖 UUID 0이다.
## 운영 및 남은 경계
- 자동화 `vignette-e2e`**Vignette 회기 E2E 정기 검증**, 매일 04:30 KST, 실패 시만 알림, ACTIVE 구성 완료.
material milestone이고 release-only patch 2회 결정성·manifest checker·canonical clean-index·NAS preflight가
통과하며 현재 배포 SHA와 다를 때만 같은 격리 프리뷰를 갱신한다.
이 증거 시점에는 첫 스케줄 실행 이력을 첨부하지 않았으며 실행 결과는 첫 실행 뒤 별도 기록한다.
- G8은 내부 구현·격리 NAS runtime promotion·**receipt-bound 실제 image rollback**까지 모두 **DONE**이다.
exact 배포 SHA와 두 receipt는 위에 고정했다.
- G7은 계속 **BUILD**다. 이 프리뷰는 물리 마이크 실증이나 Deepgram 운영 키 증거를 대신하지 않는다.
- 접근은 Tailnet 내부 HTTP 프리뷰다. 외부 공개 배포 완료 증거로 사용하지 않는다.

View file

@ -0,0 +1,136 @@
# NAS 프리뷰 G8 실제 rollback 증명 런북 (2026-08-07)
> 대상: 격리 NAS 프리뷰 `vignette-preview-20260807` (`http://100.116.83.60:8088`).
> 이 런북은 **공개 서비스(`vignette.chanpaca.net`)를 건드리지 않는다.**
> 상태 SSOT는 [`docs/dev_dashboard.html`](../dev_dashboard.html)이다.
## 0. 무엇을 증명하는가
G8 "지속 개선"의 마지막 열린 게이트는 *합성 control plane*이 아니라 **실제 NAS 런타임에서
receipt가 묶인 rollback 실행**이다. 증명 단위는 두 번이다.
1. `rollback-old` — 현재 릴리스(`6030a677…`)에서 이전 릴리스(`a8c27b0a…`) 이미지로 되돌린다.
2. `restore-current` — 다시 현재 릴리스 이미지로 복원한다.
두 번 모두 durable `executed` lifecycle receipt, artifact/approval binding, health 재확인까지
통과해야 한다. health만 보고 성공이라 부르지 않는다.
## 1. Gate6 helper 격리 계약 (2026-08-07 정정)
### 정정 이전의 잘못된 계약
이전 계획은 helper container의 `com.docker.compose.*` label 개수가 **0개**여야 한다고 요구했다.
이 계약은 성립할 수 없다. 감사 대상 런타임 이미지 자체가 image label을 갖고 있기 때문이다.
```text
sha256:52e0e816…8b2d → com.docker.compose.project=vignette-preview-20260807
com.docker.compose.service=api
com.docker.compose.version=2.20.1
```
이 이미지로 만든 모든 container는 위 label을 **상속**한다. 따라서 "0개" 요구를 지키려면
감사되지 않은 다른 이미지를 써야 하고, 그건 오히려 증명을 약하게 만든다.
### 실제로 강제하는 계약 — key 부재가 아니라 소속(membership)
`scripts/launch-nas-preview-g8-helpers.py`가 아래를 코드로 강제한다.
| # | 계약 | 구현 |
|---|---|---|
| 1 | helper는 target Compose project에 속하지 않는다 | `verify_container_isolation` + `docker ps --filter label=com.docker.compose.project=<target>` 멤버십 조회 |
| 2 | image 상속 label을 baseline으로 읽고, container의 모든 `com.docker.compose.*`는 baseline과 같거나 **선언된 isolated override**여야 한다 | `docker image inspect` baseline ↔ `docker inspect` container 비교, drift는 `helper_compose_label_drift`로 fail-closed |
| 3 | 최종 `com.docker.compose.project``vignette-preview-20260807` | isolated override `vignette-g8-helper-<run-id>` |
| 4 | 최종 `com.docker.compose.service` ∉ {`api`,`web`,`db`,`proxy`} | isolated override `g8-rollback-executor` / `g8-rollback-control-plane` |
| 5 | `docker run` argv에 target project/service label을 넣지 않는다 | `assert_argv_isolation` + fake-runner 테스트 |
| 6 | exact container name·ID와 `docker inspect` 증거를 남긴다 | `verify` 산출 evidence(`vignette.nas-preview-g8-helper-isolation.v1`) |
테스트: `scripts/test_launch_nas_preview_g8_helpers.py` (31/31). 실제 실행 없이 argv 계약이
고정되므로, 누가 나중에 target label을 주입하면 테스트가 먼저 깨진다.
### helper 하드닝 (argv에 고정)
`--network host`(loopback bind 전용), `--user 1028:100`, `--group-add 101`(docker socket),
`--read-only`, `--cap-drop ALL`, `--security-opt no-new-privileges`, `--pids-limit 256`,
`--restart no`, target root RO 마운트 + `.rollback-executor` RW 중첩, Docker binary/Compose
plugin RO, socket 마운트. `--publish`는 쓰지 않는다(host network).
## 2. 사전 상태 (읽기 전용으로 매번 재확인)
```powershell
$ssh = @('-i','C:\Users\encep\.ssh\id_ed25519','-o','IdentitiesOnly=yes','-o','BatchMode=yes','YunChan@100.116.83.60')
```
확인 항목:
- current images: API `sha256:52e0e816…8b2d`, Web `sha256:6fdbb646…f215`
- previous images: API `sha256:79ec731f…0450`, Web `sha256:c530213f…62f28`
- preview health `http://127.0.0.1:8088/api/health``status=ok`, `db=true`, `engine=true`
- listeners 18149 / 8018 = 0, helper containers = 0
- `.rollback-executor` 해시 4종(manifest / 두 plan / executor·probe 스크립트)
- pre-proof DB dump 존재와 SHA256
## 3. 실행 순서
1. 읽기 전용 재검증(§2).
2. executor env(`executor-<run-id>.env`, mode 0600) 생성 — 무작위 token과 stable journal key.
값은 출력하지 않는다.
3. control-plane env(`control-plane-<run-id>.env`, mode 0600) 생성 — preview `.env`에서
비밀값을 **NAS 안에서만** 복사하고 아래를 덮어쓴다.
- `DATABASE_URL``vignette_app@172.22.0.2:5432/vignette_preview`
- producer/drift trigger 전부 `false`
- `VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_ENABLED=true`
- `…_ENDPOINT=http://127.0.0.1:18149/rollback`
- `…_TOKEN` = executor token
- `AUTH_DEV_LOGIN_ENABLED=true`, `AUTH_ADMIN_EMAILS`에 probe 계정만
4. `launch-nas-preview-g8-helpers.py start --role executor` → isolation evidence 확인,
unauth `/healthz` 403 / token-auth 200 확인.
5. `launch-nas-preview-g8-helpers.py start --role control-plane``/api/health` 확인.
6. Windows SSH 터널 `127.0.0.1:18018 → NAS 127.0.0.1:8018` (exact PID 기록).
7. `probe-nas-preview-g8-rollback.py execute --plan rollback-old.plan.json`
release gate 2회 멱등, approval 2회 멱등, lifecycle `executed`, receipt binding,
NAS previous images 활성화 + health 3회.
8. 같은 executor/control-plane으로 `restore-current.plan.json` 실행 → current images 복원,
health 3/3, OpenAPI 126, auth 401, Web 200.
9. 증거 저장 → UUID/email/secret regex scan 0.
10. cleanup 순서: tunnel → control-plane → executor. listeners 0, helper 0 확인.
11. token/control env 삭제, journal/manifest/evidence 보존.
## 4. 금지 사항
- `docker compose down`, `docker volume rm`, `docker system prune` 금지.
- current images와 health를 확인하기 전에 helper를 종료하거나 DONE으로 기록하지 않는다.
- 비밀값·이메일·UUID·raw response를 로그·증거·채팅에 출력하지 않는다.
- 공개 복구 DB(`vignette-dev-db`)와 그 volume/dump/container는 건드리지 않는다.
## 5. 실패 시 복구
executor는 실행 직전 관측한 exact image ID로 되돌린다. 그래도 어긋나면 pinned current images
(`52e0e816…8b2d` / `6fdbb646…f215`)로 `compose up -d --no-build --no-deps --pull never
--force-recreate api web`를 수행하고 health 3회를 다시 통과시킨다. DB는 pre-proof dump
(`vignette-preview-g8-preproof-20260807T110610Z.dump`, SHA256 `92ed4736…1570f`)로 복원 가능하다.
## 6. 실행 결과 (2026-08-07 21:04~21:12 KST, run-id `20260807g8a`)
| plan | receipt | 활성화된 API/Web 이미지 |
|---|---|---|
| `rollback-old` | `nas-g8-723eeef22eab05e63e3fafb0` | `79ec…4450` / `c530…2f28` |
| `restore-current` | `nas-g8-2738846cf2cf4fbe8ce0fc26` | `52e0…8b2d` / `6fdb…f215` |
- release gate·approval 각각 2회 멱등, lifecycle `executed`, binding·control-plane 분리 검증 PASS
- DB: `audit.ci_lifecycle_event` `rollback/executed` 2 · `audit.ci_human_approval_event` `authorize_rollback` 2 ·
`app.ci_release_gate` 2(둘 다 `pending_human_approval`, silent auto-promotion 0) · `ci_gate_artifact` 8
- HMAC journal 6 records, `previous_hash` 체인 전수 PASS, SHA256 `a5594feb…0690`
- 최종: health 3/3 · OpenAPI 126 · auth 401 · Web 200 · helper 0 · listener 0 · 비밀 env 파기
- 롤백/복원 뒤 실제 브라우저 SSE→DB review 재실행 PASS (35.0s)
### 3단계 이탈 기록 — SSH 터널 사용 불가
계획된 Windows SSH 터널(`18018 → 8018`)은 NAS sshd가 direct-tcpip를
`administratively prohibited: open failed`로 거부했다. **sshd 설정은 바꾸지 않았고**, 같은 격리 계약의
NAS-side probe helper 컨테이너에서 loopback으로 실행했다. control-plane(`127.0.0.1:8018`)과
preview(`127.0.0.1:8088`) origin이 다르므로 probe의 `main_preview_self_rollback_forbidden` 계약은
그대로 강제된다. 다음 실행자도 NAS에서 포트 포워딩을 기대하지 말 것.
상세 hash와 container ID는
[`nas-preview-deployment-evidence-2026-08-07.md`](./nas-preview-deployment-evidence-2026-08-07.md)의 G8 절과
[`evidence/nas-preview-g8-actual-rollback-2026-08-07.json`](./evidence/nas-preview-g8-actual-rollback-2026-08-07.json)에 있다.