scripts/validate-t060-local-macos-evidence.ps1
|
#requires -Version 7.0 [CmdletBinding()] param( [Parameter(Mandatory)] [string] $RepoRoot, [Parameter(Mandatory)] [string] $PackagePath, [Parameter(Mandatory)] [ValidatePattern('^[0-9a-f]{40}$')] [string] $ExpectedCommit, [Parameter(Mandatory)] [ValidatePattern('^run-[a-z0-9][a-z0-9-]{0,63}$')] [string] $ExpectedRunId, [Parameter(Mandatory)] [ValidateLength(1, 256)] [string] $ExpectedAuthorizationRef, [string] $ExpectedRepositoryUrl = 'https://github.com/alonf/specrew.git' ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest function Read-T060StrictJsonObject { param( [Parameter(Mandatory)][string] $Path, [ValidateRange(1, 1048576)][int] $MaximumBytes = 262144 ) if (-not [IO.File]::Exists($Path)) { throw "missing-file:$Path" } $info = [IO.FileInfo]$Path if ($info.Length -le 0 -or $info.Length -gt $MaximumBytes) { throw "invalid-json-size:$Path" } $json = [IO.File]::ReadAllText($Path, [Text.UTF8Encoding]::new($false, $true)) $trimmed = $json.Trim() if (-not $trimmed.StartsWith('{') -or -not $trimmed.EndsWith('}')) { throw "json-object-required:$Path" } try { $document = [Text.Json.JsonDocument]::Parse($trimmed) } catch [Text.Json.JsonException] { throw "invalid-json:$Path" } try { if ($document.RootElement.ValueKind -ne [Text.Json.JsonValueKind]::Object) { throw "json-object-required:$Path" } $pending = [Collections.Generic.Stack[object]]::new() $pending.Push([pscustomobject]@{ element = $document.RootElement; path = '$' }) while ($pending.Count -gt 0) { $node = $pending.Pop() $element = [Text.Json.JsonElement]$node.element if ($element.ValueKind -eq [Text.Json.JsonValueKind]::Object) { $seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach ($property in $element.EnumerateObject()) { $child = [string]$node.path + '.' + $property.Name if (-not $seen.Add($property.Name)) { throw "duplicate-json-field:$child" } $pending.Push([pscustomobject]@{ element = $property.Value; path = $child }) } } elseif ($element.ValueKind -eq [Text.Json.JsonValueKind]::Array) { $index = 0 foreach ($item in $element.EnumerateArray()) { $pending.Push([pscustomobject]@{ element = $item; path = ([string]$node.path + "[$index]") }) $index++ } } } } finally { $document.Dispose() } try { return ($trimmed | ConvertFrom-Json -Depth 30 -ErrorAction Stop) } catch { throw "invalid-json:$Path" } } function Add-T060ClosedShapeErrors { param( [Parameter(Mandatory)] $Object, [Parameter(Mandatory)][string[]] $Fields, [Parameter(Mandatory)][string] $Path, [Parameter(Mandatory)][AllowEmptyCollection()][Collections.Generic.List[string]] $Errors ) if ($Object -isnot [pscustomobject]) { $Errors.Add("wrong-type:$Path:object") | Out-Null; return } $actual = @($Object.PSObject.Properties.Name) foreach ($field in $Fields) { if ($field -cnotin $actual) { $Errors.Add("missing-field:$Path.$field") | Out-Null } } foreach ($field in $actual) { if ($field -cnotin $Fields) { $Errors.Add("unknown-field:$Path.$field") | Out-Null } } } function Get-T060Sha256 { param([Parameter(Mandatory)][string] $Path) return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() } $errors = [Collections.Generic.List[string]]::new() $manifest = $null $preflight = $null $result = $null $findingCount = $null $resultVerdict = $null $packageRoot = $null $targetRepoRoot = $null $targetRepoDigest = $null try { $targetRepoRoot = (Resolve-Path -LiteralPath $RepoRoot).Path $packageRoot = (Resolve-Path -LiteralPath $PackagePath).Path $loadPath = Join-Path $PSScriptRoot 'internal/continuous-co-review/_load.ps1' . $loadPath $actualCommitOutput = @(& git -C $targetRepoRoot rev-parse 'HEAD^{commit}' 2>&1) if ($LASTEXITCODE -ne 0) { throw 'repository-checkout-head-unavailable' } $actualCommit = (($actualCommitOutput | ForEach-Object { [string]$_ }) -join "`n").Trim().ToLowerInvariant() if ($actualCommit -cne $ExpectedCommit) { $errors.Add('repository-checkout-commit-mismatch') | Out-Null } $actualOriginOutput = @(& git -C $targetRepoRoot remote get-url origin 2>&1) if ($LASTEXITCODE -ne 0) { throw 'repository-checkout-origin-unavailable' } $actualOrigin = (($actualOriginOutput | ForEach-Object { [string]$_ }) -join "`n").Trim() if ($actualOrigin -cne $ExpectedRepositoryUrl) { $errors.Add('repository-checkout-origin-mismatch') | Out-Null } $actualStatus = ((@(& git -C $targetRepoRoot status --porcelain=v1 --untracked-files=all 2>&1) | ForEach-Object { [string]$_ }) -join "`n").Trim() if ($LASTEXITCODE -ne 0) { throw 'repository-checkout-status-unavailable' } if (-not [string]::IsNullOrEmpty($actualStatus)) { $errors.Add('repository-checkout-not-clean') | Out-Null } $digestEvidence = Get-ContinuousCoReviewReviewedStateDigest -RepoRoot $targetRepoRoot if ($null -eq $digestEvidence -or -not [bool]$digestEvidence.ok -or [string]$digestEvidence.tree_id -cnotmatch '^[0-9a-f]{40}$') { $why = if ($null -ne $digestEvidence -and $digestEvidence.PSObject.Properties['failure_reason']) { [string]$digestEvidence.failure_reason } else { 'unknown' } throw "repository-digest-unavailable:$why" } $targetRepoDigest = [string]$digestEvidence.tree_id $manifestPath = Join-Path $packageRoot 'manifest.json' $preflightPath = Join-Path $packageRoot 'preflight.json' $resultPath = Join-Path $packageRoot 'result.json' $reportPath = Join-Path $packageRoot 'report.md' $progressPath = Join-Path $packageRoot 'progress.json' $authorityConfigPath = Join-Path $packageRoot 'campaign-authority.json' $manifest = Read-T060StrictJsonObject -Path $manifestPath $preflight = Read-T060StrictJsonObject -Path $preflightPath Add-T060ClosedShapeErrors -Object $manifest -Fields @('schema_version', 'evidence_kind', 'evidence_source', 'generated_at', 'target', 'platform', 'harness', 'authorization', 'run', 'controller') -Path '$' -Errors $errors Add-T060ClosedShapeErrors -Object $manifest.target -Fields @('repository_url', 'head_commit', 'reviewed_state_digest', 'clean_before', 'clean_after', 'head_unchanged') -Path '$.target' -Errors $errors Add-T060ClosedShapeErrors -Object $manifest.platform -Fields @('os', 'os_version', 'architecture') -Path '$.platform' -Errors $errors Add-T060ClosedShapeErrors -Object $manifest.harness -Fields @('host', 'harness_id', 'cli_version', 'auth_status') -Path '$.harness' -Errors $errors Add-T060ClosedShapeErrors -Object $manifest.authorization -Fields @('reference', 'invocation_count') -Path '$.authorization' -Errors $errors Add-T060ClosedShapeErrors -Object $manifest.run -Fields @('campaign_id', 'run_id', 'status', 'reason', 'invoked', 'preflight_file', 'result_file', 'report_file', 'progress_file', 'preflight_sha256', 'result_sha256', 'report_sha256', 'progress_sha256') -Path '$.run' -Errors $errors Add-T060ClosedShapeErrors -Object $manifest.controller -Fields @('authority_mode', 'authority_config_file', 'authority_config_sha256', 'runtime_id', 'timeout_seconds', 'terminal_result_contract_valid') -Path '$.controller' -Errors $errors Add-T060ClosedShapeErrors -Object $preflight -Fields @('schema_version', 'evidence_kind', 'evidence_source', 'generated_at', 'provider_invoked', 'target', 'platform', 'harness', 'runtime') -Path '$preflight' -Errors $errors Add-T060ClosedShapeErrors -Object $preflight.target -Fields @('repository_url', 'head_commit', 'reviewed_state_digest', 'clean') -Path '$preflight.target' -Errors $errors Add-T060ClosedShapeErrors -Object $preflight.platform -Fields @('os', 'os_version', 'architecture') -Path '$preflight.platform' -Errors $errors Add-T060ClosedShapeErrors -Object $preflight.harness -Fields @('host', 'harness_id', 'cli_version', 'auth_status', 'ready', 'reason') -Path '$preflight.harness' -Errors $errors Add-T060ClosedShapeErrors -Object $preflight.runtime -Fields @('runtime_id', 'ready', 'reason') -Path '$preflight.runtime' -Errors $errors if ([string]$manifest.schema_version -cne '1.0' -or [string]$preflight.schema_version -cne '1.0') { $errors.Add('schema-version-invalid') | Out-Null } if ([string]$manifest.evidence_kind -cne 't060-local-macos-smoke') { $errors.Add('evidence-kind-invalid') | Out-Null } if ([string]$manifest.evidence_source -cne 'local-machine' -or [string]$preflight.evidence_source -cne 'local-machine') { $errors.Add('evidence-source-must-be-local-machine') | Out-Null } if ([string]$preflight.evidence_kind -cne 't060-local-macos-preflight' -or [bool]$preflight.provider_invoked) { $errors.Add('preflight-provenance-invalid') | Out-Null } if ([string]$manifest.target.repository_url -cne $ExpectedRepositoryUrl -or [string]$preflight.target.repository_url -cne $ExpectedRepositoryUrl) { $errors.Add('repository-url-mismatch') | Out-Null } if ([string]$manifest.target.head_commit -cne $ExpectedCommit -or [string]$preflight.target.head_commit -cne $ExpectedCommit) { $errors.Add('commit-mismatch') | Out-Null } if (-not [bool]$manifest.target.clean_before -or -not [bool]$manifest.target.clean_after -or -not [bool]$manifest.target.head_unchanged -or -not [bool]$preflight.target.clean) { $errors.Add('repository-integrity-invalid') | Out-Null } if ([string]$manifest.target.reviewed_state_digest -cne [string]$preflight.target.reviewed_state_digest) { $errors.Add('preflight-digest-mismatch') | Out-Null } if ([string]$manifest.target.reviewed_state_digest -cne $targetRepoDigest) { $errors.Add('repository-digest-mismatch') | Out-Null } if ([string]$manifest.platform.os -cne 'macos' -or [string]$preflight.platform.os -cne 'macos') { $errors.Add('platform-must-be-macos') | Out-Null } if ([string]$manifest.harness.host -cne 'codex' -or [string]$manifest.harness.harness_id -cne 'codex-cli-file-primary') { $errors.Add('harness-identity-invalid') | Out-Null } if ([string]$preflight.harness.host -cne 'codex' -or [string]$preflight.harness.harness_id -cne 'codex-cli-file-primary' -or -not [bool]$preflight.harness.ready) { $errors.Add('harness-preflight-invalid') | Out-Null } if ([string]$manifest.harness.auth_status -cne 'authenticated' -or [string]$preflight.harness.auth_status -cne 'authenticated') { $errors.Add('auth-preflight-invalid') | Out-Null } if ([string]$manifest.controller.runtime_id -cne 'macos-process-group-runtime' -or [string]$preflight.runtime.runtime_id -cne 'macos-process-group-runtime' -or -not [bool]$preflight.runtime.ready) { $errors.Add('runtime-preflight-invalid') | Out-Null } if ([string]$manifest.controller.authority_mode -cne 'external-t060-campaign-config') { $errors.Add('authority-mode-provenance-invalid') | Out-Null } if ([string]$manifest.controller.authority_config_file -cne 'campaign-authority.json') { $errors.Add('authority-config-file-invalid') | Out-Null } if (-not [IO.File]::Exists($authorityConfigPath) -or [string]$manifest.controller.authority_config_sha256 -cnotmatch '^[0-9a-f]{64}$' -or (Get-T060Sha256 -Path $authorityConfigPath) -cne [string]$manifest.controller.authority_config_sha256) { $errors.Add('authority-config-hash-mismatch') | Out-Null } else { $authorityConfig = Read-T060StrictJsonObject -Path $authorityConfigPath -MaximumBytes 4096 Add-T060ClosedShapeErrors -Object $authorityConfig -Fields @('schema_version', 'mode') -Path '$authority_config' -Errors $errors if ([string]$authorityConfig.schema_version -cne '1.0' -or [string]$authorityConfig.mode -cne 'campaign') { $errors.Add('authority-config-invalid') | Out-Null } } if ([int]$manifest.controller.timeout_seconds -ne 600) { $errors.Add('timeout-must-be-600-seconds') | Out-Null } if (-not [bool]$manifest.controller.terminal_result_contract_valid) { $errors.Add('producer-contract-validation-failed') | Out-Null } if ([string]$manifest.authorization.reference -cne $ExpectedAuthorizationRef) { $errors.Add('authorization-reference-mismatch') | Out-Null } if ([int]$manifest.authorization.invocation_count -ne 1) { $errors.Add('manifest-invocation-count-must-be-one') | Out-Null } if ([string]$manifest.run.run_id -cne $ExpectedRunId) { $errors.Add('run-id-mismatch') | Out-Null } if ([string]$manifest.run.status -cne 'terminal' -or -not [bool]$manifest.run.invoked) { $errors.Add('run-not-invoked-terminal') | Out-Null } if ([string]$manifest.run.preflight_file -cne 'preflight.json' -or [string]$manifest.run.result_file -cne 'result.json' -or [string]$manifest.run.report_file -cne 'report.md' -or [string]$manifest.run.progress_file -cne 'progress.json') { $errors.Add('package-file-name-invalid') | Out-Null } foreach ($pair in @( @{ path = $preflightPath; expected = [string]$manifest.run.preflight_sha256; name = 'preflight' }, @{ path = $resultPath; expected = [string]$manifest.run.result_sha256; name = 'result' }, @{ path = $reportPath; expected = [string]$manifest.run.report_sha256; name = 'report' }, @{ path = $progressPath; expected = [string]$manifest.run.progress_sha256; name = 'progress' } )) { if (-not [IO.File]::Exists([string]$pair.path)) { $errors.Add("missing-$($pair.name)-file") | Out-Null; continue } if ([string]$pair.expected -cnotmatch '^[0-9a-f]{64}$' -or (Get-T060Sha256 -Path ([string]$pair.path)) -cne [string]$pair.expected) { $errors.Add("$($pair.name)-hash-mismatch") | Out-Null } } if ([IO.File]::Exists($reportPath) -and ([IO.FileInfo]$reportPath).Length -le 0) { $errors.Add('report-empty') | Out-Null } if ([IO.File]::Exists($resultPath)) { $resultJson = [IO.File]::ReadAllText($resultPath, [Text.UTF8Encoding]::new($false, $true)) $resultValidation = Test-ReviewAuthorityContractJson -ContractName ReviewResult -Json $resultJson ` -ExpectedCampaignId ([string]$manifest.run.campaign_id) -ExpectedRunId $ExpectedRunId ` -ExpectedTargetDigest ([string]$manifest.target.reviewed_state_digest) if (-not $resultValidation.valid) { $errors.Add(('result-contract-invalid:' + ($resultValidation.errors -join ','))) | Out-Null } else { $result = $resultJson | ConvertFrom-Json -Depth 30 $findingCount = @($result.findings).Count $resultVerdict = [string]$result.verdict } } $storeRoot = Join-Path $packageRoot 'authority' if (-not [IO.Directory]::Exists($storeRoot)) { $errors.Add('authority-store-missing') | Out-Null } else { try { $campaignId = [string]$manifest.run.campaign_id $grants = @(Get-ReviewAuthorityCampaignFacts -StoreRoot $storeRoot -CampaignId $campaignId -Kind grants) $reservations = @(Get-ReviewAuthorityCampaignFacts -StoreRoot $storeRoot -CampaignId $campaignId -Kind reservations) $spends = @(Get-ReviewAuthorityCampaignFacts -StoreRoot $storeRoot -CampaignId $campaignId -Kind spend) if (@($grants | Where-Object { [string]$_.authorization_ref -ceq $ExpectedAuthorizationRef }).Count -ne 1) { $errors.Add('authority-grant-mismatch') | Out-Null } if (@($reservations | Where-Object { [string]$_.run_id -ceq $ExpectedRunId }).Count -ne 1) { $errors.Add('authority-reservation-mismatch') | Out-Null } if ($spends.Count -ne 1 -or @($spends | Where-Object { [string]$_.run_id -ceq $ExpectedRunId }).Count -ne 1) { $errors.Add('authority-spend-count-mismatch') | Out-Null } $authorityResult = Join-Path $storeRoot ("campaigns/{0}/runs/{1}/result.json" -f $campaignId, $ExpectedRunId) $authorityReport = Join-Path $storeRoot ("campaigns/{0}/runs/{1}/report.md" -f $campaignId, $ExpectedRunId) if (-not [IO.File]::Exists($authorityResult) -or (Get-T060Sha256 -Path $authorityResult) -cne (Get-T060Sha256 -Path $resultPath)) { $errors.Add('authority-result-copy-mismatch') | Out-Null } if (-not [IO.File]::Exists($authorityReport) -or (Get-T060Sha256 -Path $authorityReport) -cne (Get-T060Sha256 -Path $reportPath)) { $errors.Add('authority-report-copy-mismatch') | Out-Null } } catch { $errors.Add(('authority-store-invalid:' + $_.Exception.Message)) | Out-Null } } } catch { $errors.Add($_.Exception.Message) | Out-Null } $valid = $errors.Count -eq 0 $smokeClean = $false if ($valid -and $null -ne $result) { $smokeClean = ( [string]$result.completion -ceq 'complete' -and [string]$result.verdict -ceq 'pass' -and [string]$result.runtime_outcome -ceq 'completed' -and [bool]$result.termination_verified -and [string]$result.containment -ceq 'verified' -and [string]$result.currentness -ceq 'current' -and [string]$result.validation -ceq 'valid' -and [bool]$result.can_approve_current -and @($result.findings).Count -eq 0 ) } $validationOutput = [pscustomobject][ordered]@{ schema_version = '1.0' package_valid = $valid smoke_clean = $smokeClean evidence_source = $(if ($null -eq $manifest) { $null } else { [string]$manifest.evidence_source }) head_commit = $(if ($null -eq $manifest) { $null } else { [string]$manifest.target.head_commit }) target_digest = $(if ($null -eq $manifest) { $null } else { [string]$manifest.target.reviewed_state_digest }) campaign_id = $(if ($null -eq $manifest) { $null } else { [string]$manifest.run.campaign_id }) run_id = $(if ($null -eq $manifest) { $null } else { [string]$manifest.run.run_id }) verdict = $resultVerdict finding_count = $findingCount errors = @($errors) } $validationOutput | ConvertTo-Json -Depth 10 if (-not $valid) { exit 1 } |