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
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 프리뷰다. 외부 공개 배포 완료 증거로 사용하지 않는다.
|
||||
Loading…
Add table
Add a link
Reference in a new issue