param( [Parameter(Mandatory=$true)] [string]$PackagePath, [string]$WorkDir = "" ) $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest function ConvertTo-Sha256Hex([string]$Text) { $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) $sha = [System.Security.Cryptography.SHA256]::Create() try { return -join ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") }) } finally { $sha.Dispose() } } function Read-FirstJsonLine([string]$Path) { if (-not (Test-Path -LiteralPath $Path)) { return $null } foreach ($line in Get-Content -LiteralPath $Path) { $trimmed = $line.Trim() if ($trimmed.StartsWith("{") -and $trimmed.EndsWith("}")) { return ($trimmed | ConvertFrom-Json) } } return $null } function Test-NonEmptyString($Value) { if ($null -eq $Value) { return $false } return -not [string]::IsNullOrWhiteSpace([string]$Value) } if (-not (Test-Path -LiteralPath $PackagePath)) { throw "package not found: $PackagePath" } $resolvedPackage = (Resolve-Path -LiteralPath $PackagePath).Path $cleanup = $false if ([string]::IsNullOrWhiteSpace($WorkDir)) { $WorkDir = Join-Path ([System.IO.Path]::GetTempPath()) ("isphere-returned-send-sandbox-" + [guid]::NewGuid().ToString("N")) $cleanup = $true } if (Test-Path -LiteralPath $WorkDir) { Remove-Item -LiteralPath $WorkDir -Recurse -Force } New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null try { if ($resolvedPackage.EndsWith(".zip", [System.StringComparison]::OrdinalIgnoreCase)) { Expand-Archive -LiteralPath $resolvedPackage -DestinationPath $WorkDir -Force $root = $WorkDir } else { $root = $resolvedPackage } $allFiles = Get-ChildItem -LiteralPath $root -Recurse -File $inputsFile = $allFiles | Where-Object { $_.Name -like "SANDBOX-SEND-INPUTS*.json" } | Sort-Object FullName | Select-Object -First 1 if (-not $inputsFile) { throw "missing SANDBOX-SEND-INPUTS json" } $inputs = Get-Content -LiteralPath $inputsFile.FullName -Raw | ConvertFrom-Json $capabilityLog = $allFiles | Where-Object { $_.Name -like "*capability-test*.txt" -or $_.Name -like "*capability*.log" } | Sort-Object FullName | Select-Object -First 1 $capability = $null if ($capabilityLog) { $capability = Read-FirstJsonLine $capabilityLog.FullName } $liveProbeZips = @($allFiles | Where-Object { $_.Name -like "isphere-live-probe-*.zip" }) $beforeMarkers = @($allFiles | Where-Object { $_.FullName -match "\\before\\" -or $_.FullName -match "/before/" -or $_.Name -like "*before*" }) $afterMarkers = @($allFiles | Where-Object { $_.FullName -match "\\after\\" -or $_.FullName -match "/after/" -or $_.Name -like "*after*" }) $contentText = [string]$inputs.content_text_to_send $expectedHash = [string]$inputs.content_sha256 $actualHash = ConvertTo-Sha256Hex $contentText $contentHashMatches = ($actualHash -eq $expectedHash) $capabilityOK = $false $capabilityToolCount = 0 $productionSendEnabled = $false if ($null -ne $capability) { $capabilityOK = ($capability.ok -eq $true) if ($null -ne $capability.tool_count) { $capabilityToolCount = [int]$capability.tool_count } if ($null -ne $capability.production_send_enabled) { $productionSendEnabled = [bool]$capability.production_send_enabled } } $hasExplicitTarget = (Test-NonEmptyString $inputs.sandbox_target_type) -and (Test-NonEmptyString $inputs.sandbox_target_id) $hasIdempotencyKey = Test-NonEmptyString $inputs.idempotency_key $hasSuccessRecord = Test-NonEmptyString $inputs.success_ack_or_sent_record $manualStartPresent = Test-NonEmptyString $inputs.manual_send_started_at_local $manualFinishPresent = Test-NonEmptyString $inputs.manual_send_finished_at_local $operatorSuccess = ($inputs.operator_observed_success -eq $true) $noSecondSend = ($inputs.do_not_send_second_time_confirmed -eq $true) $hasBeforeProbe = ($liveProbeZips.Count -gt 0 -or $beforeMarkers.Count -gt 0) $hasAfterProbe = ($afterMarkers.Count -gt 0) $partialUsable = ( $capabilityOK -and $capabilityToolCount -eq 9 -and -not $productionSendEnabled -and $hasExplicitTarget -and $contentHashMatches -and $hasIdempotencyKey -and $operatorSuccess -and $hasSuccessRecord -and $noSecondSend -and $hasBeforeProbe ) $r6dGatePass = ( $partialUsable -and $manualStartPresent -and $manualFinishPresent -and $hasAfterProbe ) $missing = New-Object System.Collections.Generic.List[string] if (-not $manualStartPresent) { $missing.Add("manual_send_started_at_local") } if (-not $manualFinishPresent) { $missing.Add("manual_send_finished_at_local") } if (-not $hasAfterProbe) { $missing.Add("after_recorder_output") } [ordered]@{ ok = $true package = $resolvedPackage input_file_name = $inputsFile.Name partial_evidence_usable = $partialUsable r6d_gate_pass = $r6dGatePass target_type_present = Test-NonEmptyString $inputs.sandbox_target_type target_id_present = Test-NonEmptyString $inputs.sandbox_target_id content_hash_matches = $contentHashMatches idempotency_key_present = $hasIdempotencyKey operator_observed_success = $operatorSuccess success_ack_or_sent_record_present = $hasSuccessRecord do_not_send_second_time_confirmed = $noSecondSend manual_send_started_at_local_present = $manualStartPresent manual_send_finished_at_local_present = $manualFinishPresent capability_test_ok = $capabilityOK capability_tool_count = $capabilityToolCount production_send_enabled = $productionSendEnabled has_before_probe = $hasBeforeProbe has_after_probe = $hasAfterProbe live_probe_zip_count = $liveProbeZips.Count missing_for_full_gate = @($missing) decision = if ($r6dGatePass) { "full_gate_pass" } elseif ($partialUsable) { "partial_use_only" } else { "not_usable" } } | ConvertTo-Json -Depth 6 -Compress } finally { if ($cleanup -and (Test-Path -LiteralPath $WorkDir)) { Remove-Item -LiteralPath $WorkDir -Recurse -Force } }