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

12 KiB
Raw Permalink Blame History

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.

시각 증거:

실배포에서 닫은 재발 방지 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/108634.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 통합 배포 증거.
  • 이 승격은 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 증명 런북, 기계 판독 증거는 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.2vignette_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-e2eVignette 회기 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 프리뷰다. 외부 공개 배포 완료 증거로 사용하지 않는다.