#Requires -Version 7.0 [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$InstallerPath, [ValidateRange(30, 600)] [int]$StartupTimeoutSeconds = 150, [string]$ReceiptPath, [switch]$KeepArtifacts, [switch]$RequireAuthenticodeSignature, [string]$ExpectedSignerThumbprint, [string]$ExpectedSignerSubject, [string]$ExpectedSourceRevision, [int]$WebViewDebugPort = 0, [switch]$VerifyManagedModel, [switch]$RequireVersionToVersionUpgrade, [string]$PreviousInstallerPath, [string]$ExpectedPreviousInstallerSha256, [string]$ExpectedPreviousVersion, [string]$ExpectedPreviousSourceRevision, [string]$ExpectedCandidateInstallerSha256, [string]$ExpectedCandidateVersion ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Assert-True { param( [Parameter(Mandatory = $true)] [bool]$Condition, [Parameter(Mandatory = $true)] [string]$Message ) if (-not $Condition) { throw $Message } } function Assert-CleanRepositoryWorktree { param( [Parameter(Mandatory = $true)] [string]$GitExecutable, [Parameter(Mandatory = $true)] [string]$RepositoryRoot ) $sourceStatus = @( & $GitExecutable -C $RepositoryRoot status --porcelain=v1 --untracked-files=all ) if ($LASTEXITCODE -ne 0) { throw 'Could not inspect the repository source state.' } if ($sourceStatus.Count -ne 0) { $sourceDetails = ($sourceStatus | ForEach-Object { [string]$_ }) -join [Environment]::NewLine throw "Installer certification requires the complete repository worktree to match the expected revision.$([Environment]::NewLine)$sourceDetails" } } function Test-CertificateTimestamp { param([AllowNull()] [object]$Value) if ($null -eq $Value) { return $false } if ($Value -is [DateTime] -or $Value -is [DateTimeOffset]) { return $true } $text = [string]$Value if ([string]::IsNullOrWhiteSpace($text)) { return $false } $parsed = [DateTimeOffset]::MinValue return [DateTimeOffset]::TryParse( $text, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind, [ref]$parsed ) } function Assert-ExpectedAuthenticodeSignature { param( [Parameter(Mandatory = $true)] [object]$Signature, [AllowEmptyString()] [string]$ExpectedThumbprint, [AllowEmptyString()] [string]$ExpectedSubject, [Parameter(Mandatory = $true)] [string]$ArtifactLabel ) $hasExpectedThumbprint = -not [string]::IsNullOrWhiteSpace($ExpectedThumbprint) $hasExpectedSubject = -not [string]::IsNullOrWhiteSpace($ExpectedSubject) Assert-True ($hasExpectedThumbprint -xor $hasExpectedSubject) ` 'Exactly one expected signer identity binding is required.' Assert-True ([string]$Signature.SignatureType -eq 'Authenticode') ` "$ArtifactLabel does not contain a portable embedded Authenticode signature." Assert-True ($Signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid) ` "$ArtifactLabel Authenticode signature is not valid: $($Signature.Status)" Assert-True ($null -ne $Signature.SignerCertificate) ` "$ArtifactLabel has no Authenticode signer certificate." if ($hasExpectedThumbprint) { $normalizedExpectedThumbprint = ($ExpectedThumbprint -replace '\s', '').ToUpperInvariant() Assert-True ($normalizedExpectedThumbprint -match '^[0-9A-F]{40}$') ` 'Expected signer thumbprint must be exactly 40 hexadecimal characters.' Assert-True ( [string]::Equals( ($Signature.SignerCertificate.Thumbprint -replace '\s', '').ToUpperInvariant(), $normalizedExpectedThumbprint, [System.StringComparison]::Ordinal ) ) "$ArtifactLabel signer does not match the imported production certificate." } else { Assert-True ( [string]::Equals( [string]$Signature.SignerCertificate.Subject, $ExpectedSubject, [System.StringComparison]::Ordinal ) ) "$ArtifactLabel signer subject does not match the approved production identity." } Assert-True ($null -ne $Signature.TimeStamperCertificate) ` "$ArtifactLabel has no validated Authenticode timestamp certificate." } function Assert-LifecycleReceiptApprovedSigner { param( [Parameter(Mandatory = $true)] [object]$Receipt, [AllowEmptyString()] [string]$ExpectedThumbprint, [AllowEmptyString()] [string]$ExpectedSubject ) $hasExpectedThumbprint = -not [string]::IsNullOrWhiteSpace($ExpectedThumbprint) $hasExpectedSubject = -not [string]::IsNullOrWhiteSpace($ExpectedSubject) Assert-True ($hasExpectedThumbprint -xor $hasExpectedSubject) ` 'Exactly one expected signer identity binding is required.' $expectedSigner = if ($hasExpectedSubject) { $ExpectedSubject } else { $normalizedExpectedThumbprint = ($ExpectedThumbprint -replace '\s', '').ToUpperInvariant() Assert-True ($normalizedExpectedThumbprint -match '^[0-9A-F]{40}$') ` 'Expected signer thumbprint must be exactly 40 hexadecimal characters.' $normalizedExpectedThumbprint } $comparison = if ($hasExpectedSubject) { [System.StringComparison]::Ordinal } else { [System.StringComparison]::OrdinalIgnoreCase } foreach ($artifact in @( $Receipt.previousInstaller, $Receipt.previousInstalledApp, $Receipt.installer, $Receipt.installedApp )) { $actualSigner = if ($hasExpectedSubject) { [string]$artifact.signerSubject } else { [string]$artifact.signerThumbprint } Assert-True ([string]::Equals( $actualSigner, $expectedSigner, $comparison )) 'Previous and candidate artifacts are not signed by the same approved signer.' } } function ConvertTo-StrictSemanticVersion { param( [Parameter(Mandatory = $true)] [string]$Value, [Parameter(Mandatory = $true)] [string]$Label ) Assert-True ($Value -match '^\d+\.\d+\.\d+$') ` "$Label must use strict numeric x.y.z format." return [version]$Value } function Get-InstalledProductVersion { param( [Parameter(Mandatory = $true)] [string]$ExecutablePath, [Parameter(Mandatory = $true)] [string]$ArtifactLabel ) $rawVersion = [string](Get-Item -LiteralPath $ExecutablePath).VersionInfo.ProductVersion Assert-True (-not [string]::IsNullOrWhiteSpace($rawVersion)) ` "$ArtifactLabel has no embedded product version." Assert-True ($rawVersion -match '^(?\d+\.\d+\.\d+)(?:\.0)?(?:[+-].*)?$') ` "$ArtifactLabel product version is not a supported x.y.z value: $rawVersion" return [ordered]@{ raw = $rawVersion normalized = $Matches['version'] } } function New-AuthenticodeArtifactReceipt { param( [Parameter(Mandatory = $true)] [System.IO.FileInfo]$File, [Parameter(Mandatory = $true)] [object]$Signature ) $artifact = [ordered]@{ name = $File.Name sha256 = (Get-FileHash -LiteralPath $File.FullName -Algorithm SHA256).Hash sizeBytes = $File.Length authenticodeStatus = [string]$Signature.Status signatureType = [string]$Signature.SignatureType signerSubject = $null signerThumbprint = $null timestampAuthoritySubject = $null timestampAuthorityThumbprint = $null timestampAuthorityNotBefore = $null timestampAuthorityNotAfter = $null } if ($Signature.SignerCertificate) { $artifact.signerSubject = $Signature.SignerCertificate.Subject $artifact.signerThumbprint = $Signature.SignerCertificate.Thumbprint } if ($Signature.TimeStamperCertificate) { $artifact.timestampAuthoritySubject = $Signature.TimeStamperCertificate.Subject $artifact.timestampAuthorityThumbprint = $Signature.TimeStamperCertificate.Thumbprint $artifact.timestampAuthorityNotBefore = $Signature.TimeStamperCertificate.NotBefore.ToUniversalTime().ToString('o') $artifact.timestampAuthorityNotAfter = $Signature.TimeStamperCertificate.NotAfter.ToUniversalTime().ToString('o') } return $artifact } function Assert-ArtifactIdentity { param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [string]$ExpectedSha256, [AllowEmptyString()] [string]$ExpectedSignerThumbprint, [AllowEmptyString()] [string]$ExpectedSignerSubject, [Parameter(Mandatory = $true)] [string]$ArtifactLabel ) Assert-True (Test-Path -LiteralPath $FilePath -PathType Leaf) ` "$ArtifactLabel is missing." $actualSha256 = (Get-FileHash -LiteralPath $FilePath -Algorithm SHA256).Hash Assert-True ([string]::Equals( $actualSha256, $ExpectedSha256, [System.StringComparison]::OrdinalIgnoreCase )) "$ArtifactLabel SHA-256 changed during certification." $signature = Get-AuthenticodeSignature -LiteralPath $FilePath Assert-ExpectedAuthenticodeSignature ` $signature $ExpectedSignerThumbprint $ExpectedSignerSubject $ArtifactLabel } function Get-FreeTcpPort { $listener = [System.Net.Sockets.TcpListener]::new( [System.Net.IPAddress]::Loopback, 0 ) try { $listener.Start() return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port } finally { $listener.Stop() } } function Assert-TcpPortAvailable { param([Parameter(Mandatory = $true)] [int]$Port) Assert-True (Test-TcpPortAvailable $Port) ` "Required desktop port $Port is already in use; refusing to disturb another service." } function Assert-VaultKeyAclRestricted { param([Parameter(Mandatory = $true)] [string]$KeyPath) Assert-True (Test-Path -LiteralPath $KeyPath -PathType Leaf) ` 'Windows vault key was not created during first boot.' $acl = Get-Acl -LiteralPath $KeyPath Assert-True ($acl.AreAccessRulesProtected) ` 'Windows vault key still inherits filesystem permissions.' $currentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User Assert-True ($null -ne $currentSid) 'Could not resolve the current Windows security identifier.' $ownerSid = $acl.GetOwner([Security.Principal.SecurityIdentifier]) Assert-True ([string]::Equals( $ownerSid.Value, $currentSid.Value, [System.StringComparison]::OrdinalIgnoreCase )) 'Windows vault key is not owned by the current user.' $rules = @( $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]) ) Assert-True ($rules.Count -eq 1) ` "Windows vault key must have exactly one access rule; found $($rules.Count)." $allowRules = @( $rules | Where-Object { $_.AccessControlType -eq [Security.AccessControl.AccessControlType]::Allow } ) $currentUserAllows = @( $allowRules | Where-Object { [string]::Equals( $_.IdentityReference.Value, $currentSid.Value, [System.StringComparison]::OrdinalIgnoreCase ) } ) $unexpectedAllows = @( $allowRules | Where-Object { -not [string]::Equals( $_.IdentityReference.Value, $currentSid.Value, [System.StringComparison]::OrdinalIgnoreCase ) } ) $unexpectedPrincipals = @( $unexpectedAllows | ForEach-Object { $_.IdentityReference.Value } ) Assert-True ($unexpectedAllows.Count -eq 0) ` "Windows vault key grants access to unexpected principals: $($unexpectedPrincipals -join ', ')" $fullControl = [Security.AccessControl.FileSystemRights]::FullControl $hasCurrentUserFullControl = @( $currentUserAllows | Where-Object { ($_.FileSystemRights -band $fullControl) -eq $fullControl } ).Count -gt 0 Assert-True $hasCurrentUserFullControl ` 'Windows vault key does not grant the current user full control.' } function Test-TcpPortAvailable { param([Parameter(Mandatory = $true)] [int]$Port) $listener = [System.Net.Sockets.TcpListener]::new( [System.Net.IPAddress]::Loopback, $Port ) try { $listener.Start() return $true } catch { return $false } finally { $listener.Stop() } } function Stop-StartedProcessTree { param([Parameter(Mandatory = $true)] [System.Diagnostics.Process]$Process) $taskkill = Join-Path $env:SystemRoot 'System32\taskkill.exe' $killInfo = [System.Diagnostics.ProcessStartInfo]::new() $killInfo.FileName = $taskkill $killInfo.Arguments = "/PID $($Process.Id) /T /F" $killInfo.UseShellExecute = $false $killInfo.CreateNoWindow = $true $killInfo.RedirectStandardOutput = $true $killInfo.RedirectStandardError = $true $killInfo.WorkingDirectory = Split-Path -Parent $taskkill $killProcess = [System.Diagnostics.Process]::new() $killProcess.StartInfo = $killInfo try { Assert-True ($killProcess.Start()) "Could not start taskkill for process $($Process.Id)" Assert-True ($killProcess.WaitForExit(20000)) "Timed out terminating process tree $($Process.Id)" $killProcess.WaitForExit() if ($killProcess.ExitCode -ne 0) { $detail = $killProcess.StandardError.ReadToEnd().Trim() throw "Could not terminate process tree $($Process.Id): $detail" } Assert-True ($Process.WaitForExit(20000)) "Process tree root $($Process.Id) did not exit" } finally { $killProcess.Dispose() } } function Remove-CertificationControlEnvironment { param([Parameter(Mandatory = $true)] [System.Diagnostics.ProcessStartInfo]$Info) $explicitNames = @( 'CI', 'GH_TOKEN', 'GITHUB_TOKEN', 'WINDOWS_CODESIGN_PFX_BASE64', 'WINDOWS_CODESIGN_PFX_PASSWORD', 'WINDOWS_CODESIGN_APPROVED_SUBJECT', 'WINDOWS_CODESIGN_APPROVED_THUMBPRINT', 'WINDOWS_UPGRADE_BASE_SHA256', 'WAGGLE_APPROVED_CODESIGN_SUBJECT', 'WAGGLE_APPROVED_CODESIGN_THUMBPRINT', 'WAGGLE_CODESIGN_SUBJECT', 'WAGGLE_CODESIGN_THUMBPRINT', 'WAGGLE_RELEASE_VERSION' ) foreach ($name in @($Info.Environment.Keys)) { if ($name -match '^(?:ACTIONS_|GITHUB_|RUNNER_|WAGGLE_UPGRADE_BASE_)' -or $explicitNames -contains $name) { $null = $Info.Environment.Remove($name) } } } function Test-TcpPortHasListener { param([Parameter(Mandatory = $true)] [int]$Port) return @( [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners() | Where-Object { $_.Port -eq $Port } ).Count -gt 0 } function Invoke-RawProcess { param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [string]$Arguments, [ValidateRange(1, 900)] [int]$TimeoutSeconds = 300 ) $info = [System.Diagnostics.ProcessStartInfo]::new() $info.FileName = $FilePath $info.Arguments = $Arguments $info.UseShellExecute = $false $info.CreateNoWindow = $true $info.WorkingDirectory = Split-Path -Parent $FilePath Remove-CertificationControlEnvironment $info $process = [System.Diagnostics.Process]::new() $process.StartInfo = $info try { Assert-True ($process.Start()) "Could not start $FilePath" if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { Stop-StartedProcessTree $process throw "Timed out after ${TimeoutSeconds}s: $FilePath $Arguments" } $process.WaitForExit() if ($process.ExitCode -ne 0) { throw "Process exited $($process.ExitCode): $FilePath $Arguments" } } finally { $process.Dispose() } } function Start-InstalledApp { param( [Parameter(Mandatory = $true)] [string]$ExecutablePath, [ValidateRange(1024, 65535)] [int]$DebugPort = 0 ) $info = [System.Diagnostics.ProcessStartInfo]::new() $info.FileName = $ExecutablePath $info.UseShellExecute = $false $info.WorkingDirectory = Split-Path -Parent $ExecutablePath Remove-CertificationControlEnvironment $info if ($DebugPort -gt 0) { $info.Environment['WAGGLE_CERTIFIER_WEBVIEW_DEBUG_PORT'] = [string]$DebugPort } $process = [System.Diagnostics.Process]::new() $process.StartInfo = $info Assert-True ($process.Start()) "Could not start installed Waggle: $ExecutablePath" return $process } function Invoke-JsonRequest { param( [Parameter(Mandatory = $true)] [string]$Uri, [hashtable]$Headers = @{}, [ValidateRange(1, 30)] [int]$TimeoutSeconds = 5 ) return Invoke-RestMethod -Uri $Uri -Method Get -Headers $Headers -TimeoutSec $TimeoutSeconds } function Invoke-BuiltInProxyLivenessProbe { param( [Parameter(Mandatory = $true)] [string]$Uri, [ValidateRange(1, 30)] [int]$AttemptTimeoutSeconds = 5, [ValidateRange(1, 2)] [int]$MaxAttempts = 2, [ValidateRange(0, 2000)] [int]$RetryDelayMilliseconds = 250 ) for ($attempt = 1; $attempt -le $MaxAttempts; $attempt += 1) { try { return Invoke-JsonRequest -Uri $Uri -TimeoutSeconds $AttemptTimeoutSeconds } catch { if ($attempt -ge $MaxAttempts) { throw } if ($RetryDelayMilliseconds -gt 0) { Start-Sleep -Milliseconds $RetryDelayMilliseconds } } } } function Test-TransientLoopbackRequestFailure { param( [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) $responseProperty = $ErrorRecord.Exception.PSObject.Properties['Response'] if ($null -ne $responseProperty) { $response = $responseProperty.Value try { return @(408, 425, 429, 500, 502, 503, 504) -contains [int]$response.StatusCode } catch { return $false } } $exception = $ErrorRecord.Exception return ( $exception -is [System.Net.Http.HttpRequestException] -or $exception -is [System.Net.WebException] -or $exception -is [System.Threading.Tasks.TaskCanceledException] -or $exception -is [System.TimeoutException] ) } function Invoke-SessionTokenBootstrapProbe { param( [Parameter(Mandatory = $true)] [string]$Uri, [hashtable]$Headers = @{}, [ValidateRange(1, 30)] [int]$AttemptTimeoutSeconds = 5, [ValidateRange(1, 2)] [int]$MaxAttempts = 2, [ValidateRange(0, 2000)] [int]$RetryDelayMilliseconds = 250 ) for ($attempt = 1; $attempt -le $MaxAttempts; $attempt += 1) { try { return Invoke-JsonRequest ` -Uri $Uri ` -Headers $Headers ` -TimeoutSeconds $AttemptTimeoutSeconds } catch { if ( $attempt -ge $MaxAttempts -or -not (Test-TransientLoopbackRequestFailure -ErrorRecord $_) ) { throw } if ($RetryDelayMilliseconds -gt 0) { Start-Sleep -Milliseconds $RetryDelayMilliseconds } } } } function Invoke-JsonPostRequest { param( [Parameter(Mandatory = $true)] [string]$Uri, [Parameter(Mandatory = $true)] [hashtable]$Body, [hashtable]$Headers = @{}, [ValidateRange(1, 3600)] [int]$TimeoutSeconds = 30 ) $json = $Body | ConvertTo-Json -Compress -Depth 8 return Invoke-WebRequest ` -Uri $Uri ` -Method Post ` -Headers $Headers ` -ContentType 'application/json; charset=utf-8' ` -Body ([System.Text.Encoding]::UTF8.GetBytes($json)) ` -TimeoutSec $TimeoutSeconds ` -UseBasicParsing } function Get-CertificateSessionHeaders { param( [Parameter(Mandatory = $true)] [string]$BaseUrl, [Parameter(Mandatory = $true)] [string]$NodeExecutable, [Parameter(Mandatory = $true)] [string]$BootstrapHelperPath, [ValidateRange(1024, 65535)] [int]$DebugPort, [switch]$AllowLegacyUi ) Assert-True (Test-Path -LiteralPath $BootstrapHelperPath -PathType Leaf) ` 'Tauri bootstrap helper is missing.' $stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) ` "waggle-bootstrap-token-$([Guid]::NewGuid().ToString('N')).stderr.log" $tokenResponse = $null try { $helperArguments = @( '--experimental-websocket', $BootstrapHelperPath, '--port', [string]$DebugPort, '--timeout-ms', '60000' ) if ($AllowLegacyUi) { $helperArguments += '--allow-legacy-ui' } $tokenJson = & $NodeExecutable @helperArguments 2> $stderrPath $tokenExitCode = $LASTEXITCODE $tokenErrorRaw = if (Test-Path -LiteralPath $stderrPath) { Get-Content -Raw -LiteralPath $stderrPath } else { '' } $tokenError = (@($tokenErrorRaw) -join [Environment]::NewLine).Trim() Assert-True ($tokenExitCode -eq 0) ` "Tauri bootstrap IPC helper failed: $tokenError" $tokenJsonText = ([string](@($tokenJson) -join [Environment]::NewLine)).Trim() $tokenPayload = $null if (-not [string]::IsNullOrWhiteSpace($tokenJsonText)) { try { $tokenPayload = ConvertFrom-Json -InputObject $tokenJsonText -ErrorAction Stop } catch { $tokenPayload = $null } } Assert-True ($null -ne $tokenPayload) ` 'Tauri bootstrap IPC helper returned invalid JSON output.' $bootstrapTokenProperty = $tokenPayload.PSObject.Properties['bootstrapToken'] Assert-True ($null -ne $bootstrapTokenProperty) ` 'Tauri bootstrap IPC helper returned no credential field.' $uiReadyProperty = $tokenPayload.PSObject.Properties['uiReady'] $uiStartupStateProperty = $tokenPayload.PSObject.Properties['uiStartupState'] $uiPathProperty = $tokenPayload.PSObject.Properties['uiPath'] $uiTextLengthProperty = $tokenPayload.PSObject.Properties['uiTextLength'] Assert-True ( $null -ne $uiReadyProperty -and $uiReadyProperty.Value -is [bool] -and $uiReadyProperty.Value ) 'Installed Waggle WebView did not render its application shell.' Assert-True ( $null -ne $uiStartupStateProperty -and $uiStartupStateProperty.Value -is [string] -and [string]$uiStartupStateProperty.Value -ceq $( if ($AllowLegacyUi) { 'legacy-ready' } else { 'ready' } ) ) 'Installed Waggle WebView did not commit its application shell.' Assert-True ( $null -ne $uiPathProperty -and $uiPathProperty.Value -is [string] -and ([string]$uiPathProperty.Value).StartsWith('/') ) 'Installed Waggle WebView returned an invalid application route.' Assert-True ( $null -ne $uiTextLengthProperty -and ($uiTextLengthProperty.Value -is [int] -or $uiTextLengthProperty.Value -is [long]) -and [long]$uiTextLengthProperty.Value -gt 0 ) 'Installed Waggle WebView rendered no visible application content.' $bootstrapToken = [string]$bootstrapTokenProperty.Value Assert-True ($bootstrapToken.Length -ge 32 -and $bootstrapToken.Length -le 200) ` 'Tauri bootstrap IPC helper returned an invalid credential.' $tokenResponse = Invoke-SessionTokenBootstrapProbe ` -Uri "$BaseUrl/api/auth/session-token" ` -Headers @{ 'x-waggle-desktop-bootstrap' = $bootstrapToken } } finally { if (Test-Path -LiteralPath $stderrPath) { Remove-Item -LiteralPath $stderrPath -Force -ErrorAction SilentlyContinue } } Assert-True ($null -ne $tokenResponse) ` 'Session-token bootstrap returned no JSON response.' $sessionTokenProperty = $tokenResponse.PSObject.Properties['token'] Assert-True ($null -ne $sessionTokenProperty) ` 'Session-token bootstrap returned no token field.' $sessionToken = [string]$sessionTokenProperty.Value Assert-True (-not [string]::IsNullOrWhiteSpace($sessionToken)) ` 'Session-token bootstrap returned no token.' return @{ Authorization = "Bearer $sessionToken" } } function Assert-CertificateLifecycleData { param( [Parameter(Mandatory = $true)] [string]$BaseUrl, [Parameter(Mandatory = $true)] [hashtable]$Headers, [Parameter(Mandatory = $true)] [object]$State, [Parameter(Mandatory = $true)] [string]$DataDir ) $escapedWorkspaceId = [uri]::EscapeDataString([string]$State.workspaceId) $workspace = Invoke-JsonRequest "$BaseUrl/api/workspaces/$escapedWorkspaceId" $Headers Assert-True ([string]$workspace.id -ceq [string]$State.workspaceId) ` 'Persisted workspace id changed or disappeared.' Assert-True ([string]$workspace.name -ceq [string]$State.workspaceName) ` 'Persisted workspace name changed or disappeared.' Assert-True ([string]$workspace.group -ceq [string]$State.workspaceGroup) ` 'Persisted workspace group changed or disappeared.' $personalList = Invoke-JsonRequest "$BaseUrl/api/memory/frames?limit=200" $Headers $personalResults = @($personalList.results) Assert-True ([long]$personalList.count -eq $personalResults.Count) ` 'Personal memory response count does not match its result set.' $personalMatches = @($personalResults | Where-Object { [long]$_.id -eq [long]$State.personalFrameId -and [string]$_.mind -ceq 'personal' -and [string]$_.source_mind -ceq 'personal' -and [string]$_.content -ceq [string]$State.personalContent }) Assert-True ($personalMatches.Count -eq 1) ` 'Persisted personal memory marker is missing or ambiguous.' $workspaceList = Invoke-JsonRequest ` "$BaseUrl/api/memory/frames?workspaceId=$escapedWorkspaceId&limit=200" ` $Headers $workspaceResults = @($workspaceList.results) Assert-True ([long]$workspaceList.count -eq $workspaceResults.Count) ` 'Workspace memory response count does not match its result set.' $workspaceMatches = @($workspaceResults | Where-Object { [long]$_.id -eq [long]$State.workspaceFrameId -and [string]$_.mind -ceq 'workspace' -and [string]$_.source_mind -ceq 'workspace' -and [string]$_.content -ceq [string]$State.workspaceContent }) Assert-True ($workspaceMatches.Count -eq 1) ` 'Persisted workspace memory marker is missing or ambiguous.' foreach ($frame in @($personalMatches[0], $workspaceMatches[0])) { Assert-True ([string]$frame.source -ceq 'tool_verified') ` 'Persisted lifecycle memory lost its provenance.' Assert-True ([string]$frame.importance -ceq 'important') ` 'Persisted lifecycle memory lost its importance.' Assert-True (@('I', 'P', 'B') -contains [string]$frame.frameType) ` 'Persisted lifecycle memory returned an invalid frame type.' Assert-True (Test-CertificateTimestamp $frame.timestamp) ` 'Persisted lifecycle memory returned an invalid timestamp.' $parsedAccessCount = 0L Assert-True ([long]::TryParse([string]$frame.accessCount, [ref]$parsedAccessCount)) ` 'Persisted lifecycle memory returned an invalid access count.' } $workspaceDir = Join-Path $DataDir "workspaces\$($State.workspaceId)" foreach ($path in @( (Join-Path $DataDir 'personal.mind'), (Join-Path $workspaceDir 'workspace.json'), (Join-Path $workspaceDir 'workspace.mind') )) { Assert-True (Test-Path -LiteralPath $path -PathType Leaf) ` "Lifecycle data file is missing: $path" } foreach ($path in @( (Join-Path $workspaceDir 'sessions'), (Join-Path $workspaceDir 'files\attachments'), (Join-Path $workspaceDir 'files\exports'), (Join-Path $workspaceDir 'files\notes') )) { Assert-True (Test-Path -LiteralPath $path -PathType Container) ` "Lifecycle workspace directory is missing: $path" } } function New-CertificateLifecycleData { param( [Parameter(Mandatory = $true)] [string]$BaseUrl, [Parameter(Mandatory = $true)] [hashtable]$Headers, [Parameter(Mandatory = $true)] [string]$RunId, [Parameter(Mandatory = $true)] [string]$DataDir ) $workspaceName = "Installer certificate $RunId" $workspaceGroup = 'Installer certificate' $workspaceResponse = Invoke-JsonPostRequest "$BaseUrl/api/workspaces" @{ name = $workspaceName group = $workspaceGroup } $Headers $workspace = $workspaceResponse.Content | ConvertFrom-Json Assert-True ([int]$workspaceResponse.StatusCode -eq 201) ` 'Could not create the lifecycle certificate workspace.' Assert-True ([string]$workspace.id -match '^[a-z0-9]+(?:-[a-z0-9]+)*$') ` 'Lifecycle certificate workspace returned an invalid id.' Assert-True ([string]$workspace.name -ceq $workspaceName) ` 'Lifecycle certificate workspace returned an unexpected name.' Assert-True ([string]$workspace.group -ceq $workspaceGroup) ` 'Lifecycle certificate workspace returned an unexpected group.' Assert-True (Test-CertificateTimestamp $workspace.created) ` 'Lifecycle certificate workspace returned an invalid creation timestamp.' $personalContent = "installer-certificate-personal-memory-$RunId" $personalResponse = Invoke-JsonPostRequest "$BaseUrl/api/memory/frames?extract=false" @{ content = $personalContent importance = 'important' source = 'tool_verified' } $Headers $personal = $personalResponse.Content | ConvertFrom-Json Assert-True ( [int]$personalResponse.StatusCode -eq 200 -and $personal.saved -eq $true -and [string]$personal.mind -ceq 'personal' -and [string]$personal.importance -ceq 'important' -and [string]$personal.source -ceq 'tool_verified' ) 'Could not seed the lifecycle certificate personal memory.' Assert-True ($personal.frameId -is [int] -or $personal.frameId -is [long]) ` 'Lifecycle certificate personal memory returned no numeric frame id.' $workspaceContent = "installer-certificate-workspace-memory-$RunId" $workspaceMemoryResponse = Invoke-JsonPostRequest "$BaseUrl/api/memory/frames?extract=false" @{ content = $workspaceContent workspaceId = [string]$workspace.id importance = 'important' source = 'tool_verified' } $Headers $workspaceMemory = $workspaceMemoryResponse.Content | ConvertFrom-Json Assert-True ( [int]$workspaceMemoryResponse.StatusCode -eq 200 -and $workspaceMemory.saved -eq $true -and [string]$workspaceMemory.mind -ceq 'workspace' -and [string]$workspaceMemory.importance -ceq 'important' -and [string]$workspaceMemory.source -ceq 'tool_verified' ) 'Could not seed the lifecycle certificate workspace memory.' Assert-True ($workspaceMemory.frameId -is [int] -or $workspaceMemory.frameId -is [long]) ` 'Lifecycle certificate workspace memory returned no numeric frame id.' $state = [ordered]@{ workspaceId = [string]$workspace.id workspaceName = $workspaceName workspaceGroup = $workspaceGroup personalContent = $personalContent personalFrameId = [long]$personal.frameId workspaceContent = $workspaceContent workspaceFrameId = [long]$workspaceMemory.frameId } Assert-CertificateLifecycleData $BaseUrl $Headers $state $DataDir return $state } function Get-CertificateRelativePath { param( [Parameter(Mandatory = $true)] [string]$Root, [Parameter(Mandatory = $true)] [string]$Child ) $resolvedRoot = [System.IO.Path]::GetFullPath($Root).TrimEnd('\') + '\' $resolvedChild = [System.IO.Path]::GetFullPath($Child) Assert-True ( $resolvedChild.StartsWith($resolvedRoot, [System.StringComparison]::OrdinalIgnoreCase) ) "Certificate profile entry escapes the owned root: $resolvedChild" return $resolvedChild.Substring($resolvedRoot.Length).Replace('\', '/') } function Get-CertificateDataManifest { param( [Parameter(Mandatory = $true)] [string]$ProfileDataDir, [switch]$SkipHashes ) $resolvedRoot = [System.IO.Path]::GetFullPath($ProfileDataDir).TrimEnd('\') Assert-True (Test-Path -LiteralPath $resolvedRoot -PathType Container) ` "Certificate profile directory is missing: $resolvedRoot" $root = Get-Item -LiteralPath $resolvedRoot -Force Assert-True (($root.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` "Certificate profile root must not be a reparse point: $resolvedRoot" $queue = [System.Collections.Generic.Queue[System.IO.DirectoryInfo]]::new() $queue.Enqueue([System.IO.DirectoryInfo]$root) $paths = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::OrdinalIgnoreCase ) $entries = @() while ($queue.Count -gt 0) { $directory = $queue.Dequeue() foreach ($item in @(Get-ChildItem -LiteralPath $directory.FullName -Force)) { Assert-True (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` "Certificate profile contains a reparse point: $($item.FullName)" $relativePath = Get-CertificateRelativePath $resolvedRoot $item.FullName Assert-True ($paths.Add($relativePath)) ` "Certificate profile contains an ambiguous path: $relativePath" if ($item.PSIsContainer) { $entries += [ordered]@{ path = $relativePath type = 'directory' sizeBytes = 0L sha256 = $null } $queue.Enqueue([System.IO.DirectoryInfo]$item) } else { $entries += [ordered]@{ path = $relativePath type = 'file' sizeBytes = [long]$item.Length sha256 = if ($SkipHashes) { $null } else { (Get-FileHash -LiteralPath $item.FullName -Algorithm SHA256).Hash } } } } } return @($entries | Sort-Object { [string]$_.path }) } function Assert-CertificateDataManifest { param( [Parameter(Mandatory = $true)] [object[]]$Expected, [Parameter(Mandatory = $true)] [object[]]$Actual ) Assert-True ($Actual.Count -eq $Expected.Count) ` 'Silent uninstall changed the default-profile manifest entry count.' for ($index = 0; $index -lt $Expected.Count; $index++) { foreach ($property in @('path', 'type', 'sizeBytes', 'sha256')) { Assert-True ( [string]::Equals( [string]$Actual[$index][$property], [string]$Expected[$index][$property], [System.StringComparison]::Ordinal ) ) "Silent uninstall changed default-profile manifest entry '$($Expected[$index].path)' ($property)." } } } function Get-CertificateDataManifestDigest { param( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]]$manifest ) $payload = ConvertTo-Json -InputObject @($manifest) -Compress -Depth 4 $sha256 = [System.Security.Cryptography.SHA256]::Create() try { $digest = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($payload)) return ([System.BitConverter]::ToString($digest)).Replace('-', '') } finally { $sha256.Dispose() } } function Get-SidecarProvenance { param([Parameter(Mandatory = $true)] [string]$Path) Assert-True (Test-Path -LiteralPath $Path -PathType Leaf) ` "Sidecar bundle is missing: $Path" $file = Get-Item -LiteralPath $Path Assert-True (($file.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` "Sidecar bundle must not be a reparse point: $Path" $bytes = [System.IO.File]::ReadAllBytes($file.FullName) $newlineIndex = [Array]::IndexOf($bytes, [byte]10) Assert-True ($newlineIndex -gt 0) 'Sidecar bundle is missing embedded provenance.' $firstLine = [System.Text.Encoding]::UTF8.GetString($bytes, 0, $newlineIndex) $prefix = '// Waggle-Sidecar-Provenance: ' Assert-True ($firstLine.StartsWith($prefix, [System.StringComparison]::Ordinal)) ` 'Sidecar bundle is missing embedded provenance.' $encoded = $firstLine.Substring($prefix.Length) Assert-True ( $encoded.Length -gt 0 -and ($encoded.Length % 4) -eq 0 -and $encoded -cmatch '^[A-Za-z0-9+/]+={0,2}$' ) 'Sidecar embedded provenance is not canonical base64.' try { $jsonBytes = [Convert]::FromBase64String($encoded) } catch { throw 'Sidecar embedded provenance is not canonical base64.' } Assert-True ( [string]::Equals( [Convert]::ToBase64String($jsonBytes), $encoded, [System.StringComparison]::Ordinal ) ) 'Sidecar embedded provenance is not canonical base64.' try { $manifest = [System.Text.Encoding]::UTF8.GetString($jsonBytes) | ConvertFrom-Json } catch { throw 'Sidecar embedded provenance is not valid JSON.' } Assert-True ($null -ne $manifest -and [int]$manifest.schemaVersion -eq 1) ` 'Sidecar embedded provenance schemaVersion must be 1.' Assert-True ([string]$manifest.sourceRevision -cmatch '^[0-9a-f]{40}$') ` 'Sidecar embedded provenance sourceRevision is invalid.' Assert-True ( [string]::Equals( [string]$manifest.entryPoint, 'packages/server/src/local/service.ts', [System.StringComparison]::Ordinal ) ) 'Sidecar embedded provenance entryPoint is invalid.' $sourceInputs = @($manifest.sourceInputs) Assert-True ($sourceInputs.Count -gt 0) 'Sidecar embedded provenance sourceInputs are missing.' $requiredInputs = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::Ordinal ) foreach ($required in @( 'package-lock.json', 'package.json', 'packages/server/src/local/service.ts', 'scripts/build-sidecar.mjs' )) { [void]$requiredInputs.Add($required) } $previousPath = $null foreach ($input in $sourceInputs) { $relative = [string]$input.path $parts = @($relative.Split('/')) Assert-True ( -not [string]::IsNullOrWhiteSpace($relative) -and -not $relative.Contains('\') -and -not $relative.Contains([char]0) -and -not [System.IO.Path]::IsPathRooted($relative.Replace('/', '\')) -and -not ($parts | Where-Object { $_ -in @('', '.', '..', 'node_modules') }) ) 'Sidecar embedded provenance source input path is unsafe.' if ($null -ne $previousPath) { Assert-True ([string]::CompareOrdinal($previousPath, $relative) -lt 0) ` 'Sidecar embedded provenance source inputs are not unique and sorted.' } Assert-True ([string]$input.sha256 -cmatch '^[0-9a-f]{64}$') ` 'Sidecar embedded provenance source input hash is invalid.' $previousPath = $relative [void]$requiredInputs.Remove($relative) } Assert-True ($requiredInputs.Count -eq 0) ` 'Sidecar embedded provenance omits required source inputs.' $payloadOffset = $newlineIndex + 1 $payloadLength = $bytes.Length - $payloadOffset Assert-True ( $null -ne $manifest.bundlePayload -and [long]$manifest.bundlePayload.sizeBytes -eq $payloadLength -and [string]$manifest.bundlePayload.sha256 -cmatch '^[0-9a-f]{64}$' ) 'Sidecar bundle payload does not match embedded provenance.' $sha256 = [System.Security.Cryptography.SHA256]::Create() try { $payloadDigest = $sha256.ComputeHash($bytes, $payloadOffset, $payloadLength) } finally { $sha256.Dispose() } $payloadSha256 = ([System.BitConverter]::ToString($payloadDigest)).Replace('-', '') Assert-True ( [string]::Equals( $payloadSha256, [string]$manifest.bundlePayload.sha256, [System.StringComparison]::OrdinalIgnoreCase ) ) 'Sidecar bundle payload does not match embedded provenance.' $provenanceSha = [System.Security.Cryptography.SHA256]::Create() try { $provenanceDigest = $provenanceSha.ComputeHash($jsonBytes) } finally { $provenanceSha.Dispose() } return [ordered]@{ manifest = $manifest bundleSha256 = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash provenanceSha256 = ([System.BitConverter]::ToString($provenanceDigest)).Replace('-', '') payloadSha256 = $payloadSha256 sourceInputCount = $sourceInputs.Count } } function Assert-SidecarBundleBinding { param( [Parameter(Mandatory = $true)] [object]$Packaged, [Parameter(Mandatory = $true)] [object]$Installed ) Assert-True ( [string]::Equals( [string]$Installed.bundleSha256, [string]$Packaged.bundleSha256, [System.StringComparison]::OrdinalIgnoreCase ) ) 'Installed resources/service.js does not match the source-bound bundle.' Assert-True ( [string]::Equals( [string]$Installed.provenanceSha256, [string]$Packaged.provenanceSha256, [System.StringComparison]::OrdinalIgnoreCase ) -and [string]::Equals( [string]$Installed.manifest.sourceRevision, [string]$Packaged.manifest.sourceRevision, [System.StringComparison]::Ordinal ) -and [int]$Installed.sourceInputCount -eq [int]$Packaged.sourceInputCount ) 'Installed resources/service.js provenance does not match the certified source.' } function Assert-SidecarSourceBinding { param( [Parameter(Mandatory = $true)] [object]$Provenance, [Parameter(Mandatory = $true)] [string]$RepositoryRoot, [Parameter(Mandatory = $true)] [string]$ExpectedRevision, [Parameter(Mandatory = $true)] [string]$GitExecutable ) Assert-True ( [string]::Equals( [string]$Provenance.manifest.sourceRevision, $ExpectedRevision, [System.StringComparison]::Ordinal ) ) 'Sidecar provenance revision does not match expected source revision.' $repositoryRootItem = Get-Item -LiteralPath $RepositoryRoot -ErrorAction Stop Assert-True $repositoryRootItem.PSIsContainer ` "Sidecar provenance repository root is not a directory: $RepositoryRoot" $resolvedRepositoryRoot = $repositoryRootItem.FullName.TrimEnd('\') $repositoryPrefix = $resolvedRepositoryRoot + '\' $sourceInputs = @($Provenance.manifest.sourceInputs) $sourcePaths = @($sourceInputs | ForEach-Object { [string]$_.path }) foreach ($input in $sourceInputs) { $relative = [string]$input.path $sourcePath = [System.IO.Path]::GetFullPath( (Join-Path $resolvedRepositoryRoot ($relative.Replace('/', '\'))) ) Assert-True ( $sourcePath.StartsWith($repositoryPrefix, [System.StringComparison]::OrdinalIgnoreCase) ) "Sidecar provenance source path escapes the repository: $relative" Assert-True (Test-Path -LiteralPath $sourcePath -PathType Leaf) ` "Sidecar provenance source input is missing: $relative" $sourceFile = Get-Item -LiteralPath $sourcePath Assert-True (($sourceFile.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` "Sidecar provenance source input is a reparse point: $relative" Assert-True ( [string]::Equals( (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash, [string]$input.sha256, [System.StringComparison]::OrdinalIgnoreCase ) ) "Sidecar provenance source hash changed: $relative" & $GitExecutable -C $RepositoryRoot ls-files --error-unmatch -- $relative 2>$null | Out-Null Assert-True ($LASTEXITCODE -eq 0) "Sidecar provenance source is not tracked: $relative" & $GitExecutable -C $RepositoryRoot cat-file -e "${ExpectedRevision}:$relative" 2>$null Assert-True ($LASTEXITCODE -eq 0) ` "Sidecar provenance source is absent from expected revision: $relative" } for ($offset = 0; $offset -lt $sourcePaths.Count; $offset += 100) { $lastIndex = [Math]::Min($offset + 99, $sourcePaths.Count - 1) $sourceChunk = @($sourcePaths[$offset..$lastIndex]) $diffArguments = @( '-C', $RepositoryRoot, 'diff', '--quiet', $ExpectedRevision, '--' ) + $sourceChunk & $GitExecutable @diffArguments Assert-True ($LASTEXITCODE -eq 0) ` 'Sidecar provenance source inputs differ from the expected revision.' } } function Get-ExternalProfileRootSnapshot { param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$Path ) $resolvedPath = [System.IO.Path]::GetFullPath($Path).TrimEnd('\') $parentPath = [System.IO.Path]::GetDirectoryName($resolvedPath) $leafName = [System.IO.Path]::GetFileName($resolvedPath) Assert-True (-not [string]::IsNullOrWhiteSpace($parentPath)) ` "External profile root has no parent directory: $resolvedPath" Assert-True (Test-Path -LiteralPath $parentPath -PathType Container) ` "External profile root parent is missing: $parentPath" $matches = @( Get-ChildItem -LiteralPath $parentPath -Force | Where-Object { [string]::Equals($_.Name, $leafName, [System.StringComparison]::OrdinalIgnoreCase) } ) Assert-True ($matches.Count -le 1) ` "External profile root has ambiguous directory entries: $resolvedPath" if ($matches.Count -eq 0) { return [ordered]@{ name = $Name path = $resolvedPath existedBefore = $false entryCount = 0 manifestSha256 = $null } } $root = $matches[0] Assert-True ($root.PSIsContainer) ` "External profile root must be a directory: $resolvedPath" Assert-True (($root.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` "External profile root must not be a reparse point: $resolvedPath" $manifest = @(Get-CertificateDataManifest $resolvedPath) return [ordered]@{ name = $Name path = $resolvedPath existedBefore = $true entryCount = $manifest.Count manifestSha256 = (Get-CertificateDataManifestDigest $manifest) } } function Assert-ExternalProfileRootsUnchanged { param([Parameter(Mandatory = $true)] [object[]]$Expected) foreach ($baseline in @($Expected)) { if (-not [bool]$baseline.existedBefore) { $actual = Get-ExternalProfileRootSnapshot ` -Name ([string]$baseline.name) ` -Path ([string]$baseline.path) Assert-True (-not [bool]$actual.existedBefore) ` "Installer created external profile root '$($baseline.name)': $($baseline.path)" continue } $actual = Get-ExternalProfileRootSnapshot ` -Name ([string]$baseline.name) ` -Path ([string]$baseline.path) Assert-True ([bool]$actual.existedBefore) ` "Installer removed external profile root '$($baseline.name)': $($baseline.path)" Assert-True ([long]$actual.entryCount -eq [long]$baseline.entryCount) ` "Installer changed external profile root entry count '$($baseline.name)'." Assert-True ([string]::Equals( [string]$actual.manifestSha256, [string]$baseline.manifestSha256, [System.StringComparison]::Ordinal )) "Installer changed external profile root '$($baseline.name)'." } } function Get-HttpStatusCode { param([Parameter(Mandatory = $true)] [string]$Uri) try { return [int](Invoke-WebRequest -Uri $Uri -Method Get -TimeoutSec 5 -UseBasicParsing).StatusCode } catch { if ($_.Exception.Response) { return [int]$_.Exception.Response.StatusCode } throw } } function Wait-ForHealth { param( [Parameter(Mandatory = $true)] [string]$BaseUrl, [Parameter(Mandatory = $true)] [int]$TimeoutSeconds ) $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) $lastError = $null while ([DateTime]::UtcNow -lt $deadline) { try { $health = Invoke-JsonRequest "$BaseUrl/health" if ($health.mode -eq 'local' -and $health.database.healthy -eq $true) { return $health } $lastError = 'Unexpected health payload' } catch { $lastError = $_.Exception.Message } Start-Sleep -Milliseconds 500 } throw "Waggle did not become healthy within ${TimeoutSeconds}s. Last error: $lastError" } function Get-InstalledProcessIds { param( [Parameter(Mandatory = $true)] [string]$AppExecutable, [Parameter(Mandatory = $true)] [string]$ServiceScript, [string]$ManagedRuntimeRoot = '' ) $ids = [System.Collections.Generic.HashSet[int]]::new() $managedRoot = if ([string]::IsNullOrWhiteSpace($ManagedRuntimeRoot)) { $null } else { [System.IO.Path]::GetFullPath($ManagedRuntimeRoot).TrimEnd('\', '/') } $processes = Get-CimInstance Win32_Process -ErrorAction Stop foreach ($process in $processes) { $exactApp = $process.ExecutablePath -and [string]::Equals( [System.IO.Path]::GetFullPath($process.ExecutablePath), [System.IO.Path]::GetFullPath($AppExecutable), [System.StringComparison]::OrdinalIgnoreCase ) $exactSidecar = $process.CommandLine -and $process.CommandLine.IndexOf( $ServiceScript, [System.StringComparison]::OrdinalIgnoreCase ) -ge 0 $managedRuntime = $managedRoot -and ( ($process.ExecutablePath -and [System.IO.Path]::GetFullPath($process.ExecutablePath).StartsWith( "$managedRoot\", [System.StringComparison]::OrdinalIgnoreCase )) -or ($process.CommandLine -and $process.CommandLine.IndexOf( $managedRoot, [System.StringComparison]::OrdinalIgnoreCase ) -ge 0) ) if ($exactApp -or $exactSidecar -or $managedRuntime) { $null = $ids.Add([int]$process.ProcessId) } } return @($ids) } function Assert-NoVisibleConsoleDescendant { param([Parameter(Mandatory = $true)] [int]$RootProcessId) if ($null -eq ('WaggleInstallerWindowProbe' -as [type])) { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public static class WaggleInstallerWindowProbe { [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool IsWindowVisible(IntPtr hWnd); } '@ } $processes = @(Get-CimInstance Win32_Process -ErrorAction Stop) $descendantIds = [System.Collections.Generic.HashSet[int]]::new() $frontier = @($RootProcessId) while ($frontier.Count -gt 0) { $next = @() foreach ($process in $processes) { if ($frontier -contains [int]$process.ParentProcessId -and $descendantIds.Add([int]$process.ProcessId)) { $next += [int]$process.ProcessId } } $frontier = $next } $consoleHosts = @( $processes | Where-Object { $descendantIds.Contains([int]$_.ProcessId) -and [string]::Equals( [string]$_.Name, 'conhost.exe', [System.StringComparison]::OrdinalIgnoreCase ) } ) $visibleConsoles = @( $consoleHosts | Where-Object { $consoleProcessId = [int]$_.ProcessId $consoleProcess = Get-Process -Id $consoleProcessId -ErrorAction SilentlyContinue if ($null -eq $consoleProcess) { return $false } try { $consoleProcess.Refresh() $windowHandle = $consoleProcess.MainWindowHandle return $windowHandle -ne [IntPtr]::Zero -and [WaggleInstallerWindowProbe]::IsWindowVisible($windowHandle) } catch { if ($null -ne (Get-Process -Id $consoleProcessId -ErrorAction SilentlyContinue)) { throw } return $false } } ) Assert-True ($visibleConsoles.Count -eq 0) ` 'Installed Waggle spawned a visible console host.' } function Wait-ForInstalledRuntimeStop { param( [Parameter(Mandatory = $true)] [string]$AppExecutable, [Parameter(Mandatory = $true)] [string]$ServiceScript, [Parameter(Mandatory = $true)] [int]$Port, [string]$ManagedRuntimeRoot = '', [int[]]$AdditionalPorts = @(), [ValidateRange(5, 120)] [int]$TimeoutSeconds = 30 ) $ports = @($Port) + @($AdditionalPorts | Where-Object { $_ -ge 1 -and $_ -le 65535 }) $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) $consecutiveAvailableProbes = 0 do { $ownedProcessIds = @( Get-InstalledProcessIds $AppExecutable $ServiceScript $ManagedRuntimeRoot ) $busyPorts = @($ports | Where-Object { Test-TcpPortHasListener $_ }) if ($ownedProcessIds.Count -eq 0 -and $busyPorts.Count -eq 0) { $consecutiveAvailableProbes++ if ($consecutiveAvailableProbes -ge 2) { return } } else { $consecutiveAvailableProbes = 0 } Start-Sleep -Milliseconds 300 } while ([DateTime]::UtcNow -lt $deadline) $remainingProcessIds = @( Get-InstalledProcessIds $AppExecutable $ServiceScript $ManagedRuntimeRoot ) $remainingBusyPorts = @($ports | Where-Object { Test-TcpPortHasListener $_ }) throw "Installed runtime did not stop cleanly; owned PIDs=$($remainingProcessIds -join ',') ports=$($remainingBusyPorts -join ',')" } function Assert-NoForeignWaggleProcesses { param([Parameter(Mandatory = $true)] [string]$ExpectedAppExecutable) $expectedPath = [System.IO.Path]::GetFullPath($ExpectedAppExecutable) $foreignProcesses = @( Get-CimInstance Win32_Process -Filter "Name = 'waggle.exe'" -ErrorAction Stop | Where-Object { -not $_.ExecutablePath -or -not [string]::Equals( [System.IO.Path]::GetFullPath($_.ExecutablePath), $expectedPath, [System.StringComparison]::OrdinalIgnoreCase ) } ) $details = @($foreignProcesses | ForEach-Object { "$($_.ProcessId):$($_.ExecutablePath)" }) Assert-True ($foreignProcesses.Count -eq 0) ` "A non-certificate Waggle process could be terminated by NSIS: $($details -join ', ')" } function Test-RegistryValue { param( [Parameter(Mandatory = $true)] [string]$KeyPath, [Parameter(Mandatory = $true)] [string]$ValueName ) if (-not (Test-Path -LiteralPath $KeyPath)) { return $false } $key = Get-Item -LiteralPath $KeyPath return $key.GetValueNames() -contains $ValueName } function Stop-InstalledProcesses { param( [Parameter(Mandatory = $true)] [string]$AppExecutable, [Parameter(Mandatory = $true)] [string]$ServiceScript, [string]$ManagedRuntimeRoot = '' ) $taskkill = Join-Path $env:SystemRoot 'System32\taskkill.exe' foreach ($processId in (Get-InstalledProcessIds $AppExecutable $ServiceScript $ManagedRuntimeRoot)) { try { Invoke-RawProcess $taskkill "/PID $processId /T /F" 20 } catch { # Killing the main process tree can make a separately captured child PID # disappear. Re-check exact ownership before treating that as a failure. $stillOwned = Get-InstalledProcessIds $AppExecutable $ServiceScript $ManagedRuntimeRoot if ($stillOwned -contains $processId) { throw } } } } function Wait-ForPathState { param( [Parameter(Mandatory = $true)] [string]$LiteralPath, [Parameter(Mandatory = $true)] [bool]$ShouldExist, [ValidateRange(5, 120)] [int]$TimeoutSeconds = 60 ) $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) while ([DateTime]::UtcNow -lt $deadline) { if ((Test-Path -LiteralPath $LiteralPath) -eq $ShouldExist) { return } Start-Sleep -Milliseconds 300 } throw "Path did not reach expected state (exists=$ShouldExist): $LiteralPath" } function Get-WaggleShortcutPaths { $desktop = [Environment]::GetFolderPath('Desktop') $programs = [Environment]::GetFolderPath('Programs') return @( (Join-Path $desktop 'Waggle.lnk'), (Join-Path $programs 'Waggle.lnk'), (Join-Path $programs 'Waggle\Waggle.lnk') ) } function Assert-CertificateUninstallPostconditions { param( [Parameter(Mandatory = $true)] [string]$InstallDir, [Parameter(Mandatory = $true)] [string]$UninstallRegistry, [Parameter(Mandatory = $true)] [string]$ProductRegistry, [Parameter(Mandatory = $true)] [string]$RunRegistry, [Parameter(Mandatory = $true)] [string]$RunRegistryValue, [Parameter(Mandatory = $true)] [string[]]$ShortcutPaths, [Parameter(Mandatory = $true)] [string]$AppExecutable, [Parameter(Mandatory = $true)] [string]$ServiceScript, [Parameter(Mandatory = $true)] [int]$Port, [string]$ManagedRuntimeRoot = '', [int[]]$AdditionalPorts = @() ) Wait-ForPathState $InstallDir $false 90 Wait-ForPathState $UninstallRegistry $false 30 Wait-ForPathState $ProductRegistry $false 30 Assert-True (-not (Test-RegistryValue $RunRegistry $RunRegistryValue)) ` 'Silent uninstall left or replaced the Waggle autostart entry.' foreach ($shortcut in $ShortcutPaths) { Wait-ForPathState $shortcut $false 30 } Wait-ForInstalledRuntimeStop $AppExecutable $ServiceScript $Port ` -ManagedRuntimeRoot $ManagedRuntimeRoot -AdditionalPorts $AdditionalPorts Assert-NoForeignWaggleProcesses $AppExecutable Assert-TcpPortAvailable $Port } function Assert-ShortcutTargets { param( [Parameter(Mandatory = $true)] [string[]]$ShortcutPaths, [Parameter(Mandatory = $true)] [string]$ExpectedTarget ) $shell = New-Object -ComObject WScript.Shell foreach ($shortcutPath in $ShortcutPaths) { $shortcut = $shell.CreateShortcut($shortcutPath) Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath($shortcut.TargetPath), [System.IO.Path]::GetFullPath($ExpectedTarget), [System.StringComparison]::OrdinalIgnoreCase ) ) "Shortcut does not target the installed Waggle executable: $shortcutPath" } } function Assert-SafeReceiptPath { param([Parameter(Mandatory = $true)] [string]$ReceiptPath) $resolvedReceipt = [System.IO.Path]::GetFullPath($ReceiptPath) $existingReceipt = Get-Item -LiteralPath $resolvedReceipt -Force -ErrorAction SilentlyContinue Assert-True ($null -eq $existingReceipt) ` "Receipt path already exists; refusing to overwrite: $resolvedReceipt" $parent = Split-Path -Parent $resolvedReceipt Assert-True (-not [string]::IsNullOrWhiteSpace($parent)) ` "Receipt path has no parent directory: $resolvedReceipt" $resolvedParent = [System.IO.Path]::GetFullPath($parent).TrimEnd('\') Assert-True (Test-Path -LiteralPath $resolvedParent -PathType Container) ` "Receipt parent directory must already exist: $resolvedParent" $cursor = Get-Item -LiteralPath $resolvedParent -Force while ($null -ne $cursor) { Assert-True ($cursor -is [System.IO.DirectoryInfo]) ` "Receipt parent is not a directory: $($cursor.FullName)" Assert-True (($cursor.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` "Receipt parent must not be a reparse point: $($cursor.FullName)" $cursor = $cursor.Parent } } function Reserve-CertificateReceiptPath { param([Parameter(Mandatory = $true)] [string]$ReceiptPath) Assert-SafeReceiptPath $ReceiptPath return [System.IO.File]::Open( $ReceiptPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None ) } function Write-CertificateReceipt { param( [Parameter(Mandatory = $true)] [System.IO.FileStream]$Reservation, [Parameter(Mandatory = $true)] [string]$Content ) $bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($Content) try { Assert-True ($Reservation.CanWrite -and $Reservation.Length -eq 0) ` 'Certificate receipt reservation is not exclusively writable and empty.' $Reservation.Write($bytes, 0, $bytes.Length) $Reservation.Flush($true) } finally { $Reservation.Dispose() } } function Assert-SafeScratchRoot { param( [Parameter(Mandatory = $true)] [string]$ScratchRoot, [Parameter(Mandatory = $true)] [string]$RunId ) $resolved = [System.IO.Path]::GetFullPath($ScratchRoot).TrimEnd('\') $expected = [System.IO.Path]::GetFullPath( (Join-Path ([System.IO.Path]::GetTempPath()) "waggle-installer-cert-$RunId") ).TrimEnd('\') Assert-True ([string]::Equals( $resolved, $expected, [System.StringComparison]::OrdinalIgnoreCase )) "Scratch root is not the exact temp root for certificate run ${RunId}: $resolved" } function Assert-CertificateScratchOwnership { param( [Parameter(Mandatory = $true)] [string]$ScratchRoot, [Parameter(Mandatory = $true)] [string]$OwnershipMarker, [Parameter(Mandatory = $true)] [string]$RunId, [switch]$RequireOnlyMarker ) $resolvedRoot = [System.IO.Path]::GetFullPath($ScratchRoot).TrimEnd('\') Assert-SafeScratchRoot $resolvedRoot $RunId $rootItem = Get-Item -LiteralPath $resolvedRoot -Force Assert-True ($rootItem.PSIsContainer -and ( $rootItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint ) -eq 0) "Scratch root is missing or is a reparse point: $resolvedRoot" $expectedMarker = Join-Path $resolvedRoot ".waggle-installer-certificate-owner-$RunId" Assert-True ([string]::Equals( [System.IO.Path]::GetFullPath($OwnershipMarker), $expectedMarker, [System.StringComparison]::OrdinalIgnoreCase )) "Scratch ownership marker has an unexpected path: $OwnershipMarker" $markerItem = Get-Item -LiteralPath $expectedMarker -Force Assert-True (-not $markerItem.PSIsContainer -and ( $markerItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint ) -eq 0) "Scratch ownership marker is missing or is a reparse point: $expectedMarker" Assert-True ((Get-Content -Raw -LiteralPath $expectedMarker) -ceq $RunId) ` "Scratch ownership marker does not match certificate run ${RunId}." $manifest = @(Get-CertificateDataManifest $resolvedRoot -SkipHashes) if ($RequireOnlyMarker) { Assert-True ( $manifest.Count -eq 1 -and [string]$manifest[0].path -ceq (Split-Path -Leaf $expectedMarker) ) "Scratch root changed before ownership was established: $resolvedRoot" } } function New-CertificateScratchRoot { param( [Parameter(Mandatory = $true)] [string]$ScratchRoot, [Parameter(Mandatory = $true)] [string]$RunId ) $resolvedRoot = [System.IO.Path]::GetFullPath($ScratchRoot).TrimEnd('\') Assert-SafeScratchRoot $resolvedRoot $RunId Assert-True (-not (Test-Path -LiteralPath $resolvedRoot)) ` "Scratch root already exists; refusing to adopt it: $resolvedRoot" $createdRoot = New-Item -ItemType Directory -Path $resolvedRoot -ErrorAction Stop Assert-True ($createdRoot.PSIsContainer -and ( $createdRoot.Attributes -band [System.IO.FileAttributes]::ReparsePoint ) -eq 0) "New scratch root is not a normal directory: $resolvedRoot" $ownershipMarker = Join-Path $resolvedRoot ".waggle-installer-certificate-owner-$RunId" $markerBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($RunId) $markerStream = [System.IO.File]::Open( $ownershipMarker, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None ) try { $markerStream.Write($markerBytes, 0, $markerBytes.Length) $markerStream.Flush($true) } finally { $markerStream.Dispose() } Assert-CertificateScratchOwnership $resolvedRoot $ownershipMarker $RunId -RequireOnlyMarker return $ownershipMarker } function Remove-CertificateScratchRoot { param( [Parameter(Mandatory = $true)] [string]$ScratchRoot, [Parameter(Mandatory = $true)] [string]$OwnershipMarker, [Parameter(Mandatory = $true)] [string]$RunId ) $resolvedRoot = [System.IO.Path]::GetFullPath($ScratchRoot).TrimEnd('\') Assert-CertificateScratchOwnership $resolvedRoot $OwnershipMarker $RunId $manifest = @(Get-CertificateDataManifest $resolvedRoot -SkipHashes) $markerRelativePath = Get-CertificateRelativePath $resolvedRoot $OwnershipMarker foreach ($entry in @($manifest | Where-Object { $_.type -eq 'file' -and -not [string]::Equals( [string]$_.path, $markerRelativePath, [System.StringComparison]::OrdinalIgnoreCase ) })) { $entryPath = Join-Path $resolvedRoot ([string]$entry.path).Replace('/', '\') $item = Get-Item -LiteralPath $entryPath -Force -ErrorAction Stop Assert-True (-not $item.PSIsContainer -and ( $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint ) -eq 0) "Refusing to remove replaced scratch file: $entryPath" Remove-Item -LiteralPath $entryPath -Force } $directories = @($manifest | Where-Object { $_.type -eq 'directory' } | Sort-Object { ([string]$_.path).Length } -Descending) foreach ($entry in $directories) { $entryPath = Join-Path $resolvedRoot ([string]$entry.path).Replace('/', '\') $item = Get-Item -LiteralPath $entryPath -Force -ErrorAction Stop Assert-True ($item.PSIsContainer -and ( $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint ) -eq 0) "Refusing to remove replaced scratch directory: $entryPath" Assert-True (@(Get-ChildItem -LiteralPath $entryPath -Force).Count -eq 0) ` "Certificate scratch directory changed during cleanup: $entryPath" Remove-Item -LiteralPath $entryPath -Force } $remaining = @(Get-ChildItem -LiteralPath $resolvedRoot -Force) Assert-True ( $remaining.Count -eq 1 -and [string]::Equals( $remaining[0].FullName, [System.IO.Path]::GetFullPath($OwnershipMarker), [System.StringComparison]::OrdinalIgnoreCase ) ) 'Certificate scratch changed before ownership-marker cleanup.' Assert-CertificateScratchOwnership $resolvedRoot $OwnershipMarker $RunId -RequireOnlyMarker Remove-Item -LiteralPath $OwnershipMarker -Force $rootItem = Get-Item -LiteralPath $resolvedRoot -Force -ErrorAction Stop Assert-True ($rootItem.PSIsContainer -and ( $rootItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint ) -eq 0) "Refusing to remove replaced scratch root: $resolvedRoot" Assert-True (@(Get-ChildItem -LiteralPath $resolvedRoot -Force).Count -eq 0) ` "Certificate scratch root changed during cleanup: $resolvedRoot" Remove-Item -LiteralPath $resolvedRoot -Force Assert-True (-not (Test-Path -LiteralPath $resolvedRoot)) ` "Certificate scratch cleanup did not remove the owned root: $resolvedRoot" } function Remove-CertificateProductRegistry { param( [Parameter(Mandatory = $true)] [string]$ProductRegistry, [Parameter(Mandatory = $true)] [string]$ExpectedInstallDir ) if (-not (Test-Path -LiteralPath $ProductRegistry)) { return } $item = Get-Item -LiteralPath $ProductRegistry $storedInstallDir = [string]$item.GetValue('') Assert-True (-not [string]::IsNullOrWhiteSpace($storedInstallDir)) ` "Refusing to remove product registry key without an owned install path: $ProductRegistry" Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath($storedInstallDir.Trim('"')), [System.IO.Path]::GetFullPath($ExpectedInstallDir), [System.StringComparison]::OrdinalIgnoreCase ) ) "Refusing to remove product registry key owned by another install: $storedInstallDir" Remove-Item -LiteralPath $ProductRegistry -Recurse -Force } function Clear-AbandonedCertificateProductRegistry { param( [Parameter(Mandatory = $true)] [string]$ProductRegistry, [Parameter(Mandatory = $true)] [string]$UninstallRegistry ) if (-not (Test-Path -LiteralPath $ProductRegistry)) { return } Assert-True (-not (Test-Path -LiteralPath $UninstallRegistry)) ` 'Refusing to remove product metadata while Waggle is registered.' $item = Get-Item -LiteralPath $ProductRegistry $storedInstallDir = [string]$item.GetValue('') Assert-True (-not [string]::IsNullOrWhiteSpace($storedInstallDir)) ` "Refusing to remove product registry key without an install path: $ProductRegistry" $resolvedInstallDir = [System.IO.Path]::GetFullPath($storedInstallDir.Trim('"')).TrimEnd('\') $certificateRoot = Split-Path -Parent $resolvedInstallDir $tempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd('\') + '\' Assert-True ($resolvedInstallDir.StartsWith($tempRoot, [System.StringComparison]::OrdinalIgnoreCase)) ` "Refusing to remove product metadata outside the system temp directory: $resolvedInstallDir" Assert-True ((Split-Path -Leaf $resolvedInstallDir) -eq 'install') ` "Refusing to remove product metadata for an unexpected install directory: $resolvedInstallDir" Assert-True ((Split-Path -Leaf $certificateRoot).StartsWith('waggle-installer-cert-', [System.StringComparison]::Ordinal)) ` "Refusing to remove product metadata not owned by the installer certificate: $resolvedInstallDir" Assert-True (-not (Test-Path -LiteralPath $resolvedInstallDir)) ` "Refusing to remove product metadata for an install directory that still exists: $resolvedInstallDir" Remove-CertificateProductRegistry $ProductRegistry $resolvedInstallDir } function Remove-CertificateProfileData { param( [Parameter(Mandatory = $true)] [string]$ProfileDataDir, [Parameter(Mandatory = $true)] [string]$ProfileDataMarker, [Parameter(Mandatory = $true)] [string]$RunId, [Parameter(Mandatory = $true)] [bool]$ProfileAbsenceProven, [Parameter(Mandatory = $true)] [bool]$RuntimeConfirmedStopped ) Assert-True $ProfileAbsenceProven ` 'Refusing to clean a profile whose pre-certificate absence was not proven.' Assert-True $RuntimeConfirmedStopped ` 'Refusing to clean the certificate profile before runtime shutdown is proven.' $expectedDataDir = [System.IO.Path]::GetFullPath((Join-Path $env:USERPROFILE '.waggle')).TrimEnd('\') $resolvedDataDir = [System.IO.Path]::GetFullPath($ProfileDataDir).TrimEnd('\') Assert-True ( [string]::Equals($resolvedDataDir, $expectedDataDir, [System.StringComparison]::OrdinalIgnoreCase) ) "Refusing to clean an unexpected profile directory: $resolvedDataDir" Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath((Split-Path -Parent $ProfileDataMarker)).TrimEnd('\'), $resolvedDataDir, [System.StringComparison]::OrdinalIgnoreCase ) ) "Refusing to clean a profile marker outside the certificate directory: $ProfileDataMarker" Assert-True ((Split-Path -Leaf $ProfileDataMarker) -eq "installer-certificate-profile-marker-$RunId.txt") ` "Refusing to clean an unexpected profile marker: $ProfileDataMarker" Assert-True (Test-Path -LiteralPath $ProfileDataMarker -PathType Leaf) ` "Certificate ownership marker is missing: $ProfileDataMarker" Assert-True ((Get-Content -Raw -LiteralPath $ProfileDataMarker).Trim() -eq $RunId) ` "Refusing to remove a profile marker not owned by this certificate: $ProfileDataMarker" $entries = @(Get-CertificateDataManifest $resolvedDataDir -SkipHashes) $markerRelativePath = Get-CertificateRelativePath $resolvedDataDir $ProfileDataMarker foreach ($entry in @($entries | Where-Object { $_.type -eq 'file' -and -not [string]::Equals( [string]$_.path, $markerRelativePath, [System.StringComparison]::OrdinalIgnoreCase ) })) { $entryPath = Join-Path $resolvedDataDir ([string]$entry.path).Replace('/', '\') $item = Get-Item -LiteralPath $entryPath -Force Assert-True (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` "Refusing to remove a replaced profile entry: $entryPath" Remove-Item -LiteralPath $entryPath -Force } $directories = @($entries | Where-Object { $_.type -eq 'directory' } | Sort-Object { ([string]$_.path).Length } -Descending) foreach ($entry in $directories) { $entryPath = Join-Path $resolvedDataDir ([string]$entry.path).Replace('/', '\') $item = Get-Item -LiteralPath $entryPath -Force Assert-True (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` "Refusing to remove a replaced profile directory: $entryPath" Assert-True (@(Get-ChildItem -LiteralPath $entryPath -Force).Count -eq 0) ` "Certificate profile directory changed during cleanup: $entryPath" Remove-Item -LiteralPath $entryPath -Force } $remaining = @(Get-ChildItem -LiteralPath $resolvedDataDir -Force) Assert-True ( $remaining.Count -eq 1 -and [string]::Equals( $remaining[0].FullName, [System.IO.Path]::GetFullPath($ProfileDataMarker), [System.StringComparison]::OrdinalIgnoreCase ) ) 'Certificate profile changed before ownership-marker cleanup.' Remove-Item -LiteralPath $ProfileDataMarker -Force Remove-Item -LiteralPath $resolvedDataDir -Force Assert-True (-not (Test-Path -LiteralPath $resolvedDataDir)) ` 'Certificate profile cleanup did not remove the owned profile root.' } $hasExpectedSignerThumbprint = -not [string]::IsNullOrWhiteSpace($ExpectedSignerThumbprint) $hasExpectedSignerSubject = -not [string]::IsNullOrWhiteSpace($ExpectedSignerSubject) if ($RequireAuthenticodeSignature) { Assert-True ($hasExpectedSignerThumbprint -xor $hasExpectedSignerSubject) ` 'Exactly one expected signer identity binding is required when Authenticode is required.' if ($hasExpectedSignerThumbprint) { $normalizedExpectedSignerThumbprint = ( $ExpectedSignerThumbprint -replace '\s', '' ).ToUpperInvariant() Assert-True ($normalizedExpectedSignerThumbprint -match '^[0-9A-F]{40}$') ` 'Expected signer thumbprint must be exactly 40 hexadecimal characters.' } } $installer = Get-Item -LiteralPath $InstallerPath Assert-True (-not $installer.PSIsContainer) 'InstallerPath must be a file' Assert-True ($installer.Extension -eq '.exe') 'InstallerPath must be an NSIS .exe' $InstallerPath = $installer.FullName $candidateInstallerHash = (Get-FileHash -LiteralPath $InstallerPath -Algorithm SHA256).Hash $previousInstaller = $null $previousInstallerSignature = $null $previousInstallerEvidence = $null $previousVersionObject = $null $candidateVersionObject = $null $upgradeInputs = @( $PreviousInstallerPath, $ExpectedPreviousInstallerSha256, $ExpectedPreviousVersion, $ExpectedPreviousSourceRevision, $ExpectedCandidateInstallerSha256, $ExpectedCandidateVersion ) $hasUpgradeInputs = @( $upgradeInputs | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } ).Count -gt 0 Assert-True ($RequireVersionToVersionUpgrade -or -not $hasUpgradeInputs) ` 'Version-to-version inputs require RequireVersionToVersionUpgrade.' if ($RequireVersionToVersionUpgrade) { Assert-True $RequireAuthenticodeSignature ` 'Version-to-version certification requires Authenticode signature enforcement.' Assert-True ($hasUpgradeInputs -and @( $upgradeInputs | Where-Object { [string]::IsNullOrWhiteSpace([string]$_) } ).Count -eq 0) 'Version-to-version certification requires every previous and candidate input.' Assert-True ($ExpectedPreviousInstallerSha256 -match '^[0-9A-Fa-f]{64}$') ` 'Previous installer SHA-256 must be exactly 64 hexadecimal characters.' Assert-True ($ExpectedCandidateInstallerSha256 -match '^[0-9A-Fa-f]{64}$') ` 'Candidate installer SHA-256 must be exactly 64 hexadecimal characters.' Assert-True ($ExpectedPreviousSourceRevision -match '^[0-9A-Fa-f]{40}$') ` 'Previous source revision must be exactly 40 hexadecimal characters.' Assert-True ([string]::Equals( $candidateInstallerHash, $ExpectedCandidateInstallerSha256, [System.StringComparison]::OrdinalIgnoreCase )) 'Candidate installer does not match the pre-certification SHA-256.' $previousVersionObject = ConvertTo-StrictSemanticVersion ` $ExpectedPreviousVersion 'Previous version' $candidateVersionObject = ConvertTo-StrictSemanticVersion ` $ExpectedCandidateVersion 'Candidate version' Assert-True ($candidateVersionObject -gt $previousVersionObject) ` 'Candidate version must be newer than the previous version.' $previousInstaller = Get-Item -LiteralPath $PreviousInstallerPath Assert-True (-not $previousInstaller.PSIsContainer) 'PreviousInstallerPath must be a file.' Assert-True ($previousInstaller.Extension -eq '.exe') ` 'PreviousInstallerPath must be an NSIS .exe.' $PreviousInstallerPath = $previousInstaller.FullName Assert-True (-not [string]::Equals( $PreviousInstallerPath, $InstallerPath, [System.StringComparison]::OrdinalIgnoreCase )) 'Previous installer and candidate installer must be distinct files.' $previousInstallerHash = ( Get-FileHash -LiteralPath $PreviousInstallerPath -Algorithm SHA256 ).Hash Assert-True ([string]::Equals( $previousInstallerHash, $ExpectedPreviousInstallerSha256, [System.StringComparison]::OrdinalIgnoreCase )) 'Previous installer does not match the protected SHA-256.' $previousInstallerSignature = Get-AuthenticodeSignature -LiteralPath $PreviousInstallerPath Assert-ExpectedAuthenticodeSignature ` $previousInstallerSignature ` $ExpectedSignerThumbprint ` $ExpectedSignerSubject ` 'Previous release installer' $previousInstallerEvidence = New-AuthenticodeArtifactReceipt ` $previousInstaller $previousInstallerSignature $previousInstallerEvidence['expectedVersion'] = $ExpectedPreviousVersion } if (-not $ReceiptPath) { $ReceiptPath = Join-Path $installer.DirectoryName 'windows-installer-certificate.json' } $ReceiptPath = [System.IO.Path]::GetFullPath($ReceiptPath) $installerHookPath = [System.IO.Path]::GetFullPath( (Join-Path $PSScriptRoot '..\app\src-tauri\nsis\installer.nsi') ) $runId = [Guid]::NewGuid().ToString('N') $scratchRoot = Join-Path ([System.IO.Path]::GetTempPath()) "waggle-installer-cert-$runId" Assert-SafeScratchRoot $scratchRoot $runId $installDir = Join-Path $scratchRoot 'install' $profileDataDir = Join-Path $env:USERPROFILE '.waggle' $externalProfileRootTargets = @( [ordered]@{ name = '.hive-mind'; path = (Join-Path $env:USERPROFILE '.hive-mind') } [ordered]@{ name = '.ollama'; path = (Join-Path $env:USERPROFILE '.ollama') } ) $dataDir = $profileDataDir $appExecutable = Join-Path $installDir 'waggle.exe' $serviceScript = Join-Path $installDir 'resources\service.js' $packagedServiceScript = [System.IO.Path]::GetFullPath( (Join-Path $PSScriptRoot '..\app\src-tauri\resources\service.js') ) $canonicalMarketplaceDb = [System.IO.Path]::GetFullPath( (Join-Path $PSScriptRoot '..\packages\marketplace\marketplace.db') ) $installedMarketplaceDb = Join-Path $installDir 'resources\marketplace.db' $writableMarketplaceDb = Join-Path $dataDir 'marketplace.db' $bundledNode = Join-Path $installDir 'resources\node.exe' $bundledNpmRuntime = Join-Path $installDir 'resources\node_modules\waggle-node-runtime' $bundledNpmCli = Join-Path $bundledNpmRuntime 'node_modules\npm\bin\npm-cli.js' $bundledNpxCli = Join-Path $bundledNpmRuntime 'node_modules\npm\bin\npx-cli.js' $bundledNpmWrapper = Join-Path $bundledNpmRuntime 'bin\npm.cmd' $bundledNpxWrapper = Join-Path $bundledNpmRuntime 'bin\npx.cmd' $uninstaller = Join-Path $installDir 'uninstall.exe' $dataMarker = Join-Path $dataDir 'installer-certificate-marker.txt' $profileDataMarker = Join-Path $profileDataDir "installer-certificate-profile-marker-$runId.txt" $uninstallRegistry = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Waggle' $productRegistry = 'HKCU:\Software\egzakta\Waggle' $runRegistry = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' $runRegistryValue = 'Waggle' $shortcutCandidates = Get-WaggleShortcutPaths $startedAt = [DateTime]::UtcNow $installerStarted = $false $profileAbsenceProven = $false $externalProfileRootsPreProven = $false $externalProfileRootBaselines = @() $profileRootOwned = $false $runtimeConfirmedStopped = $false $certificateLifecycleData = $null $preUninstallDataManifest = @() $installedShortcuts = @() $uninstallPostconditionsConfirmed = $false $environmentNamesToClear = @( 'WAGGLE_NODE_PATH', 'WAGGLE_DATA_DIR', 'NODE_PATH', 'NODE_OPTIONS', 'DOCKER_HOST', 'WAGGLE_TRUST_LOCALHOST', 'WAGGLE_SQLITE_VEC_PATH', 'ONNXRUNTIME_NODE_BINDING_PATH', 'EMBEDDING_PROVIDER', 'EMBEDDING_MODEL', 'OLLAMA_EMBED_MODEL', 'WAGGLE_EMBEDDING_PROVIDER', 'HIVE_MIND_EMBEDDING_PROVIDER', 'VOYAGE_API_KEY', 'WAGGLE_VOYAGE_API_KEY', 'WAGGLE_EVAL_MODE', 'WAGGLE_SUPPRESS_EMBEDDING_WARNING', 'WAGGLE_LITELLM_URL', 'WAGGLE_NPM_LIFECYCLE_WITNESS_PREFIX', 'OLLAMA_MODELS', 'HF_HOME', 'HF_HUB_CACHE', 'TRANSFORMERS_CACHE', 'XDG_CACHE_HOME', 'LITELLM_API_KEY', 'LITELLM_MASTER_KEY', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'XAI_API_KEY', 'DEEPSEEK_API_KEY', 'MISTRAL_API_KEY', 'DASHSCOPE_API_KEY', 'MINIMAX_API_KEY', 'ZHIPU_API_KEY', 'MOONSHOT_API_KEY', 'PERPLEXITY_API_KEY', 'OPENROUTER_API_KEY' ) $environmentNamesToClear += @( Get-ChildItem Env: | Where-Object { $_.Name -match '(?i)^npm_' } | ForEach-Object { $_.Name } ) $environmentNamesToClear = @($environmentNamesToClear | Sort-Object -Unique) $isolatedEnvironmentNames = @( 'PATH', 'WAGGLE_PORT', 'WAGGLE_DATA_DIR', 'WAGGLE_HOST', 'OLLAMA_HOST', 'VLLM_HOST', 'WAGGLE_SKIP_MARKETPLACE_SYNC' ) + $environmentNamesToClear $environmentSnapshot = @{} foreach ($name in $isolatedEnvironmentNames) { $environmentSnapshot[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') } $receipt = [ordered]@{ schemaVersion = 4 certificationMode = if ($RequireVersionToVersionUpgrade) { 'version-to-version-upgrade' } else { 'same-version-repair' } status = 'running' startedAt = $startedAt.ToString('o') finishedAt = $null installer = [ordered]@{ name = $installer.Name sha256 = $candidateInstallerHash sizeBytes = $installer.Length authenticodeStatus = $null signatureType = $null signerSubject = $null signerThumbprint = $null timestampAuthoritySubject = $null timestampAuthorityThumbprint = $null timestampAuthorityNotBefore = $null timestampAuthorityNotAfter = $null } previousInstaller = $previousInstallerEvidence previousInstalledApp = $null installedApp = $null platform = [ordered]@{ os = [Environment]::OSVersion.VersionString architecture = $env:PROCESSOR_ARCHITECTURE powershell = $PSVersionTable.PSVersion.ToString() } evidence = [ordered]@{ certificateRunId = $runId sourceRevision = $null workflowRunId = [Environment]::GetEnvironmentVariable('GITHUB_RUN_ID', 'Process') workflowRunAttempt = [Environment]::GetEnvironmentVariable('GITHUB_RUN_ATTEMPT', 'Process') certifierSha256 = $null installerHookSha256 = $null generatedInstallerScriptSha256 = $null generatedInstallerHookPath = $null sidecarSourceRevision = $null sidecarBundleSha256 = $null sidecarProvenanceSha256 = $null sidecarSourceInputCount = 0 windowsInboxTools = [ordered]@{} } scratchRoot = $scratchRoot embeddingPayloadReady = $false embeddingPayload = $null certifiedTier = $null managedModelVerified = $false managedModelDigest = $null lifecycleData = $null bundledNpm = $null upgrade = if ($RequireVersionToVersionUpgrade) { [ordered]@{ previousVersion = $ExpectedPreviousVersion candidateVersion = $ExpectedCandidateVersion previousSourceRevision = $ExpectedPreviousSourceRevision.ToLowerInvariant() installDirectory = $installDir observedPreviousVersion = $null observedCandidateVersion = $null configuredDataMarkerSha256 = $null profileMarkerSha256 = $null vaultKeySha256 = $null managedModelName = $null managedModelDigest = $null } } else { $null } checks = [ordered]@{} error = $null } if ($RequireVersionToVersionUpgrade) { $receipt.checks['previousInstallerHash'] = $true $receipt.checks['previousInstallerAuthenticodeSignature'] = $true $receipt.checks['previousInstallerAuthenticodeSigner'] = $true $receipt.checks['previousInstallerAuthenticodeTimestamp'] = $true $receipt.checks['candidateInstallerHash'] = $true $receipt.checks['versionOrder'] = $true } $receiptReservation = Reserve-CertificateReceiptPath $ReceiptPath $scratchOwnershipMarker = $null $ollamaPort = 0 Assert-True ( $WebViewDebugPort -eq 0 -or ($WebViewDebugPort -ge 1024 -and $WebViewDebugPort -le 65535) ) 'WebViewDebugPort must be 0 or an integer between 1024 and 65535.' $managedRuntimeRoot = Join-Path $dataDir 'runtimes\ollama' $managedCertificateModel = 'qwen2.5:0.5b' $managedOperationTimeoutSeconds = 3600 try { $scratchOwnershipMarker = New-CertificateScratchRoot $scratchRoot $runId Assert-True (Test-Path -LiteralPath $installerHookPath -PathType Leaf) ` 'Configured NSIS installer hook is missing.' $installerHookFile = Get-Item -LiteralPath $installerHookPath $installerHook = Get-Content -Raw -LiteralPath $installerHookPath Assert-True (-not ($installerHook -match '(?im)^\s*!macro\s+NSIS_HOOK_POSTUNINSTALL\b')) ` 'Custom post-uninstall behavior could affect profile data.' Assert-True ($installerHook -match '(?im)^\s*!macro\s+NSIS_HOOK_PREUNINSTALL\b') ` 'Custom pre-uninstall data-preservation hook is missing.' Assert-True ($installerHook -match '(?im)^\s*StrCpy\s+\$DeleteAppDataCheckboxState\s+0\s*$') ` 'Custom pre-uninstall hook does not neutralize Tauri app-data deletion.' Assert-True (-not ( $installerHook -match '(?im)^\s*(?:RMDir|Delete)\b[^\r\n]*\$PROFILE[\\/]+\.waggle(?:[\\/"\s]|$)' )) 'NSIS hook contains a destructive Waggle-data operation.' $nodeCommand = Get-Command node -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 Assert-True ($null -ne $nodeCommand) 'Node.js is required to reproduce the sidecar bundle.' $bootstrapHelperPath = Join-Path $PSScriptRoot 'read-tauri-bootstrap-token.mjs' Assert-True (Test-Path -LiteralPath $bootstrapHelperPath -PathType Leaf) ` 'Tauri bootstrap helper is missing.' Assert-True (Test-Path -LiteralPath $packagedServiceScript -PathType Leaf) ` 'Packaged resources/service.js is missing before certification.' $sidecarHashBeforeRebuild = ( Get-FileHash -LiteralPath $packagedServiceScript -Algorithm SHA256 ).Hash & $nodeCommand.Source (Join-Path $PSScriptRoot 'build-sidecar.mjs') Assert-True ($LASTEXITCODE -eq 0) 'Could not reproduce packaged resources/service.js.' $sidecarHashAfterRebuild = ( Get-FileHash -LiteralPath $packagedServiceScript -Algorithm SHA256 ).Hash Assert-True ( [string]::Equals( $sidecarHashBeforeRebuild, $sidecarHashAfterRebuild, [System.StringComparison]::OrdinalIgnoreCase ) ) 'Packaged resources/service.js does not match a clean sidecar rebuild.' $packagedSidecarProvenance = Get-SidecarProvenance $packagedServiceScript & $nodeCommand.Source ` (Join-Path $PSScriptRoot 'check-sidecar-resources.mjs') ` --expected-source-revision ` ([string]$packagedSidecarProvenance.manifest.sourceRevision) Assert-True ($LASTEXITCODE -eq 0) 'Packaged sidecar resources failed provenance preflight.' if ($RequireAuthenticodeSignature) { Assert-True (-not [string]::IsNullOrWhiteSpace($ExpectedSourceRevision)) ` 'Release certification requires the expected source revision.' } if (-not [string]::IsNullOrWhiteSpace($ExpectedSourceRevision)) { $normalizedExpectedSourceRevision = $ExpectedSourceRevision.Trim().ToLowerInvariant() Assert-True ($normalizedExpectedSourceRevision -match '^[0-9a-f]{40}$') ` 'Expected source revision must be exactly 40 hexadecimal characters.' $gitCommand = Get-Command git -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 Assert-True ($null -ne $gitCommand) 'git is required to bind the installer receipt to source.' $repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $revisionOutput = @(& $gitCommand.Source -C $repositoryRoot rev-parse HEAD 2>$null) Assert-True ($LASTEXITCODE -eq 0 -and $revisionOutput.Count -eq 1) ` 'Could not resolve the repository source revision.' $repositoryRevision = ([string]$revisionOutput[0]).Trim().ToLowerInvariant() Assert-True ($repositoryRevision -eq $normalizedExpectedSourceRevision) ` "Repository revision $repositoryRevision does not match expected source $normalizedExpectedSourceRevision." Assert-CleanRepositoryWorktree ` -GitExecutable $gitCommand.Source ` -RepositoryRoot $repositoryRoot $receipt.evidence.sourceRevision = $repositoryRevision $receipt.checks['sourceRevision'] = $true $receipt.checks['sourceFilesClean'] = $true Assert-SidecarSourceBinding ` -Provenance $packagedSidecarProvenance ` -RepositoryRoot $repositoryRoot ` -ExpectedRevision $repositoryRevision ` -GitExecutable $gitCommand.Source } else { $receipt.evidence.sourceRevision = [Environment]::GetEnvironmentVariable('GITHUB_SHA', 'Process') } if ([string]::IsNullOrWhiteSpace([string]$receipt.evidence.sourceRevision)) { $receipt.evidence.sourceRevision = [string]$packagedSidecarProvenance.manifest.sourceRevision } $receipt.evidence.sidecarSourceRevision = [string]$packagedSidecarProvenance.manifest.sourceRevision $receipt.evidence.sidecarBundleSha256 = [string]$packagedSidecarProvenance.bundleSha256 $receipt.evidence.sidecarProvenanceSha256 = [string]$packagedSidecarProvenance.provenanceSha256 $receipt.evidence.sidecarSourceInputCount = [int]$packagedSidecarProvenance.sourceInputCount $receipt.evidence.certifierSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash $receipt.evidence.installerHookSha256 = (Get-FileHash -LiteralPath $installerHookPath -Algorithm SHA256).Hash $releaseDirectory = Split-Path -Parent (Split-Path -Parent $installer.DirectoryName) $generatedInstallerScripts = @( Get-ChildItem -LiteralPath (Join-Path $releaseDirectory 'nsis') -Recurse -Filter 'installer.nsi' -File ) Assert-True ($generatedInstallerScripts.Count -eq 1) ` "Expected exactly one generated NSIS script, found $($generatedInstallerScripts.Count)" $generatedInstallerScript = $generatedInstallerScripts[0] Assert-True ($generatedInstallerScript.LastWriteTimeUtc -ge $installerHookFile.LastWriteTimeUtc) ` 'Generated NSIS source predates the configured installer hook; the build is stale.' Assert-True ($installer.LastWriteTimeUtc -ge $installerHookFile.LastWriteTimeUtc) ` 'Setup executable predates the configured installer hook; the artifact is stale.' Assert-True ($installer.LastWriteTimeUtc -ge $generatedInstallerScript.LastWriteTimeUtc) ` 'Generated NSIS source is newer than the setup executable; the artifact is stale.' $generatedInstallerContent = Get-Content -Raw -LiteralPath $generatedInstallerScript.FullName $matchingHookIncludes = 0 foreach ($includeMatch in [regex]::Matches( $generatedInstallerContent, '(?im)^\s*!include\s+"(?[^"]+)"\s*$' )) { try { $resolvedIncludePath = [System.IO.Path]::GetFullPath($includeMatch.Groups['path'].Value) } catch { continue } if ([string]::Equals( $resolvedIncludePath, $installerHookFile.FullName, [System.StringComparison]::OrdinalIgnoreCase )) { $matchingHookIncludes++ } } Assert-True ($matchingHookIncludes -eq 1) ` "Generated NSIS source must include the exact configured hook once; found $matchingHookIncludes matches." $receipt.evidence.generatedInstallerScriptSha256 = ( Get-FileHash -LiteralPath $generatedInstallerScript.FullName -Algorithm SHA256 ).Hash $receipt.evidence.generatedInstallerHookPath = $installerHookFile.FullName $receipt.checks['generatedInstallerSource'] = $true $receipt.checks['generatedInstallerInclude'] = $true $receipt.checks['profileDataDeletionAbsent'] = $true $receipt.checks['baseAppDataDeletionNeutralized'] = $true $signature = Get-AuthenticodeSignature -LiteralPath $InstallerPath $receipt.installer.authenticodeStatus = [string]$signature.Status $receipt.installer.signatureType = [string]$signature.SignatureType if ($signature.SignerCertificate) { $receipt.installer.signerSubject = $signature.SignerCertificate.Subject $receipt.installer.signerThumbprint = $signature.SignerCertificate.Thumbprint } if ($signature.TimeStamperCertificate) { $receipt.installer.timestampAuthoritySubject = $signature.TimeStamperCertificate.Subject $receipt.installer.timestampAuthorityThumbprint = $signature.TimeStamperCertificate.Thumbprint $receipt.installer.timestampAuthorityNotBefore = $signature.TimeStamperCertificate.NotBefore.ToUniversalTime().ToString('o') $receipt.installer.timestampAuthorityNotAfter = $signature.TimeStamperCertificate.NotAfter.ToUniversalTime().ToString('o') } if ($RequireAuthenticodeSignature) { Assert-ExpectedAuthenticodeSignature ` $signature $ExpectedSignerThumbprint $ExpectedSignerSubject 'Release installer' $receipt.checks['authenticodeSignature'] = $true $receipt.checks['authenticodeSigner'] = $true $receipt.checks['authenticodeTimestamp'] = $true } Clear-AbandonedCertificateProductRegistry $productRegistry $uninstallRegistry Assert-True (-not (Test-Path -LiteralPath $uninstallRegistry)) ` 'A current-user Waggle installation is already registered; refusing to replace it during certification.' Assert-True (-not (Test-Path -LiteralPath $productRegistry)) ` 'Waggle installer metadata already exists; refusing to overwrite another installation location.' Assert-True (-not (Test-RegistryValue $runRegistry $runRegistryValue)) ` 'A pre-existing Waggle autostart entry makes this certificate unsafe.' $receipt.checks['runRegistryCollisionGuard'] = $true Assert-NoForeignWaggleProcesses $appExecutable $receipt.checks['foreignProcessCollisionGuard'] = $true foreach ($shortcut in $shortcutCandidates) { Assert-True (-not (Test-Path -LiteralPath $shortcut)) ` "A pre-existing Waggle shortcut makes this certificate unsafe: $shortcut" } Assert-True (-not (Test-Path -LiteralPath $profileDataDir)) ` "Profile data already exists at $profileDataDir; run this certificate under a disposable Windows user." $profileAbsenceProven = $true $processHome = [Environment]::GetEnvironmentVariable('HOME', 'Process') if (-not [string]::IsNullOrWhiteSpace($processHome)) { Assert-True ([string]::Equals( [System.IO.Path]::GetFullPath($processHome).TrimEnd('\'), [System.IO.Path]::GetFullPath($env:USERPROFILE).TrimEnd('\'), [System.StringComparison]::OrdinalIgnoreCase )) 'HOME must resolve to USERPROFILE so external profile isolation cannot be redirected.' } $externalProfileRootBaselines = @( foreach ($target in $externalProfileRootTargets) { Get-ExternalProfileRootSnapshot ` -Name ([string]$target.name) ` -Path ([string]$target.path) } ) $receipt.evidence['externalProfileRoots'] = @( foreach ($baseline in $externalProfileRootBaselines) { [ordered]@{ name = [string]$baseline.name path = [string]$baseline.path existedBefore = [bool]$baseline.existedBefore entryCount = [long]$baseline.entryCount manifestSha256 = $baseline.manifestSha256 } } ) $externalProfileRootsPreProven = $true $receipt.checks['externalProfileRootsPreProven'] = $true # The desktop webview and Rust shell currently share the fixed loopback port # 3333 contract. Refuse before creating profile state rather than clean up # after colliding with another installation. Assert-TcpPortAvailable 3333 New-Item -ItemType Directory -Path $profileDataDir | Out-Null $profileRoot = Get-Item -LiteralPath $profileDataDir -Force Assert-True (($profileRoot.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) ` 'Certificate profile root must not be a reparse point.' Set-Content -LiteralPath $profileDataMarker -Value $runId -Encoding UTF8 $profileRootOwned = $true $env:WAGGLE_PORT = '3333' $env:WAGGLE_HOST = '127.0.0.1' $ollamaPort = Get-FreeTcpPort $env:OLLAMA_HOST = "http://127.0.0.1:$ollamaPort" $env:VLLM_HOST = "http://127.0.0.1:$(Get-FreeTcpPort)" $env:WAGGLE_SKIP_MARKETPLACE_SYNC = '1' $isolationPath = Join-Path $scratchRoot 'isolated-path' New-Item -ItemType Directory -Path $isolationPath -Force | Out-Null $systemDirectory = [Environment]::GetFolderPath('System') foreach ($toolName in @('tar.exe', 'taskkill.exe')) { $sourceTool = Join-Path $systemDirectory $toolName Assert-True (Test-Path -LiteralPath $sourceTool -PathType Leaf) ` "Required Windows inbox tool is missing: $sourceTool" $toolSignature = Get-AuthenticodeSignature -LiteralPath $sourceTool Assert-True ($toolSignature.Status -eq [System.Management.Automation.SignatureStatus]::Valid) ` "Required Windows inbox tool has no valid signature: $sourceTool" $stagedTool = Join-Path $isolationPath $toolName Copy-Item -LiteralPath $sourceTool -Destination $stagedTool $sourceToolHash = (Get-FileHash -LiteralPath $sourceTool -Algorithm SHA256).Hash Assert-True ((Get-FileHash -LiteralPath $stagedTool -Algorithm SHA256).Hash -eq $sourceToolHash) ` "Staged Windows inbox tool differs from its signed source: $toolName" $receipt.evidence.windowsInboxTools[$toolName] = $sourceToolHash } $env:PATH = $isolationPath foreach ($toolName in @('tar.exe', 'taskkill.exe')) { $resolvedTool = Get-Command $toolName -CommandType Application -ErrorAction Stop | Select-Object -First 1 Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath($resolvedTool.Source), [System.IO.Path]::GetFullPath((Join-Path $isolationPath $toolName)), [System.StringComparison]::OrdinalIgnoreCase ) ) "Isolation PATH did not resolve the staged Windows inbox tool: $toolName" } $receipt.checks['windowsInboxTools'] = $true foreach ($name in $environmentNamesToClear) { [Environment]::SetEnvironmentVariable($name, $null, 'Process') } Assert-True ( [string]::IsNullOrEmpty( [Environment]::GetEnvironmentVariable('WAGGLE_DATA_DIR', 'Process') ) ) 'WAGGLE_DATA_DIR must be absent so the packaged default-profile fallback is exercised.' Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath($dataDir).TrimEnd('\'), [System.IO.Path]::GetFullPath((Join-Path $env:USERPROFILE '.waggle')).TrimEnd('\'), [System.StringComparison]::OrdinalIgnoreCase ) ) 'Certificate data directory is not the real Windows default profile.' $receipt.checks['defaultProfileDataDir'] = $true $unexpectedApplications = @( @('node.exe', 'npm.cmd', 'npx.cmd', 'docker.exe', 'ollama.exe', 'python.exe') | Where-Object { Get-Command $_ -CommandType Application -ErrorAction SilentlyContinue } ) Assert-True ($unexpectedApplications.Count -eq 0) ` "Isolation PATH still exposes external runtimes: $($unexpectedApplications -join ', ')" $receipt.checks['externalRuntimeIsolation'] = $true # NSIS /D must be the final argument and must not be quoted, even when its # absolute path contains spaces. Do not add /R: the certificate launches and # owns the exact installed process itself. Assert-NoForeignWaggleProcesses $appExecutable $installerStarted = $true $upgradeVaultKeySha256 = $null $upgradeDataMarkerSha256 = $null $upgradeProfileMarkerSha256 = $null $upgradeManagedModelName = $null $upgradeManagedModelDigest = $null if ($RequireVersionToVersionUpgrade) { Assert-ArtifactIdentity ` $PreviousInstallerPath ` $ExpectedPreviousInstallerSha256 ` $ExpectedSignerThumbprint ` $ExpectedSignerSubject ` 'Previous installer before install' Invoke-RawProcess $PreviousInstallerPath "/S /D=$installDir" 420 Wait-ForPathState $appExecutable $true Assert-True (Test-Path -LiteralPath $uninstaller -PathType Leaf) ` 'Previous installer did not create uninstall.exe.' $previousRegistration = Get-ItemProperty -LiteralPath $uninstallRegistry Assert-True ([string]$previousRegistration.DisplayVersion -eq $ExpectedPreviousVersion) ` 'Previous installer registry version does not match the protected previous version.' Assert-True ([string]::Equals( ([string]$previousRegistration.InstallLocation).Trim('"'), $installDir, [System.StringComparison]::OrdinalIgnoreCase )) 'Previous installer did not use the isolated install directory.' $previousRegisteredUninstaller = ([string]$previousRegistration.UninstallString).Trim().Trim('"') Assert-True ([string]::Equals( [System.IO.Path]::GetFullPath($previousRegisteredUninstaller), [System.IO.Path]::GetFullPath($uninstaller), [System.StringComparison]::OrdinalIgnoreCase )) 'Previous installer registered an unexpected uninstaller.' $previousProductRegistration = Get-Item -LiteralPath $productRegistry Assert-True ([string]::Equals( [System.IO.Path]::GetFullPath(([string]$previousProductRegistration.GetValue('')).Trim('"')), [System.IO.Path]::GetFullPath($installDir), [System.StringComparison]::OrdinalIgnoreCase )) 'Previous installer product metadata does not match the isolated target.' $previousInstalledAppFile = Get-Item -LiteralPath $appExecutable $previousInstalledAppSignature = Get-AuthenticodeSignature -LiteralPath $appExecutable Assert-ExpectedAuthenticodeSignature ` $previousInstalledAppSignature ` $ExpectedSignerThumbprint ` $ExpectedSignerSubject ` 'Previous installed Waggle executable' $previousInstalledProductVersion = Get-InstalledProductVersion ` $appExecutable 'Previous installed Waggle executable' Assert-True ($previousInstalledProductVersion.normalized -eq $ExpectedPreviousVersion) ` 'Previous installed executable version does not match the protected previous version.' $receipt.previousInstalledApp = New-AuthenticodeArtifactReceipt ` $previousInstalledAppFile $previousInstalledAppSignature $receipt.previousInstalledApp['productVersion'] = $previousInstalledProductVersion.raw $receipt.upgrade.observedPreviousVersion = $previousInstalledProductVersion.normalized $receipt.checks['previousInstall'] = $true $receipt.checks['previousVersion'] = $true $receipt.checks['previousInstalledAppAuthenticodeSignature'] = $true $receipt.checks['previousInstalledAppAuthenticodeSigner'] = $true $receipt.checks['previousInstalledAppAuthenticodeTimestamp'] = $true $previousBaseUrl = "http://127.0.0.1:$($env:WAGGLE_PORT)" Assert-TcpPortAvailable 3333 $webViewDebugPort = if ($WebViewDebugPort -gt 0) { $WebViewDebugPort } else { Get-FreeTcpPort } $previousProcess = Start-InstalledApp $appExecutable $webViewDebugPort try { $null = Wait-ForHealth $previousBaseUrl $StartupTimeoutSeconds $previousProcess.Refresh() Assert-True (-not $previousProcess.HasExited) ` 'The previous desktop process exited before upgrade state was prepared.' $previousVaultKeyPath = Join-Path $dataDir '.vault-key' Assert-VaultKeyAclRestricted $previousVaultKeyPath New-Item -ItemType Directory -Path $dataDir -Force | Out-Null Set-Content -LiteralPath $dataMarker -Value $runId -Encoding UTF8 Assert-True (Test-Path -LiteralPath $profileDataMarker -PathType Leaf) ` 'Previous launch removed the profile preservation marker.' $previousHeaders = Get-CertificateSessionHeaders ` $previousBaseUrl $nodeCommand.Source $bootstrapHelperPath $webViewDebugPort ` -AllowLegacyUi $receipt.checks['previousUi'] = $true $previousTier = Invoke-JsonRequest "$previousBaseUrl/api/tier" $previousHeaders Assert-True ([string]$previousTier.tier -ceq 'FREE') ` "The protected previous release reported an unexpected effective tier: $($previousTier.tier)" $receipt.checks['previousSoloTier'] = $true if ($VerifyManagedModel) { $previousBootstrapResponse = Invoke-JsonPostRequest ` "$previousBaseUrl/api/local-inference/bootstrap" ` @{} ` $previousHeaders ` $managedOperationTimeoutSeconds $previousBootstrap = $previousBootstrapResponse.Content | ConvertFrom-Json Assert-True ( [int]$previousBootstrapResponse.StatusCode -eq 200 -and $previousBootstrap.ok -eq $true -and $previousBootstrap.dockerRequired -eq $false ) 'The protected previous release could not bootstrap its managed local runtime.' $previousPullResponse = Invoke-JsonPostRequest ` "$previousBaseUrl/api/local-inference/pull" ` @{ model = $managedCertificateModel } ` $previousHeaders ` $managedOperationTimeoutSeconds $previousPull = $previousPullResponse.Content | ConvertFrom-Json Assert-True ( [int]$previousPullResponse.StatusCode -eq 200 -and $previousPull.ok -eq $true -and $previousPull.verifiedGeneration -eq $true ) 'The protected previous release could not seed the managed local model.' Assert-True ([string]$previousPull.digest -match '^sha256:[0-9a-f]{64}$') ` 'The protected previous release returned no immutable managed-model digest.' $upgradeManagedModelName = [string]$previousPull.model $upgradeManagedModelDigest = [string]$previousPull.digest Assert-True (-not [string]::IsNullOrWhiteSpace($upgradeManagedModelName)) ` 'The protected previous release returned no managed-model identity.' $receipt.upgrade.managedModelName = $upgradeManagedModelName $receipt.upgrade.managedModelDigest = $upgradeManagedModelDigest $receipt.checks['previousManagedModelSeeded'] = $true } $certificateLifecycleData = New-CertificateLifecycleData ` $previousBaseUrl $previousHeaders $runId $dataDir $receipt.lifecycleData = $certificateLifecycleData $receipt.checks['realWorkspaceAndMemorySeeded'] = $true $upgradeVaultKeySha256 = ( Get-FileHash -LiteralPath $previousVaultKeyPath -Algorithm SHA256 ).Hash $upgradeDataMarkerSha256 = ( Get-FileHash -LiteralPath $dataMarker -Algorithm SHA256 ).Hash $upgradeProfileMarkerSha256 = ( Get-FileHash -LiteralPath $profileDataMarker -Algorithm SHA256 ).Hash $receipt.upgrade.vaultKeySha256 = $upgradeVaultKeySha256 $receipt.upgrade.configuredDataMarkerSha256 = $upgradeDataMarkerSha256 $receipt.upgrade.profileMarkerSha256 = $upgradeProfileMarkerSha256 $receipt.checks['previousLaunch'] = $true } finally { Stop-InstalledProcesses $appExecutable $serviceScript $managedRuntimeRoot Wait-ForInstalledRuntimeStop $appExecutable $serviceScript 3333 ` -ManagedRuntimeRoot $managedRuntimeRoot -AdditionalPorts @($ollamaPort, $webViewDebugPort) $previousProcess.Dispose() } Assert-NoForeignWaggleProcesses $appExecutable Assert-ArtifactIdentity ` $InstallerPath ` $ExpectedCandidateInstallerSha256 ` $ExpectedSignerThumbprint ` $ExpectedSignerSubject ` 'Candidate installer before upgrade' Invoke-RawProcess $InstallerPath "/S /D=$installDir" 420 Assert-ArtifactIdentity ` $InstallerPath ` $ExpectedCandidateInstallerSha256 ` $ExpectedSignerThumbprint ` $ExpectedSignerSubject ` 'Candidate installer after upgrade' Wait-ForInstalledRuntimeStop $appExecutable $serviceScript 3333 ` -ManagedRuntimeRoot $managedRuntimeRoot -AdditionalPorts @($ollamaPort, $webViewDebugPort) Wait-ForPathState $appExecutable $true $upgradeRegistration = Get-ItemProperty -LiteralPath $uninstallRegistry Assert-True ([string]$upgradeRegistration.DisplayVersion -eq $ExpectedCandidateVersion) ` 'Candidate installer registry version does not match the protected candidate version.' Assert-True ([string]::Equals( ([string]$upgradeRegistration.InstallLocation).Trim('"'), $installDir, [System.StringComparison]::OrdinalIgnoreCase )) 'Upgrade changed the registered install directory.' Assert-True ([string]::Equals( [System.IO.Path]::GetFullPath(([string]$upgradeRegistration.UninstallString).Trim().Trim('"')), [System.IO.Path]::GetFullPath($previousRegisteredUninstaller), [System.StringComparison]::OrdinalIgnoreCase )) 'Upgrade changed the registered uninstaller location.' $upgradeProductRegistration = Get-Item -LiteralPath $productRegistry Assert-True ([string]::Equals( [System.IO.Path]::GetFullPath(([string]$upgradeProductRegistration.GetValue('')).Trim('"')), [System.IO.Path]::GetFullPath($installDir), [System.StringComparison]::OrdinalIgnoreCase )) 'Upgrade changed the product install location.' Assert-True ( (Get-FileHash -LiteralPath $dataMarker -Algorithm SHA256).Hash -eq $upgradeDataMarkerSha256 ) 'Upgrade changed or removed configured user data.' Assert-True ( (Get-FileHash -LiteralPath $profileDataMarker -Algorithm SHA256).Hash -eq $upgradeProfileMarkerSha256 ) 'Upgrade changed or removed the profile data marker.' $receipt.checks['candidateVersion'] = $true $receipt.checks['versionToVersionUpgrade'] = $true $receipt.checks['upgradeSameInstallDirectory'] = $true $receipt.checks['upgradeConfiguredDataPreserved'] = $true $receipt.checks['upgradeProfileDataPreserved'] = $true $receipt.checks['upgradeRegistrations'] = $true } else { Invoke-RawProcess $InstallerPath "/S /D=$installDir" 420 } Wait-ForPathState $appExecutable $true Assert-True (Test-Path -LiteralPath $uninstaller -PathType Leaf) 'Installer did not create uninstall.exe' Assert-True (Test-Path -LiteralPath $serviceScript -PathType Leaf) 'Installer omitted resources/service.js' $installedSidecarProvenance = Get-SidecarProvenance $serviceScript Assert-SidecarBundleBinding ` -Packaged $packagedSidecarProvenance ` -Installed $installedSidecarProvenance $receipt.checks['sidecarSourceProvenance'] = $true Assert-True (Test-Path -LiteralPath $canonicalMarketplaceDb -PathType Leaf) ` 'The tracked canonical marketplace database is missing.' Assert-True (Test-Path -LiteralPath $installedMarketplaceDb -PathType Leaf) ` 'Installer omitted resources\marketplace.db' $installedMarketplaceFile = Get-Item -LiteralPath $installedMarketplaceDb Assert-True ( ($installedMarketplaceFile.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0 ) 'Installed resources\marketplace.db must not be a reparse point.' $marketplaceResourceSha256 = ( Get-FileHash -LiteralPath $canonicalMarketplaceDb -Algorithm SHA256 ).Hash Assert-True ( (Get-FileHash -LiteralPath $installedMarketplaceDb -Algorithm SHA256).Hash -eq $marketplaceResourceSha256 ) 'Installed resources\marketplace.db does not match the tracked canonical database.' $receipt.evidence['marketplaceResourceSha256'] = $marketplaceResourceSha256 $receipt.checks['marketplaceResource'] = $true Assert-True (Test-Path -LiteralPath $bundledNode -PathType Leaf) ` 'Installer omitted the bundled Node.js runtime' Assert-True (Test-Path -LiteralPath (Join-Path $installDir 'resources\node_modules') -PathType Container) ` 'Installer omitted staged runtime dependencies' $bundledRuntimeFiles = @( (Join-Path $bundledNpmRuntime 'package.json'), (Join-Path $bundledNpmRuntime 'NODE-LICENSE'), (Join-Path $bundledNpmRuntime 'node_modules\npm\LICENSE'), $bundledNpmCli, $bundledNpxCli, $bundledNpmWrapper, $bundledNpxWrapper ) foreach ($runtimeFile in $bundledRuntimeFiles) { Assert-True (Test-Path -LiteralPath $runtimeFile -PathType Leaf) ` "Installer omitted bundled npm runtime file: $runtimeFile" } $npmVersionOutput = @(& $bundledNode $bundledNpmCli --version 2>$null) Assert-True ($LASTEXITCODE -eq 0 -and $npmVersionOutput.Count -eq 1) ` 'Bundled npm CLI did not execute through the installed Node runtime.' $npxVersionOutput = @(& $bundledNode $bundledNpxCli --version 2>$null) Assert-True ($LASTEXITCODE -eq 0 -and $npxVersionOutput.Count -eq 1) ` 'Bundled npx CLI did not execute through the installed Node runtime.' $npmVersion = ([string]$npmVersionOutput[0]).Trim() $npxVersion = ([string]$npxVersionOutput[0]).Trim() Assert-True ($npmVersion -match '^\d+\.\d+\.\d+$' -and $npmVersion -eq $npxVersion) ` "Bundled npm/npx versions do not match: npm=$npmVersion npx=$npxVersion" $cmdExe = Join-Path $env:SystemRoot 'System32\cmd.exe' Invoke-RawProcess $cmdExe ('/d /s /c ""{0}" --version"' -f $bundledNpmWrapper) 60 Invoke-RawProcess $cmdExe ('/d /s /c ""{0}" --version"' -f $bundledNpxWrapper) 60 $offlinePackageSource = Join-Path $scratchRoot 'offline-npm-package' $offlinePackageArchiveRoot = Join-Path $scratchRoot 'offline-npm-archive' $offlinePackageArchivePayload = Join-Path $offlinePackageArchiveRoot 'package' $offlinePackageArchive = Join-Path $scratchRoot 'waggle-offline-install-probe-1.0.0.tgz' $offlinePackageTar = Join-Path $isolationPath 'tar.exe' $offlineLifecycleWitnessPrefix = Join-Path $scratchRoot 'offline-npm-lifecycle' $offlineInstallRoot = Join-Path $scratchRoot 'offline-npm-install' $offlineCache = Join-Path $scratchRoot 'offline-npm-cache' $isolatedUserConfig = Join-Path $scratchRoot 'empty-user.npmrc' $isolatedGlobalConfig = Join-Path $scratchRoot 'empty-global.npmrc' New-Item -ItemType Directory -Path @( $offlinePackageSource, $offlinePackageArchivePayload, $offlineInstallRoot, $offlineCache ) -Force | Out-Null Set-Content -LiteralPath $isolatedUserConfig -Value '' -NoNewline Set-Content -LiteralPath $isolatedGlobalConfig -Value '' -NoNewline $offlinePackageManifest = [ordered]@{ name = 'waggle-offline-install-probe' version = '1.0.0' scripts = [ordered]@{ prepare = "node -e `"require('node:fs').writeFileSync(process.env.WAGGLE_NPM_LIFECYCLE_WITNESS_PREFIX + '-prepare-ran.txt','unexpected')`"" prepack = "node -e `"require('node:fs').writeFileSync(process.env.WAGGLE_NPM_LIFECYCLE_WITNESS_PREFIX + '-prepack-ran.txt','unexpected')`"" install = "node -e `"require('node:fs').writeFileSync(process.env.WAGGLE_NPM_LIFECYCLE_WITNESS_PREFIX + '-install-ran.txt','unexpected')`"" } } $offlinePackageManifest | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $offlinePackageSource 'package.json') -Encoding UTF8 Copy-Item ` -LiteralPath (Join-Path $offlinePackageSource 'package.json') ` -Destination (Join-Path $offlinePackageArchivePayload 'package.json') Invoke-RawProcess $offlinePackageTar ( '-czf "{0}" -C "{1}" package' -f $offlinePackageArchive, $offlinePackageArchiveRoot ) 60 $offlinePackageArchiveItem = Get-Item -LiteralPath $offlinePackageArchive -Force Assert-True ( $offlinePackageArchiveItem -is [System.IO.FileInfo] -and ($offlinePackageArchiveItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0 ) 'Offline npm probe archive is missing or is a reparse point.' Assert-True (@(Get-ChildItem -LiteralPath $offlineCache -Force).Count -eq 0) ` 'Offline npm certificate cache was not clean before the probe.' [Environment]::SetEnvironmentVariable( 'WAGGLE_NPM_LIFECYCLE_WITNESS_PREFIX', $offlineLifecycleWitnessPrefix, 'Process' ) $npmInstallOutput = @( & $bundledNode $bundledNpmCli install --offline --ignore-scripts --no-audit --no-fund ` --package-lock=false --save=false --userconfig $isolatedUserConfig ` --globalconfig $isolatedGlobalConfig --cache $offlineCache --prefix $offlineInstallRoot ` -- $offlinePackageArchive 2>&1 ) Assert-True ($LASTEXITCODE -eq 0) ` "Bundled npm offline local install failed: $($npmInstallOutput -join [Environment]::NewLine)" $installedOfflinePackage = Join-Path $offlineInstallRoot 'node_modules\waggle-offline-install-probe' Assert-True (Test-Path -LiteralPath (Join-Path $installedOfflinePackage 'package.json') -PathType Leaf) ` 'Bundled npm did not install the local offline package.' $installedOfflinePackageItem = Get-Item -LiteralPath $installedOfflinePackage -Force Assert-True ( $installedOfflinePackageItem -is [System.IO.DirectoryInfo] -and ($installedOfflinePackageItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0 ) 'Bundled npm installed the local offline package as a reparse point.' foreach ($lifecycleMarker in @('prepare-ran.txt', 'prepack-ran.txt', 'install-ran.txt')) { $lifecycleWitness = "$offlineLifecycleWitnessPrefix-$lifecycleMarker" Assert-True (-not (Test-Path -LiteralPath $lifecycleWitness)) ` "Bundled npm executed lifecycle script marker $lifecycleMarker despite --ignore-scripts." } $receipt.checks['silentInstall'] = $true $receipt.checks['bundledRuntimePayload'] = $true $receipt.checks['bundledNpmCli'] = $true $receipt.checks['bundledNpmWrappers'] = $true $receipt.checks['bundledNpmOfflineInstall'] = $true $receipt.checks['bundledNpmIgnoreScriptsFlagHonored'] = $true $receipt.bundledNpm = [ordered]@{ version = $npmVersion npmCli = $bundledNpmCli npxCli = $bundledNpxCli cacheWasClean = $true offlinePackage = 'waggle-offline-install-probe@1.0.0' ignoreScriptsFlagHonored = $true } $installedAppFile = Get-Item -LiteralPath $appExecutable $installedAppSignature = Get-AuthenticodeSignature -LiteralPath $appExecutable $receipt.installedApp = [ordered]@{ name = $installedAppFile.Name sha256 = (Get-FileHash -LiteralPath $appExecutable -Algorithm SHA256).Hash sizeBytes = $installedAppFile.Length authenticodeStatus = [string]$installedAppSignature.Status signatureType = [string]$installedAppSignature.SignatureType signerSubject = $null signerThumbprint = $null timestampAuthoritySubject = $null timestampAuthorityThumbprint = $null timestampAuthorityNotBefore = $null timestampAuthorityNotAfter = $null } if ($installedAppSignature.SignerCertificate) { $receipt.installedApp.signerSubject = $installedAppSignature.SignerCertificate.Subject $receipt.installedApp.signerThumbprint = $installedAppSignature.SignerCertificate.Thumbprint } if ($installedAppSignature.TimeStamperCertificate) { $receipt.installedApp.timestampAuthoritySubject = $installedAppSignature.TimeStamperCertificate.Subject $receipt.installedApp.timestampAuthorityThumbprint = $installedAppSignature.TimeStamperCertificate.Thumbprint $receipt.installedApp.timestampAuthorityNotBefore = $installedAppSignature.TimeStamperCertificate.NotBefore.ToUniversalTime().ToString('o') $receipt.installedApp.timestampAuthorityNotAfter = $installedAppSignature.TimeStamperCertificate.NotAfter.ToUniversalTime().ToString('o') } $receipt.checks['installedAppPayload'] = $true if ($RequireAuthenticodeSignature) { Assert-ExpectedAuthenticodeSignature ` $installedAppSignature $ExpectedSignerThumbprint $ExpectedSignerSubject 'Installed Waggle executable' $receipt.checks['installedAppAuthenticodeSignature'] = $true $receipt.checks['installedAppAuthenticodeSigner'] = $true $receipt.checks['installedAppAuthenticodeTimestamp'] = $true } if ($RequireVersionToVersionUpgrade) { $installedProductVersion = Get-InstalledProductVersion ` $appExecutable 'Installed Waggle executable' Assert-True ($installedProductVersion.normalized -eq $ExpectedCandidateVersion) ` 'Installed candidate executable version does not match the protected candidate version.' $receipt.installedApp['productVersion'] = $installedProductVersion.raw $receipt.upgrade.observedCandidateVersion = $installedProductVersion.normalized Assert-LifecycleReceiptApprovedSigner ` $receipt $ExpectedSignerThumbprint $ExpectedSignerSubject $receipt.checks['sameApprovedSigner'] = $true } $registered = Get-ItemProperty -LiteralPath $uninstallRegistry Assert-True ( [string]::Equals( ([string]$registered.InstallLocation).Trim('"'), $installDir, [System.StringComparison]::OrdinalIgnoreCase ) ) 'Installer registry location does not match the isolated target' $registeredUninstallerCommand = ([string]$registered.UninstallString).Trim() Assert-True ( $registeredUninstallerCommand.StartsWith('"') -and $registeredUninstallerCommand.EndsWith('"') ) 'Installer registered an ambiguous uninstall command' $registeredUninstaller = $registeredUninstallerCommand.Trim('"') Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath($registeredUninstaller), [System.IO.Path]::GetFullPath($uninstaller), [System.StringComparison]::OrdinalIgnoreCase ) ) 'Installer registry uninstall command does not target the isolated uninstaller' $receipt.checks['uninstallRegistration'] = $true $receipt.checks['registeredUninstaller'] = $true $productRegistration = Get-Item -LiteralPath $productRegistry Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath(([string]$productRegistration.GetValue('')).Trim('"')), [System.IO.Path]::GetFullPath($installDir), [System.StringComparison]::OrdinalIgnoreCase ) ) 'Installer product metadata does not match the isolated target' $receipt.checks['productRegistration'] = $true $desktopShortcut = $shortcutCandidates[0] $startMenuShortcuts = @( $shortcutCandidates | Select-Object -Skip 1 | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } ) Assert-True (Test-Path -LiteralPath $desktopShortcut -PathType Leaf) ` 'Installer did not create the desktop shortcut' Assert-True ($startMenuShortcuts.Count -ge 1) 'Installer did not create a Start Menu shortcut' $installedShortcuts = @($desktopShortcut) + $startMenuShortcuts Assert-ShortcutTargets $installedShortcuts $appExecutable $receipt.checks['shortcuts'] = $true $baseUrl = "http://127.0.0.1:$($env:WAGGLE_PORT)" Assert-TcpPortAvailable 3333 $webViewDebugPort = if ($WebViewDebugPort -gt 0) { $WebViewDebugPort } else { Get-FreeTcpPort } $firstProcess = Start-InstalledApp $appExecutable $webViewDebugPort try { $health = Wait-ForHealth $baseUrl $StartupTimeoutSeconds $firstProcess.Refresh() Assert-True (-not $firstProcess.HasExited) 'The installed desktop process exited during first boot' Assert-NoVisibleConsoleDescendant $firstProcess.Id $serviceLogPath = Join-Path $dataDir 'logs\service.log' Assert-True (Test-Path -LiteralPath $serviceLogPath -PathType Leaf) ` 'Hidden sidecar did not create its diagnostic service log.' $receipt.checks['firstBootHiddenService'] = $true $receipt.checks['serviceLog'] = $true $sidecarNpmPrefix = Join-Path $dataDir 'npm\prefix' $sidecarNpmCache = Join-Path $dataDir 'npm\cache' Assert-True (Test-Path -LiteralPath $sidecarNpmPrefix -PathType Container) ` 'First boot did not create the sidecar npm prefix directory.' Assert-True (Test-Path -LiteralPath $sidecarNpmCache -PathType Container) ` 'First boot did not create the sidecar npm cache directory.' $receipt.checks['sidecarNpmDataDirectories'] = $true $vaultKeyPath = Join-Path $dataDir '.vault-key' Assert-VaultKeyAclRestricted $vaultKeyPath $receipt.checks['vaultKeyAclRestricted'] = $true if ($RequireVersionToVersionUpgrade) { Assert-True ( (Get-FileHash -LiteralPath $vaultKeyPath -Algorithm SHA256).Hash -eq $upgradeVaultKeySha256 ) 'Upgrade changed the existing Windows vault key.' Assert-True ( (Get-FileHash -LiteralPath $dataMarker -Algorithm SHA256).Hash -eq $upgradeDataMarkerSha256 ) 'Candidate launch changed or removed configured user data.' Assert-True ( (Get-FileHash -LiteralPath $profileDataMarker -Algorithm SHA256).Hash -eq $upgradeProfileMarkerSha256 ) 'Candidate launch changed or removed the profile data marker.' $receipt.checks['upgradeVaultKeyPreserved'] = $true $receipt.checks['candidateLaunch'] = $true $receipt.checks['relaunchAfterUpgrade'] = $true } $proxy = Invoke-BuiltInProxyLivenessProbe -Uri "$baseUrl/v1/health/liveliness" Assert-True ($proxy.status -eq 'healthy') 'Built-in provider proxy is not healthy' Assert-True ((Get-HttpStatusCode "$baseUrl/api/tier") -eq 401) ` 'A protected API route did not reject an unauthenticated loopback request' $receipt.checks['unauthenticatedProtectedRoute'] = $true $headers = Get-CertificateSessionHeaders ` $baseUrl $nodeCommand.Source $bootstrapHelperPath $webViewDebugPort $receipt.checks['firstBootUi'] = $true $tier = Invoke-JsonRequest "$baseUrl/api/tier" $headers Assert-True ([string]$tier.tier -ceq 'FREE') ` "A clean Solo install reported an unexpected effective tier: $($tier.tier)" $receipt.certifiedTier = 'FREE' $receipt.checks['soloTier'] = $true if ($RequireVersionToVersionUpgrade) { Assert-True ($null -ne $certificateLifecycleData) ` 'Upgrade lifecycle data was not created by the previous release.' Assert-CertificateLifecycleData $baseUrl $headers $certificateLifecycleData $dataDir $receipt.checks['upgradeRealWorkspaceAndMemoryPreserved'] = $true } else { $certificateLifecycleData = New-CertificateLifecycleData ` $baseUrl $headers $runId $dataDir $receipt.lifecycleData = $certificateLifecycleData $receipt.checks['realWorkspaceAndMemorySeeded'] = $true } $marketplace = Invoke-JsonRequest ` "$baseUrl/api/marketplace/search?type=mcp&source=mcp_registry&limit=100" ` $headers ` -TimeoutSeconds 30 $marketplacePackages = @($marketplace.packages) Assert-True ($marketplacePackages.Count -ge 1) ` 'Clean installed marketplace API returned no trusted MCP catalog entries.' $marketplaceNames = @($marketplacePackages | ForEach-Object { [string]$_.name }) Assert-True ($marketplaceNames -contains 'memory') ` 'Clean installed marketplace API did not return the canonical memory MCP.' Assert-True (Test-Path -LiteralPath $writableMarketplaceDb -PathType Leaf) ` 'First boot did not create the writable marketplace database.' Assert-True ( (Get-FileHash -LiteralPath $installedMarketplaceDb -Algorithm SHA256).Hash -eq $marketplaceResourceSha256 ) 'First boot modified the immutable resources\marketplace.db payload.' $receipt.checks['marketplaceApi'] = $true if (-not $RequireVersionToVersionUpgrade) { $chatProbeMessage = "installer-certificate-no-model-$runId" $chatResponse = Invoke-JsonPostRequest "$baseUrl/api/chat" @{ message = $chatProbeMessage sessionId = "installer-certificate-no-model-$runId" } $headers $chatContent = [string]$chatResponse.Content Assert-True ([int]$chatResponse.StatusCode -eq 200) ` 'Clean no-model chat did not return HTTP 200.' Assert-True ( ([string]$chatResponse.Headers['Content-Type']).StartsWith('text/event-stream') ) 'Clean no-model chat did not return an SSE stream.' Assert-True ([regex]::Matches( $chatContent, '(?m)^event:[ \t]*done[ \t]*\r?$' ).Count -eq 1) 'Clean no-model chat did not complete with exactly one done event.' Assert-True (-not ($chatContent -match '(?m)^event:[ \t]*error[ \t]*\r?$')) ` 'Clean no-model chat emitted an error event.' Assert-True ($chatContent.Contains('No AI model is ready')) ` 'Clean no-model chat did not report that model setup is required.' Assert-True (-not $chatContent.Contains($chatProbeMessage)) ` 'Clean no-model chat echoed the prompt instead of reporting setup-required state.' $receipt.checks['noModelChatSetupRequired'] = $true } $embedding = Invoke-JsonRequest "$baseUrl/api/embedding/status" $headers Assert-True ($embedding.activeProvider -eq 'inprocess') ` "Clean install did not load the in-process embedding model: $($embedding.activeProvider)" Assert-True ($embedding.modelName -eq 'Xenova/all-MiniLM-L6-v2') ` "Clean install loaded an unexpected embedding model: $($embedding.modelName)" $embeddingModelPath = Join-Path $dataDir 'models\Xenova\all-MiniLM-L6-v2\onnx\model.onnx' Assert-True (Test-Path -LiteralPath $embeddingModelPath -PathType Leaf) ` 'In-process embedding model reported ready without a model payload' $embeddingModelFile = Get-Item -LiteralPath $embeddingModelPath Assert-True ($embeddingModelFile.Length -gt 10MB) 'Downloaded embedding model payload is unexpectedly small' $localInference = Invoke-JsonRequest "$baseUrl/api/local-inference/status" $headers Assert-True ($localInference.dockerRequired -eq $false) 'Local inference incorrectly requires Docker' Assert-True ($localInference.managedRuntime.supported -eq $true) ` 'Managed local inference runtime is not supported by the packaged Windows app' if ($VerifyManagedModel) { $bootstrapResponse = Invoke-JsonPostRequest ` "$baseUrl/api/local-inference/bootstrap" ` @{} ` $headers ` $managedOperationTimeoutSeconds $bootstrap = $bootstrapResponse.Content | ConvertFrom-Json Assert-True ([int]$bootstrapResponse.StatusCode -eq 200 -and $bootstrap.ok -eq $true) ` 'The packaged managed local runtime did not bootstrap successfully.' Assert-True ($bootstrap.dockerRequired -eq $false) ` 'The packaged managed local runtime unexpectedly requires Docker.' $receipt.checks['managedRuntimeBootstrap'] = $true if ($RequireVersionToVersionUpgrade) { Assert-True ( -not [string]::IsNullOrWhiteSpace($upgradeManagedModelName) -and $upgradeManagedModelDigest -match '^sha256:[0-9a-f]{64}$' ) 'The upgrade certificate has no previous managed-model identity.' $preservedStatus = Invoke-JsonRequest "$baseUrl/api/local-inference/status" $headers $preservedOllamaServers = @( $preservedStatus.servers | Where-Object { [string]$_.type -eq 'ollama' } ) Assert-True ($preservedStatus.offlineReady -eq $true -and $preservedOllamaServers.Count -eq 1) ` 'The candidate did not start the managed model preserved from the previous release.' Assert-True (@($preservedOllamaServers[0].models) -contains $upgradeManagedModelName) ` 'The candidate did not advertise the managed model preserved from the previous release.' Assert-True ($null -ne $preservedOllamaServers[0].modelDigests) ` 'The candidate did not advertise managed-model digests after upgrade.' $preservedDigestProperty = $preservedOllamaServers[0].modelDigests.PSObject.Properties[ $upgradeManagedModelName ] Assert-True ( $null -ne $preservedDigestProperty -and [string]$preservedDigestProperty.Value -ceq $upgradeManagedModelDigest ) 'The candidate changed the managed-model digest during upgrade.' $receipt.checks['upgradeManagedModelPreserved'] = $true } $pullResponse = Invoke-JsonPostRequest ` "$baseUrl/api/local-inference/pull" ` @{ model = $managedCertificateModel } ` $headers ` $managedOperationTimeoutSeconds $pull = $pullResponse.Content | ConvertFrom-Json Assert-True ( [int]$pullResponse.StatusCode -eq 200 -and $pull.ok -eq $true -and $pull.verifiedGeneration -eq $true ) 'The managed local model did not complete its generation probe.' Assert-True (-not [string]::IsNullOrWhiteSpace([string]$pull.model)) ` 'The managed local model pull returned no installed model identity.' Assert-True ([string]$pull.digest -match '^sha256:[0-9a-f]{64}$') ` 'The managed local model pull returned no immutable manifest digest.' if ($RequireVersionToVersionUpgrade) { Assert-True ( [string]$pull.model -ceq $upgradeManagedModelName -and [string]$pull.digest -ceq $upgradeManagedModelDigest ) 'The candidate managed-model verification did not preserve the previous release digest.' } $receipt.checks['managedModelPull'] = $true $managedStatus = Invoke-JsonRequest "$baseUrl/api/local-inference/status" $headers Assert-True ($managedStatus.offlineReady -eq $true) ` 'The managed local model was pulled but offline readiness is false.' Assert-True ([int]$managedStatus.totalLocalModels -ge 1) ` 'The managed runtime did not advertise an installed local model.' $localChatResponse = Invoke-JsonPostRequest "$baseUrl/api/chat" @{ message = 'Reply with one short sentence confirming that local inference works.' model = "ollama/$($pull.model)" sessionId = "installer-certificate-managed-model-$runId" } $headers 300 $localChatContent = [string]$localChatResponse.Content Assert-True ([int]$localChatResponse.StatusCode -eq 200) ` 'Managed local-model chat did not return HTTP 200.' Assert-True ( ([string]$localChatResponse.Headers['Content-Type']).StartsWith('text/event-stream') ) 'Managed local-model chat did not return an SSE stream.' Assert-True (-not ($localChatContent -match '(?m)^event:[ \t]*error[ \t]*\r?$')) ` 'Managed local-model chat emitted an error event.' Assert-True (-not $localChatContent.Contains('No AI model is ready')) ` 'Managed local-model chat fell back to setup-required mode.' $localDoneMatches = [regex]::Matches( $localChatContent, '(?m)^event:[ \t]*done[ \t]*\r?\ndata:[ \t]*(?[^\r\n]+)\r?$' ) Assert-True ($localDoneMatches.Count -eq 1) ` 'Managed local-model chat did not complete with exactly one done event.' $localDone = $localDoneMatches[0].Groups['data'].Value | ConvertFrom-Json Assert-True (-not [string]::IsNullOrWhiteSpace([string]$localDone.content)) ` 'Managed local-model chat completed without response content.' Assert-True ([string]$localDone.model -eq "ollama/$($pull.model)") ` 'Managed local-model chat reported a model other than the requested local model.' $receipt.managedModelVerified = $true $receipt.managedModelDigest = [string]$pull.digest $receipt.managedModel = [ordered]@{ name = [string]$pull.model manifestDigest = [string]$pull.digest pullGenerationVerified = $true chatResponseChars = ([string]$localDone.content).Length } $receipt.checks['managedModelChat'] = $true } $receipt.checks['firstBoot'] = $true $receipt.checks['database'] = $health.database.healthy $receipt.checks['builtInProxyLiveness'] = $true $receipt.checks['sessionAuth'] = $true $receipt.embeddingPayloadReady = $true $receipt.embeddingPayload = [ordered]@{ name = $embedding.modelName sizeBytes = $embeddingModelFile.Length } $receipt.checks['embeddingPayloadReady'] = $true $receipt.checks['dockerIndependentRuntimePrerequisites'] = $true if ($RequireVersionToVersionUpgrade) { Assert-True ( (Get-FileHash -LiteralPath (Join-Path $dataDir '.vault-key') -Algorithm SHA256).Hash -eq $upgradeVaultKeySha256 ) 'Candidate activity changed the upgraded Windows vault key.' Assert-True ( (Get-FileHash -LiteralPath $dataMarker -Algorithm SHA256).Hash -eq $upgradeDataMarkerSha256 ) 'Candidate activity changed or removed upgraded user data.' Assert-True ( (Get-FileHash -LiteralPath $profileDataMarker -Algorithm SHA256).Hash -eq $upgradeProfileMarkerSha256 ) 'Candidate activity changed or removed upgraded profile data.' } else { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null Set-Content -LiteralPath $dataMarker -Value $runId -Encoding UTF8 } } finally { Stop-InstalledProcesses $appExecutable $serviceScript $managedRuntimeRoot Wait-ForInstalledRuntimeStop $appExecutable $serviceScript 3333 ` -ManagedRuntimeRoot $managedRuntimeRoot -AdditionalPorts @($ollamaPort, $webViewDebugPort) $firstProcess.Dispose() } # Prove a same-version repair actually restores installed bytes instead of # merely returning success while leaving a stale payload in place. $serviceHash = (Get-FileHash -LiteralPath $serviceScript -Algorithm SHA256).Hash Set-Content -LiteralPath $serviceScript -Value '// deliberately corrupted by installer certificate' -Encoding UTF8 Set-Content -LiteralPath $installedMarketplaceDb ` -Value 'deliberately corrupted by installer certificate' -Encoding UTF8 Assert-True ((Get-FileHash -LiteralPath $serviceScript -Algorithm SHA256).Hash -ne $serviceHash) ` 'Could not prepare the repair probe' Assert-True ( (Get-FileHash -LiteralPath $installedMarketplaceDb -Algorithm SHA256).Hash -ne $marketplaceResourceSha256 ) 'Could not prepare the marketplace repair probe' Assert-NoForeignWaggleProcesses $appExecutable if ($RequireVersionToVersionUpgrade) { Assert-ArtifactIdentity ` $InstallerPath ` $ExpectedCandidateInstallerSha256 ` $ExpectedSignerThumbprint ` $ExpectedSignerSubject ` 'Candidate installer before repair' } Invoke-RawProcess $InstallerPath "/S /D=$installDir" 420 if ($RequireVersionToVersionUpgrade) { Assert-ArtifactIdentity ` $InstallerPath ` $ExpectedCandidateInstallerSha256 ` $ExpectedSignerThumbprint ` $ExpectedSignerSubject ` 'Candidate installer after repair' } Wait-ForInstalledRuntimeStop $appExecutable $serviceScript 3333 ` -ManagedRuntimeRoot $managedRuntimeRoot -AdditionalPorts @($ollamaPort, $webViewDebugPort) Assert-True ((Get-FileHash -LiteralPath $serviceScript -Algorithm SHA256).Hash -eq $serviceHash) ` 'Same-version repair did not restore resources/service.js' Assert-True ( (Get-FileHash -LiteralPath $installedMarketplaceDb -Algorithm SHA256).Hash -eq $marketplaceResourceSha256 ) 'Same-version repair did not restore resources/marketplace.db' Assert-True (Test-Path -LiteralPath $dataMarker -PathType Leaf) 'Repair removed user data' if ($RequireVersionToVersionUpgrade) { Assert-True ( (Get-FileHash -LiteralPath (Join-Path $dataDir '.vault-key') -Algorithm SHA256).Hash -eq $upgradeVaultKeySha256 ) 'Same-version repair changed the upgraded Windows vault key.' Assert-True ( (Get-FileHash -LiteralPath $dataMarker -Algorithm SHA256).Hash -eq $upgradeDataMarkerSha256 ) 'Same-version repair changed upgraded user data.' Assert-True ( (Get-FileHash -LiteralPath $profileDataMarker -Algorithm SHA256).Hash -eq $upgradeProfileMarkerSha256 ) 'Same-version repair changed upgraded profile data.' } $repairRegistration = Get-ItemProperty -LiteralPath $uninstallRegistry Assert-True ( [string]::Equals( ([string]$repairRegistration.InstallLocation).Trim('"'), $installDir, [System.StringComparison]::OrdinalIgnoreCase ) ) 'Repair changed the registered install location' Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath(([string]$repairRegistration.UninstallString).Trim().Trim('"')), [System.IO.Path]::GetFullPath($registeredUninstaller), [System.StringComparison]::OrdinalIgnoreCase ) ) 'Repair changed the registered uninstaller' $repairProductRegistration = Get-Item -LiteralPath $productRegistry Assert-True ( [string]::Equals( [System.IO.Path]::GetFullPath(([string]$repairProductRegistration.GetValue('')).Trim('"')), [System.IO.Path]::GetFullPath($installDir), [System.StringComparison]::OrdinalIgnoreCase ) ) 'Repair changed the product install location' foreach ($shortcut in $installedShortcuts) { Assert-True (Test-Path -LiteralPath $shortcut -PathType Leaf) ` "Repair removed an installed shortcut: $shortcut" } Assert-ShortcutTargets $installedShortcuts $appExecutable $receipt.checks['sameVersionRepair'] = $true $receipt.checks['repairRegistrations'] = $true $runtimeConfirmedStopped = $false $webViewDebugPort = if ($WebViewDebugPort -gt 0) { $WebViewDebugPort } else { Get-FreeTcpPort } $secondProcess = Start-InstalledApp $appExecutable $webViewDebugPort try { $null = Wait-ForHealth $baseUrl $StartupTimeoutSeconds $secondProcess.Refresh() Assert-True (-not $secondProcess.HasExited) 'The installed desktop process exited after repair' Assert-NoVisibleConsoleDescendant $secondProcess.Id $receipt.checks['repairHiddenService'] = $true $repairHeaders = Get-CertificateSessionHeaders ` $baseUrl $nodeCommand.Source $bootstrapHelperPath $webViewDebugPort $receipt.checks['repairUi'] = $true $repairTier = Invoke-JsonRequest "$baseUrl/api/tier" $repairHeaders Assert-True ([string]$repairTier.tier -ceq 'FREE') ` "The repaired Solo install reported an unexpected effective tier: $($repairTier.tier)" $receipt.checks['repairSoloTier'] = $true Assert-CertificateLifecycleData $baseUrl $repairHeaders $certificateLifecycleData $dataDir if ($VerifyManagedModel) { $managedModelName = [string]$receipt.managedModel.name Assert-True (-not [string]::IsNullOrWhiteSpace($managedModelName)) ` 'The repair certificate lost the managed model identity.' $repairManagedStatus = $null $repairManagedDeadline = [DateTime]::UtcNow.AddSeconds(300) do { try { $repairManagedStatus = Invoke-JsonRequest "$baseUrl/api/local-inference/status" $repairHeaders } catch { $repairManagedStatus = $null } if ($null -ne $repairManagedStatus -and $repairManagedStatus.offlineReady -eq $true) { break } Start-Sleep -Seconds 1 } while ([DateTime]::UtcNow -lt $repairManagedDeadline) Assert-True ($null -ne $repairManagedStatus -and $repairManagedStatus.offlineReady -eq $true) ` 'The persisted managed local model did not become ready after repair.' $repairOllamaServers = @( $repairManagedStatus.servers | Where-Object { [string]$_.type -eq 'ollama' } ) Assert-True ($repairOllamaServers.Count -eq 1) ` 'The repaired install did not expose exactly one Ollama runtime.' Assert-True (@($repairOllamaServers[0].models) -contains $managedModelName) ` 'The repaired install did not preserve the certified managed model.' Assert-True ($null -ne $repairOllamaServers[0].modelDigests) ` 'The repaired install did not advertise managed-model digests.' $repairDigestProperty = $repairOllamaServers[0].modelDigests.PSObject.Properties[ $managedModelName ] Assert-True ( $null -ne $repairDigestProperty -and [string]$repairDigestProperty.Value -ceq [string]$receipt.managedModelDigest ) 'The repaired install changed the certified managed-model digest.' $receipt.checks['repairManagedModelDigestPreserved'] = $true $proxyChatResponse = Invoke-JsonPostRequest "$baseUrl/v1/chat/completions" @{ model = "ollama/$managedModelName" messages = @( [ordered]@{ role = 'user' content = 'Reply with one short sentence confirming that local proxy inference works.' } ) max_tokens = 32 stream = $false } $repairHeaders 300 Assert-True ([int]$proxyChatResponse.StatusCode -eq 200) ` 'The repaired built-in proxy did not complete a managed local-model request.' $proxyChat = $proxyChatResponse.Content | ConvertFrom-Json $proxyChoices = @($proxyChat.choices) Assert-True ($proxyChoices.Count -eq 1) ` 'The repaired built-in proxy returned an unexpected choice count.' $proxyContent = [string]$proxyChoices[0].message.content Assert-True (-not [string]::IsNullOrWhiteSpace($proxyContent)) ` 'The repaired built-in proxy completed without response content.' Assert-True ([string]$proxyChat.model -eq $managedModelName) ` 'The repaired built-in proxy did not strip the Ollama routing prefix.' $receipt.managedModel['proxyRestartChatResponseChars'] = $proxyContent.Length $receipt.checks['managedModelProxyRestartChat'] = $true } $receipt.checks['repairPreservedData'] = $true $receipt.checks['repairRealWorkspaceAndMemoryPreserved'] = $true $receipt.checks['relaunchAfterRepair'] = $true } finally { Stop-InstalledProcesses $appExecutable $serviceScript $managedRuntimeRoot Wait-ForInstalledRuntimeStop $appExecutable $serviceScript 3333 ` -ManagedRuntimeRoot $managedRuntimeRoot -AdditionalPorts @($ollamaPort, $webViewDebugPort) $runtimeConfirmedStopped = $true $secondProcess.Dispose() } Assert-NoForeignWaggleProcesses $appExecutable $preUninstallDataManifest = @(Get-CertificateDataManifest $profileDataDir) Assert-True ($preUninstallDataManifest.Count -gt 0) ` 'Default-profile manifest is empty before uninstall.' $receipt.lifecycleData['preUninstallManifestEntryCount'] = $preUninstallDataManifest.Count $receipt.lifecycleData['preUninstallManifestSha256'] = Get-CertificateDataManifestDigest ` $preUninstallDataManifest $runtimeConfirmedStopped = $false Invoke-RawProcess $registeredUninstaller '/S' 300 Wait-ForPathState $installDir $false 90 # NSIS copies the uninstaller to a temporary process. The launcher can exit # and the install directory can disappear before that process finishes the # registry and shortcut tail, so wait on the actual postconditions. Wait-ForPathState $uninstallRegistry $false 30 $receipt.checks['uninstallerCleanup'] = $true Remove-CertificateProductRegistry $productRegistry $installDir Assert-CertificateUninstallPostconditions ` -InstallDir $installDir ` -UninstallRegistry $uninstallRegistry ` -ProductRegistry $productRegistry ` -RunRegistry $runRegistry ` -RunRegistryValue $runRegistryValue ` -ShortcutPaths $shortcutCandidates ` -AppExecutable $appExecutable ` -ServiceScript $serviceScript ` -Port 3333 ` -ManagedRuntimeRoot $managedRuntimeRoot ` -AdditionalPorts @($ollamaPort, $webViewDebugPort) $uninstallPostconditionsConfirmed = $true $runtimeConfirmedStopped = $true $postUninstallDataManifest = @(Get-CertificateDataManifest $profileDataDir) Assert-CertificateDataManifest $preUninstallDataManifest $postUninstallDataManifest $postUninstallManifestSha256 = Get-CertificateDataManifestDigest $postUninstallDataManifest Assert-True ( [string]$postUninstallManifestSha256 -ceq [string]$receipt.lifecycleData.preUninstallManifestSha256 ) 'Silent uninstall changed the default-profile manifest digest.' $receipt.lifecycleData['postUninstallManifestSha256'] = $postUninstallManifestSha256 $receipt.checks['uninstallRealWorkspaceAndMemoryPreserved'] = $true if ($VerifyManagedModel) { $receipt.checks['managedRuntimeCleanup'] = $true } $installerStarted = $false Assert-True (Test-Path -LiteralPath $dataMarker -PathType Leaf) ` 'Silent uninstall did not preserve user data by default' Assert-True (Test-Path -LiteralPath $profileDataMarker -PathType Leaf) ` 'Silent uninstall removed the Windows profile data path' Assert-True ((Get-Content -Raw -LiteralPath $profileDataMarker).Trim() -eq $runId) ` 'Silent uninstall changed the Windows profile data marker' if ($RequireVersionToVersionUpgrade) { Assert-True ( (Get-FileHash -LiteralPath (Join-Path $dataDir '.vault-key') -Algorithm SHA256).Hash -eq $upgradeVaultKeySha256 ) 'Silent uninstall changed the upgraded Windows vault key.' Assert-True ( (Get-FileHash -LiteralPath $dataMarker -Algorithm SHA256).Hash -eq $upgradeDataMarkerSha256 ) 'Silent uninstall changed upgraded user data.' Assert-True ( (Get-FileHash -LiteralPath $profileDataMarker -Algorithm SHA256).Hash -eq $upgradeProfileMarkerSha256 ) 'Silent uninstall changed upgraded profile data.' } $receipt.checks['silentUninstall'] = $true $receipt.checks['certificateRegistryCleanup'] = $true $receipt.checks['configuredDataDirPreserved'] = $true $receipt.checks['profileDataPathPreserved'] = $true Remove-CertificateProfileData ` $profileDataDir $profileDataMarker $runId $profileAbsenceProven $runtimeConfirmedStopped $profileRootOwned = $false $receipt.checks['certificateProfileCleanup'] = $true $receipt.status = 'passed' } catch { $receipt.status = 'failed' $receipt.error = $_.Exception.Message $serviceLog = Join-Path $dataDir 'logs\service.log' if (Test-Path -LiteralPath $serviceLog -PathType Leaf) { $receipt['serviceLogTail'] = @(Get-Content -LiteralPath $serviceLog -Tail 80) } throw } finally { try { $runtimeConfirmedStopped = $false Stop-InstalledProcesses $appExecutable $serviceScript $managedRuntimeRoot Wait-ForInstalledRuntimeStop $appExecutable $serviceScript 3333 ` -ManagedRuntimeRoot $managedRuntimeRoot -AdditionalPorts @($ollamaPort, $webViewDebugPort) Assert-NoForeignWaggleProcesses $appExecutable Assert-TcpPortAvailable 3333 $runtimeConfirmedStopped = $true } catch { $receipt.status = 'failed' $receipt['runtimeCleanupError'] = $_.Exception.Message } if (Test-Path -LiteralPath $uninstaller -PathType Leaf) { try { $runtimeConfirmedStopped = $false Assert-NoForeignWaggleProcesses $appExecutable Invoke-RawProcess $uninstaller '/S' 300 Wait-ForPathState $installDir $false 90 } catch { $receipt.status = 'failed' $receipt['uninstallerCleanupError'] = $_.Exception.Message } } if ($installerStarted) { try { Remove-CertificateProductRegistry $productRegistry $installDir } catch { $receipt.status = 'failed' $receipt['cleanupError'] = $_.Exception.Message } } if ($installerStarted) { $uninstallPostconditionsConfirmed = $false try { Assert-CertificateUninstallPostconditions ` -InstallDir $installDir ` -UninstallRegistry $uninstallRegistry ` -ProductRegistry $productRegistry ` -RunRegistry $runRegistry ` -RunRegistryValue $runRegistryValue ` -ShortcutPaths $shortcutCandidates ` -AppExecutable $appExecutable ` -ServiceScript $serviceScript ` -Port 3333 ` -ManagedRuntimeRoot $managedRuntimeRoot ` -AdditionalPorts @($ollamaPort, $webViewDebugPort) $runtimeConfirmedStopped = $true $uninstallPostconditionsConfirmed = $true } catch { $receipt.status = 'failed' $receipt['uninstallPostconditionError'] = $_.Exception.Message } } elseif (-not $uninstallPostconditionsConfirmed) { $uninstallPostconditionsConfirmed = $runtimeConfirmedStopped } if ($profileRootOwned -and $uninstallPostconditionsConfirmed) { try { Remove-CertificateProfileData ` $profileDataDir $profileDataMarker $runId $profileAbsenceProven $runtimeConfirmedStopped $profileRootOwned = $false } catch { $receipt.status = 'failed' $receipt['profileCleanupError'] = $_.Exception.Message } } elseif ($profileRootOwned) { $receipt.status = 'failed' $receipt['profileCleanupError'] = ` 'Certificate profile was preserved because uninstall postconditions were not proven.' } if ($externalProfileRootsPreProven) { try { Assert-ExternalProfileRootsUnchanged $externalProfileRootBaselines $receipt.checks['externalProfileRootsUnchanged'] = $true } catch { $receipt.status = 'failed' $receipt['externalProfileIsolationError'] = $_.Exception.Message } } elseif ($receipt.status -eq 'passed') { $receipt.status = 'failed' $receipt['externalProfileIsolationError'] = ` 'External profile roots were not proven before installer execution.' } if ($receipt.status -eq 'passed' -and -not $KeepArtifacts) { try { Remove-CertificateScratchRoot $scratchRoot $scratchOwnershipMarker $runId } catch { $receipt.status = 'failed' $receipt['scratchCleanupError'] = $_.Exception.Message } } try { foreach ($name in $isolatedEnvironmentNames) { [Environment]::SetEnvironmentVariable($name, $environmentSnapshot[$name], 'Process') } $receipt.checks['environmentRestored'] = $true } catch { $receipt.status = 'failed' $receipt['environmentRestoreError'] = $_.Exception.Message } $receipt.finishedAt = [DateTime]::UtcNow.ToString('o') $receipt['durationSeconds'] = [Math]::Round(([DateTime]::UtcNow - $startedAt).TotalSeconds, 3) $receiptJson = $receipt | ConvertTo-Json -Depth 8 Write-CertificateReceipt $receiptReservation $receiptJson } if ($receipt.status -ne 'passed') { $failureDetail = if ($receipt.error) { $receipt.error } elseif ($receipt.Contains('externalProfileIsolationError')) { $receipt['externalProfileIsolationError'] } elseif ($receipt.Contains('environmentRestoreError')) { $receipt['environmentRestoreError'] } elseif ($receipt.Contains('scratchCleanupError')) { $receipt['scratchCleanupError'] } else { 'unknown failure' } throw "Windows installer certificate failed: $failureDetail" } Write-Host "Windows installer certificate passed: $ReceiptPath"