B3 Phase 3 PR-B: RCSI Apply live behind informed-consent gate#1041
Merged
Conversation
Registers RcsiHandler in the production handler array (RemediationApplyService.cs:55) — the reachability seam that makes the destructive RCSI Apply LIVE — and wires the acknowledge-each-risk confirm dialog that gates it. Gate enforcement (the whole point): RemediationConfirmWindow.ComputeConfirmEnabled is the pure enablement predicate — for a destructive (RequiresInformedConsent) request, Apply is enabled ONLY when EVERY risk checkbox is ticked AND base apply-ability holds AND (if resolved-by-name) the by-name ack is ticked. Generalizes the LOW-2 single-ack to N boxes; the consent gate is ADDITIVE to AnyActionable, never a replacement. The dialog is the trust boundary; the confirm callback returns true only when satisfied. Real-figures fix: the UI apply call site passes no finding, so only the persisted RemediationAction survives. The risk-of-not-changing figures (blocking/deadlock counts + reader-writer pct) are now captured on the action at BuildRcsiAction time (finding in hand), round-tripped via RemediationActionDto, and read action-first in FactRiskDisclosure.GetForAction — so the dialog shows REAL numbers, not the weak-case baseline. Falls back to the finding/weak-case only when genuinely absent. Second affordance: AnalysisNotificationService emits a separate "Enable RCSI (advanced)" code-block detail item (singular Remediation, FactKey "RCSI") on any config_issues-bearing finding where RCSI is OFF + enrichment present — NOT gated behind the always-safe RemediationTsql condition. Distinct from the always-safe DB_CONFIG item; the two Apply buttons live on two views and can never cross. Cross-surface disclosure (read-only, no consent UI off-app): AdviceBlock gains Risks, populated in FactAdvice.GetForFinding for a destructive action; rendered as a prose disclosure item that flows to BOTH email bodies (HTML + plain-text) and the webhook (Teams + Slack) via context.Details, and surfaced as destructive_risk_disclosure in the Dashboard + Lite MCP findings output. Tests: Dashboard.Tests 189 passing, Lite.Tests 312 passing — incl. the HARD gate-enforcement test, second-item emission, real-figures-survive-persistence, and cross-surface (both email bodies + webhook) tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st has no risk disclosure (LOW-1) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this ships
PR-B is the reachability + UI + disclosure half of B3 Phase 3 (§10 of the approved plan). It makes the destructive RCSI Apply LIVE — an operator can now enable
READ_COMMITTED_SNAPSHOTon a production database — strictly behind the informed-consent (acknowledge-each-risk) gate. PR-A (#1040, the privileged core) is already ondev.1. Registration (the reachability seam)
new RcsiHandler()added to the production handler array atDashboard/Services/Remediation/RemediationApplyService.cs:55.HasHandlerFor("RCSI")is now true, so the Apply affordance appears and routes to the destructive handler — but only through the gatedRemediationApplyServicefacade.2. The gate (file:line — the whole point of this PR)
RemediationConfirmWindow.ComputeConfirmEnabled(Dashboard/RemediationConfirmWindow.xaml.cs) is the pure, statically-testable enablement predicate the dialog uses:ByNameAckCheckto N risk checkboxes (one perRisksOfChanging/RisksOfNotChangingitem, both sections always rendered, read-only/wrap-enabled text).AnyActionable, never a replacement.IsDefault=falsefor a destructive request (no Enter click-through).No path bypasses it: the privileged handler runs only after
confirm(request)returns true (PR-A's gate atRemediationApplyService.cs:204); the dialog is the sole confirm caller (AlertDetailWindow.ConfirmAsync). The always-safe DB_CONFIG path can never execute an RCSI target —ExtractDbConfigTargetsnever emitsReadCommittedSnapshotOn, the RCSI target is built only byBuildRcsiActionwith FactKey"RCSI"→RcsiHandler, and the two are distinct detail items with distinct singularRemediations.consent_acknowledged=1is written byRcsiHandleron success/skip (PR-A, now live).3. Real-figures-in-dialog fix (CRITICAL correctness)
The UI apply call site passes no finding (
AlertDetailWindow.xaml.cs:147); only the persistedRemediationActionsurvives. Without a fix the dialog would show the weak-case baseline even when real blocking exists.Mechanism chosen: carry the figures ON the persisted action.
RcsiInactionFigures(BlockingEvents, Deadlocks, ReaderWriterPct)onRemediationAction, populated inFactRemediation.BuildRcsiAction(when the finding + §3.3 enrichment ARE in hand).RemediationActionDto.RcsiFigures(mirrors the Phase-2DbConfigTargetDtoround-trip; backward-compatible — legacy/non-RCSI JSON deserializes to null).FactRiskDisclosure.GetForActionreads the action's figures action-first, falling back to the finding (analysis-time surfaces) and then the weak-case baseline.Apply_Destructive_RealFigures_SurvivePersistence_NoFindingAtApplyTime: build → serialize → deserialize → apply with no finding → the request'sRisksshow the REAL 12 events / 3 deadlocks / 80% (RCSI eliminates), NOT the baseline.4. Second affordance
AnalysisNotificationService.BuildContextemits a separate "Enable RCSI (advanced)" code-block detail item (BuildRcsiAction, FactKey"RCSI") on anyconfig_issues-bearing finding with RCSI OFF + enrichment — NOT gated behind the always-safeadvice.RemediationTsqlcondition (per the round-2 residual note). RCSI-on or no-enrichment yields no second item.5. Cross-surface disclosure (read-only — no consent UI off-app)
AdviceBlockgainsRisks, populated inFactAdvice.GetForFindingfor a destructive action. Rendered as:context.Details;destructive_risk_disclosurein the Dashboard + Lite MCP findings output.Cross-surface parity test asserts the disclosure header + "Risks of CHANGING / NOT changing" + "RCSI eliminates" appear in HTML, plain-text, Teams, AND Slack (per
feedback_emailtemplatebuilder_two_bodies).REAL test results
New PR-B tests include: the HARD gate-enforcement battery (
Gate_Destructive_Disabled_UntilAllRiskBoxesChecked,Gate_Destructive_UncheckingAnyBox_ReDisables,Gate_Destructive_ByName_RequiresBoth_RiskBoxesAndByNameAck,Gate_NotActionable_NeverEnabled_EvenWithAllConsent,Gate_NonDestructive_Unaffected_ByConsentArm), real-figures-survive-persistence, second-item emission + not-gated-behind-always-safe + both-items, round-trip with figures, and the cross-surface (both email bodies + Teams + Slack) test. PR-A'sRcsi_Handler_IsUnregistered_DeadCodeSafewas updated toRcsi_Handler_IsRegistered_AndReachableThroughGate. TheCoreMachinery_*reachability guards remain green.Maintainer real-server pre-merge gate (plan §9, steps 1–5 — folded into your end-to-end)
collect.*→ finding offers the RCSI affordance; dialog shows real counts + high reader-writer %; all boxes → Apply →is_read_committed_snapshot_on = 1, audit row action RCSI,prior_value='OFF',consent_acknowledged=1. Re-apply → freshness-skip.🤖 Generated with Claude Code