Skip to content

B3 Phase 3 PR-B: RCSI Apply live behind informed-consent gate#1041

Merged
erikdarlingdata merged 2 commits into
devfrom
feature/b3-phase3-rcsi-pr-b
Jun 1, 2026
Merged

B3 Phase 3 PR-B: RCSI Apply live behind informed-consent gate#1041
erikdarlingdata merged 2 commits into
devfrom
feature/b3-phase3-rcsi-pr-b

Conversation

@erikdarlingdata
Copy link
Copy Markdown
Owner

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_SNAPSHOT on a production database — strictly behind the informed-consent (acknowledge-each-risk) gate. PR-A (#1040, the privileged core) is already on dev.

1. Registration (the reachability seam)

new RcsiHandler() added to the production handler array at Dashboard/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 gated RemediationApplyService facade.

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:

if (!baseActionable)                     return false;   // audit-absent / nothing applyable
if (requiresConsent && !allRiskBoxesChecked) return false;   // every risk box must be ticked
if (resolvedByName && !byNameAck)        return false;   // LOW-2 by-name ack
return true;
  • Generalizes the Phase-1 LOW-2 single ByNameAckCheck to N risk checkboxes (one per RisksOfChanging / RisksOfNotChanging item, both sections always rendered, read-only/wrap-enabled text).
  • The consent gate is additive to AnyActionable, never a replacement. IsDefault=false for a destructive request (no Enter click-through).
  • A destructive RCSI on a by-name-resolved server requires BOTH all risk boxes AND the by-name ack.
  • RCSI banner arm: "RCSI is a database-wide concurrency change. Read both risk lists; every box must be checked to enable Apply."

No path bypasses it: the privileged handler runs only after confirm(request) returns true (PR-A's gate at RemediationApplyService.cs:204); the dialog is the sole confirm caller (AlertDetailWindow.ConfirmAsync). The always-safe DB_CONFIG path can never execute an RCSI target — ExtractDbConfigTargets never emits ReadCommittedSnapshotOn, the RCSI target is built only by BuildRcsiAction with FactKey "RCSI"RcsiHandler, and the two are distinct detail items with distinct singular Remediations. consent_acknowledged=1 is written by RcsiHandler on 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 persisted RemediationAction survives. 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.

  • New RcsiInactionFigures(BlockingEvents, Deadlocks, ReaderWriterPct) on RemediationAction, populated in FactRemediation.BuildRcsiAction (when the finding + §3.3 enrichment ARE in hand).
  • Round-trips via RemediationActionDto.RcsiFigures (mirrors the Phase-2 DbConfigTargetDto round-trip; backward-compatible — legacy/non-RCSI JSON deserializes to null).
  • FactRiskDisclosure.GetForAction reads the action's figures action-first, falling back to the finding (analysis-time surfaces) and then the weak-case baseline.
  • Proven by Apply_Destructive_RealFigures_SurvivePersistence_NoFindingAtApplyTime: build → serialize → deserialize → apply with no finding → the request's Risks show the REAL 12 events / 3 deadlocks / 80% (RCSI eliminates), NOT the baseline.

4. Second affordance

AnalysisNotificationService.BuildContext emits a separate "Enable RCSI (advanced)" code-block detail item (BuildRcsiAction, FactKey "RCSI") on any config_issues-bearing finding with RCSI OFF + enrichment — NOT gated behind the always-safe advice.RemediationTsql condition (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)

AdviceBlock gains Risks, populated in FactAdvice.GetForFinding for a destructive action. Rendered as:

  • a prose disclosure detail item → BOTH email bodies (HTML + plain-text) and the webhook (Teams + Slack), all via context.Details;
  • destructive_risk_disclosure in 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

dotnet build Dashboard/Dashboard.csproj -c Debug   →  Build succeeded. 0 Warning(s) 0 Error(s)
dotnet test Dashboard.Tests  →  Passed!  Failed: 0, Passed: 189, Skipped: 0, Total: 189
dotnet test Lite.Tests       →  Passed!  Failed: 0, Passed: 312, Skipped: 0, Total: 312

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's Rcsi_Handler_IsUnregistered_DeadCodeSafe was updated to Rcsi_Handler_IsRegistered_AndReachableThroughGate. The CoreMachinery_* reachability guards remain green.

Maintainer real-server pre-merge gate (plan §9, steps 1–5 — folded into your end-to-end)

  1. Scratch DB, RCSI OFF + induced reader/writer blocking captured in 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.
  2. Scratch DB, RCSI OFF but predominantly writer/writer → disclosure shows low reader-writer % + the "RCSI won't resolve this" line (honest-both-directions).
  3. Fail-closed on a db_owner-of-PerformanceMonitor-only login → PermissionDenied, no ALTER.
  4. Hold an open transaction in the target DB, attempt apply → the ALTER waits then errors on timeout; per-target Error, no partial state.
  5. Audit-absent + DB-not-found behave as Phase 2.

🤖 Generated with Claude Code

erikdarlingdata and others added 2 commits June 1, 2026 14:58
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>
@erikdarlingdata erikdarlingdata merged commit b896ecc into dev Jun 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant