From 9851d558d686536b38311c389c0379ccb4b0c0c5 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Thu, 17 Sep 2026 14:34:52 -0700 Subject: [PATCH] Guard retained Windows provider pins against drift Compare product and investigation provider metadata with isolated drift regressions. Record the exact public source-retention tags and active narrowly scoped provider protection rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8a98a529-f045-4c43-979c-00bae7640885 Signed-off-by: Colin Neilens --- Tools/windows/PACKAGING.md | 30 ++++++++ Tools/windows/Tests/ProviderPins.Tests.ps1 | 73 +++++++++++++++++++ Tools/windows/Tests/TerminalGate.Tests.ps1 | 30 +++++++- .../windows/Tests/ValidationRunner.Tests.ps1 | 3 + Tools/windows/validate.ps1 | 1 + Tools/windows/validation-matrix.md | 1 + .../spikes/windows-terminal-gate/README.md | 12 +++ 7 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Tools/windows/Tests/ProviderPins.Tests.ps1 diff --git a/Tools/windows/PACKAGING.md b/Tools/windows/PACKAGING.md index a3e48b45..18d0787f 100644 --- a/Tools/windows/PACKAGING.md +++ b/Tools/windows/PACKAGING.md @@ -79,3 +79,33 @@ DLLs, metadata, manifests, file sets, and checksums. Its OS signature-trust decisions are simulated without modifying certificate stores; it is not a production Authenticode or signed-installer lifecycle proof. It runs before the existing real-product packaging/lifecycle suite under `validate.ps1 -Task packaging`. + +## Retained provider sources + +Both exact public provider pins have the annotated source-retention tag +`graphcode-windows-baseline-2026-09-17`: + +| Provider | Pinned commit | Retained branch | +|---|---|---| +| [coneilen/winghostty](https://github.com/coneilen/winghostty/tree/graphcode-windows-baseline-2026-09-17) | `f5abc059e4ca58b376eb209313aca7784659c679` | `graphcode-host` | +| [coneilen/zmx](https://github.com/coneilen/zmx/tree/graphcode-windows-baseline-2026-09-17) | `029e11d2b19162fb3bdf90c8270237d303b8bfb4` | `graphcode-quickchat-hang` | + +As verified on 2026-09-17, each fork has an active ruleset forbidding updates or +deletion of `refs/tags/graphcode-windows-*`, without bypass actors. Separate +active rulesets prevent deletion and non-fast-forward changes of the branches +above; normal forward development is allowed. The Winghostty tag/branch ruleset +IDs are `23625509`/`23625510`; zmx's are `23625508`/`23625511`. +Administrators can still change rulesets or repository availability; these +settings are retention controls, not an irrevocable archival guarantee. + +These tags preserve source, not signed product releases. No installer or binary +asset is published by creating them. Public CI can fetch them without provider +credentials; collaborator permissions were not changed. + +`graphcode-windows\provider-pins.json` remains the source of truth. Bootstrap and +packaging still use exact commit SHAs, not moving branch or tag resolution. +The terminal gate checks every provider field against its investigation copy, +including repository, remote URL, SHA, artifact path, and Zig version. +`ProviderPins.Tests.ps1` proves drift in either file is rejected, while JSON +property order and explanatory fallback wording are immaterial. Run it directly +or through `validate.ps1 -Task terminal-gate`. diff --git a/Tools/windows/Tests/ProviderPins.Tests.ps1 b/Tools/windows/Tests/ProviderPins.Tests.ps1 new file mode 100644 index 00000000..f39dc78a --- /dev/null +++ b/Tools/windows/Tests/ProviderPins.Tests.ps1 @@ -0,0 +1,73 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path +$contract = Join-Path $PSScriptRoot "TerminalGate.Tests.ps1" +$productPath = Join-Path $repoRoot "graphcode-windows\provider-pins.json" +$gatePath = Join-Path $repoRoot "investigation\spikes\windows-terminal-gate\provider-pins.json" +$fixture = Join-Path $repoRoot ".build\provider-pin-contract-$([guid]::NewGuid())" +$pwsh = (Get-Process -Id $PID).Path + +function Test-Pins([string] $label, [scriptblock] $mutate, [string] $failure) { + $product = Get-Content -LiteralPath $productPath -Raw | ConvertFrom-Json -AsHashtable + $gate = Get-Content -LiteralPath $gatePath -Raw | ConvertFrom-Json -AsHashtable + & $mutate $product $gate + $product | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $fixture "product.json") + $gate | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $fixture "gate.json") + $output = & $pwsh -NoProfile -File $contract ` + -ProductPinsPath (Join-Path $fixture "product.json") ` + -GatePinsPath (Join-Path $fixture "gate.json") 2>&1 | Out-String + if ($failure) { + if ($LASTEXITCODE -eq 0) { throw "RED: $label was accepted" } + if ($output -notmatch [regex]::Escape($failure)) { + throw "$label failed for the wrong reason: $output" + } + } elseif ($LASTEXITCODE -ne 0) { + throw "$label did not pass: $output" + } +} + +try { + New-Item -ItemType Directory -Path $fixture | Out-Null + foreach ($provider in @("winghostty", "zmx")) { + foreach ($field in @("repository", "remoteUrl", "sha", "artifact", "minimumZig")) { + foreach ($side in @("product", "gate")) { + Test-Pins "$side $provider.$field drift" { + param($product, $gate) + $pins = if ($side -eq "product") { $product } else { $gate } + $pins[$provider][$field] += "-drift" + } "provider pins differ: $provider.$field" + } + } + Test-Pins "$provider missing field" { + param($product, $gate) + $gate[$provider].Remove("artifact") + } "provider pins differ: $provider.artifact" + Test-Pins "$provider additional field" { + param($product, $gate) + $product[$provider].newPinField = "future-value" + } "provider pins differ: $provider.newPinField" + Test-Pins "$provider field type" { + param($product, $gate) + $product[$provider].minimumZig = 152 + } "provider pins differ: $provider.minimumZig" + } + Test-Pins "schema drift" { + param($product, $gate) + $product.schemaVersion = 2 + } "provider pin schemas differ" + Test-Pins "matching sources" { param($product, $gate) } "" + Test-Pins "order and explanatory metadata" { + param($product, $gate) + $reordered = [ordered]@{} + @($product.winghostty.Keys) | Sort-Object -Descending | ForEach-Object { + $reordered[$_] = $product.winghostty[$_] + } + $product.winghostty = $reordered + $product.localFallback.reason = "different explanatory wording" + } "" + Write-Output "Provider pin no-divergence contracts: PASS" +} finally { + Remove-Item -LiteralPath $fixture -Recurse -Force +} diff --git a/Tools/windows/Tests/TerminalGate.Tests.ps1 b/Tools/windows/Tests/TerminalGate.Tests.ps1 index a57c3138..d93806ff 100644 --- a/Tools/windows/Tests/TerminalGate.Tests.ps1 +++ b/Tools/windows/Tests/TerminalGate.Tests.ps1 @@ -1,6 +1,8 @@ [CmdletBinding()] param( - [switch] $List + [switch] $List, + [string] $ProductPinsPath, + [string] $GatePinsPath ) $ErrorActionPreference = "Stop" @@ -36,8 +38,32 @@ foreach ($path in @( "required gate file is missing: $path" } -$pins = Get-Content -LiteralPath (Join-Path $gateRoot "provider-pins.json") -Raw | +if (-not $ProductPinsPath) { + $ProductPinsPath = Join-Path $repoRoot "graphcode-windows\provider-pins.json" +} +if (-not $GatePinsPath) { + $GatePinsPath = Join-Path $gateRoot "provider-pins.json" +} +$pins = Get-Content -LiteralPath $GatePinsPath -Raw | ConvertFrom-Json +$productPins = Get-Content -LiteralPath $ProductPinsPath -Raw | ConvertFrom-Json +Assert-Contract ($productPins.schemaVersion -eq $pins.schemaVersion) "provider pin schemas differ" +foreach ($provider in @("winghostty", "zmx")) { + $product = $productPins.$provider + $gate = $pins.$provider + Assert-Contract ($product -is [pscustomobject] -and $gate -is [pscustomobject]) ` + "provider pins differ: $provider" + $fields = @(@($product.PSObject.Properties.Name) + @($gate.PSObject.Properties.Name)) | + Sort-Object -Unique -CaseSensitive + foreach ($field in $fields) { + Assert-Contract ( + $product.PSObject.Properties.Name -ccontains $field -and + $gate.PSObject.Properties.Name -ccontains $field -and + $product.$field -is [string] -and $gate.$field -is [string] -and + $product.$field -ceq $gate.$field + ) "provider pins differ: $provider.$field" + } +} Assert-Contract ($pins.schemaVersion -eq 1) "provider pin schema is not 1" Assert-Contract ($pins.winghostty.sha -eq "f5abc059e4ca58b376eb209313aca7784659c679") "Winghostty SHA is not exact" diff --git a/Tools/windows/Tests/ValidationRunner.Tests.ps1 b/Tools/windows/Tests/ValidationRunner.Tests.ps1 index 1aa358e1..05890bfb 100644 --- a/Tools/windows/Tests/ValidationRunner.Tests.ps1 +++ b/Tools/windows/Tests/ValidationRunner.Tests.ps1 @@ -173,6 +173,9 @@ try { if ($runnerSource -notmatch '(?s)"packaging" \{\s*& .*?Packaging\.Signing\.Tests\.ps1.*?Packaging\.Tests\.ps1') { throw "RED: packaging validation does not run signed catalog integrity contracts" } + if ($runnerSource -notmatch '(?s)"terminal-gate" \{\s*& .*?ProviderPins\.Tests\.ps1.*?TerminalGate\.Tests\.ps1') { + throw "RED: terminal validation does not run provider pin no-divergence contracts" + } foreach ($source in @($runnerSource, $hardeningSource)) { if ($source -notmatch '-StubResponseDelayMilliseconds 150') { throw "RED: shell validation does not exercise delayed correlated responses" diff --git a/Tools/windows/validate.ps1 b/Tools/windows/validate.ps1 index 3d137682..ee639693 100644 --- a/Tools/windows/validate.ps1 +++ b/Tools/windows/validate.ps1 @@ -563,6 +563,7 @@ function Invoke-Task([string] $name) { Write-Host "Privacy checks passed" } "terminal-gate" { + & (Join-Path $repoRoot "Tools\windows\Tests\ProviderPins.Tests.ps1") & (Join-Path $repoRoot "Tools\windows\Tests\TerminalGate.Tests.ps1") if ($LASTEXITCODE -ne 0) { throw "Windows terminal gate contract failed with exit code $LASTEXITCODE" diff --git a/Tools/windows/validation-matrix.md b/Tools/windows/validation-matrix.md index 9e093f07..b58d093d 100644 --- a/Tools/windows/validation-matrix.md +++ b/Tools/windows/validation-matrix.md @@ -17,6 +17,7 @@ The Windows port must have runnable commands before implementation fleets begin. | Production Swift platform package | `pwsh Tools/windows/validate.ps1 -Task swift-production` | | Deterministic release hardening fixtures | `pwsh Tools/windows/validate.ps1 -Task hardening` | | Signed catalog integrity and publisher policy | `pwsh Tools\windows\Tests\Packaging.Signing.Tests.ps1` | +| Product/investigation provider pin consistency | `pwsh Tools\windows\Tests\ProviderPins.Tests.ps1` | | Real-product packaging/install/upgrade/rollback | `pwsh Tools\windows\validate.ps1 -Task packaging` | | Shared Swift package | `swift test --package-path ` once extracted | | macOS app/daemon/CLI | `make test` | diff --git a/investigation/spikes/windows-terminal-gate/README.md b/investigation/spikes/windows-terminal-gate/README.md index 7d183f81..5cbe433a 100644 --- a/investigation/spikes/windows-terminal-gate/README.md +++ b/investigation/spikes/windows-terminal-gate/README.md @@ -17,6 +17,18 @@ Both commits are published on dedicated branches in the public `coneilen` provider repositories. The bootstrap creates detached, exact-revision checkouts without copying provider source into GraphCode. +The annotated `graphcode-windows-baseline-2026-09-17` tag preserves each exact +pin. The provider forks protect these tags against updates/deletion and the +dedicated branches against deletion/history rewrites; see +`Tools\windows\PACKAGING.md` for the recorded ruleset IDs and retention limits. +These are source-retention tags, not signed Windows releases. + +`graphcode-windows\provider-pins.json` is authoritative. The terminal gate's +contract compares its schema and all provider fields against this copy, and +`Tools\windows\Tests\ProviderPins.Tests.ps1` injects drift into both sides. +Changing only one copy must fail validation; JSON property order and explanatory +fallback wording do not change the provider identity. + ## Build Build Winghostty's host artifact at its pinned local SHA, build zmx at its