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:
parent
76d0b9ae9b
commit
93dd8f82d7
22 changed files with 10057 additions and 473 deletions
|
|
@ -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 개통 후
|
||||
|
|
|
|||
238
docs/ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json
Normal file
238
docs/ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json
Normal 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"
|
||||
}
|
||||
}
|
||||
166
docs/ops/nas-preview-deployment-evidence-2026-08-07.md
Normal file
166
docs/ops/nas-preview-deployment-evidence-2026-08-07.md
Normal 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 프리뷰다. 외부 공개 배포 완료 증거로 사용하지 않는다.
|
||||
136
docs/ops/nas-preview-g8-rollback-proof-runbook.md
Normal file
136
docs/ops/nas-preview-g8-rollback-proof-runbook.md
Normal 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)에 있다.
|
||||
Loading…
Add table
Add a link
Reference in a new issue