vignette/docs/ops/nas-preview-deployment-evidence-2026-08-07.md
Yun Chan 93dd8f82d7 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 체커가 실패한다.
2026-08-07 22:17:20 +09:00

166 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 프리뷰다. 외부 공개 배포 완료 증거로 사용하지 않는다.