diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc3266c..ad9e6da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,14 @@ jobs: - name: Installer detection regression checks shell: pwsh run: ./installer/checks/Test-Detection.ps1 + - name: Release Defender activation contract checks (isolated mocks) + shell: pwsh + run: ./scripts/checks/Test-ReleaseDefender.ps1 + - name: Verify Defender activation on hosted runner + shell: pwsh + run: | + ./scripts/Enable-ReleaseDefender.ps1 + ./scripts/Check-ReleaseArtifacts.ps1 -Paths @('ClaudeRevit/bin/Release/release/ClaudeRevit.dll') -ReportPath defender-ci.json -SourceCommit '${{ github.sha }}' - name: Upload Revit 2027 build uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37c26cb..70f81c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,7 +116,7 @@ jobs: - name: Scan release payloads and final packages shell: pwsh run: | - Update-MpSignature -ErrorAction Stop + ./scripts/Enable-ReleaseDefender.ps1 $targets = @((Resolve-Path 'installer/payload').Path) $targets += Get-ChildItem "ClaudeRevit-${{ steps.vars.outputs.version }}-Revit*.zip" | ForEach-Object FullName $targets += '${{ steps.installer.outputs.path }}' diff --git a/README.md b/README.md index e54a86a..9520434 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ MCP agents: choose **Claude Code · MCP** or **Codex · MCP**, then select a mod and reasoning effort. Choices are saved independently. Codex reads its model catalog from the installed CLI. See [agent selection and setup](docs/openai-codex.md). -**v3.8.4:** mandatory objective benchmark checks, repeated-run calibration, channel/angle/tube/paired-timber sections, dependent nodes and actual bore checks, multicategory live SPDS sources, optional verified experience and saved resumable jobs. Seven generated sections, shared nesting, checkpoint recovery, dependent nodes, live steel/timber quantities and eight BIMStarter references have now been exercised in Revit. [Executed results and limits](docs/native-acceptance-2026-10.md) · [Release notes](docs/releases/v3.8.4.md) · [BIMStarter findings](docs/bimstarter-reference-cases.md). +**v3.8.5:** mandatory objective benchmark checks, repeated-run calibration, channel/angle/tube/paired-timber sections, dependent nodes and actual bore checks, multicategory live SPDS sources, optional verified experience and saved resumable jobs. Seven generated sections, shared nesting, checkpoint recovery, dependent nodes, live steel/timber quantities and eight BIMStarter references have now been exercised in Revit. Includes the release-runner Defender activation fix after v3.8.4 publication was blocked. [Executed results and limits](docs/native-acceptance-2026-10.md) · [Release notes](docs/releases/v3.8.5.md) · [BIMStarter findings](docs/bimstarter-reference-cases.md). **v3.8.3:** live SPDS steel/timber schedules, native steel connection inspection, persistent keyed nodes and geometry-driving parametric sections with independent flex checks. Instance values survive type changes; compiler/worker failures and timing are journaled. Code execution is always enabled, with no permission checkbox. [Structural workflows and limits](docs/structural-workflows.md) · [Release notes](docs/releases/v3.8.3.md). diff --git a/README.ru.md b/README.ru.md index aa5f2a5..eaf0f5f 100644 --- a/README.ru.md +++ b/README.ru.md @@ -6,7 +6,7 @@ глубину рассуждений. Настройки агентов сохраняются отдельно; Codex получает список моделей из установленного CLI. [Выбор агента и настройка](docs/openai-codex.md). -**v3.8.4:** обязательные объективные проверки бенчмарка, повторы и калибровка времени, швеллер/уголок/труба/парное деревянное сечение, зависимости узлов и проверка реальных отверстий, живые таблицы нескольких категорий, необязательный проверенный опыт и возобновляемые задания. В Revit проверены семь созданных сечений, вложенность, восстановление заданий, зависимости узлов, обновление стальных/деревянных таблиц и восемь семейств BIMStarter. [Результаты и ограничения](docs/native-acceptance-2026-10.md) · [Изменения](docs/releases/v3.8.4.md) · [Разбор семейств BIMStarter](docs/bimstarter-reference-cases.md). +**v3.8.5:** обязательные объективные проверки бенчмарка, повторы и калибровка времени, швеллер/уголок/труба/парное деревянное сечение, зависимости узлов и проверка реальных отверстий, живые таблицы нескольких категорий, необязательный проверенный опыт и возобновляемые задания. В Revit проверены семь созданных сечений, вложенность, восстановление заданий, зависимости узлов, обновление стальных/деревянных таблиц и восемь семейств BIMStarter. Добавлено включение Defender на сервере сборки: публикация v3.8.4 была остановлена проверкой безопасности. [Результаты и ограничения](docs/native-acceptance-2026-10.md) · [Изменения](docs/releases/v3.8.5.md) · [Разбор семейств BIMStarter](docs/bimstarter-reference-cases.md). **v3.8.3:** живые спецификации СПДС для стали и дерева, проверка загруженных нативных стальных соединений, сохраняемые узлы с адресным обновлением и параметрические сечения с независимыми flex-проверками. Параметры экземпляра сохраняются при смене типа; журнал отдельно учитывает компиляцию и её ошибки. Выполнение кода доступно всегда, переключатель разрешения удалён. [Инструменты и ограничения](docs/structural-workflows.md) · [Релиз](docs/releases/v3.8.3.md). diff --git a/docs/installer-security.md b/docs/installer-security.md index e491ef3..4ec376b 100644 --- a/docs/installer-security.md +++ b/docs/installer-security.md @@ -71,3 +71,12 @@ archive contents and exclusions are handled by the scanner without changing real-time protection or restoring files. See the [official command reference](https://learn.microsoft.com/en-us/defender-endpoint/command-line-arguments-microsoft-defender-antivirus). A passing scan is a time-specific result from one engine, not a safety guarantee. + +GitHub's [Windows image configuration](https://github.com/actions/runner-images/blob/main/images/windows/scripts/build/Configure-WindowsDefender.ps1) +disables monitoring and can force passive mode. The first v3.8.4 release attempt +built every package but failed closed at this gate; nothing was published. +`Enable-ReleaseDefender.ps1` now activates protection only on a disposable hosted +Windows runner, removes that image's two whole-drive exclusions and updates +intelligence. It follows Microsoft's [always-on protection settings](https://learn.microsoft.com/en-us/defender-endpoint/configure-real-time-protection-microsoft-defender-antivirus). +Developer-machine invocation is rejected. Activation failure still blocks release; +the original scan gate is retained. Isolated contract tests mock all system calls. diff --git a/docs/releases/v3.8.5.md b/docs/releases/v3.8.5.md new file mode 100644 index 0000000..95290d5 --- /dev/null +++ b/docs/releases/v3.8.5.md @@ -0,0 +1,7 @@ +# v3.8.5 — verified modeling and checked release packaging + +Includes all [v3.8.4 modeling changes](https://github.com/debug23win/ClaudeRevit/blob/main/docs/releases/v3.8.4.md): objective benchmark evidence, repeated-run calibration, seven constrained section shapes, dependent nodes/bore inspection, live multicategory SPDS summaries, optional verified experience and saved checkpoint jobs. See [executed native results and limits](https://github.com/debug23win/ClaudeRevit/blob/main/docs/native-acceptance-2026-10.md). + +The v3.8.4 build completed, but publication was blocked because GitHub's Windows image had Defender inactive. This patch activates it only on the disposable hosted runner, removes its whole-drive exclusions, updates intelligence and retains the mandatory scan of payloads, final ZIPs and the exact installer. Activation, detection or scan failures still block release. No developer-computer security settings are changed. + +215 unit tests and WPF/MCP/subscription CLI/installer checks passed. Native Revit 2027 cases cover generated sections, shared nesting, recovery, dependent-node edits, bores, eight BIMStarter references and live steel/timber quantities. Reference visibility variants and generated structural-template placement remain bounded as described in the acceptance report. Steel summaries are separate live grouped schedules, not an exact single-table form 2 facsimile; agree formatting before issuing drawings. diff --git a/scripts/Check-ReleaseArtifacts.ps1 b/scripts/Check-ReleaseArtifacts.ps1 index 1a7cada..b0eca43 100644 --- a/scripts/Check-ReleaseArtifacts.ps1 +++ b/scripts/Check-ReleaseArtifacts.ps1 @@ -7,9 +7,10 @@ param( # Fail closed. A successful scan is evidence from one engine at one time, not a # guarantee of safety. Do not add exclusions, restore quarantine or disable AV. $ErrorActionPreference = 'Stop' -$report = [ordered]@{ source_commit = $SourceCommit; utc = [DateTime]::UtcNow.ToString('o'); status = 'incomplete'; engine = $null; files = @(); error = $null } +$report = [ordered]@{ source_commit = $SourceCommit; utc = [DateTime]::UtcNow.ToString('o'); status = 'incomplete'; protection = $null; engine = $null; files = @(); error = $null } try { $status = Get-MpComputerStatus -ErrorAction Stop + $report.protection = @{ mode = $status.AMRunningMode; antivirus_enabled = [bool]$status.AntivirusEnabled; realtime_enabled = [bool]$status.RealTimeProtectionEnabled } if (-not $status.AntivirusEnabled -or -not $status.RealTimeProtectionEnabled) { throw 'Microsoft Defender must be active; release scanning cannot be skipped.' } diff --git a/scripts/Enable-ReleaseDefender.ps1 b/scripts/Enable-ReleaseDefender.ps1 new file mode 100644 index 0000000..b2f66fc --- /dev/null +++ b/scripts/Enable-ReleaseDefender.ps1 @@ -0,0 +1,25 @@ +# GitHub's disposable Windows images disable monitoring and can force passive mode. +# Strengthen that runner before packaging; never change a developer's AV settings. +$ErrorActionPreference = 'Stop' +if ($env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_OS -ne 'Windows' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted') { + throw 'Defender activation is restricted to disposable GitHub-hosted Windows runners.' +} +$policy = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection' +if (Test-Path -LiteralPath $policy) { + Set-ItemProperty -LiteralPath $policy -Name ForceDefenderPassiveMode -Value 0 -Type DWord +} +Start-Service -Name WinDefend -ErrorAction Stop +Set-MpPreference -DisableRealtimeMonitoring $false -DisableBehaviorMonitoring $false -DisableArchiveScanning $false -DisableScriptScanning $false -DisableIOAVProtection $false -ErrorAction Stop +# Remove only the two whole-drive exclusions supplied by the runner image. +$broadExclusions = @((Get-MpPreference).ExclusionPath | Where-Object { $_ -in @('C:\','D:\') }) +if ($broadExclusions.Count) { Remove-MpPreference -ExclusionPath $broadExclusions -ErrorAction Stop } +Update-MpSignature -ErrorAction Stop +for ($attempt = 0; $attempt -lt 12; $attempt++) { + $status = Get-MpComputerStatus -ErrorAction Stop + if ($status.AntivirusEnabled -and $status.RealTimeProtectionEnabled) { + $status | Select-Object AMRunningMode, AntivirusEnabled, RealTimeProtectionEnabled, AntivirusSignatureVersion + return + } + Start-Sleep -Seconds 2 +} +throw 'Defender activation failed; release publication remains blocked.' diff --git a/scripts/checks/Test-ReleaseDefender.ps1 b/scripts/checks/Test-ReleaseDefender.ps1 new file mode 100644 index 0000000..fdc7d52 --- /dev/null +++ b/scripts/checks/Test-ReleaseDefender.ps1 @@ -0,0 +1,31 @@ +# Isolated contract checks: mocks never change AV, services, registry or network. +$ErrorActionPreference = 'Stop' +$activationScript = Join-Path $PSScriptRoot '..\Enable-ReleaseDefender.ps1' +$original = @{} +foreach ($name in @('GITHUB_ACTIONS','RUNNER_OS','RUNNER_ENVIRONMENT')) { $original[$name] = [Environment]::GetEnvironmentVariable($name) } +function Assert($condition, $message) { if (-not $condition) { throw $message } } +try { + $env:GITHUB_ACTIONS = 'false' + try { & $activationScript; throw 'Local invocation was allowed' } + catch { Assert ($_.Exception.Message -like '*restricted to disposable*') 'Local guard failed' } + $qaMock=@{} + function Test-Path { param($LiteralPath) return $true } + function Set-ItemProperty { param($LiteralPath,$Name,$Value,$Type) Assert ($Name -eq 'ForceDefenderPassiveMode' -and $Value -eq 0) 'Policy must enable active mode'; $qaMock.policyWrites++ } + function Start-Service { [CmdletBinding()]param($Name) Assert ($Name -eq 'WinDefend') 'Unexpected service'; $qaMock.serviceStarts++ } + function Set-MpPreference { [CmdletBinding()]param($DisableRealtimeMonitoring,$DisableBehaviorMonitoring,$DisableArchiveScanning,$DisableScriptScanning,$DisableIOAVProtection) Assert (-not ($DisableRealtimeMonitoring -or $DisableBehaviorMonitoring -or $DisableArchiveScanning -or $DisableScriptScanning -or $DisableIOAVProtection)) 'Protection was weakened'; $qaMock.preferenceWrites++ } + function Get-MpPreference { return @{ExclusionPath=@('C:\','D:\','C:\retained-example')} } + function Remove-MpPreference { [CmdletBinding()]param($ExclusionPath) Assert (($ExclusionPath -join ',') -eq 'C:\,D:\') 'Only runner whole-drive exclusions may be removed'; $qaMock.exclusionRemovals++ } + function Update-MpSignature { [CmdletBinding()]param() $qaMock.signatureUpdates++ } + function Get-MpComputerStatus { [CmdletBinding()]param() $qaMock.statusReads++; return @{ AntivirusEnabled=$qaMock.ready; RealTimeProtectionEnabled=$qaMock.ready } } + function Start-Sleep { param($Seconds) $qaMock.waits++ } + $env:GITHUB_ACTIONS='true'; $env:RUNNER_OS='Windows'; $env:RUNNER_ENVIRONMENT='github-hosted' + $qaMock.policyWrites=0; $qaMock.serviceStarts=0; $qaMock.preferenceWrites=0; $qaMock.exclusionRemovals=0; $qaMock.signatureUpdates=0; $qaMock.statusReads=0; $qaMock.waits=0; $qaMock.ready=$true + & $activationScript | Out-Null + Assert ($qaMock.policyWrites -eq 1 -and $qaMock.serviceStarts -eq 1 -and $qaMock.preferenceWrites -eq 1 -and $qaMock.exclusionRemovals -eq 1 -and $qaMock.signatureUpdates -eq 1 -and $qaMock.statusReads -eq 1) 'Ready scanner was not verified' + $qaMock.ready=$false; $qaMock.statusReads=0; $qaMock.waits=0 + try { & $activationScript | Out-Null; throw 'Inactive scanner was accepted' } + catch { Assert ($_.Exception.Message -like '*publication remains blocked*') 'Inactive scanner did not fail closed' } + Assert ($qaMock.statusReads -eq 12 -and $qaMock.waits -eq 12) 'Activation wait must be bounded' + Write-Output 'RELEASE_DEFENDER_CHECKS_PASSED' +} +finally { foreach ($name in $original.Keys) { [Environment]::SetEnvironmentVariable($name,$original[$name]) } }