param( [string]$PackagePath = "", [string]$ZipPath = "", [string]$WorkDir = "", [switch]$StrictV2 ) $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) } function Get-JsonPropertyValue($Object, [string]$Name) { if ($null -eq $Object) { return $null } $property = $Object.PSObject.Properties[$Name] if ($null -eq $property) { return $null } return $property.Value } if (-not [string]::IsNullOrWhiteSpace($ZipPath)) { $PackagePath = $ZipPath } if ([string]::IsNullOrWhiteSpace($PackagePath)) { throw "PackagePath or ZipPath is required" } 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) $operatorStartedAtLocalPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "operator_started_at_local") $operatorClickedSendAtLocalPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "operator_clicked_send_at_local") $operatorObservedSuccessAtLocalPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "operator_observed_success_at_local") $beforeRecorderPresentFlag = ((Get-JsonPropertyValue $inputs "before_recorder_present") -eq $true) $afterRecorderPresentFlag = ((Get-JsonPropertyValue $inputs "after_recorder_present") -eq $true) $successAckOrSentRecordPresentFlag = ((Get-JsonPropertyValue $inputs "success_ack_or_sent_record_present") -eq $true) $duplicateSecondSendAttemptedValue = Get-JsonPropertyValue $inputs "duplicate_second_send_attempted" $duplicateSecondSendAttemptedFalse = ($null -ne $duplicateSecondSendAttemptedValue -and $duplicateSecondSendAttemptedValue -eq $false) $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") } if (-not $operatorStartedAtLocalPresent) { $missing.Add("operator_started_at_local") } if (-not $operatorClickedSendAtLocalPresent) { $missing.Add("operator_clicked_send_at_local") } if (-not $operatorObservedSuccessAtLocalPresent) { $missing.Add("operator_observed_success_at_local") } if (-not $beforeRecorderPresentFlag) { $missing.Add("before_recorder_present") } if (-not $afterRecorderPresentFlag) { $missing.Add("after_recorder_present") } if (-not $successAckOrSentRecordPresentFlag) { $missing.Add("success_ack_or_sent_record_present") } if (-not $duplicateSecondSendAttemptedFalse) { $missing.Add("duplicate_second_send_attempted_false") } $strictV2Pass = ( $r6dGatePass -and $operatorStartedAtLocalPresent -and $operatorClickedSendAtLocalPresent -and $operatorObservedSuccessAtLocalPresent -and $beforeRecorderPresentFlag -and $afterRecorderPresentFlag -and $successAckOrSentRecordPresentFlag -and $duplicateSecondSendAttemptedFalse ) $summary = [ordered]@{ ok = $true package = $resolvedPackage input_file_name = $inputsFile.Name partial_evidence_usable = $partialUsable r6d_gate_pass = $r6dGatePass strict_v2_pass = $strictV2Pass 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 operator_started_at_local_present = $operatorStartedAtLocalPresent operator_clicked_send_at_local_present = $operatorClickedSendAtLocalPresent operator_observed_success_at_local_present = $operatorObservedSuccessAtLocalPresent before_recorder_present_flag = $beforeRecorderPresentFlag after_recorder_present_flag = $afterRecorderPresentFlag success_ack_or_sent_record_present_flag = $successAckOrSentRecordPresentFlag duplicate_second_send_attempted_false = $duplicateSecondSendAttemptedFalse 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 ($strictV2Pass) { "strict_v2_gate_pass" } elseif ($r6dGatePass) { "full_gate_pass" } elseif ($partialUsable) { "partial_use_only" } else { "not_usable" } } $summary | ConvertTo-Json -Depth 6 -Compress if ($StrictV2 -and -not $strictV2Pass) { exit 1 } } finally { if ($cleanup -and (Test-Path -LiteralPath $WorkDir)) { Remove-Item -LiteralPath $WorkDir -Recurse -Force } }