Files
awoooi/agent99-deploy.ps1
ogt 4ea132d992
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m41s
CD Pipeline / build-and-deploy (push) Successful in 9m24s
CD Pipeline / post-deploy-checks (push) Successful in 1m56s
fix(agent99): close canonical host readbacks
2026-07-14 18:49:12 +08:00

506 lines
23 KiB
PowerShell

param(
[string]$SourceRoot = (Split-Path -Parent $MyInvocation.MyCommand.Path),
[string]$AgentRoot = "C:\Wooo\Agent99",
[string]$SourceRevision = "",
[switch]$RegisterTasks,
[switch]$ValidateOnly
)
$ErrorActionPreference = "Stop"
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$BinDir = Join-Path $AgentRoot "bin"
$ConfigPath = Join-Path $AgentRoot "config\agent99.config.json"
$EvidenceDir = Join-Path $AgentRoot "evidence"
$StateDir = Join-Path $AgentRoot "state"
$DeployRoot = Join-Path $AgentRoot "deploy"
$StageDir = Join-Path $DeployRoot "staging-$stamp"
$BackupDir = Join-Path $DeployRoot "backup-$stamp"
$ManifestPath = Join-Path $StateDir "runtime-manifest.json"
$EvidencePath = Join-Path $EvidenceDir "agent99-deploy-$stamp.json"
$script:ConfigMigrations = @()
$script:RuntimeCopyRetries = @()
$runtimeFiles = @(
"agent99-bootstrap.ps1",
"agent99-contract-check.ps1",
"agent99-control-plane.ps1",
"agent99-deploy.ps1",
"agent99-register-tasks.ps1",
"agent99-sre-alert-inbox.ps1",
"agent99-sre-alert-relay.ps1",
"agent99-submit-request.ps1",
"agent99-synthetic-tests.ps1",
"agent99-telegram-inbox.ps1",
"agent99-windows-update-policy.ps1",
"host-reboot-scorecard.ps1",
"agent99.config.example.json",
"agent99.config.99.example.json"
)
function Ensure-Utf8Bom {
param([string]$Path)
$bytes = [IO.File]::ReadAllBytes($Path)
$text = [Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
[IO.File]::WriteAllText($Path, $text, (New-Object System.Text.UTF8Encoding($true)))
}
function Add-DefaultProperty {
param([object]$Object, [string]$Name, [object]$Value)
if (-not $Object.PSObject.Properties[$Name] -or $null -eq $Object.PSObject.Properties[$Name].Value) {
if ($Object.PSObject.Properties[$Name]) {
$Object.PSObject.Properties[$Name].Value = $Value
} else {
$Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
}
}
}
function Set-AgentProperty {
param([object]$Object, [string]$Name, [object]$Value)
if ($Object.PSObject.Properties[$Name]) {
$Object.PSObject.Properties[$Name].Value = $Value
} else {
$Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
}
}
function Get-AgentHost112CanonicalVm {
param([string]$PolicyPath)
if (-not (Test-Path -LiteralPath $PolicyPath -PathType Leaf)) {
throw "Agent99 Host112 canonical policy config is unavailable: $PolicyPath"
}
$policy = Get-Content -LiteralPath $PolicyPath -Raw | ConvertFrom-Json
$candidates = @($policy.vms | Where-Object {
[string]$_.host -eq "192.168.0.112" -or [string]$_.name -eq "host112-kali"
})
if ($candidates.Count -ne 1) {
throw "Agent99 Host112 canonical policy must contain exactly one Host112 VM row; found $($candidates.Count)."
}
$candidate = $candidates[0]
$candidateVmx = ([string]$candidate.vmx).Trim()
if (
[string]$candidate.name -ne "host112-kali" -or
[string]$candidate.host -ne "192.168.0.112" -or
-not $candidateVmx
) {
throw "Agent99 Host112 canonical policy identity is invalid."
}
return ($candidate | ConvertTo-Json -Depth 10 | ConvertFrom-Json)
}
function Copy-AgentRuntimeFileWithRetry {
param(
[string]$Source,
[string]$Destination,
[int]$MaxAttempts = 20,
[int]$DelayMilliseconds = 500
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt += 1) {
try {
Copy-Item -Force $Source $Destination
return
} catch [System.IO.IOException] {
if ($attempt -ge $MaxAttempts) { throw }
$script:RuntimeCopyRetries += [pscustomobject]@{
file = Split-Path -Leaf $Destination
attempt = $attempt
reason = "file_in_use"
}
Start-Sleep -Milliseconds $DelayMilliseconds
}
}
}
function Write-DeployEvidence {
param([bool]$Ok, [string]$Status, [string]$ErrorText, [object[]]$Files, [object]$TaskResult)
New-Item -ItemType Directory -Force -Path $EvidenceDir | Out-Null
[pscustomobject]@{
schemaVersion = "agent99_deploy_receipt_v2"
timestamp = (Get-Date -Format o)
ok = $Ok
status = $Status
sourceRoot = $SourceRoot
sourceRevision = $SourceRevision
agentRoot = $AgentRoot
stageDir = $StageDir
backupDir = $BackupDir
runtimeManifest = $ManifestPath
validateOnly = [bool]$ValidateOnly
registerTasks = [bool]$RegisterTasks
error = $ErrorText
configMigrations = @($script:ConfigMigrations)
runtimeCopyRetryCount = @($script:RuntimeCopyRetries).Count
runtimeCopyRetries = @($script:RuntimeCopyRetries)
files = $Files
taskRegistration = $TaskResult
} | ConvertTo-Json -Depth 10 | Set-Content -Path $EvidencePath -Encoding UTF8
}
function Restore-Agent99Deployment {
param([object[]]$Rows, [bool]$ConfigBackedUp, [bool]$ManifestBackedUp)
foreach ($item in $Rows) {
$backupPath = Join-Path $BackupDir $item.name
if ($item.existed -and (Test-Path $backupPath)) {
Copy-AgentRuntimeFileWithRetry $backupPath $item.livePath
} elseif (-not $item.existed -and (Test-Path $item.livePath)) {
Remove-Item -Force $item.livePath
}
}
if ($ConfigBackedUp) { Copy-Item -Force (Join-Path $BackupDir "agent99.config.json") $ConfigPath }
if ($ManifestBackedUp) {
Copy-Item -Force (Join-Path $BackupDir "runtime-manifest.json") $ManifestPath
} elseif (Test-Path $ManifestPath) {
Remove-Item -Force $ManifestPath
}
}
if (-not $SourceRevision) {
try {
$revision = & git -C $SourceRoot rev-parse HEAD 2>$null
if ($LASTEXITCODE -eq 0 -and $revision) { $SourceRevision = ([string]$revision).Trim() }
} catch {
$SourceRevision = ""
}
}
if (-not $SourceRevision) { $SourceRevision = "unknown" }
New-Item -ItemType Directory -Force -Path $StageDir, $EvidenceDir, $StateDir | Out-Null
foreach ($name in $runtimeFiles) {
$source = Join-Path $SourceRoot $name
if (-not (Test-Path $source)) { throw "Required deployment file missing: $source" }
$target = Join-Path $StageDir $name
Copy-Item -Force $source $target
if ($name.EndsWith(".ps1")) { Ensure-Utf8Bom $target }
}
& (Join-Path $StageDir "agent99-contract-check.ps1") -SourceRoot $StageDir | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-DeployEvidence $false "preflight_failed" "contract_check_failed" @() $null
throw "Agent99 staged contract check failed."
}
& (Join-Path $StageDir "agent99-synthetic-tests.ps1") -SourceRoot $StageDir -WorkRoot (Join-Path $StageDir "synthetic-work") | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-DeployEvidence $false "preflight_failed" "synthetic_tests_failed" @() $null
throw "Agent99 staged synthetic tests failed."
}
$stagedFiles = @($runtimeFiles | ForEach-Object {
$path = Join-Path $StageDir $_
[pscustomobject]@{ name = $_; sha256 = (Get-FileHash -Algorithm SHA256 -Path $path).Hash.ToLowerInvariant() }
})
$policyConfigPath = Join-Path $StageDir "agent99.config.99.example.json"
$policyConfig = Get-Content -LiteralPath $policyConfigPath -Raw | ConvertFrom-Json
$canonicalHost112Vm = Get-AgentHost112CanonicalVm $policyConfigPath
$canonicalHost112Vmx = [string]$canonicalHost112Vm.vmx
if ($ValidateOnly) {
Write-DeployEvidence $true "validated" $null $stagedFiles $null
[pscustomobject]@{ ok = $true; status = "validated"; sourceRevision = $SourceRevision; evidence = $EvidencePath; files = $stagedFiles } | ConvertTo-Json -Depth 8
exit 0
}
if (-not (Test-Path -LiteralPath $canonicalHost112Vmx -PathType Leaf)) {
throw "Host112 canonical VMX path from staged policy is unavailable: $canonicalHost112Vmx"
}
New-Item -ItemType Directory -Force -Path $BinDir, $BackupDir | Out-Null
$promotionRows = @()
$configBackedUp = $false
$manifestBackedUp = $false
try {
if (-not (Test-Path $ConfigPath)) { throw "Live Agent99 config not found: $ConfigPath" }
Copy-Item -Force $ConfigPath (Join-Path $BackupDir "agent99.config.json")
$configBackedUp = $true
if (Test-Path $ManifestPath) {
Copy-Item -Force $ManifestPath (Join-Path $BackupDir "runtime-manifest.json")
$manifestBackedUp = $true
}
foreach ($name in $runtimeFiles) {
$livePath = Join-Path $BinDir $name
$existed = Test-Path $livePath
if ($existed) { Copy-Item -Force $livePath (Join-Path $BackupDir $name) }
$promotionRows += [pscustomobject]@{ name = $name; livePath = $livePath; existed = [bool]$existed }
Copy-AgentRuntimeFileWithRetry (Join-Path $StageDir $name) $livePath
}
$config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
Add-DefaultProperty $config "sshIdentityFile" (Join-Path $AgentRoot "keys\agent99_ed25519")
Add-DefaultProperty $config "sshUsers" ([pscustomobject]@{})
$previousHost111User = if ($config.sshUsers.PSObject.Properties["192.168.0.111"]) { [string]$config.sshUsers.PSObject.Properties["192.168.0.111"].Value } else { "" }
if ($previousHost111User -ne "ooo") {
Set-AgentProperty $config.sshUsers "192.168.0.111" "ooo"
$script:ConfigMigrations += [pscustomobject]@{
name = "host111_canonical_ssh_user"
from = if ($previousHost111User) { $previousHost111User } else { "missing" }
to = "ooo"
}
}
$previousHost112User = if ($config.sshUsers.PSObject.Properties["192.168.0.112"]) { [string]$config.sshUsers.PSObject.Properties["192.168.0.112"].Value } else { "" }
if ($previousHost112User -ne "kali") {
Set-AgentProperty $config.sshUsers "192.168.0.112" "kali"
$script:ConfigMigrations += [pscustomobject]@{
name = "host112_canonical_ssh_user"
from = if ($previousHost112User) { $previousHost112User } else { "missing" }
to = "kali"
}
}
Add-DefaultProperty $config "sshTransport" ([pscustomobject]@{})
Add-DefaultProperty $config.sshTransport "serialized" $true
Add-DefaultProperty $config.sshTransport "lockTimeoutSeconds" 90
Add-DefaultProperty $config.sshTransport "lockPollMilliseconds" 200
Add-DefaultProperty $config "sshProcessGuard" ([pscustomobject]@{})
Add-DefaultProperty $config.sshProcessGuard "enabled" $true
Add-DefaultProperty $config.sshProcessGuard "staleMinutes" 15
Add-DefaultProperty $config.sshProcessGuard "hardStaleMinutes" 60
Add-DefaultProperty $config "hosts" @("192.168.0.110", "192.168.0.111", "192.168.0.112", "192.168.0.120", "192.168.0.121", "192.168.0.188")
$configuredHosts = @($config.hosts | ForEach-Object { [string]$_ } | Where-Object { $_ })
if ("192.168.0.111" -notin $configuredHosts) {
Set-AgentProperty $config "hosts" @($configuredHosts + "192.168.0.111" | Select-Object -Unique)
$script:ConfigMigrations += [pscustomobject]@{
name = "host111_canonical_recovery_target"
from = ($configuredHosts -join ",")
to = ((@($configuredHosts + "192.168.0.111") | Select-Object -Unique) -join ",")
}
}
Add-DefaultProperty $config "externalHosts" @()
$externalHosts = @($config.externalHosts)
$host111External = $externalHosts | Where-Object { [string]$_.host -eq "192.168.0.111" } | Select-Object -First 1
if (-not $host111External) {
$host111External = [pscustomobject]@{
name = "host111-workstation"
host = "192.168.0.111"
recoveryAction = "wake_on_lan"
macAddresses = @("00:E0:4C:AF:10:0A", "DA:64:76:98:F1:0E")
broadcastAddresses = @("255.255.255.255", "192.168.0.255")
ports = @(7, 9)
packetBursts = 5
verifyTimeoutSeconds = 180
verifyIntervalSeconds = 5
verifyTcpPorts = @(22)
enabled = $true
required = $true
}
Set-AgentProperty $config "externalHosts" @($externalHosts + $host111External)
$script:ConfigMigrations += [pscustomobject]@{
name = "host111_wol_executor"
from = "missing"
to = "wake_on_lan"
}
} else {
Add-DefaultProperty $host111External "name" "host111-workstation"
Add-DefaultProperty $host111External "recoveryAction" "wake_on_lan"
Add-DefaultProperty $host111External "macAddresses" @("00:E0:4C:AF:10:0A", "DA:64:76:98:F1:0E")
Add-DefaultProperty $host111External "broadcastAddresses" @("255.255.255.255", "192.168.0.255")
Add-DefaultProperty $host111External "ports" @(7, 9)
Add-DefaultProperty $host111External "packetBursts" 5
Add-DefaultProperty $host111External "verifyTimeoutSeconds" 180
Add-DefaultProperty $host111External "verifyIntervalSeconds" 5
Add-DefaultProperty $host111External "verifyTcpPorts" @(22)
Add-DefaultProperty $host111External "enabled" $true
Add-DefaultProperty $host111External "required" $true
}
Add-DefaultProperty $config "k3s" ([pscustomobject]@{})
Add-DefaultProperty $config.k3s "jumpHost" "192.168.0.110"
Add-DefaultProperty $config.k3s "preferJumpHost" $true
Add-DefaultProperty $config.k3s "directHosts" @("192.168.0.110", "192.168.0.112")
$directHosts = @($config.k3s.directHosts | ForEach-Object { [string]$_ } | Where-Object { $_ })
if ("192.168.0.112" -notin $directHosts) {
Set-AgentProperty $config.k3s "directHosts" @($directHosts + "192.168.0.112" | Select-Object -Unique)
$script:ConfigMigrations += [pscustomobject]@{
name = "host112_canonical_direct_route"
from = ($directHosts -join ",")
to = ((@($directHosts + "192.168.0.112") | Select-Object -Unique) -join ",")
}
}
Add-DefaultProperty $config "vms" @()
$host112Vm = @($config.vms | Where-Object {
[string]$_.host -eq "192.168.0.112" -or [string]$_.name -eq "host112-kali"
})
$nonHost112Vms = @($config.vms | Where-Object {
-not ([string]$_.host -eq "192.168.0.112" -or [string]$_.name -eq "host112-kali")
})
$canonicalHost112Json = $canonicalHost112Vm | ConvertTo-Json -Depth 10 -Compress
$currentHost112Json = if ($host112Vm.Count -eq 1) {
$host112Vm[0] | ConvertTo-Json -Depth 10 -Compress
} else {
""
}
$host112MigrationRequired = [bool](
$host112Vm.Count -ne 1 -or
$currentHost112Json -cne $canonicalHost112Json
)
if ($host112MigrationRequired) {
$previousHost112Identity = if ($host112Vm.Count -eq 0) {
"missing"
} else {
(@($host112Vm | ForEach-Object { "$([string]$_.name)|$([string]$_.host)|$([string]$_.vmx)" }) -join ";")
}
$script:ConfigMigrations += [pscustomobject]@{
name = "host112_canonical_vmx_path"
from = $previousHost112Identity
to = $canonicalHost112Vmx
}
}
Set-AgentProperty $config "vms" @(@($nonHost112Vms) + @($canonicalHost112Vm))
Add-DefaultProperty $config "autoRecovery" ([pscustomobject]@{})
Add-DefaultProperty $config.autoRecovery "enabled" $true
Add-DefaultProperty $config.autoRecovery "triggerOnHostUnreachable" $true
Add-DefaultProperty $config.autoRecovery "minimumUnreachableHosts" 1
Add-DefaultProperty $config.autoRecovery "cooldownMinutes" 10
Add-DefaultProperty $config "recoverySlo" ([pscustomobject]@{})
Add-DefaultProperty $config.recoverySlo "targetMinutes" 10
Add-DefaultProperty $config.recoverySlo "hostTimeoutSeconds" 90
Add-DefaultProperty $config.recoverySlo "harborTimeoutSeconds" 120
Add-DefaultProperty $config.recoverySlo "workloadTimeoutSeconds" 180
Add-DefaultProperty $config "recoveryCoordinator" ([pscustomobject]@{})
Add-DefaultProperty $config.recoveryCoordinator "enabled" $true
Add-DefaultProperty $config.recoveryCoordinator "controllerHost" "192.168.0.110"
Add-DefaultProperty $config.recoveryCoordinator "readbackTimeoutSeconds" 15
Add-DefaultProperty $config.recoveryCoordinator "pollIntervalSeconds" 10
Add-DefaultProperty $config.recoveryCoordinator "freshWaitTimeoutSeconds" 420
Add-DefaultProperty $config.recoveryCoordinator "maxArtifactAgeSeconds" 600
Add-DefaultProperty $config.recoveryCoordinator "artifactClockSkewSeconds" 90
Add-DefaultProperty $config.recoveryCoordinator "requiredHostAliases" @("99", "110", "111", "112", "120", "121", "188")
Add-DefaultProperty $config "selfHealth" ([pscustomobject]@{})
Add-DefaultProperty $config.selfHealth "requiredHosts" @("192.168.0.110", "192.168.0.111", "192.168.0.112", "192.168.0.120", "192.168.0.121", "192.168.0.188")
$selfHealthHosts = @($config.selfHealth.requiredHosts | ForEach-Object { [string]$_ } | Where-Object { $_ })
if ("192.168.0.111" -notin $selfHealthHosts) {
Set-AgentProperty $config.selfHealth "requiredHosts" @($selfHealthHosts + "192.168.0.111" | Select-Object -Unique)
$script:ConfigMigrations += [pscustomobject]@{
name = "host111_self_health_coverage"
from = ($selfHealthHosts -join ",")
to = ((@($selfHealthHosts + "192.168.0.111") | Select-Object -Unique) -join ",")
}
}
Add-DefaultProperty $config "completionCallback" ([pscustomobject]@{})
Add-DefaultProperty $config.completionCallback "enabled" $true
Add-DefaultProperty $config.completionCallback "url" "https://awoooi.wooo.work/api/v1/agents/agent99/completion-callback"
Add-DefaultProperty $config.completionCallback "tokenEnv" "AGENT99_SRE_RELAY_TOKEN"
Add-DefaultProperty $config.completionCallback "timeoutSeconds" 15
Add-DefaultProperty $config.completionCallback "maxAttempts" 2
Add-DefaultProperty $config.completionCallback "batchLimit" 5
Add-DefaultProperty $config.completionCallback "retryAfterMinutes" 5
Add-DefaultProperty $config "telegram" ([pscustomobject]@{})
Add-DefaultProperty $config.telegram "repeatAfterMinutes" 120
Add-DefaultProperty $config "performance" ([pscustomobject]@{})
Add-DefaultProperty $config.performance "processCpuHostPercentWarning" 25
Add-DefaultProperty $config.performance "processCpuHostPercentCritical" 50
foreach ($sourceAction in @($policyConfig.performance.loadShedding.actions | Where-Object { $_.PSObject.Properties["policyVersion"] })) {
$liveAction = $config.performance.loadShedding.actions | Where-Object { $_.name -eq $sourceAction.name } | Select-Object -First 1
if (-not $liveAction) { continue }
$sourceVersion = [int]$sourceAction.policyVersion
$liveVersion = if ($liveAction.PSObject.Properties["policyVersion"]) { [int]$liveAction.policyVersion } else { 0 }
if ($sourceVersion -le $liveVersion) { continue }
foreach ($propertyName in @("policyVersion", "host", "command", "timeoutSeconds", "postVerifyCommand", "postVerifyMaxDiskUsedPercent", "postVerifyOkRegex", "rollback", "cooldownMinutes", "failureBackoffMinutes", "minSeverity", "reasonMatches", "enabled")) {
if ($sourceAction.PSObject.Properties[$propertyName]) {
Set-AgentProperty $liveAction $propertyName $sourceAction.PSObject.Properties[$propertyName].Value
}
}
$script:ConfigMigrations += [pscustomobject]@{
name = [string]$sourceAction.name
fromPolicyVersion = $liveVersion
toPolicyVersion = $sourceVersion
}
}
$config | ConvertTo-Json -Depth 20 | Set-Content -Path $ConfigPath -Encoding UTF8
$persistedConfig = Get-Content $ConfigPath -Raw | ConvertFrom-Json
$persistedHost111User = if ($persistedConfig.sshUsers -and $persistedConfig.sshUsers.PSObject.Properties["192.168.0.111"]) { [string]$persistedConfig.sshUsers.PSObject.Properties["192.168.0.111"].Value } else { "" }
$persistedHost112User = if ($persistedConfig.sshUsers -and $persistedConfig.sshUsers.PSObject.Properties["192.168.0.112"]) { [string]$persistedConfig.sshUsers.PSObject.Properties["192.168.0.112"].Value } else { "" }
$persistedDirectHosts = if ($persistedConfig.k3s -and $persistedConfig.k3s.PSObject.Properties["directHosts"]) { @($persistedConfig.k3s.directHosts | ForEach-Object { [string]$_ }) } else { @() }
$persistedHost112Vm = @($persistedConfig.vms | Where-Object {
[string]$_.host -eq "192.168.0.112" -or [string]$_.name -eq "host112-kali"
})
$persistedHost112Vmx = if ($persistedHost112Vm.Count -eq 1) { [string]$persistedHost112Vm[0].vmx } else { "" }
$persistedHosts = @($persistedConfig.hosts | ForEach-Object { [string]$_ })
$persistedSelfHealthHosts = @($persistedConfig.selfHealth.requiredHosts | ForEach-Object { [string]$_ })
$persistedHost111External = @($persistedConfig.externalHosts | Where-Object { [string]$_.host -eq "192.168.0.111" })
$host111ExternalReady = [bool](
$persistedHost111External.Count -eq 1 -and
[string]$persistedHost111External[0].recoveryAction -eq "wake_on_lan" -and
[bool]$persistedHost111External[0].enabled -and
[bool]$persistedHost111External[0].required -and
@($persistedHost111External[0].macAddresses).Count -ge 1 -and
22 -in @($persistedHost111External[0].verifyTcpPorts | ForEach-Object { [int]$_ })
)
if (
$persistedHost111User -ne "ooo" -or
$persistedHost112User -ne "kali" -or
"192.168.0.112" -notin $persistedDirectHosts -or
"192.168.0.111" -notin $persistedHosts -or
"192.168.0.111" -notin $persistedSelfHealthHosts -or
-not $host111ExternalReady -or
$persistedHost112Vm.Count -ne 1 -or
[string]$persistedHost112Vm[0].name -ne [string]$canonicalHost112Vm.name -or
[string]$persistedHost112Vm[0].host -ne [string]$canonicalHost112Vm.host -or
$persistedHost112Vmx -ne $canonicalHost112Vmx -or
-not (Test-Path -LiteralPath $persistedHost112Vmx -PathType Leaf)
) {
throw "Canonical Host111 SSH/recovery and Host112 direct/SSD config post-verifier failed."
}
$manifestRows = @()
foreach ($staged in $stagedFiles) {
$livePath = Join-Path $BinDir $staged.name
$runtimeSha = if (Test-Path $livePath) { (Get-FileHash -Algorithm SHA256 -Path $livePath).Hash.ToLowerInvariant() } else { "" }
$matched = [bool]($runtimeSha -and $runtimeSha -eq $staged.sha256)
$manifestRows += [pscustomobject]@{
name = $staged.name
sourceExists = $true
runtimeExists = [bool](Test-Path $livePath)
sourceSha256 = $staged.sha256
runtimeSha256 = $runtimeSha
contentMatched = $matched
matched = $matched
}
}
$runtimeMatched = [bool](@($manifestRows | Where-Object { -not $_.matched }).Count -eq 0)
[pscustomobject]@{
schemaVersion = "agent99_runtime_manifest_v1"
generatedAt = (Get-Date -Format o)
sourceRevision = $SourceRevision
runtimeMatched = $runtimeMatched
fileCount = $manifestRows.Count
mismatchCount = @($manifestRows | Where-Object { -not $_.matched }).Count
files = $manifestRows
} | ConvertTo-Json -Depth 10 | Set-Content -Path $ManifestPath -Encoding UTF8
if (-not $runtimeMatched) { throw "Promoted Agent99 runtime does not match staged source." }
& (Join-Path $BinDir "agent99-contract-check.ps1") -SourceRoot $BinDir | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Post-promotion Agent99 contract check failed." }
& (Join-Path $BinDir "agent99-synthetic-tests.ps1") -SourceRoot $BinDir -WorkRoot (Join-Path $StageDir "live-synthetic-work") | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Post-promotion Agent99 synthetic tests failed." }
} catch {
Restore-Agent99Deployment $promotionRows $configBackedUp $manifestBackedUp
Write-DeployEvidence $false "rolled_back" $_.Exception.Message $stagedFiles $null
throw
}
$taskResult = $null
if ($RegisterTasks) {
try {
& (Join-Path $BinDir "agent99-register-tasks.ps1") -AgentRoot $AgentRoot | Out-Null
$taskResult = [pscustomobject]@{ ok = $true; status = "registered" }
} catch {
$taskResult = [pscustomobject]@{ ok = $false; status = "registration_failed"; error = $_.Exception.Message }
}
}
$deployOk = [bool](-not $taskResult -or $taskResult.ok)
$deployStatus = if ($deployOk) { "deployed" } else { "deployed_task_registration_failed" }
Write-DeployEvidence $deployOk $deployStatus $(if ($taskResult -and -not $taskResult.ok) { $taskResult.error } else { $null }) $stagedFiles $taskResult
[pscustomobject]@{
ok = $deployOk
status = $deployStatus
sourceRevision = $SourceRevision
evidence = $EvidencePath
backupDir = $BackupDir
runtimeManifest = $ManifestPath
taskRegistration = $taskResult
files = $stagedFiles
} | ConvertTo-Json -Depth 10
if (-not $deployOk) { exit 2 }
exit 0