param( [string]$PackagePath = "", [string]$ZipPath = "", [string]$WorkDir = "", [switch]$Strict ) $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest 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 } 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 } 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-file-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 "FILE-SEND-INPUTS*.json" } | Sort-Object FullName | Select-Object -First 1 if (-not $inputsFile) { throw "missing FILE-SEND-INPUTS json" } $inputs = Get-Content -LiteralPath $inputsFile.FullName -Raw | ConvertFrom-Json $capabilityLog = $allFiles | Where-Object { $_.Name -like "*capability-smoke*.txt" -or $_.Name -like "*capability*.log" } | Sort-Object FullName | Select-Object -First 1 $capability = $null if ($capabilityLog) { $capability = Read-FirstJsonLine $capabilityLog.FullName } $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*" }) $targetTypePresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "sandbox_target_type") $targetIDPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "sandbox_target_id") $allowedDirPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "ISPHERE_SEND_FILE_ALLOWED_DIR") $filePathPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "sandbox_file_path") $fileHash = [string](Get-JsonPropertyValue $inputs "file_sha256") $fileHashPresent = ($fileHash -match "^[0-9a-f]{64}$") $fileSize = Get-JsonPropertyValue $inputs "file_size_bytes" $fileSizePresent = ($null -ne $fileSize -and [int64]$fileSize -gt 0) $idempotencyKeyPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "idempotency_key") $operatorStartedAtLocalPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "operator_started_at_local") $operatorClickedSendFileAtLocalPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "operator_clicked_send_file_at_local") $operatorObservedFileSuccessAtLocalPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "operator_observed_file_success_at_local") $manualStartPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "manual_file_send_started_at_local") $manualFinishPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "manual_file_send_finished_at_local") $operatorObservedFileSuccess = ((Get-JsonPropertyValue $inputs "operator_observed_file_success") -eq $true) $fileUploadOrSentRecordPresent = Test-NonEmptyString (Get-JsonPropertyValue $inputs "file_upload_or_sent_record") $fileUploadOrSentRecordPresentFlag = ((Get-JsonPropertyValue $inputs "file_upload_or_sent_record_present") -eq $true) $beforeRecorderPresentFlag = ((Get-JsonPropertyValue $inputs "before_recorder_present") -eq $true) $afterRecorderPresentFlag = ((Get-JsonPropertyValue $inputs "after_recorder_present") -eq $true) $duplicateSecondFileSendAttemptedFalse = ((Get-JsonPropertyValue $inputs "duplicate_second_file_send_attempted") -eq $false) $doNotSendSecondFileConfirmed = ((Get-JsonPropertyValue $inputs "do_not_send_second_file_confirmed") -eq $true) $productionFileSendEnabledFalse = ((Get-JsonPropertyValue $inputs "production_file_send_enabled") -eq $false) $capabilityOK = $false $capabilityToolCount = 0 $capabilitySendFilePreview = $false $capabilityProductionFileEnabled = $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.send_file_preview_tool_present) { $capabilitySendFilePreview = [bool]$capability.send_file_preview_tool_present } if ($null -ne $capability.send_file_production_enabled) { $capabilityProductionFileEnabled = [bool]$capability.send_file_production_enabled } } $hasBeforeProbe = ($beforeMarkers.Count -gt 0) $hasAfterProbe = ($afterMarkers.Count -gt 0) $missing = New-Object System.Collections.Generic.List[string] if (-not $targetTypePresent) { $missing.Add("sandbox_target_type") } if (-not $targetIDPresent) { $missing.Add("sandbox_target_id") } if (-not $allowedDirPresent) { $missing.Add("ISPHERE_SEND_FILE_ALLOWED_DIR") } if (-not $filePathPresent) { $missing.Add("sandbox_file_path") } if (-not $fileHashPresent) { $missing.Add("file_sha256") } if (-not $fileSizePresent) { $missing.Add("file_size_bytes") } if (-not $idempotencyKeyPresent) { $missing.Add("idempotency_key") } if (-not $operatorStartedAtLocalPresent) { $missing.Add("operator_started_at_local") } if (-not $operatorClickedSendFileAtLocalPresent) { $missing.Add("operator_clicked_send_file_at_local") } if (-not $operatorObservedFileSuccessAtLocalPresent) { $missing.Add("operator_observed_file_success_at_local") } if (-not $manualStartPresent) { $missing.Add("manual_file_send_started_at_local") } if (-not $manualFinishPresent) { $missing.Add("manual_file_send_finished_at_local") } if (-not $operatorObservedFileSuccess) { $missing.Add("operator_observed_file_success") } if (-not $fileUploadOrSentRecordPresent) { $missing.Add("file_upload_or_sent_record") } if (-not $fileUploadOrSentRecordPresentFlag) { $missing.Add("file_upload_or_sent_record_present") } if (-not $beforeRecorderPresentFlag) { $missing.Add("before_recorder_present") } if (-not $afterRecorderPresentFlag) { $missing.Add("after_recorder_present") } if (-not $duplicateSecondFileSendAttemptedFalse) { $missing.Add("duplicate_second_file_send_attempted_false") } if (-not $doNotSendSecondFileConfirmed) { $missing.Add("do_not_send_second_file_confirmed") } if (-not $productionFileSendEnabledFalse) { $missing.Add("production_file_send_enabled_false") } if (-not $hasBeforeProbe) { $missing.Add("before_recorder_output") } if (-not $hasAfterProbe) { $missing.Add("after_recorder_output") } $strictPass = ( $targetTypePresent -and $targetIDPresent -and $allowedDirPresent -and $filePathPresent -and $fileHashPresent -and $fileSizePresent -and $idempotencyKeyPresent -and $operatorStartedAtLocalPresent -and $operatorClickedSendFileAtLocalPresent -and $operatorObservedFileSuccessAtLocalPresent -and $manualStartPresent -and $manualFinishPresent -and $operatorObservedFileSuccess -and $fileUploadOrSentRecordPresent -and $fileUploadOrSentRecordPresentFlag -and $beforeRecorderPresentFlag -and $afterRecorderPresentFlag -and $duplicateSecondFileSendAttemptedFalse -and $doNotSendSecondFileConfirmed -and $productionFileSendEnabledFalse -and $hasBeforeProbe -and $hasAfterProbe ) $summary = [ordered]@{ ok = $true package = $resolvedPackage input_file_name = $inputsFile.Name strict_gate_pass = $strictPass target_type_present = $targetTypePresent target_id_present = $targetIDPresent allowed_dir_present = $allowedDirPresent sandbox_file_path_present = $filePathPresent file_sha256_present = $fileHashPresent file_size_bytes_present = $fileSizePresent idempotency_key_present = $idempotencyKeyPresent operator_started_at_local_present = $operatorStartedAtLocalPresent operator_clicked_send_file_at_local_present = $operatorClickedSendFileAtLocalPresent operator_observed_file_success_at_local_present = $operatorObservedFileSuccessAtLocalPresent manual_file_send_started_at_local_present = $manualStartPresent manual_file_send_finished_at_local_present = $manualFinishPresent operator_observed_file_success = $operatorObservedFileSuccess file_upload_or_sent_record_present = $fileUploadOrSentRecordPresent file_upload_or_sent_record_present_flag = $fileUploadOrSentRecordPresentFlag before_recorder_present_flag = $beforeRecorderPresentFlag after_recorder_present_flag = $afterRecorderPresentFlag duplicate_second_file_send_attempted_false = $duplicateSecondFileSendAttemptedFalse do_not_send_second_file_confirmed = $doNotSendSecondFileConfirmed production_file_send_enabled = -not $productionFileSendEnabledFalse has_before_probe = $hasBeforeProbe has_after_probe = $hasAfterProbe capability_test_ok = $capabilityOK capability_tool_count = $capabilityToolCount capability_send_file_preview_tool_present = $capabilitySendFilePreview capability_send_file_production_enabled = $capabilityProductionFileEnabled missing_for_full_gate = @($missing) decision = if ($strictPass) { "strict_file_gate_pass" } else { "not_usable_for_production_file_send" } } $summary | ConvertTo-Json -Depth 6 -Compress if ($Strict -and -not $strictPass) { exit 1 } } finally { if ($cleanup -and (Test-Path -LiteralPath $WorkDir)) { Remove-Item -LiteralPath $WorkDir -Recurse -Force } }