$ErrorActionPreference = "Stop" # thread_bootstrap_check_v1.0.ps1 # PS 5.1, UTF-8 BOM, ASCII comments, read-only. # Purpose: quick gate before any write: devlock valid, bootpack KB pointer OK, wrappers latest OK, fast encoding samples. function Get-LatestVersionFile { param( [string]$Root, [string]$Base ) $dir = Join-Path $Root "scripts" if (!(Test-Path -LiteralPath $dir)) { return $null } $pattern = $Base + "_v*.ps1" $files = Get-ChildItem -LiteralPath $dir -Filter $pattern -File if ($files.Count -eq 0) { return $null } $best = $null $bestParts = @() foreach ($f in $files) { $m = [regex]::Match($f.Name, "_v([0-9]+(?:\.[0-9]+)*)\.ps1$") if ($m.Success) { $ver = $m.Groups[1].Value $parts = @() foreach ($p in ($ver -split "\.")) { $n = 0 if ([int]::TryParse($p, [ref]$n)) { $parts += $n } else { $parts += 0 } } if ($best -eq $null) { $best = $f; $bestParts = $parts } else { $cmp = 0 $len = [Math]::Max($bestParts.Count, $parts.Count) for ($i=0; $i -lt $len; $i++) { $a = 0; $b = 0 if ($i -lt $bestParts.Count) { $a = $bestParts[$i] } if ($i -lt $parts.Count) { $b = $parts[$i] } if ($a -lt $b) { $cmp = -1; break } if ($a -gt $b) { $cmp = 1; break } } if ($cmp -lt 0) { $best = $f; $bestParts = $parts } } } } return $best } function Test-WrapperCallsVersion { param( [string]$WrapperPath, [string]$TargetFileName ) if ([string]::IsNullOrWhiteSpace($WrapperPath)) { return $false } if (!(Test-Path -LiteralPath $WrapperPath)) { return $false } $encNoBom = New-Object System.Text.UTF8Encoding($false) $bytes = [IO.File]::ReadAllBytes($WrapperPath) $txt = $encNoBom.GetString($bytes) if ($txt.Length -gt 0) { if ([int]$txt[0] -eq 65279) { $txt = $txt.Substring(1) } } return $txt -like ("*" + $TargetFileName + "*") } function Get-FastBomAsciiSample { param( [string]$Root, [int]$Count ) $dir = Join-Path $Root "scripts" $sample = @() if (Test-Path -LiteralPath $dir) { $all = Get-ChildItem -LiteralPath $dir -Filter "*.ps1" -File | Sort-Object LastWriteTime -Descending $take = $Count if ($all.Count -lt $take) { $take = $all.Count } for ($i=0; $i -lt $take; $i++) { $sample += $all[$i] } } $badBom = 0 $badAscii = 0 foreach ($f in $sample) { $b = Get-Content -LiteralPath $f.FullName -Encoding Byte -TotalCount 3 $bom = "NO" if ($b.Length -ge 3) { if ($b[0] -eq 239 -and $b[1] -eq 187 -and $b[2] -eq 191) { $bom = "YES" } } if ($bom -ne "YES") { $badBom++ } $raw = Get-Content -LiteralPath $f.FullName -Raw $asciiOk = -not ($raw -match "[^\x00-\x7F]") if (-not $asciiOk) { $badAscii++ } } $o = New-Object PSObject -Property @{ badBom = $badBom; badAscii = $badAscii; total = $sample.Count } return $o } $root = "\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry" # 1) Devlock check (read-only) $dev = Join-Path $root "dev\devlock_state.json" $devOk = $false $devMsg = "" if (Test-Path -LiteralPath $dev) { $encNoBom = New-Object System.Text.UTF8Encoding($false) $raw = [IO.File]::ReadAllText($dev, $encNoBom) $obj = $null try { $obj = $raw | ConvertFrom-Json } catch { $obj = $null } if ($obj -ne $null) { $exp = $null try { $exp = [DateTime]::Parse($obj.expires_utc) } catch { $exp = $null } if ($exp -ne $null) { $now = [DateTime]::UtcNow if ($now -lt $exp) { $devOk = $true; $devMsg = "valid until " + $exp.ToString("yyyy-MM-ddTHH:mm:ssZ") } else { $devMsg = "expired at " + $exp.ToString("yyyy-MM-ddTHH:mm:ssZ") } } else { $devMsg = "expires_utc missing or invalid" } } else { $devMsg = "invalid JSON" } } else { $devMsg = "devlock file missing" } # 2) Bootpack KB pointer check (read-only) $bpOk = $false $bpMsg = "" $boot = Join-Path $root "bootpack\bootpack.txt" if (Test-Path -LiteralPath $boot) { $encNoBom = New-Object System.Text.UTF8Encoding($false) $rawBoot = [IO.File]::ReadAllText($boot, $encNoBom) if ($rawBoot -match "\[BUG_KB_JSON\]") { $mx = [regex]::Match($rawBoot, "(?ms)^\[BUG_KB_JSON_POINTER\]\s*(.*?)(?=^\[|\Z)") if ($mx.Success) { $blk = $mx.Groups[1].Value $kbPath = [regex]::$(if(Match($blk, "(){ mi)^\s*Path\s* } else { \s*(.+)$").Groups[1].Value.Trim() }) $shaPtr = [regex]::$(if(Match($blk, "(){ mi)^\s*SHA256\s* } else { \s*([0-9A-Fa-f]+)$").Groups[1].Value.Trim().ToLower() }) $entPtr = [regex]::$(if(Match($blk, "(){ mi)^\s*Entries\s* } else { \s*([0-9]+)$").Groups[1].Value.Trim() }) if ((-not [string]::IsNullOrWhiteSpace($kbPath)) -and (Test-Path -LiteralPath $kbPath)) { $shaReal = (Get-FileHash -LiteralPath $kbPath -Algorithm SHA256).Hash.ToLower() $bytes = [IO.File]::ReadAllBytes($kbPath) $txt = $encNoBom.GetString($bytes) if ($txt.Length -gt 0) { if ([int]$txt[0] -eq 65279) { $txt = $txt.Substring(1) } } $txt2 = $txt $i = $txt.LastIndexOf("]}") if ($i -gt 0) { $txt2 = $txt.Substring(0, $i + 2) } $entReal = -1 try { $entReal = (($txt2 | ConvertFrom-Json).entries).Count } catch { $entReal = ([regex]::Matches($txt2, '"id"\s*:')).Count } $okSha = ($shaPtr -ne "") -and ($shaPtr -eq $shaReal) $okEnt = ([string]$entPtr -ne "") -and ([string]$entPtr -eq [string]$entReal) if ($okSha -and $okEnt) { $bpOk = $true; $bpMsg = "pointer match" } else { $bpMsg = "sha or entries mismatch" } } else { $bpMsg = "KB Path missing or not found" } } else { $bpMsg = "pointer block missing" } } else { $bpMsg = "BUG_KB_JSON missing" } } else { $bpMsg = "bootpack.txt missing" } # 3) Wrappers latest check (known bases) $wrapOk = $true $wrapLines = @() $known = @("gov_pipeline_guard","gov_profile_launcher","kb_bulk_ingest") foreach ($base in $known) { $best = Get-LatestVersionFile -Root $root -Base $base $latestName = "" if ($best -ne $null) { $latestName = $best.Name } $wrOk = $false $w1 = Join-Path $root ("scripts\" + $base + "_latest.ps1") $w2 = Join-Path $root ("bin\" + $base + "_latest.ps1") $w3 = Join-Path $root ("scripts\" + $base + "_latest.cmd") $w4 = Join-Path $root ("bin\" + $base + "_latest.cmd") if ($latestName -ne "") { if (Test-WrapperCallsVersion -WrapperPath $w1 -TargetFileName $latestName) { $wrOk = $true } elseif (Test-WrapperCallsVersion -WrapperPath $w2 -TargetFileName $latestName) { $wrOk = $true } elseif (Test-WrapperCallsVersion -WrapperPath $w3 -TargetFileName $latestName) { $wrOk = $true } elseif (Test-WrapperCallsVersion -WrapperPath $w4 -TargetFileName $latestName) { $wrOk = $true } } if (-not $wrOk) { $wrapOk = $false } $line = "BASE=" + $base + " | latest=" + $latestName + " | wrapper_ok=" + $wrOk $wrapLines += $line } # 4) Fast encoding sample (read-only) $samp = Get-FastBomAsciiSample -Root $root -Count 5 # 5) Summary Write-Host "=== THREAD BOOTSTRAP CHECK v1.0 ===" Write-Host ("devlock: " + ($(if($devOk){"OK"}else{"BLOCK"})) + " (" + $devMsg + ")") Write-Host ("bootpack pointer: " + ($(if($bpOk){"OK"}else{"BLOCK"})) + " (" + $bpMsg + ")") foreach ($l in $wrapLines) { Write-Host $l } Write-Host ("fast sample ps1: total=" + $samp.total + " badBOM=" + $samp.badBom + " badASCII=" + $samp.badAscii) $allOk = $false if ($devOk -and $bpOk -and $wrapOk -and ($samp.badBom -eq 0) -and ($samp.badAscii -eq 0)) { $allOk = $true } if ($allOk) { Write-Host "[OK] GATE PASS" exit 0 } else { Write-Host "[GATE-BLOCK] see lines above" exit 5 }