아바타 저장소 승격 계약을 완성

This commit is contained in:
Yun Chan 2026-08-29 23:58:33 +09:00
parent ac9b702688
commit ccdcfcd2f5
36 changed files with 14734 additions and 222 deletions

View file

@ -23,6 +23,12 @@ param(
[Parameter(Mandatory = $true)]
[ValidatePattern("^[0-9a-fA-F]{64}$")]
[string]$ExpectedStartScriptSha256,
[ValidatePattern("^$|^[0-9a-fA-F]{64}$")]
[string]$ExpectedPythonSha256 = "",
[ValidatePattern("^$|^[0-9a-fA-F]{64}$")]
[string]$ExpectedCloudflaredSha256 = "",
[ValidatePattern("^$|^[0-9a-fA-F]{64}$")]
[string]$ExpectedCloudflaredConfigSha256 = "",
[string]$DockerDesktop = "C:\Program Files\Docker\Docker\Docker Desktop.exe",
[int]$DaemonTimeoutSec = 360,
[int]$DbTimeoutSec = 90,
@ -33,6 +39,17 @@ param(
[int]$WhisperPort = 9882,
[int]$MeloTtsPort = 9883,
[string]$Python = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
[string]$Cloudflared = "$env:LOCALAPPDATA\Microsoft\WinGet\Links\cloudflared.exe",
[string]$CloudflaredConfig = "$env:USERPROFILE\.cloudflared\vignette-config.yml",
[Parameter(Mandatory = $true)]
[string]$UserUploadDir,
[Parameter(Mandatory = $true)]
[string]$UserUploadManifestPath,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[0-9a-f]{64}$")]
[string]$ExpectedUserUploadManifestSha256,
[Parameter(Mandatory = $true)]
[string]$UserUploadWriteFreezePath,
[string]$BootLog = ""
)
@ -42,6 +59,10 @@ $resolvedSourceRoot = (Resolve-Path -LiteralPath $StableSourceRoot).Path
$expectedBootScript = Join-Path $resolvedSourceRoot "scripts\boot-public-runtime.ps1"
$startScript = Join-Path $resolvedSourceRoot "scripts\start-public-runtime.ps1"
$voiceSidecarProbe = Join-Path $resolvedSourceRoot "scripts\probe-public-voice-sidecars.py"
$uploadRootContract = Join-Path $resolvedSourceRoot "scripts\public-runtime-upload-root.ps1"
$uploadRootProbe = Join-Path $resolvedSourceRoot "scripts\probe-public-runtime-upload-root.py"
$uploadManifestProbe = Join-Path $resolvedSourceRoot "scripts\validate-public-runtime-upload-manifest.py"
$databaseIdentityHelper = Join-Path $resolvedSourceRoot "scripts\public_runtime_database_identity.py"
function Invoke-GitText {
param([string[]]$Arguments)
@ -54,7 +75,15 @@ function Invoke-GitText {
}
function Assert-StableSourceProvenance {
foreach ($requiredScript in @($expectedBootScript, $startScript, $voiceSidecarProbe)) {
foreach ($requiredScript in @(
$expectedBootScript,
$startScript,
$voiceSidecarProbe,
$uploadRootContract,
$uploadRootProbe,
$uploadManifestProbe,
$databaseIdentityHelper
)) {
if (-not (Test-Path -LiteralPath $requiredScript -PathType Leaf)) {
throw "Pinned public runtime script not found at $requiredScript"
}
@ -104,7 +133,13 @@ function Assert-StableSourceProvenance {
foreach ($relativePath in @(
"scripts/boot-public-runtime.ps1",
"scripts/start-public-runtime.ps1",
"scripts/probe-public-voice-sidecars.py"
"scripts/probe-public-voice-sidecars.py",
"scripts/public-runtime-upload-root.ps1",
"scripts/probe-public-runtime-upload-root.py",
"scripts/validate-public-runtime-upload-manifest.py",
"scripts/public_runtime_database_identity.py",
"apps/api/app/upload_storage.py",
"apps/api/app/upload_runtime.py"
)) {
Invoke-GitText -Arguments @("ls-files", "--error-unmatch", "--", $relativePath) | Out-Null
}
@ -117,10 +152,40 @@ function Assert-StableSourceProvenance {
if ($actualStartScriptSha256 -ne $ExpectedStartScriptSha256.ToLowerInvariant()) {
throw "Pinned start script SHA256 drift"
}
foreach ($pin in @(
[pscustomobject]@{ Path = $Python; Expected = $ExpectedPythonSha256; Role = "Python" },
[pscustomobject]@{ Path = $Cloudflared; Expected = $ExpectedCloudflaredSha256; Role = "cloudflared" },
[pscustomobject]@{ Path = $CloudflaredConfig; Expected = $ExpectedCloudflaredConfigSha256; Role = "cloudflared config" }
)) {
if ([string]::IsNullOrWhiteSpace([string]$pin.Expected)) {
continue
}
if (-not (Test-Path -LiteralPath $pin.Path -PathType Leaf)) {
throw "Pinned $($pin.Role) is unavailable"
}
$actual = (Get-FileHash -LiteralPath $pin.Path -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -cne ([string]$pin.Expected).ToLowerInvariant()) {
throw "Pinned $($pin.Role) SHA256 drift"
}
}
}
# Docker/DB/process mutation보다 먼저 stable source를 매 실행 재검증한다.
Assert-StableSourceProvenance
. $uploadRootContract
$resolvedUserUploadDir = Resolve-PublicRuntimeUploadRoot `
-SourceRoot $resolvedSourceRoot `
-UploadRoot $UserUploadDir `
-ProbeWritable
$resolvedUserUploadManifestPath = Resolve-PublicRuntimePrivateStatePath `
-SourceRoot $resolvedSourceRoot `
-UploadRoot $resolvedUserUploadDir `
-StatePath $UserUploadManifestPath `
-RequireFile
$resolvedUserUploadWriteFreezePath = Resolve-PublicRuntimePrivateStatePath `
-SourceRoot $resolvedSourceRoot `
-UploadRoot $resolvedUserUploadDir `
-StatePath $UserUploadWriteFreezePath
if (!$BootLog) {
$BootLog = Join-Path $resolvedSourceRoot "boot-public-runtime.log"
@ -152,6 +217,28 @@ function Test-Tcp([string]$Host_, [int]$Port) {
} catch { return $false }
}
function Get-LocalApiHealthSnapshot {
try {
$request = [System.Net.HttpWebRequest]::Create("http://127.0.0.1:$ApiPort/health")
$request.Timeout = 5000
$request.ReadWriteTimeout = 5000
$request.Proxy = $null
$response = $request.GetResponse()
try {
$reader = New-Object System.IO.StreamReader($response.GetResponseStream())
try {
return ($reader.ReadToEnd() | ConvertFrom-Json)
} finally {
$reader.Dispose()
}
} finally {
$response.Dispose()
}
} catch {
return $null
}
}
function Test-ApiControlPlaneHealthy {
# HttpWebRequest + Proxy=$null: WININET/시스템 프록시에 영향받지 않는 가장 직결적인 검사.
# 비대화형 스케줄러 컨텍스트에서도 127.0.0.1 로 직접 연결한다. 3회 재시도.
@ -166,7 +253,14 @@ function Test-ApiControlPlaneHealthy {
$body = $reader.ReadToEnd()
$reader.Close(); $resp.Close()
$h = $body | ConvertFrom-Json
if ($h.environment -eq "prod" -and $h.db -eq $true -and $h.engine -eq $true) { return $true }
if (
$h.environment -eq "prod" -and
$h.db -eq $true -and
$h.engine -eq $true -and
$h.upload_write_freeze.capable -eq $true -and
$h.upload_write_freeze.active -eq $false -and
$h.upload_write_freeze.valid -eq $true
) { return $true }
Write-BootLog (" health probe attempt {0}: not-healthy body={1}" -f $i, $body)
return $false
} catch {
@ -304,22 +398,81 @@ if (-not (Test-Tcp -Host_ "127.0.0.1" -Port $DbPort)) {
exit 1
}
Write-BootLog "postgres 127.0.0.1:$DbPort up"
$uploadManifestHealthy = Test-PublicRuntimeUploadManifest `
-PythonPath $Python `
-ProbePath $uploadManifestProbe `
-UploadRoot $resolvedUserUploadDir `
-ManifestPath $resolvedUserUploadManifestPath `
-ExpectedManifestSha256 $ExpectedUserUploadManifestSha256 `
-ExpectedWriteFreezePath $resolvedUserUploadWriteFreezePath
if (-not $uploadManifestHealthy.Ok) {
Write-BootLog "ERROR: public upload migration receipt or current DB inventory is invalid"
exit 1
}
$expectedDatabaseTargetSha256 = [string]$uploadManifestHealthy.Payload.database_target_sha256
if ($expectedDatabaseTargetSha256 -notmatch "^[0-9a-f]{64}$") {
Write-BootLog "ERROR: public upload inventory proof did not return a valid database target identity"
exit 1
}
if (Test-Path -LiteralPath $resolvedUserUploadWriteFreezePath -PathType Leaf) {
$promotionHealth = Get-LocalApiHealthSnapshot
$promotionFreeze = $null
if ($null -ne $promotionHealth) {
$promotionFreeze = $promotionHealth.upload_write_freeze
}
if (
$null -ne $promotionFreeze -and
$promotionFreeze.capable -eq $true -and
$promotionFreeze.active -eq $true -and
$promotionFreeze.valid -eq $true -and
[int]$promotionFreeze.in_flight -eq 0 -and
[string]$promotionFreeze.token_sha256 -ceq
[string]$uploadManifestHealthy.Payload.write_freeze_token_sha256
) {
Write-BootLog "promotion-in-progress: valid drained upload freeze is active; skipping runtime mutation"
exit 0
}
Write-BootLog "ERROR: upload freeze sentinel exists without exact active/drained API proof; refusing runtime mutation"
exit 1
}
# 3) 엔진/API/web/cloudflared — 이미 healthy 면 스킵(불필요한 재시작/다운타임 방지)
# web preview는 살아 있을 때만 -SkipWebRestart 한다. 무조건 스킵하면 재부팅 직후처럼
# vite가 죽은 상태에서 boot 경로로는 web이 영영 복구되지 않는다.
$webHealthy = Test-WebPreviewHealthy
if ((Test-ApiControlPlaneHealthy) -and (Test-EngineHealthy) -and (Test-VoiceApiHealthy) -and (Test-VoiceSidecarStack) -and $webHealthy) {
if (
(Test-ApiControlPlaneHealthy) -and
(Test-EngineHealthy) -and
(Test-VoiceApiHealthy) -and
(Test-VoiceSidecarStack) -and
(Test-PublicRuntimeApiUploadRoot `
-PythonPath $Python `
-ProbePath $uploadRootProbe `
-ExpectedUploadRoot $resolvedUserUploadDir `
-ExpectedApiCwd (Join-Path $resolvedSourceRoot "apps\api") `
-ExpectedManifestPath $resolvedUserUploadManifestPath `
-ExpectedManifestSha256 $ExpectedUserUploadManifestSha256 `
-ExpectedWriteFreezePath $resolvedUserUploadWriteFreezePath `
-ExpectedDatabaseTargetSha256 $expectedDatabaseTargetSha256 `
-ApiPort $ApiPort).Ok -and
$webHealthy
) {
Write-BootLog "control plane, engine, exact local voice API, sidecars, and web preview already healthy; skipping runtime restart"
} else {
$startArgs = @(
"-Workspace", $resolvedSourceRoot,
"-Python", $Python,
"-Cloudflared", $Cloudflared,
"-CloudflaredConfig", $CloudflaredConfig,
"-ApiPort", $ApiPort,
"-WebPort", $WebPort,
"-EnginePort", $EnginePort,
"-WhisperPort", $WhisperPort,
"-MeloTtsPort", $MeloTtsPort
"-MeloTtsPort", $MeloTtsPort,
"-UserUploadDir", $resolvedUserUploadDir,
"-UserUploadManifestPath", $resolvedUserUploadManifestPath,
"-ExpectedUserUploadManifestSha256", $ExpectedUserUploadManifestSha256,
"-UserUploadWriteFreezePath", $resolvedUserUploadWriteFreezePath
)
if ($webHealthy) {
$startArgs += "-SkipWebRestart"
@ -342,7 +495,17 @@ if ((Test-ApiControlPlaneHealthy) -and (Test-EngineHealthy) -and (Test-VoiceApiH
# 4) 최종 확인
if (Test-ApiControlPlaneHealthy) {
if ((Test-EngineHealthy) -and (Test-VoiceApiHealthy) -and (Test-VoiceSidecarStack)) {
$apiUploadRootHealthy = Test-PublicRuntimeApiUploadRoot `
-PythonPath $Python `
-ProbePath $uploadRootProbe `
-ExpectedUploadRoot $resolvedUserUploadDir `
-ExpectedApiCwd (Join-Path $resolvedSourceRoot "apps\api") `
-ExpectedManifestPath $resolvedUserUploadManifestPath `
-ExpectedManifestSha256 $ExpectedUserUploadManifestSha256 `
-ExpectedWriteFreezePath $resolvedUserUploadWriteFreezePath `
-ExpectedDatabaseTargetSha256 $expectedDatabaseTargetSha256 `
-ApiPort $ApiPort
if ((Test-EngineHealthy) -and (Test-VoiceApiHealthy) -and (Test-VoiceSidecarStack) -and $apiUploadRootHealthy.Ok) {
Write-BootLog "boot OK: control plane, engine, exact local voice API, and sidecars healthy"
exit 0
} else {