Payload/scripts/kaden/harness/lib/HarnessGateFilePolicy.ps1
|
# Gate-file recorder-only policy and provenance stamps. # Dot-sourced into HarnessContract.psm1 so Export-ModuleMember -Function * keeps these helpers exported. # Do not Import-Module this file standalone without Contract helpers already loaded. function Get-HarnessGateFileKind { <# .SYNOPSIS Classify recorder-owned gate paths: projection, journal ledger, or provenance stamp. #> param( [AllowNull()][string]$Path, [string]$RepoRoot = '' ) $n = ConvertTo-HarnessNormalizedRepoPath -Path $Path -RepoRoot $RepoRoot if ([string]::IsNullOrWhiteSpace($n)) { return '' } $leaf = ($n -split '[\\/]' | Select-Object -Last 1) if ($leaf -eq '.review-loop.json') { return 'review-loop' } if ($n -match '(^|/)journal/events\.jsonl$') { return 'journal' } if ($n -match '(^|/)journal/gate-file-provenance\.json$') { return 'provenance' } return '' } function Test-HarnessGateFileDirectWritePath { <# .SYNOPSIS True when Path is a recorder-owned gate file anywhere in the tree (projection, journal, or provenance stamp). #> param( [AllowNull()][string]$Path, [string]$RepoRoot = '' ) return -not [string]::IsNullOrWhiteSpace((Get-HarnessGateFileKind -Path $Path -RepoRoot $RepoRoot)) } function Get-HarnessGateFileDirectWriteDenyReason { param( [ValidateSet('review-loop', 'journal', 'provenance', '')][string]$Kind = '' ) if ($Kind -eq 'journal') { return 'journal/events.jsonl is append-only through harness scripts. Use pwsh -File scripts/kaden/harness-invoke.ps1 (harness-journal, harness-record-phase, turn-start/stop); do not edit the journal directly.' } if ($Kind -eq 'provenance') { return 'journal/gate-file-provenance.json is recorder-only. Harness scripts stamp it after legitimate writes; do not edit the provenance stamp directly.' } return '.review-loop.json is recorder-only. Use pwsh -File scripts/kaden/harness-invoke.ps1 (harness-record-phase, turn-start/stop, init-harness-feature); do not edit the projection directly.' } function Get-HarnessGateFileProvenancePath { param([Parameter(Mandatory = $true)][string]$FeatureRoot) return Join-HarnessPath -Root $FeatureRoot -Child 'journal/gate-file-provenance.json' } function Get-HarnessGateFileSha256 { param([Parameter(Mandatory = $true)][string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return '' } try { $hash = Get-FileHash -LiteralPath $Path -Algorithm SHA256 -ErrorAction Stop if ($null -eq $hash -or [string]::IsNullOrWhiteSpace([string]$hash.Hash)) { return '' } return [string]$hash.Hash.ToLowerInvariant() } catch { return '' } } function Write-HarnessGateFileProvenance { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Non-interactive harness write - agents and hooks must not prompt.')] param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)][ValidateSet('review-loop', 'journal')][string]$GateFile, [string]$Source = 'harness-script', [string]$Sha256 = '', [switch]$ForceRepair ) $journalDir = Join-HarnessPath -Root $FeatureRoot -Child 'journal' if (-not (Test-Path -LiteralPath $journalDir)) { New-Item -ItemType Directory -Path $journalDir -Force | Out-Null } $provPath = Get-HarnessGateFileProvenancePath -FeatureRoot $FeatureRoot $doc = $null if (Test-Path -LiteralPath $provPath) { try { $doc = Get-Content -LiteralPath $provPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { if (-not $ForceRepair) { throw "gate-file provenance stamp is unreadable at '$provPath'. Pass -ForceRepair to reset after confirming no dual-write forgery, or rebuild via harness scripts." } Write-Warning "Resetting unreadable gate-file provenance stamp at '$provPath' (-ForceRepair)." $doc = $null } } if ($null -eq $doc) { $doc = [pscustomobject]@{ schema = 'harness.gate-file-provenance/v1' files = [pscustomobject]@{} } } if (-not ($doc.PSObject.Properties.Name -contains 'files') -or $null -eq $doc.files) { $doc | Add-Member -NotePropertyName 'files' -NotePropertyValue ([pscustomobject]@{}) -Force } $targetPath = if ($GateFile -eq 'journal') { Join-HarnessPath -Root $FeatureRoot -Child 'journal/events.jsonl' } else { Join-HarnessPath -Root $FeatureRoot -Child '.review-loop.json' } $hash = if (-not [string]::IsNullOrWhiteSpace($Sha256)) { $Sha256.ToLowerInvariant() } else { Get-HarnessGateFileSha256 -Path $targetPath } $entry = [pscustomobject]@{ sha256 = $hash updated_at = (Get-HarnessIso8601UtcNow) source = $Source } $doc.files | Add-Member -NotePropertyName $GateFile -NotePropertyValue $entry -Force $json = $doc | ConvertTo-Json -Depth 6 [System.IO.File]::WriteAllText($provPath, $json + [Environment]::NewLine) } function Add-HarnessGateFileProvenanceMismatchFinding { param( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[object]]$Findings, [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)]$Files, [Parameter(Mandatory = $true)][ValidateSet('review-loop', 'journal')][string]$GateFile, [Parameter(Mandatory = $true)][string]$RelativePath, [Parameter(Mandatory = $true)][string]$Message ) $entry = Get-HarnessProperty -Object $Files -Name $GateFile if ($null -eq $entry) { return } $expected = [string](Get-HarnessProperty -Object $entry -Name 'sha256') $actual = Get-HarnessGateFileSha256 -Path (Join-HarnessPath -Root $FeatureRoot -Child $RelativePath) if (-not [string]::IsNullOrWhiteSpace($expected) -and $actual -ne $expected) { [void]$Findings.Add((Format-HarnessValidationFinding -Code 'DIRECT_GATE_FILE_MUTATION' -Message $Message)) } } function Test-HarnessGateFileProvenance { <# .SYNOPSIS Return DIRECT_GATE_FILE_MUTATION findings when gate-file bytes diverge from script provenance stamps. Missing stamps are ignored (legacy fixtures); mismatch after a script stamp fails closed. #> param([Parameter(Mandatory = $true)][string]$FeatureRoot) $findings = [System.Collections.Generic.List[object]]::new() $provPath = Get-HarnessGateFileProvenancePath -FeatureRoot $FeatureRoot if (-not (Test-Path -LiteralPath $provPath)) { return @() } try { $doc = Get-Content -LiteralPath $provPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { [void]$findings.Add((Format-HarnessValidationFinding -Code 'DIRECT_GATE_FILE_MUTATION' ` -Message 'gate-file provenance stamp is unreadable; rebuild via harness scripts (do not hand-edit gate files)')) return @($findings) } $files = Get-HarnessProperty -Object $doc -Name 'files' if ($null -eq $files) { return @() } Add-HarnessGateFileProvenanceMismatchFinding -Findings $findings -FeatureRoot $FeatureRoot -Files $files ` -GateFile 'review-loop' -RelativePath '.review-loop.json' ` -Message '.review-loop.json changed without script provenance (projection is recorder-only)' Add-HarnessGateFileProvenanceMismatchFinding -Findings $findings -FeatureRoot $FeatureRoot -Files $files ` -GateFile 'journal' -RelativePath 'journal/events.jsonl' ` -Message 'journal/events.jsonl changed without harness append provenance (journal is append-only through scripts)' return @($findings) } function Test-HarnessPretooluseSkipJournalAllowed { <# .SYNOPSIS True when -SkipJournal may suppress deny telemetry (isolated FeatureRoot under TEMP only). #> param([AllowNull()][string]$FeatureRoot = '') if ([string]::IsNullOrWhiteSpace($FeatureRoot)) { return $false } if (-not (Test-Path -LiteralPath $FeatureRoot)) { return $false } $full = [System.IO.Path]::GetFullPath($FeatureRoot) $temp = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()) return $full.StartsWith($temp, [System.StringComparison]::OrdinalIgnoreCase) } |