[CmdletBinding()] param( [string[]]$HostIds = @(), [string]$ReceiptDir = '', [string]$RunnerNode = '', [switch]$AuthenticatedTasks ) $ErrorActionPreference = 'Stop' if ($env:OS -ne 'Windows_NT') { throw 'This guarded real-agent runner is Windows-only.' } if ($AuthenticatedTasks) { throw 'AuthenticatedTasks is disabled; use scripts/test-windows-official-auth-canaries.ps1 for no-copy user-auth evidence.' } function Resolve-ReceiptLayout([string]$RequestedReceiptDir) { if ([string]::IsNullOrWhiteSpace($RequestedReceiptDir)) { return $null } $fullPath = [IO.Path]::GetFullPath($RequestedReceiptDir) $trimmedPath = $fullPath.TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) $trimmedRoot = [IO.Path]::GetPathRoot($fullPath).TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) if ([string]::IsNullOrWhiteSpace($trimmedPath) -or $trimmedPath.Equals($trimmedRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'ReceiptDir must name a child directory, not a filesystem root.' } $parent = [IO.Path]::GetFullPath([IO.Path]::GetDirectoryName($trimmedPath)) $leaf = [IO.Path]::GetFileName($trimmedPath) if ([string]::IsNullOrWhiteSpace($leaf)) { throw 'ReceiptDir must have a non-empty final directory name.' } return [pscustomobject]@{ Root = $trimmedPath Parent = $parent StagingRoot = Join-Path $parent ( ".$leaf.staging-$([guid]::NewGuid().ToString('N'))" ) } } $repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path $script:runnerNodePath = if ([string]::IsNullOrWhiteSpace($RunnerNode)) { (Get-Command node.exe -ErrorAction Stop).Source } else { (Resolve-Path -LiteralPath $RunnerNode -ErrorAction Stop).Path } $script:playwrightCli = (Resolve-Path -LiteralPath ( Join-Path $repoRoot 'node_modules\playwright\cli.js' )).Path $script:vitestCli = (Resolve-Path -LiteralPath ( Join-Path $repoRoot 'node_modules\vitest\vitest.mjs' )).Path $tempBase = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()) $runRoot = Join-Path $tempBase ("waggle-windows-external-agents-" + [guid]::NewGuid().ToString('N')) $hookProfile = Join-Path $runRoot 'hook-profile' $receiptLayout = Resolve-ReceiptLayout -RequestedReceiptDir $ReceiptDir $receiptRoot = if ($null -eq $receiptLayout) { $null } else { $receiptLayout.Root } if ($AuthenticatedTasks -and $null -eq $receiptRoot) { throw 'AuthenticatedTasks requires a fresh ReceiptDir for durable release evidence.' } $rawReceiptRoot = Join-Path $runRoot 'raw-receipts' $receiptParent = if ($null -eq $receiptLayout) { $null } else { $receiptLayout.Parent } $receiptStagingRoot = if ($null -eq $receiptLayout) { $null } else { $receiptLayout.StagingRoot } $receiptStagingOwned = $false $expectedReceiptNames = @('hooks-report.json', 'tools-report.json') if ($AuthenticatedTasks) { $expectedReceiptNames += 'authenticated-tasks-report.json' } $originalEnvironment = @{} $profileVariables = @( 'USERPROFILE', 'HOME', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'HERMES_HOME', 'HERMES_PROFILE' ) $secretVariables = @( 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN', 'OPENAI_API_KEY', 'OPENAI_ACCESS_TOKEN', 'OPENROUTER_API_KEY', 'GOOGLE_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY', 'XAI_API_KEY', 'MISTRAL_API_KEY', 'COHERE_API_KEY', 'DEEPSEEK_API_KEY', 'AZURE_OPENAI_API_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_PROFILE', 'AWS_CONFIG_FILE', 'AWS_SHARED_CREDENTIALS_FILE', 'GOOGLE_APPLICATION_CREDENTIALS', 'CLOUDSDK_CONFIG', 'AZURE_CONFIG_DIR', 'KUBECONFIG', 'DOCKER_CONFIG', 'DOCKER_HOST', 'GITHUB_TOKEN', 'GH_TOKEN', 'STRIPE_SECRET_KEY', 'DATABASE_URL', 'NPM_TOKEN', 'HF_TOKEN', 'HUGGING_FACE_HUB_TOKEN', 'RENDER_API_KEY', 'SSH_AUTH_SOCK', 'GIT_ASKPASS', 'SSH_ASKPASS', 'GIT_SSH_COMMAND', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NODE_OPTIONS' ) $secretNamePattern = '(?i)(^|_)(API_KEY|ACCESS_TOKEN|AUTH_TOKEN|TOKEN|SECRET|PASSWORD|CREDENTIALS?|COOKIE|DSN)(_|$)' $ambientSecretVariables = @( Get-ChildItem Env: | Where-Object { $_.Name -match $secretNamePattern } | ForEach-Object { $_.Name } ) $secretVariables = @($secretVariables + $ambientSecretVariables | Select-Object -Unique) $runnerVariables = @( 'WAGGLE_E2E_HOST_IDS', 'WAGGLE_E2E_REAL_HOOKS', 'WAGGLE_E2E_HOOK_HOME', 'WAGGLE_E2E_REAL_TOOLS', 'WAGGLE_E2E_TEMP_ROOT', 'WAGGLE_E2E_DATA_DIR', 'WAGGLE_E2E_PORT', 'WAGGLE_E2E_BASE_URL', 'WAGGLE_E2E_SKIP_LITELLM', 'WAGGLE_E2E_REUSE_EXISTING_SERVER', 'PLAYWRIGHT_JSON_OUTPUT_FILE', 'PATH', 'WAGGLE_LIVE_EXTERNAL_AGENTS', 'WAGGLE_LIVE_HERMES_PROVIDER', 'WAGGLE_LIVE_HERMES_MODEL' ) $environmentToRestore = @($profileVariables + $secretVariables + $runnerVariables | Select-Object -Unique) $requestedHostIds = if ($HostIds.Count -eq 0) { $null } else { $emptyHostIds = @( $HostIds | Where-Object { $_ -match '(^|,)\s*(?=,|$)' } ) if ($emptyHostIds.Count -gt 0) { throw 'HostIds cannot contain empty values.' } $normalizedHostIds = @( $HostIds | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } ) $duplicateHostIds = @( $normalizedHostIds | Group-Object | Where-Object { $_.Count -gt 1 } | ForEach-Object { $_.Name } ) if ($duplicateHostIds.Count -gt 0) { throw "HostIds cannot contain duplicate values: $($duplicateHostIds -join ', ')" } $normalizedHostIds -join ',' } $authenticatedHostIds = @('claude-code', 'codex', 'hermes') if ($AuthenticatedTasks) { if ($null -eq $requestedHostIds) { $requestedHostIds = $authenticatedHostIds -join ',' } $selectedAuthenticatedHosts = @($requestedHostIds -split ',') $hostDelta = @(Compare-Object -ReferenceObject $authenticatedHostIds -DifferenceObject $selectedAuthenticatedHosts) if ($hostDelta.Count -gt 0 -or $selectedAuthenticatedHosts.Count -ne $authenticatedHostIds.Count) { throw "AuthenticatedTasks requires exactly: $($authenticatedHostIds -join ', ')" } $requestedHostIds = $authenticatedHostIds -join ',' } $receiptHostIds = if ($null -eq $requestedHostIds) { @() } else { @($requestedHostIds -split ',') } foreach ($name in $environmentToRestore) { $item = Get-Item -LiteralPath "Env:$name" -ErrorAction SilentlyContinue $originalEnvironment[$name] = if ($null -eq $item) { $null } else { $item.Value } } function Set-ProcessEnvironment([string]$Name, [AllowNull()][string]$Value) { [Environment]::SetEnvironmentVariable($Name, $Value, 'Process') } function Restore-ProcessEnvironment([string]$Name) { Set-ProcessEnvironment -Name $Name -Value $originalEnvironment[$Name] } function Assert-NoReparsePointInPath([string]$Path, [string]$FailureMessage) { $current = [IO.Path]::GetFullPath($Path) while (-not [string]::IsNullOrWhiteSpace($current)) { $item = $null try { $item = Get-Item -LiteralPath $current -Force -ErrorAction Stop } catch [System.Management.Automation.ItemNotFoundException] { $item = $null } if ($null -ne $item -and ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw "$FailureMessage`: $current" } $parent = [IO.Path]::GetDirectoryName($current) if ([string]::IsNullOrWhiteSpace($parent) -or $parent.Equals($current, [StringComparison]::OrdinalIgnoreCase)) { break } $current = $parent } } function Copy-IsolatedAuthenticationFile( [string]$Source, [string]$Destination, [string]$OwnedRoot ) { $sourceItem = Get-Item -LiteralPath $Source -Force -ErrorAction Stop if ($sourceItem.PSIsContainer -or ($sourceItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $sourceItem.Length -le 0) { throw 'Authentication source must be a non-empty regular file' } $resolvedOwnedRoot = [IO.Path]::GetFullPath($OwnedRoot).TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) + [IO.Path]::DirectorySeparatorChar $resolvedDestination = [IO.Path]::GetFullPath($Destination) if (-not $resolvedDestination.StartsWith($resolvedOwnedRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'Authentication copy escaped the owned run profile' } $destinationParent = [IO.Path]::GetDirectoryName($resolvedDestination) Assert-NoReparsePointInPath -Path $destinationParent ` -FailureMessage 'Authentication copy path contains a reparse point' $null = New-Item -ItemType Directory -Path $destinationParent -Force Assert-NoReparsePointInPath -Path $destinationParent ` -FailureMessage 'Authentication copy path contains a reparse point' [IO.File]::Copy($sourceItem.FullName, $resolvedDestination, $false) $destinationItem = Get-Item -LiteralPath $resolvedDestination -Force if ($destinationItem.PSIsContainer -or ($destinationItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $destinationItem.Length -ne $sourceItem.Length) { throw 'Isolated authentication copy failed verification' } } function New-IsolatedCodexShim( [string]$ShimRoot, [string]$CodexHome, [string]$OwnedRoot ) { $resolvedOwnedRoot = [IO.Path]::GetFullPath($OwnedRoot).TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) + [IO.Path]::DirectorySeparatorChar $resolvedShimRoot = [IO.Path]::GetFullPath($ShimRoot) $resolvedCodexHome = [IO.Path]::GetFullPath($CodexHome) foreach ($candidate in @($resolvedShimRoot, $resolvedCodexHome)) { if (-not ($candidate + [IO.Path]::DirectorySeparatorChar).StartsWith( $resolvedOwnedRoot, [StringComparison]::OrdinalIgnoreCase )) { throw 'Codex isolation path escaped the owned run profile' } Assert-NoReparsePointInPath -Path $candidate ` -FailureMessage 'Codex isolation path contains a reparse point' } $realCodexCmd = (Get-Command codex.cmd -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source $realCodexCmdItem = Get-Item -LiteralPath $realCodexCmd -Force if ($realCodexCmdItem.PSIsContainer -or ($realCodexCmdItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Codex npm command must be a regular file' } $cmdText = [IO.File]::ReadAllText($realCodexCmdItem.FullName) $entryMatch = [regex]::Match( $cmdText, '"%dp0%[\\/]+([^"\r\n]*node_modules[\\/]@openai[\\/]codex[\\/]bin[\\/]codex\.js)"', [Text.RegularExpressions.RegexOptions]::IgnoreCase ) if (-not $entryMatch.Success) { throw 'Codex npm command did not expose a shell-free JavaScript entrypoint' } $realCodexEntry = [IO.Path]::GetFullPath(( Join-Path ([IO.Path]::GetDirectoryName($realCodexCmdItem.FullName)) $entryMatch.Groups[1].Value )) $realCodexEntryItem = Get-Item -LiteralPath $realCodexEntry -Force if ($realCodexEntryItem.PSIsContainer -or ($realCodexEntryItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Codex JavaScript entrypoint must be a regular file' } $null = New-Item -ItemType Directory -Path $resolvedShimRoot Assert-NoReparsePointInPath -Path $resolvedShimRoot ` -FailureMessage 'Codex isolation path contains a reparse point' $launcherPath = Join-Path $resolvedShimRoot 'codex-isolated.mjs' $shimPath = Join-Path $resolvedShimRoot 'codex.cmd' $encodedCodexHome = ConvertTo-Json -InputObject $resolvedCodexHome -Compress $encodedCodexEntry = ConvertTo-Json -InputObject $realCodexEntryItem.FullName -Compress $launcher = @" import { spawn } from 'node:child_process'; process.env.CODEX_HOME = $encodedCodexHome; const child = spawn(process.execPath, [$encodedCodexEntry, ...process.argv.slice(2)], { env: process.env, stdio: 'inherit', windowsHide: true, }); child.once('error', (error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); }); child.once('exit', (code) => process.exit(code ?? 1)); "@ $shim = @' @ECHO off SET "_prog=node" "%_prog%" "%dp0%\codex-isolated.mjs" %* '@ [IO.File]::WriteAllText($launcherPath, $launcher, [Text.UTF8Encoding]::new($false)) [IO.File]::WriteAllText($shimPath, $shim, [Text.ASCIIEncoding]::new()) return [pscustomobject]@{ Directory = $resolvedShimRoot Launcher = $launcherPath Shim = $shimPath } } function New-OwnedHermesProfile( [string]$ProfilesRoot, [string]$ProfileName, [string]$OwnedRoot ) { $resolvedProfilesRoot = [IO.Path]::GetFullPath($ProfilesRoot) $resolvedOwnedRoot = [IO.Path]::GetFullPath($OwnedRoot).TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) $ownedPrefix = $resolvedOwnedRoot + [IO.Path]::DirectorySeparatorChar if (-not ($resolvedProfilesRoot + [IO.Path]::DirectorySeparatorChar).StartsWith( $ownedPrefix, [StringComparison]::OrdinalIgnoreCase )) { throw 'Hermes authenticated profile root escaped the owned run tree' } Assert-NoReparsePointInPath -Path $resolvedProfilesRoot ` -FailureMessage 'Hermes authenticated profile path contains a reparse point' $profileHome = Join-Path $resolvedProfilesRoot $ProfileName if (Test-Path -LiteralPath $profileHome) { throw 'Refusing to overwrite an existing Hermes authenticated profile' } $null = & hermes profile create $ProfileName --no-alias --no-skills $profileCreateExitCode = $LASTEXITCODE if ($profileCreateExitCode -ne 0 -or -not (Test-Path -LiteralPath $profileHome -PathType Container)) { throw 'Hermes authenticated profile creation failed' } Assert-NoReparsePointInPath -Path $profileHome ` -FailureMessage 'Hermes authenticated profile path contains a reparse point' $profileItem = Get-Item -LiteralPath $profileHome -Force $resolvedProfileHome = [IO.Path]::GetFullPath($profileItem.FullName) if (($profileItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not [IO.Path]::GetDirectoryName($resolvedProfileHome).Equals( $resolvedProfilesRoot, [StringComparison]::OrdinalIgnoreCase )) { throw 'Hermes authenticated profile ownership verification failed' } $ownerToken = [guid]::NewGuid().ToString('N') $ownershipMarker = Join-Path $resolvedProfileHome ".waggle-e2e-owner-$ownerToken" Write-HermesOwnershipMarker -Path $ownershipMarker -Token $ownerToken return [pscustomobject]@{ ProfileName = $ProfileName ProfilesRoot = $resolvedProfilesRoot ProfileHome = $resolvedProfileHome OwnershipMarker = $ownershipMarker OwnerToken = $ownerToken } } function Write-HermesOwnershipMarker([string]$Path, [string]$Token) { $markerBytes = [Text.UTF8Encoding]::new($false).GetBytes($Token) $markerStream = [IO.File]::Open( $Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None ) try { $markerStream.Write($markerBytes, 0, $markerBytes.Length) } finally { $markerStream.Dispose() } } function Remove-OwnedHermesProfile($Lease) { if ($null -eq $Lease) { return } $profileItem = Get-Item -LiteralPath $Lease.ProfileHome -Force $markerItem = Get-Item -LiteralPath $Lease.OwnershipMarker -Force $resolvedProfileHome = [IO.Path]::GetFullPath($profileItem.FullName) if (-not $profileItem.PSIsContainer -or ($profileItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $markerItem.PSIsContainer -or ($markerItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not [IO.Path]::GetDirectoryName($resolvedProfileHome).Equals( [IO.Path]::GetFullPath($Lease.ProfilesRoot), [StringComparison]::OrdinalIgnoreCase ) -or [IO.File]::ReadAllText($markerItem.FullName) -cne $Lease.OwnerToken) { throw 'Hermes authenticated profile ownership verification failed' } $null = & hermes profile delete $Lease.ProfileName -y $profileDeleteExitCode = $LASTEXITCODE if ((Test-Path -LiteralPath $Lease.ProfileHome) -or $profileDeleteExitCode -ne 0) { throw 'Hermes authenticated profile cleanup failed' } } function Invoke-NativePreflight( [string]$FilePath, [string[]]$ArgumentList, [string]$FailureMessage ) { $previousErrorActionPreference = $ErrorActionPreference $exitCode = $null try { $ErrorActionPreference = 'Continue' & $FilePath @ArgumentList *> $null $exitCode = $LASTEXITCODE } finally { $ErrorActionPreference = $previousErrorActionPreference } if ($exitCode -ne 0) { throw $FailureMessage } } function Remove-IsolatedAuthenticationFiles( [string[]]$Paths, [string]$OwnedRoot ) { $errors = [Collections.Generic.List[string]]::new() $resolvedOwnedRoot = [IO.Path]::GetFullPath($OwnedRoot).TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) $ownedPrefix = $resolvedOwnedRoot + [IO.Path]::DirectorySeparatorChar foreach ($path in $Paths) { try { $resolvedPath = [IO.Path]::GetFullPath($path) if (-not $resolvedPath.StartsWith($ownedPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw 'Authentication cleanup path escaped the owned run profile' } Assert-NoReparsePointInPath -Path $resolvedPath ` -FailureMessage 'Authentication cleanup path contains a reparse point' if (-not (Test-Path -LiteralPath $resolvedPath)) { continue } $item = Get-Item -LiteralPath $resolvedPath -Force if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Authentication cleanup target must be a regular file' } $lastError = $null for ($attempt = 0; $attempt -lt 8; $attempt += 1) { try { [IO.File]::Delete($resolvedPath) } catch { $lastError = $_ } if (-not (Test-Path -LiteralPath $resolvedPath)) { break } Start-Sleep -Milliseconds 125 } if (Test-Path -LiteralPath $resolvedPath) { if ($null -ne $lastError) { throw $lastError } throw 'Authentication cleanup target survived deletion retries' } } catch { $errors.Add("Failed to remove an isolated authentication copy: $($_.Exception.Message)") } } if ($errors.Count -gt 0) { throw ($errors -join '; ') } } function Complete-AuthenticatedIsolationCleanup( $HermesLease, [object[]]$SourceAuthEvidence, [string[]]$ProfileVariables, [string[]]$IsolatedAuthPaths, [string]$OwnedRoot ) { $errors = [Collections.Generic.List[string]]::new() try { Set-ProcessEnvironment -Name 'WAGGLE_LIVE_EXTERNAL_AGENTS' -Value $null } catch { $errors.Add("Failed to clear the live-agent flag: $($_.Exception.Message)") } try { Remove-OwnedHermesProfile -Lease $HermesLease } catch { $errors.Add("Hermes profile cleanup failed: $($_.Exception.Message)") } foreach ($name in $ProfileVariables) { try { Restore-ProcessEnvironment -Name $name } catch { $errors.Add("Failed to restore $name`: $($_.Exception.Message)") } } try { Remove-IsolatedAuthenticationFiles -Paths $IsolatedAuthPaths -OwnedRoot $OwnedRoot } catch { $errors.Add($_.Exception.Message) } foreach ($evidence in $SourceAuthEvidence) { try { if ((Get-FileHash -LiteralPath $evidence.Path -Algorithm SHA256).Hash -cne $evidence.Hash) { $errors.Add('A real external-agent authentication source changed during the isolated live test') } } catch { $errors.Add("Could not verify source authentication file integrity: $($_.Exception.Message)") } } if ($errors.Count -gt 0) { throw "Authenticated isolation cleanup failed: $($errors -join '; ')" } } function Get-ReceiptStrings($Node) { if ($null -eq $Node) { return } if ($Node -is [string]) { $Node return } if ($Node -is [pscustomobject]) { foreach ($property in $Node.PSObject.Properties) { Get-ReceiptStrings -Node $property.Value } return } if ($Node -is [System.Collections.IEnumerable]) { foreach ($item in $Node) { Get-ReceiptStrings -Node $item } } } function Test-ReceiptIntegerProperty($Node, [string]$Name) { if ($Node -isnot [pscustomobject]) { return $false } $property = $Node.PSObject.Properties[$Name] if ($null -eq $property) { return $false } return $property.Value -is [int] -or $property.Value -is [long] } function Test-ReceiptBooleanProperty($Node, [string]$Name) { if ($Node -isnot [pscustomobject]) { return $false } $property = $Node.PSObject.Properties[$Name] return $null -ne $property -and $property.Value -is [bool] } function Assert-SafeReceiptContent($Receipt, [string]$ReceiptText) { $receiptStrings = @(Get-ReceiptStrings -Node $Receipt) $absoluteHostPaths = @($receiptStrings | Where-Object { $_ -match '(?i)(?:^|[\s"(=])(?:[A-Z]:[\\/]|\\\\[^\\/\s]+[\\/]|file:///[A-Z]:/)' }) if ($absoluteHostPaths.Count -gt 0) { throw 'Receipt contains absolute host paths' } $leakedVariables = @() foreach ($name in $secretVariables) { $value = $originalEnvironment[$name] if ([string]::IsNullOrEmpty($value)) { continue } $jsonValue = ConvertTo-Json -InputObject $value -Compress $escapedValue = if ($jsonValue.Length -ge 2) { $jsonValue.Substring(1, $jsonValue.Length - 2) } else { $jsonValue } $leaked = $ReceiptText.Contains($value) -or $ReceiptText.Contains($escapedValue) foreach ($candidate in $receiptStrings) { if ($candidate.Contains($value) -or $candidate.Contains($escapedValue)) { $leaked = $true } if ($candidate.Length -ge 8 -and $candidate.Length % 4 -eq 0 -and $candidate -match '^[A-Za-z0-9+/]*={0,2}$') { try { $decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($candidate)) if ($decoded.Contains($value) -or $decoded.Contains($escapedValue)) { $leaked = $true } } catch { # Not every base64-shaped reporter string is valid base64. } } } if ($leaked) { $leakedVariables += $name } } if ($leakedVariables.Count -gt 0) { throw ( 'Unsafe receipt: captured secret values found for environment variables: ' + (@($leakedVariables | Select-Object -Unique) -join ', ') ) } } function Publish-SafeReceipt( [string]$RawReceiptPath, [string]$SafeReceiptPath, [ValidateSet('playwright-summary', 'vitest-summary')][string]$ExpectedKind, [string]$ExpectedSpec, [string]$ReceiptLane, [string[]]$ExpectedHostIds, [string]$HermesProvider = '', [string]$HermesModel = '' ) { $tempProjectionPath = $null $tempProjectionOwned = $false try { if (-not (Test-Path -LiteralPath $RawReceiptPath -PathType Leaf)) { throw "Expected reporter receipt was not created: $RawReceiptPath" } $rawPrefix = [IO.Path]::GetFullPath($rawReceiptRoot).TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) + [IO.Path]::DirectorySeparatorChar $resolvedRaw = [IO.Path]::GetFullPath($RawReceiptPath) if (-not $resolvedRaw.StartsWith($rawPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw "Raw reporter receipt escaped the owned run tree: $resolvedRaw" } Assert-NoReparsePointInPath -Path $resolvedRaw ` -FailureMessage 'Raw reporter receipt path contains a reparse point' $safeParent = [IO.Path]::GetFullPath([IO.Path]::GetDirectoryName($SafeReceiptPath)) if (-not $safeParent.Equals([IO.Path]::GetFullPath($receiptStagingRoot), [StringComparison]::OrdinalIgnoreCase)) { throw "Safe receipt escaped the staging directory: $SafeReceiptPath" } Assert-NoReparsePointInPath -Path $safeParent ` -FailureMessage 'Safe receipt path contains a reparse point' if (Test-Path -LiteralPath $SafeReceiptPath) { throw "Safe receipt target already exists: $SafeReceiptPath" } try { $rawReceipt = Get-Content -Raw -LiteralPath $resolvedRaw | ConvertFrom-Json } catch { throw "Invalid reporter receipt JSON: $resolvedRaw" } $receiptName = [IO.Path]::GetFileNameWithoutExtension($SafeReceiptPath) if ($receiptName -cne "$ReceiptLane-report") { throw 'Safe receipt filename did not match its declared lane' } $normalizedExpectedSpec = $ExpectedSpec.Replace('\', '/') $expectedSpecPath = [IO.Path]::GetFullPath((Join-Path $repoRoot $normalizedExpectedSpec)) if ($ExpectedKind -ceq 'playwright-summary') { $configProperty = $rawReceipt.PSObject.Properties['config'] $suitesProperty = $rawReceipt.PSObject.Properties['suites'] $rootDirProperty = if ($null -ne $configProperty -and $configProperty.Value -is [pscustomobject]) { $configProperty.Value.PSObject.Properties['rootDir'] } else { $null } $specEvidence = @( if ($null -ne $rootDirProperty -and $rootDirProperty.Value -is [string] -and -not [string]::IsNullOrWhiteSpace($rootDirProperty.Value) -and $null -ne $suitesProperty) { $reportRoot = [IO.Path]::GetFullPath($rootDirProperty.Value) $reportRootPrefix = $reportRoot.TrimEnd( [IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar ) + [IO.Path]::DirectorySeparatorChar if ($expectedSpecPath.StartsWith($reportRootPrefix, [StringComparison]::OrdinalIgnoreCase)) { $reportRelativeSpec = $expectedSpecPath.Substring($reportRootPrefix.Length).Replace('\', '/') foreach ($suite in @($suitesProperty.Value)) { if ($suite -isnot [pscustomobject]) { continue } $fileProperty = $suite.PSObject.Properties['file'] if ($null -ne $fileProperty -and $fileProperty.Value -is [string] -and $fileProperty.Value.Replace('\', '/').Equals( $reportRelativeSpec, [StringComparison]::OrdinalIgnoreCase )) { $fileProperty.Value } } } } ) } else { $testResultsProperty = $rawReceipt.PSObject.Properties['testResults'] if ($null -eq $testResultsProperty -or $testResultsProperty.Value -isnot [array]) { throw 'Vitest receipt field testResults must be an array' } $specEvidence = @( foreach ($testResult in $testResultsProperty.Value) { if ($testResult -isnot [pscustomobject]) { throw 'Vitest receipt testResults entries must be objects' } $nameProperty = $testResult.PSObject.Properties['name'] if ($null -eq $nameProperty -or $nameProperty.Value -isnot [string] -or [string]::IsNullOrWhiteSpace($nameProperty.Value)) { throw 'Vitest receipt testResults[].name must be a non-empty string' } try { [IO.Path]::GetFullPath($nameProperty.Value) } catch { throw 'Vitest receipt testResults[].name must be a valid path' } } ) if ($specEvidence.Count -ne 1 -or -not [string]::Equals( $specEvidence[0], $expectedSpecPath, [StringComparison]::OrdinalIgnoreCase )) { throw 'Vitest receipt result-file set did not exactly match the expected specification' } } if ($specEvidence.Count -eq 0) { throw 'Reporter receipt did not bind the expected test specification' } $statsProperty = $rawReceipt.PSObject.Properties['stats'] $successProperty = $rawReceipt.PSObject.Properties['success'] $suiteCountProperty = $rawReceipt.PSObject.Properties['numTotalTestSuites'] if ($null -ne $statsProperty) { if ($ExpectedKind -cne 'playwright-summary') { throw 'Reporter receipt kind did not match the declared lane' } $stats = $statsProperty.Value foreach ($name in @('expected', 'unexpected', 'flaky', 'skipped')) { if (-not (Test-ReceiptIntegerProperty $stats $name)) { throw "Playwright receipt field $name must be an integer" } } $receipt = [pscustomobject][ordered]@{ schemaVersion = 1 kind = 'playwright-summary' name = $receiptName lane = $ReceiptLane spec = $normalizedExpectedSpec hostIds = @($ExpectedHostIds) success = ([int]$stats.expected -eq 1 -and [int]$stats.unexpected -eq 0 -and [int]$stats.flaky -eq 0 -and [int]$stats.skipped -eq 0) expected = [int]$stats.expected unexpected = [int]$stats.unexpected flaky = [int]$stats.flaky skipped = [int]$stats.skipped } } elseif ($null -ne $successProperty -or $null -ne $suiteCountProperty) { if ($ExpectedKind -cne 'vitest-summary') { throw 'Reporter receipt kind did not match the declared lane' } if (-not (Test-ReceiptBooleanProperty $rawReceipt 'success')) { throw 'Vitest receipt field success must be a boolean' } foreach ($name in @( 'numTotalTestSuites', 'numPassedTestSuites', 'numFailedTestSuites', 'numPendingTestSuites', 'numTotalTests', 'numPassedTests', 'numFailedTests', 'numPendingTests', 'numTodoTests' )) { if (-not (Test-ReceiptIntegerProperty $rawReceipt $name)) { throw "Vitest receipt field $name must be an integer" } } $receipt = [pscustomobject][ordered]@{ schemaVersion = 1 kind = 'vitest-summary' name = $receiptName lane = $ReceiptLane spec = $normalizedExpectedSpec hostIds = @($ExpectedHostIds) hermesProvider = $HermesProvider hermesModel = $HermesModel success = ($rawReceipt.success -and [int]$rawReceipt.numTotalTestSuites -gt 0 -and [int]$rawReceipt.numPassedTestSuites -eq [int]$rawReceipt.numTotalTestSuites -and [int]$rawReceipt.numFailedTestSuites -eq 0 -and [int]$rawReceipt.numPendingTestSuites -eq 0 -and [int]$rawReceipt.numTotalTests -eq 1 -and [int]$rawReceipt.numPassedTests -eq 1 -and [int]$rawReceipt.numFailedTests -eq 0 -and [int]$rawReceipt.numPendingTests -eq 0 -and [int]$rawReceipt.numTodoTests -eq 0) totalTestSuites = [int]$rawReceipt.numTotalTestSuites passedTestSuites = [int]$rawReceipt.numPassedTestSuites failedTestSuites = [int]$rawReceipt.numFailedTestSuites pendingTestSuites = [int]$rawReceipt.numPendingTestSuites totalTests = [int]$rawReceipt.numTotalTests passedTests = [int]$rawReceipt.numPassedTests failedTests = [int]$rawReceipt.numFailedTests pendingTests = [int]$rawReceipt.numPendingTests todoTests = [int]$rawReceipt.numTodoTests } } else { throw 'Unknown reporter receipt schema' } if (-not $receipt.success) { throw 'Reporter receipt did not prove an exact successful lane' } $receiptText = $receipt | ConvertTo-Json -Depth 4 Assert-SafeReceiptContent -Receipt $receipt -ReceiptText $receiptText $tempProjectionPath = "$SafeReceiptPath.partial-$([guid]::NewGuid().ToString('N'))" $projectionBytes = [Text.UTF8Encoding]::new($false).GetBytes($receiptText) $projectionStream = [IO.File]::Open( $tempProjectionPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None ) $tempProjectionOwned = $true try { $projectionStream.Write($projectionBytes, 0, $projectionBytes.Length) } finally { $projectionStream.Dispose() } [IO.File]::Move($tempProjectionPath, $SafeReceiptPath) $tempProjectionPath = $null $tempProjectionOwned = $false } finally { if ($tempProjectionOwned -and $tempProjectionPath -and (Test-Path -LiteralPath $tempProjectionPath -PathType Leaf)) { Remove-Item -LiteralPath $tempProjectionPath -Force } if (Test-Path -LiteralPath $RawReceiptPath -PathType Leaf) { Remove-Item -LiteralPath $RawReceiptPath -Force } } } function Assert-SafeReceiptSet([string]$StagingRoot, [string[]]$ExpectedReceiptNames) { Assert-NoReparsePointInPath -Path $StagingRoot ` -FailureMessage 'Receipt staging path contains a reparse point' $stagingItem = Get-Item -LiteralPath $StagingRoot -Force if (-not $stagingItem.PSIsContainer -or ($stagingItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Receipt staging root must be an owned regular directory' } $items = @(Get-ChildItem -LiteralPath $StagingRoot -Force) if (@($items | Where-Object { $_.PSIsContainer -or ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) }).Count -gt 0) { throw 'Receipt staging contains a directory or reparse point' } $actualNames = @($items | ForEach-Object Name | Sort-Object) $expectedNames = @($ExpectedReceiptNames | Sort-Object) if (@(Compare-Object -ReferenceObject $expectedNames -DifferenceObject $actualNames -CaseSensitive).Count -gt 0) { throw "Receipt staging did not contain the exact expected set: $($expectedNames -join ', ')" } foreach ($file in $items) { $text = [IO.File]::ReadAllText($file.FullName) $receipt = $text | ConvertFrom-Json if (-not (Test-ReceiptIntegerProperty $receipt 'schemaVersion') -or [int]$receipt.schemaVersion -ne 1 -or -not (Test-ReceiptBooleanProperty $receipt 'success') -or -not $receipt.success) { throw "Safe receipt failed final schema validation: $($file.Name)" } $contract = switch -CaseSensitive ($file.Name) { 'hooks-report.json' { [pscustomobject]@{ kind = 'playwright-summary' lane = 'hooks' spec = 'tests/e2e/launcher-real-hook-lifecycle.spec.ts' hostIds = @($receiptHostIds) } } 'tools-report.json' { [pscustomobject]@{ kind = 'playwright-summary' lane = 'tools' spec = 'tests/e2e/launcher-real-tool-lifecycle.spec.ts' hostIds = @($receiptHostIds) } } 'authenticated-tasks-report.json' { [pscustomobject]@{ kind = 'vitest-summary' lane = 'authenticated-tasks' spec = 'tests/integration/external-agent-collaboration.live.test.ts' hostIds = @($authenticatedHostIds) hermesProvider = 'openai-codex' hermesModel = 'gpt-5.5' } } default { throw "Safe receipt had no release contract: $($file.Name)" } } foreach ($name in @('kind', 'lane', 'spec')) { $property = $receipt.PSObject.Properties[$name] if ($null -eq $property -or $property.Value -isnot [string] -or $property.Value -cne $contract.$name) { throw "Safe receipt identity mismatch for $($file.Name): $name" } } $hostIdsProperty = $receipt.PSObject.Properties['hostIds'] $actualHostIds = if ($null -eq $hostIdsProperty) { @() } else { @($hostIdsProperty.Value) } if ($null -eq $hostIdsProperty -or @(Compare-Object -ReferenceObject @($contract.hostIds) -DifferenceObject $actualHostIds -CaseSensitive).Count -gt 0 -or $actualHostIds.Count -ne @($contract.hostIds).Count) { throw "Safe receipt host roster mismatch for $($file.Name)" } if ($file.Name -ceq 'authenticated-tasks-report.json') { foreach ($name in @('hermesProvider', 'hermesModel')) { $property = $receipt.PSObject.Properties[$name] if ($null -eq $property -or $property.Value -isnot [string] -or $property.Value -cne $contract.$name) { throw "Safe receipt Hermes identity mismatch for $($file.Name): $name" } } } Assert-SafeReceiptContent -Receipt $receipt -ReceiptText $text } } function Publish-ReceiptSet( [string]$StagingRoot, [string]$FinalRoot, [string[]]$ExpectedReceiptNames ) { if (Test-Path -LiteralPath $FinalRoot) { throw "ReceiptDir became occupied before publication: $FinalRoot" } Assert-NoReparsePointInPath -Path $StagingRoot ` -FailureMessage 'Receipt publication staging path contains a reparse point' Assert-NoReparsePointInPath -Path ([IO.Path]::GetDirectoryName($FinalRoot)) ` -FailureMessage 'Receipt publication destination path contains a reparse point' $stagingParent = [IO.Path]::GetFullPath([IO.Path]::GetDirectoryName($StagingRoot)) $finalParent = [IO.Path]::GetFullPath([IO.Path]::GetDirectoryName($FinalRoot)) if (-not $stagingParent.Equals($finalParent, [StringComparison]::OrdinalIgnoreCase)) { throw 'Receipt staging and final directories must be same-volume siblings' } Assert-SafeReceiptSet -StagingRoot $StagingRoot -ExpectedReceiptNames $ExpectedReceiptNames [IO.Directory]::Move($StagingRoot, $FinalRoot) } function Remove-VerifiedReceiptStaging([string]$StagingRoot, [string]$FinalRoot, [bool]$Owned) { if (-not $Owned -or [string]::IsNullOrWhiteSpace($StagingRoot) -or -not (Test-Path -LiteralPath $StagingRoot)) { return } $resolvedStaging = [IO.Path]::GetFullPath($StagingRoot) $finalParent = [IO.Path]::GetFullPath([IO.Path]::GetDirectoryName($FinalRoot)) Assert-NoReparsePointInPath -Path $resolvedStaging ` -FailureMessage 'Receipt cleanup path contains a reparse point' Assert-NoReparsePointInPath -Path $finalParent ` -FailureMessage 'Receipt cleanup destination path contains a reparse point' $stagingParent = [IO.Path]::GetFullPath([IO.Path]::GetDirectoryName($resolvedStaging)) $expectedPrefix = ".$([IO.Path]::GetFileName($FinalRoot)).staging-" if (-not $stagingParent.Equals($finalParent, [StringComparison]::OrdinalIgnoreCase) -or -not [IO.Path]::GetFileName($resolvedStaging).StartsWith($expectedPrefix, [StringComparison]::Ordinal)) { throw "Refusing receipt staging cleanup outside the owned sibling: $resolvedStaging" } $stagingItem = Get-Item -LiteralPath $resolvedStaging -Force if (-not $stagingItem.PSIsContainer -or ($stagingItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw "Refusing unsafe receipt staging cleanup: $resolvedStaging" } Remove-Item -LiteralPath $resolvedStaging -Recurse -Force } function Get-FreeLoopbackPort { $listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0) try { $listener.Start() return ([Net.IPEndPoint]$listener.LocalEndpoint).Port } finally { $listener.Stop() } } function Remove-VerifiedTempTree([string]$Target) { $resolved = [IO.Path]::GetFullPath($Target) $tempPrefix = $tempBase.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar if (-not $resolved.StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw "Refusing recursive cleanup outside the OS temp directory: $resolved" } $extended = if ($resolved.StartsWith('\\')) { '\\?\UNC\' + $resolved.Substring(2) } else { '\\?\' + $resolved } if (-not [IO.Directory]::Exists($extended)) { return } $lastError = $null for ($attempt = 0; $attempt -lt 40; $attempt += 1) { try { $targetItem = Get-Item -LiteralPath $resolved -Force if (-not $targetItem.PSIsContainer -or ($targetItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw "Refusing unsafe temp-tree cleanup: $resolved" } [IO.Directory]::Delete($extended, $true) return } catch { if (-not [IO.Directory]::Exists($extended)) { return } $lastError = $_ Start-Sleep -Milliseconds 250 } } if (-not [IO.Directory]::Exists($extended)) { return } throw $lastError } function Invoke-PlaywrightLane([string]$Spec, [string]$DataDir, [string]$ReceiptName) { $port = Get-FreeLoopbackPort $rawReceiptPath = $null $safeReceiptPath = $null $playwrightOutput = Join-Path $runRoot "playwright-$ReceiptName" Set-ProcessEnvironment -Name 'WAGGLE_E2E_DATA_DIR' -Value $DataDir Set-ProcessEnvironment -Name 'WAGGLE_E2E_PORT' -Value ([string]$port) Set-ProcessEnvironment -Name 'WAGGLE_E2E_BASE_URL' -Value "http://127.0.0.1:$port" Write-Host "Running $Spec on isolated port $port" $playwrightArgs = @( 'test', $Spec, '--config=playwright.config.ts', '--project=chromium', '--retries=0', '--output', $playwrightOutput ) if ($null -ne $receiptStagingRoot) { $rawReceiptPath = Join-Path $rawReceiptRoot "$ReceiptName-report.raw.json" $safeReceiptPath = Join-Path $receiptStagingRoot "$ReceiptName-report.json" Set-ProcessEnvironment -Name 'PLAYWRIGHT_JSON_OUTPUT_FILE' -Value $rawReceiptPath $playwrightArgs += @( '--reporter=list,json' ) } else { Set-ProcessEnvironment -Name 'PLAYWRIGHT_JSON_OUTPUT_FILE' -Value $null $playwrightArgs += '--reporter=list' } try { & $script:runnerNodePath $script:playwrightCli @playwrightArgs $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { throw "$Spec failed with exit code $exitCode" } if ($rawReceiptPath) { Publish-SafeReceipt -RawReceiptPath $rawReceiptPath -SafeReceiptPath $safeReceiptPath ` -ExpectedKind 'playwright-summary' -ExpectedSpec $Spec -ReceiptLane $ReceiptName ` -ExpectedHostIds $receiptHostIds } } finally { if ($rawReceiptPath -and (Test-Path -LiteralPath $rawReceiptPath -PathType Leaf)) { Remove-Item -LiteralPath $rawReceiptPath -Force } } } function Invoke-VitestLane( [string]$Spec, [string]$ReceiptName, [string]$HermesProvider, [string]$HermesModel ) { $rawReceiptPath = $null $safeReceiptPath = $null $vitestArgs = @( 'run', $Spec, '--retry=0', '--reporter=verbose' ) if ($null -ne $receiptStagingRoot) { $rawReceiptPath = Join-Path $rawReceiptRoot "$ReceiptName-report.raw.json" $safeReceiptPath = Join-Path $receiptStagingRoot "$ReceiptName-report.json" $vitestArgs += @( '--reporter=json', "--outputFile=$rawReceiptPath" ) } try { & $script:runnerNodePath $script:vitestCli @vitestArgs $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { throw "$Spec failed with exit code $exitCode" } if ($rawReceiptPath) { Publish-SafeReceipt -RawReceiptPath $rawReceiptPath -SafeReceiptPath $safeReceiptPath ` -ExpectedKind 'vitest-summary' -ExpectedSpec $Spec -ReceiptLane $ReceiptName ` -ExpectedHostIds $receiptHostIds -HermesProvider $HermesProvider -HermesModel $HermesModel } } finally { if ($rawReceiptPath -and (Test-Path -LiteralPath $rawReceiptPath -PathType Leaf)) { Remove-Item -LiteralPath $rawReceiptPath -Force } } } $publicationCompleted = $false try { try { $null = New-Item -ItemType Directory -Path $hookProfile -Force $null = New-Item -ItemType Directory -Path $rawReceiptRoot -Force if ($null -ne $receiptRoot) { if (Test-Path -LiteralPath $receiptRoot) { throw "ReceiptDir must be a fresh path owned by this run: $receiptRoot" } Assert-NoReparsePointInPath -Path $receiptParent ` -FailureMessage 'ReceiptDir path must not contain a reparse point' $null = New-Item -ItemType Directory -Path $receiptParent -Force Assert-NoReparsePointInPath -Path $receiptParent ` -FailureMessage 'ReceiptDir path must not contain a reparse point' $null = New-Item -ItemType Directory -Path $receiptStagingRoot $receiptStagingOwned = $true } foreach ($name in $secretVariables) { Set-ProcessEnvironment -Name $name -Value $null } Set-ProcessEnvironment -Name 'WAGGLE_E2E_SKIP_LITELLM' -Value '1' Set-ProcessEnvironment -Name 'WAGGLE_E2E_REUSE_EXISTING_SERVER' -Value '0' Set-ProcessEnvironment -Name 'WAGGLE_E2E_TEMP_ROOT' -Value $runRoot Set-ProcessEnvironment -Name 'WAGGLE_E2E_HOST_IDS' -Value $requestedHostIds Set-ProcessEnvironment -Name 'PLAYWRIGHT_JSON_OUTPUT_FILE' -Value $null Push-Location $repoRoot try { Set-ProcessEnvironment -Name 'USERPROFILE' -Value $hookProfile Set-ProcessEnvironment -Name 'HOME' -Value $hookProfile Set-ProcessEnvironment -Name 'HOMEDRIVE' -Value (Split-Path -Qualifier $hookProfile) Set-ProcessEnvironment -Name 'HOMEPATH' -Value $hookProfile.Substring((Split-Path -Qualifier $hookProfile).Length) Set-ProcessEnvironment -Name 'APPDATA' -Value (Join-Path $hookProfile 'AppData\Roaming') Set-ProcessEnvironment -Name 'LOCALAPPDATA' -Value (Join-Path $hookProfile 'AppData\Local') Set-ProcessEnvironment -Name 'HERMES_HOME' -Value (Join-Path $hookProfile '.hermes') Set-ProcessEnvironment -Name 'HERMES_PROFILE' -Value $null Set-ProcessEnvironment -Name 'WAGGLE_E2E_HOOK_HOME' -Value $hookProfile Set-ProcessEnvironment -Name 'WAGGLE_E2E_REAL_HOOKS' -Value '1' Set-ProcessEnvironment -Name 'WAGGLE_E2E_REAL_TOOLS' -Value $null Invoke-PlaywrightLane -Spec 'tests/e2e/launcher-real-hook-lifecycle.spec.ts' -DataDir (Join-Path $runRoot 'hook-data') -ReceiptName 'hooks' foreach ($name in $profileVariables) { Restore-ProcessEnvironment -Name $name } Set-ProcessEnvironment -Name 'WAGGLE_E2E_HOOK_HOME' -Value $null Set-ProcessEnvironment -Name 'WAGGLE_E2E_REAL_HOOKS' -Value $null Set-ProcessEnvironment -Name 'WAGGLE_E2E_REAL_TOOLS' -Value '1' Invoke-PlaywrightLane -Spec 'tests/e2e/launcher-real-tool-lifecycle.spec.ts' -DataDir (Join-Path $runRoot 'tool-data') -ReceiptName 'tools' if ($AuthenticatedTasks) { $authenticatedHermesProfile = 'wagglee2e-' + [guid]::NewGuid().ToString('N') $authenticatedHermesLease = $null $authenticatedProfileRoot = Join-Path $runRoot 'authenticated-profile' $authenticatedHermesRoot = Join-Path $authenticatedProfileRoot 'hermes-root' $hermesProfilesRoot = Join-Path $authenticatedHermesRoot 'profiles' $null = New-Item -ItemType Directory -Path $authenticatedHermesRoot -Force Assert-NoReparsePointInPath -Path $authenticatedHermesRoot ` -FailureMessage 'Hermes authenticated root contains a reparse point' $originalUserHome = if (-not [string]::IsNullOrWhiteSpace($originalEnvironment['USERPROFILE'])) { $originalEnvironment['USERPROFILE'] } elseif (-not [string]::IsNullOrWhiteSpace($originalEnvironment['HOME'])) { $originalEnvironment['HOME'] } else { [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile) } $sourceClaudeConfig = if (-not [string]::IsNullOrWhiteSpace($originalEnvironment['CLAUDE_CONFIG_DIR'])) { $originalEnvironment['CLAUDE_CONFIG_DIR'] } else { Join-Path $originalUserHome '.claude' } $sourceCodexHome = if (-not [string]::IsNullOrWhiteSpace($originalEnvironment['CODEX_HOME'])) { $originalEnvironment['CODEX_HOME'] } else { Join-Path $originalUserHome '.codex' } $sourceHermesHome = if (-not [string]::IsNullOrWhiteSpace($originalEnvironment['HERMES_HOME'])) { $originalEnvironment['HERMES_HOME'] } else { Join-Path $originalEnvironment['LOCALAPPDATA'] 'hermes' } $sourceClaudeCredentials = Join-Path $sourceClaudeConfig '.credentials.json' $sourceCodexAuth = Join-Path $sourceCodexHome 'auth.json' $sourceHermesAuth = Join-Path $sourceHermesHome 'auth.json' $isolatedClaudeCredentials = Join-Path $authenticatedProfileRoot '.claude\.credentials.json' $isolatedCodexAuth = Join-Path $authenticatedProfileRoot '.codex\auth.json' $isolatedHermesAuth = Join-Path ( Join-Path $hermesProfilesRoot $authenticatedHermesProfile ) 'auth.json' $isolatedAuthPaths = @($isolatedClaudeCredentials, $isolatedCodexAuth, $isolatedHermesAuth) $sourceAuthEvidence = @( [pscustomobject]@{ Path = $sourceClaudeCredentials; Hash = (Get-FileHash -LiteralPath $sourceClaudeCredentials -Algorithm SHA256).Hash }, [pscustomobject]@{ Path = $sourceCodexAuth; Hash = (Get-FileHash -LiteralPath $sourceCodexAuth -Algorithm SHA256).Hash }, [pscustomobject]@{ Path = $sourceHermesAuth; Hash = (Get-FileHash -LiteralPath $sourceHermesAuth -Algorithm SHA256).Hash } ) try { Set-ProcessEnvironment -Name 'HERMES_HOME' -Value $authenticatedHermesRoot Set-ProcessEnvironment -Name 'HERMES_PROFILE' -Value $null $authenticatedHermesLease = New-OwnedHermesProfile ` -ProfilesRoot $hermesProfilesRoot -ProfileName $authenticatedHermesProfile ` -OwnedRoot $authenticatedProfileRoot Copy-IsolatedAuthenticationFile ` -Source $sourceClaudeCredentials ` -Destination $isolatedClaudeCredentials ` -OwnedRoot $authenticatedProfileRoot Copy-IsolatedAuthenticationFile ` -Source $sourceCodexAuth ` -Destination $isolatedCodexAuth ` -OwnedRoot $authenticatedProfileRoot Copy-IsolatedAuthenticationFile ` -Source $sourceHermesAuth ` -Destination $isolatedHermesAuth ` -OwnedRoot $authenticatedProfileRoot $codexShim = New-IsolatedCodexShim ` -ShimRoot (Join-Path $authenticatedProfileRoot 'bin') ` -CodexHome (Join-Path $authenticatedProfileRoot '.codex') ` -OwnedRoot $authenticatedProfileRoot $hermesConfig = @' model: default: gpt-5.5 provider: openai-codex agent: reasoning_effort: xhigh '@ [IO.File]::WriteAllText( (Join-Path $authenticatedHermesLease.ProfileHome 'config.yaml'), $hermesConfig + [Environment]::NewLine, [Text.UTF8Encoding]::new($false) ) $authenticatedDrive = Split-Path -Qualifier $authenticatedProfileRoot Set-ProcessEnvironment -Name 'USERPROFILE' -Value $authenticatedProfileRoot Set-ProcessEnvironment -Name 'HOME' -Value $authenticatedProfileRoot Set-ProcessEnvironment -Name 'HOMEDRIVE' -Value $authenticatedDrive Set-ProcessEnvironment -Name 'HOMEPATH' -Value $authenticatedProfileRoot.Substring($authenticatedDrive.Length) Set-ProcessEnvironment -Name 'APPDATA' -Value (Join-Path $authenticatedProfileRoot 'AppData\Roaming') Set-ProcessEnvironment -Name 'LOCALAPPDATA' -Value (Join-Path $authenticatedProfileRoot 'AppData\Local') Set-ProcessEnvironment -Name 'CLAUDE_CONFIG_DIR' -Value $null Set-ProcessEnvironment -Name 'CODEX_HOME' -Value $null Set-ProcessEnvironment -Name 'PATH' -Value ( $codexShim.Directory + [IO.Path]::PathSeparator + $originalEnvironment['PATH'] ) Set-ProcessEnvironment -Name 'WAGGLE_E2E_REAL_TOOLS' -Value $null Set-ProcessEnvironment -Name 'WAGGLE_LIVE_EXTERNAL_AGENTS' -Value '1' Set-ProcessEnvironment -Name 'WAGGLE_LIVE_HERMES_PROVIDER' -Value $null Set-ProcessEnvironment -Name 'WAGGLE_LIVE_HERMES_MODEL' -Value $null Set-ProcessEnvironment -Name 'HERMES_HOME' -Value $authenticatedHermesLease.ProfileHome Invoke-NativePreflight -FilePath 'claude' -ArgumentList @('auth', 'status') ` -FailureMessage 'Isolated Claude Code authentication preflight failed' Invoke-NativePreflight -FilePath $script:runnerNodePath ` -ArgumentList @($codexShim.Launcher, 'login', 'status') ` -FailureMessage 'Isolated Codex authentication preflight failed' Invoke-VitestLane -Spec 'tests/integration/external-agent-collaboration.live.test.ts' ` -ReceiptName 'authenticated-tasks' -HermesProvider 'openai-codex' -HermesModel 'gpt-5.5' } finally { Complete-AuthenticatedIsolationCleanup ` -HermesLease $authenticatedHermesLease ` -SourceAuthEvidence $sourceAuthEvidence ` -ProfileVariables $profileVariables ` -IsolatedAuthPaths $isolatedAuthPaths ` -OwnedRoot $authenticatedProfileRoot } } } finally { Pop-Location } } finally { foreach ($name in $environmentToRestore) { Restore-ProcessEnvironment -Name $name } Remove-VerifiedTempTree -Target $runRoot } if ($null -ne $receiptRoot) { Publish-ReceiptSet -StagingRoot $receiptStagingRoot -FinalRoot $receiptRoot ` -ExpectedReceiptNames $expectedReceiptNames $receiptStagingOwned = $false } $publicationCompleted = $true } finally { if (-not $publicationCompleted -and $null -ne $receiptRoot) { Remove-VerifiedReceiptStaging -StagingRoot $receiptStagingRoot -FinalRoot $receiptRoot ` -Owned $receiptStagingOwned } } $authenticatedMessage = if ($AuthenticatedTasks) { ', authenticated task' } else { '' } Write-Host "Windows external-agent route, hook$authenticatedMessage lifecycle passed with isolated cleanup."