Files
waggle-os/scripts/test-windows-external-agents.ps1
Oleg Maslov b20b138fe4 moving
2026-09-02 10:14:22 +02:00

1232 lines
51 KiB
PowerShell

[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."