Files
NODEDC_MISSION_CORE/simulation/ai-polygon/Install-RealtimeWorker.ps1
T

142 lines
7.9 KiB
PowerShell

param(
[Parameter(Mandatory=$true)][ValidatePattern('^[a-f0-9]{16}$')][string]$Release,
[switch]$Remove
)
$ErrorActionPreference='Stop'
$ProgressPreference='SilentlyContinue'
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
$root='D:\NDC_MISSIONCORE\runtime\simulation'
$taskName='ndc-ai-polygon-worker'
$isaac=Join-Path $root 'isaac-sim-6.1.0'
$python=Join-Path $isaac 'kit\python\python.exe'
$worker=Join-Path $root "releases\ai-polygon-$Release\simulation\ai-polygon\realtime_worker.py"
$firewallBackup=Join-Path $root 'private\stream-firewall-backup.json'
if (Test-Path (Join-Path $root 'state\active.json')) {
throw 'Finish or reconcile the active episode before changing the service.'
}
if ($Remove) {
if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
Stop-ScheduledTask -TaskName $taskName
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
}
foreach ($name in @('ndc-ai-polygon-signal','ndc-ai-polygon-media')) {
Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue | Remove-NetFirewallRule
}
if (Test-Path $firewallBackup) {
foreach ($rule in (Get-Content $firewallBackup -Raw | ConvertFrom-Json)) {
Get-NetFirewallRule -Name $rule.Name -ErrorAction Stop | Set-NetFirewallRule -Action Block -LocalPort $rule.LocalPort
Get-NetFirewallRule -Name $rule.ExceptionName -ErrorAction SilentlyContinue | Remove-NetFirewallRule
}
}
return
}
foreach ($file in @($python,$worker,(Join-Path $root 'private\worker.token'))) {
if (!(Test-Path $file)) { throw "Missing prepared payload: $file" }
}
$payloadRoot=Join-Path $root "releases\ai-polygon-$Release"
$manifestPath=Join-Path $root "releases\ai-polygon-$Release.manifest.json"
if (!(Get-FileHash $manifestPath -Algorithm SHA256).Hash.ToLower().StartsWith($Release)) {
throw 'Manifest identity mismatch'
}
$manifest=Get-Content $manifestPath -Raw | ConvertFrom-Json
$verified=0
foreach($entry in $manifest.PSObject.Properties) {
$file=[IO.Path]::GetFullPath((Join-Path $payloadRoot $entry.Name))
if (!$file.StartsWith($payloadRoot+'\',[StringComparison]::OrdinalIgnoreCase) -or
$entry.Value -notmatch '^[a-f0-9]{64}$' -or
(Get-FileHash $file -Algorithm SHA256).Hash.ToLower() -ne $entry.Value) {
throw "Payload identity mismatch: $($entry.Name)"
}
$verified++
}
if($verified -lt 1) { throw 'Empty release manifest' }
$address=(& 'C:\Program Files\Tailscale\tailscale.exe' ip -4).Trim()
$clientAddress=($env:SSH_CONNECTION -split ' ')[0]
foreach ($value in @($address,$clientAddress)) {
$ip=[Net.IPAddress]::Parse($value)
$bytes=$ip.GetAddressBytes()
if ($bytes.Length -ne 4 -or $bytes[0] -ne 100 -or $bytes[1] -lt 64 -or $bytes[1] -gt 127) {
throw 'This installer admits only the existing private Tailscale path.'
}
}
$kit=Join-Path $isaac 'kit\python\kit.exe'
# Windows' user-prompt Block rules override scoped Allow rules. Keep those rules
# blocking every other port; for the two admitted ports retain an explicit Block
# for every remote address EXCEPT the current private operator. Preserve rollback.
$oldRules=@()
if (Test-Path $firewallBackup) { $oldRules=@(Get-Content $firewallBackup -Raw | ConvertFrom-Json) }
$clientBytes=([Net.IPAddress]::Parse($clientAddress)).GetAddressBytes()
# Windows rejects prefix /0 here. These disjoint CIDRs are the exact IPv4
# complement of the operator /32, plus both IPv6 halves. No address is exposed.
$number=[uint64]$clientBytes[0]*16777216+[uint64]$clientBytes[1]*65536+[uint64]$clientBytes[2]*256+$clientBytes[3]
$excluded=@('::/1','8000::/1')
for($prefix=1;$prefix -le 32;$prefix++) {
$bit=[uint64][math]::Pow(2,32-$prefix)
$network=([uint64]([math]::Floor($number/$bit)*$bit)) -bxor $bit
$networkBytes=[byte[]]@((($network -shr 24) -band 255),(($network -shr 16) -band 255),(($network -shr 8) -band 255),($network -band 255))
$excluded+="$([Net.IPAddress]::new($networkBytes))/$prefix"
}
try {
$blocked=Get-NetFirewallRule | Where-Object {
$_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and
($_.Action -eq 'Block' -or $_.Name -in @($oldRules.Name)) -and
$_.Name -notlike 'ndc-*' -and (($_ | Get-NetFirewallApplicationFilter).Program -ieq $kit)
}
foreach($rule in $blocked) {
$filter=$rule | Get-NetFirewallPortFilter
$port=if($filter.Protocol -eq 'TCP'){49100}elseif($filter.Protocol -eq 'UDP'){47998}else{throw 'Unexpected Isaac block protocol'}
$exceptionName="ndc-ai-polygon-block-other-$($filter.Protocol.ToLower())"
$backup=$oldRules | Where-Object {$_.Name -eq $rule.Name}
if(!$backup) {
if($filter.LocalPort -ne 'Any' -or ($rule | Get-NetFirewallAddressFilter).RemoteAddress -ne 'Any') {
throw 'Inspect a nonstandard Isaac firewall policy before migrating it.'
}
$oldRules+=@{Name=$rule.Name;LocalPort='Any';ExceptionName=$exceptionName}
$oldRules | ConvertTo-Json -Depth 4 | Set-Content -Encoding UTF8 $firewallBackup
}
Get-NetFirewallRule -Name $exceptionName -ErrorAction SilentlyContinue | Remove-NetFirewallRule
New-NetFirewallRule -Name $exceptionName -DisplayName $exceptionName -Group 'ndc-ai-polygon' `
-Direction Inbound -Action Block -Program $kit -Protocol $filter.Protocol -LocalPort $port `
-RemoteAddress $excluded -Profile $rule.Profile | Out-Null
$rule | Set-NetFirewallRule -Action Block -LocalPort @("1-$($port-1)","$($port+1)-65535")
}
foreach ($spec in @(@('ndc-ai-polygon-signal','TCP',49100),@('ndc-ai-polygon-media','UDP',47998))) {
$existing=Get-NetFirewallRule -Name $spec[0] -ErrorAction SilentlyContinue
if ($existing) { $existing | Remove-NetFirewallRule }
New-NetFirewallRule -Name $spec[0] -DisplayName $spec[0] -Group 'ndc-ai-polygon' `
-Direction Inbound -Action Allow -Program $kit -Protocol $spec[1] -LocalPort $spec[2] `
-LocalAddress $address -RemoteAddress $clientAddress -Profile Any | Out-Null
}
} catch {
foreach($old in $oldRules) {
Get-NetFirewallRule -Name $old.Name -ErrorAction Stop | Set-NetFirewallRule -Action Block -LocalPort $old.LocalPort
Get-NetFirewallRule -Name $old.ExceptionName -ErrorAction SilentlyContinue | Remove-NetFirewallRule
}
foreach($name in @('ndc-ai-polygon-signal','ndc-ai-polygon-media')) {
Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue | Remove-NetFirewallRule
}
throw
}
if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) {
Stop-ScheduledTask -TaskName $taskName
}
$arguments="-u `"$worker`" --core http://127.0.0.1:18080 --token-file `"$root\private\worker.token`" --state `"$root\state`" --isaac `"$isaac`" --stream-address $address"
$action=New-ScheduledTaskAction -Execute $python -Argument $arguments -WorkingDirectory $root
$user=[Security.Principal.WindowsIdentity]::GetCurrent().Name
$principal=New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited
$trigger=New-ScheduledTaskTrigger -AtLogOn -User $user
$settings=New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -RestartCount 3 `
-RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal `
-Trigger $trigger -Settings $settings -Force | Out-Null
Start-ScheduledTask -TaskName $taskName
[ordered]@{schema_version='missioncore.ai-polygon-service/v1'; release=$Release;
verified_files=$verified;
installed_at=[DateTime]::UtcNow.ToString('o'); task=$taskName;
lifecycle='Windows task, at user logon; independent of SSH/viewer';
control='existing operator tunnel 18080'; video='private Tailscale only';
rollback='Install-RealtimeWorker.ps1 -Release <previous> or -Remove; requires no active episode'
} | ConvertTo-Json | Set-Content -Encoding UTF8 (Join-Path $root 'realtime-service-installation.json')
Get-Content (Join-Path $root 'realtime-service-installation.json')