G7 증명과 G8 clean-head 승격 준비

This commit is contained in:
Yun Chan 2026-08-09 18:22:03 +09:00
parent 94c681d450
commit 5221f79e3f
52 changed files with 6876 additions and 506 deletions

View file

@ -1,134 +1,233 @@
# Public Runtime Watchdog
This runbook keeps the public Vignette runtime recoverable after Windows
reboot, update, or process crash. It covers:
이 runbook은 Windows 재부팅, 업데이트, 프로세스 장애 뒤 Vignette 공개 런타임을 복구한다.
- engine gateway: `http://127.0.0.1:9099`
- prod API: `http://127.0.0.1:8001`
- Cloudflare tunnel for `https://api-vignette.chanpaca.net`
- engine gateway: http://127.0.0.1:9099
- prod API: http://127.0.0.1:8001
- web preview: http://127.0.0.1:5174
- Cloudflare tunnel: https://api-vignette.chanpaca.net
## Secret Handling
## 복구 소스 신뢰 계약
Do not put secrets in scheduled task arguments.
운영 task는 개발 중인 shared branch worktree를 실행하지 않는다. 아래 조건을 모두 만족하는 별도 release
worktree만 허용한다.
The scripts use the existing runtime locations:
- 승인된 commit을 가리키는 detached HEAD
- tracked/untracked non-ignored 변경 0
- 설치 시 기록한 Git commit SHA와 tree SHA 일치
- watchdog 또는 boot script SHA-256과 start-public-runtime.ps1 SHA-256 일치
- task action의 working directory와 실행 script가 동일 release root
- API secrets stay in `apps/api/.env`.
- Cloudflared credentials stay under the current user's `.cloudflared` config.
- Claude CLI OAuth/config stays in the current Windows user profile.
watchdog은 이 증거를 health probe와 failcount 기록보다 먼저 다시 확인한다. boot recovery는 Docker, DB,
프로세스 mutation보다 먼저 확인한다. 하나라도 달라지면 현재 운영 프로세스를 유지하고 nonzero로 종료한다.
The installer creates a per-user interactive scheduled task. It starts at user
logon and repeats as a watchdog. Fully unattended boot before any user logs in
requires an operator-managed service account or Task Scheduler credential; do
that in Windows, not by adding secrets to these scripts.
API secret은 release root의 apps/api/.env에 두되 task 인자에는 넣지 않는다. 이 파일과 web node_modules,
runtime log는 Git ignore 대상이다. Cloudflared와 Claude CLI credential은 현재 Windows 사용자 profile에 둔다.
## Install Or Update
## Stable Release 준비
From the repo root:
아래 작업은 승인된 clean commit이 생긴 뒤 단일 public mutation owner가 수행한다. 기존 release root를
덮어쓰지 않는다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -RunNow
```
$ErrorActionPreference = 'Stop'
$repoRoot = 'D:\workspace\vignette'
$commit = (& git.exe -C $repoRoot rev-parse --verify HEAD).Trim()
if ($LASTEXITCODE -ne 0) { throw 'HEAD 조회 실패' }
$releaseRoot = "D:\workspace\vignette-public-runtime-$($commit.Substring(0, 12))"
if (Test-Path -LiteralPath $releaseRoot) { throw "release root already exists: $releaseRoot" }
The installer is idempotent. Re-running it updates the same task:
& git.exe -C $repoRoot worktree add --detach $releaseRoot $commit
if ($LASTEXITCODE -ne 0) { throw 'detached release worktree 생성 실패' }
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 -IntervalMinutes 5
```
Copy-Item -LiteralPath (Join-Path $repoRoot 'apps\api\.env') `
-Destination (Join-Path $releaseRoot 'apps\api\.env')
Push-Location (Join-Path $releaseRoot 'apps\web')
& npm.cmd ci
if ($LASTEXITCODE -ne 0) { throw 'release web npm ci 실패' }
Pop-Location
Do not add future domains to the default watchdog until DNS and Cloudflare
routing are live. For example, `api-vnet.18ka.net` is intentionally excluded
from the default checks while that domain has no DNS. After a public domain is
actually reachable, add it explicitly:
$dirty = @(& git.exe -C $releaseRoot status --porcelain=v1 --untracked-files=normal)
if ($LASTEXITCODE -ne 0 -or $dirty.Count -ne 0) {
throw "release source가 clean하지 않음: $($dirty -join '; ')"
}
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-public-runtime-task.ps1 `
-AdditionalPublicHealthUrls https://api-vnet.18ka.net/health
```
apps/api/.env의 내용을 console이나 evidence에 출력하지 않는다. 새 root에 node_modules와 .env를 준비한 뒤에도
위 Git status 결과는 빈 값이어야 한다.
Task name:
## Task 설치 또는 승격
```powershell
VignettePublicRuntimeWatchdog
```
두 registrar 자체도 동일 stable release root에서 실행해야 한다. 다른 worktree의 registrar로 target만
바꾸는 호출은 거부된다.
## Manual Start
$bootRegistrar = Join-Path $releaseRoot 'scripts\register-boot-task.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootRegistrar `
-StableSourceRoot $releaseRoot
if ($LASTEXITCODE -ne 0) { throw 'boot task 등록 실패' }
Use this when you want to force a runtime restore immediately:
$watchdogInstaller = Join-Path $releaseRoot 'scripts\install-public-runtime-task.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $watchdogInstaller `
-StableSourceRoot $releaseRoot `
-IntervalMinutes 5
if ($LASTEXITCODE -ne 0) { throw 'watchdog task 등록 실패' }
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1
```
task 이름과 역할:
`start-public-runtime.ps1`는 관리자·인증 제어면과 엔진을 분리한다. 이미
`environment=prod`, `db=true`인 API와 정상 웹·터널은 유지하고, 엔진만 실패한 경우
엔진만 복구한다. 엔진이 늦게 준비돼도 관리자·인증 API 기동을 막지 않는다.
- VignettePublicRuntime: 사용자 로그온 시 Docker, PostgreSQL, runtime 복구
- VignettePublicRuntimeWatchdog: 사용자 로그온 및 5분 반복 health/recovery
API 코드 변경을 운영 프로세스에 반영할 때는 다른 표면을 유지한 채 API만 명시적으로 교체한다.
둘 다 현재 사용자의 Interactive/Limited task다. 사용자 로그인 전 headless boot가 필요하면 별도
operator-managed service account가 필요하며 credential을 script나 task arguments에 넣지 않는다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1 `
-ForceApiRestart `
-SkipEngineRestart `
-SkipWebRestart `
-SkipCloudflaredRestart
```
### 등록 직후 source pin 검증
## Health Checks
RunNow 전에 action을 읽어 두 task가 같은 release root와 commit을 가리키는지 확인한다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 -CheckOnly
Invoke-RestMethod http://127.0.0.1:9099/health
Invoke-RestMethod http://127.0.0.1:8001/health
Invoke-RestMethod https://api-vignette.chanpaca.net/health
```
$requirements = @{
VignettePublicRuntime = @(
'-StableSourceRoot',
'-ExpectedSourceCommit',
'-ExpectedSourceTree',
'-ExpectedBootScriptSha256',
'-ExpectedStartScriptSha256'
)
VignettePublicRuntimeWatchdog = @(
'-StableSourceRoot',
'-ExpectedSourceCommit',
'-ExpectedSourceTree',
'-ExpectedWatchdogSha256',
'-ExpectedStartScriptSha256'
)
}
Optional extra public host check, only after the host resolves:
foreach ($taskName in $requirements.Keys) {
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction Stop
$action = @($task.Actions)[0]
if ($action.WorkingDirectory -ne $releaseRoot) {
throw "$taskName working directory drift: $($action.WorkingDirectory)"
}
if ($action.Arguments.IndexOf($releaseRoot, [StringComparison]::OrdinalIgnoreCase) -lt 0) {
throw "$taskName release root pin 누락"
}
if ($action.Arguments.IndexOf($commit, [StringComparison]::OrdinalIgnoreCase) -lt 0) {
throw "$taskName commit pin 누락"
}
foreach ($marker in $requirements[$taskName]) {
if ($action.Arguments.IndexOf($marker, [StringComparison]::Ordinal) -lt 0) {
throw "$taskName action pin 누락: $marker"
}
}
}
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\watch-public-runtime.ps1 `
-CheckOnly `
-AdditionalPublicHealthUrls https://api-vnet.18ka.net/health
```
검증 뒤 watchdog만 명시적으로 실행하고 완료 상태를 확인한다.
Optional real Claude CLI readiness smoke:
Start-ScheduledTask -TaskName VignettePublicRuntimeWatchdog
Get-ScheduledTaskInfo -TaskName VignettePublicRuntimeWatchdog
```powershell
Invoke-RestMethod http://127.0.0.1:9099/ready
```
LastTaskResult=0과 stable release root의 public-runtime-watchdog.failcount=0을 확인한다. health만 정상이라고
새 source 배포가 완료된 것은 아니다. 공개 API process cwd, Git commit, OpenAPI, auth, voice provider/model,
실제 session smoke까지 별도 배포 gate에서 확인한다.
`/ready` can consume a small Claude budget because it performs a real generation.
## 숨김 수동 Trigger
## Logs And Task State
watch-public-runtime-hidden.vbs는 source script를 직접 실행하지 않는다. 등록된 watchdog task action에
StableSourceRoot, commit, tree, watchdog SHA, start SHA marker가 모두 있고 legacy Workspace action이 아님을
검사한 뒤 Start-ScheduledTask만 호출한다.
```powershell
Get-ScheduledTask -TaskName VignettePublicRuntimeWatchdog
Get-ScheduledTaskInfo -TaskName VignettePublicRuntimeWatchdog
Get-Content .\public-runtime-watchdog.log -Tail 50
Get-Content .\apps\api\engine.public.err.log -Tail 50
Get-Content .\apps\api\api.public.err.log -Tail 50
Get-Content .\cloudflared.public.err.log -Tail 50
```
cscript.exe //nologo scripts\watch-public-runtime-hidden.vbs
## Remove
task가 아직 legacy shared-worktree action이면 VBS도 fail-closed한다.
```powershell
Unregister-ScheduledTask -TaskName VignettePublicRuntimeWatchdog -Confirm:$false
```
## Manual Source Recovery
## Recovery Notes
운영 code를 강제로 교체해야 할 때도 shared worktree의 start-public-runtime.ps1을 실행하지 않는다.
위 pin 검증을 끝낸 release root의 script만 사용한다.
If local health is good but public health fails, inspect the cloudflared process
and `C:\Users\<user>\.cloudflared\vignette-config.yml`.
$startScript = Join-Path $releaseRoot 'scripts\start-public-runtime.ps1'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startScript `
-Workspace $releaseRoot `
-ForceApiRestart `
-SkipEngineRestart `
-SkipWebRestart `
-SkipCloudflaredRestart
if ($LASTEXITCODE -ne 0) { throw 'public API 교체 실패' }
If engine health fails, verify that `claude` runs for the same Windows user that
owns the scheduled task and that the user has completed Claude CLI login.
start-public-runtime.ps1은 engine, API, web, tunnel을 분리해 이미 healthy인 표면을 유지한다. local Whisper와
MeloTTS exact readiness가 닫히기 전에는 API restart gate를 통과하지 않는다.
엔진 장애 중에도 `http://127.0.0.1:8001/health``environment=prod`, `db=true`
유지되면 관리자·인증 제어면은 정상이다. 이때 watchdog은 API·웹·터널을 재시작하지
않는다. 예약 작업 확인 기준은 `VignettePublicRuntime``LastTaskResult=0`
`public-runtime-watchdog.failcount=0`다.
위 명령은 routine API-only 복구라 cloudflared를 유지하며 **G7 fresh topology 증거를 만들지 않는다**. G7 공개
승격에서는 기존 API/cloudflared PID를 재사용하지 않고 아래 opt-in 계약을 사용한다. 실행 전에 detached-clean
commit/tree와 Python/cloudflared/config SHA를 read-only로 고정하고, config ingress가 이미 exact public topology인지
확인한다. 이 모드는 `-ForceApiRestart`가 필수이고 `-SkipCloudflaredRestart`를 허용하지 않는다.
If API health fails with `environment`, `db`, or auth configuration errors,
inspect `apps/api/.env`; do not copy secret values into scripts or task
arguments.
$python = 'C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe'
$cloudflared = 'C:\Users\encep\AppData\Local\Microsoft\WinGet\Links\cloudflared.exe'
$cloudflaredConfig = 'C:\Users\encep\.cloudflared\vignette-config.yml'
$commit = (& git.exe -C $releaseRoot rev-parse --verify HEAD).Trim()
$tree = (& git.exe -C $releaseRoot rev-parse --verify 'HEAD^{tree}').Trim()
$pythonSha = (Get-FileHash -LiteralPath $python -Algorithm SHA256).Hash.ToLowerInvariant()
$cloudflaredSha = (Get-FileHash -LiteralPath $cloudflared -Algorithm SHA256).Hash.ToLowerInvariant()
$configSha = (Get-FileHash -LiteralPath $cloudflaredConfig -Algorithm SHA256).Hash.ToLowerInvariant()
$receipt = Join-Path (Join-Path 'D:\workspace\vignette-runtime-evidence' $commit) 'public-runtime-launch-provenance.json'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $startScript `
-Workspace $releaseRoot `
-ForceApiRestart `
-SkipEngineRestart `
-SkipWebRestart `
-RequireFreshPublicProvenance `
-ExpectedSourceCommit $commit `
-ExpectedSourceTree $tree `
-ExpectedPythonSha256 $pythonSha `
-ExpectedCloudflaredSha256 $cloudflaredSha `
-ExpectedCloudflaredConfigSha256 $configSha `
-RuntimeProvenancePath $receipt
if ($LASTEXITCODE -ne 0) { throw 'fresh public provenance 승격 실패' }
receipt에는 raw command line·config contents를 넣지 않고 PID/start/executable·command SHA/실제 cwd와 topology 입력만
남긴다. 이 receipt의 PID와 pin을 `run-g7-external-proof-window.py --topology-mode windows-host`에 그대로 전달하고,
공개 health·auth·OpenAPI·local provider ready를 확인하기 전에는 task action을 새 root로 재등록하지 않는다.
## Read-only CheckOnly
watchdog script를 직접 CheckOnly로 실행할 때도 task와 같은 pin을 모두 전달해야 한다.
$watchScript = Join-Path $releaseRoot 'scripts\watch-public-runtime.ps1'
$startScript = Join-Path $releaseRoot 'scripts\start-public-runtime.ps1'
$commit = (& git.exe -C $releaseRoot rev-parse --verify HEAD).Trim()
$tree = (& git.exe -C $releaseRoot rev-parse --verify 'HEAD^{tree}').Trim()
$watchSha = (Get-FileHash -LiteralPath $watchScript -Algorithm SHA256).Hash.ToLowerInvariant()
$startSha = (Get-FileHash -LiteralPath $startScript -Algorithm SHA256).Hash.ToLowerInvariant()
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $watchScript `
-StableSourceRoot $releaseRoot `
-ExpectedSourceCommit $commit `
-ExpectedSourceTree $tree `
-ExpectedWatchdogSha256 $watchSha `
-ExpectedStartScriptSha256 $startSha `
-CheckOnly
추가 public host는 DNS와 routing이 실제로 열린 뒤 installer의
AdditionalPublicHealthUrls에 명시한다. 아직 열리지 않은 future host를 기본 probe에 넣어 restart loop를 만들지 않는다.
## Health와 로그
Invoke-RestMethod http://127.0.0.1:9099/health
Invoke-RestMethod http://127.0.0.1:8001/health
Invoke-RestMethod https://api-vignette.chanpaca.net/health
Get-ScheduledTaskInfo -TaskName VignettePublicRuntimeWatchdog
Get-Content -LiteralPath (Join-Path $releaseRoot 'public-runtime-watchdog.log') -Tail 50
Get-Content -LiteralPath (Join-Path $releaseRoot 'apps\api\engine.public.err.log') -Tail 50
Get-Content -LiteralPath (Join-Path $releaseRoot 'apps\api\api.public.err.log') -Tail 50
engine /ready는 실제 Claude generation을 수행할 수 있어 소량의 budget을 사용한다. shared secret으로 기동한
gateway는 token header가 필요하다.
엔진 장애 중에도 API health가 environment=prod, db=true이면 관리자와 인증 제어면은 유지한다. source
provenance failure는 재시작으로 우회하지 말고 task action과 stable release를 다시 승격한다.
## Task 제거
제거는 명시적 운영 결정으로만 수행한다.
Unregister-ScheduledTask -TaskName VignettePublicRuntimeWatchdog -Confirm:$false
Unregister-ScheduledTask -TaskName VignettePublicRuntime -Confirm:$false