아바타 저장소 승격 계약을 완성
This commit is contained in:
parent
ac9b702688
commit
ccdcfcd2f5
36 changed files with 14734 additions and 222 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue