status(redfix): record the b5f2b10 MERGE PRECONDITION; widen B-redfix-5 to both post-undeploy sites

Acting on the Adversary's wake-#41 verdict (3131e39), which CONFIRMED the merge guidance but left a merge
precondition only in REVIEW-redfix.md — Adversary-owned, and not what an operator reads before merging.
Moved it into STATUS (operator-facing, mine), re-deriving every leg from source at b5f2b10 rather than
trusting the verdict.

- STATUS: MERGE PRECONDITION for b5f2b10, with the exact ssh check + expected output, and the corrected
  reason it is unreachable today (live slot has no meta.json — NOT "F-redfix-4 is closed").
- Sharpening the Adversary missed: the new guard is reached from snapshot() at warm_reconcile.py:514 as
  well as restore() at :536. Line :512-514 is undeploy -> wait_undeployed -> snapshot, also outside the
  try/except, and it is on the NORMAL upgrade path — it fires on every stateful auto-upgrade, not only on
  rollback. B-redfix-5 as filed named only the rollback site.
- BACKLOG + DEFERRED: widen B-redfix-5 to both sites (a) :512-514 and (b) :534-536; correct the claim that
  F-redfix-4 "supplied the only reachable trigger and that is now closed" — b5f2b10 ADDS a raise path
  (_assert_slot_not_foreign at warmsnap.py:158 and :209).

Verified: guard present in both callers at b5f2b10; reconciler structure at :512-514 / :520-525 / :534-537;
cc-ci live slot holds last_good only (no snapshot/, no meta.json, no canon-* slot). Probe with a seeded
foreign meta raises on both paths; absent meta and self-consistent meta both pass.

No DoD item touched. M1+M2 PASS stand, ## DONE stands, no VETO. Docs-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZmH4rVNue3irZ6EsZzavb
This commit is contained in:
2026-07-09 08:16:06 +00:00
parent 3131e39f64
commit 93614f114e
4 changed files with 113 additions and 22 deletions

View File

@ -58,17 +58,26 @@ hold). Concrete fix designs from M1 evidence:
`_assert_slot_not_foreign()` guard. Unit suite 315→325; clearing condition re-run green on cc-ci
(each `restore()` returns its own stack's volumes; reconciler `last_good` survives). Verify per
STATUS-redfix.md "Gate: M2 RE-CLAIMED".
- [ ] **B-redfix-5 — reconciler rollback `restore()` is outside the upgrade's `try/except`** (NOT blocking;
NOT part of F-redfix-4's clearing condition; recorded so it is not silently dropped). In
`warm_reconcile.py`, the unhealthy-rollback path runs `abra.undeploy(domain)` `wait_undeployed`
`warmsnap.restore(...)``deploy_version(last_good)`. `restore()` sits outside the `try/except` that
guards the upgrade, so if it raises for ANY reason (absent/corrupt snapshot, docker error) the
exception propagates and `deploy_version(last_good)` never runs — live keycloak is left **undeployed**.
F-redfix-4 supplied one way to make `restore()` raise (the shared slot) and that is now fixed, but the
structural gap predates it. Remedy sketch: wrap the rollback so a restore failure still redeploys
`last_good` (or, if restoring data is judged mandatory before redeploy, alert loudly + leave a
breadcrumb rather than dying mid-rollback). Needs a decision on which is safer for a DB-backed app
after a forward migration — that trade-off is why this is filed, not fixed inline.
- [ ] **B-redfix-5 — reconciler's post-`undeploy` warmsnap calls are outside the upgrade's `try/except`**
(NOT blocking; NOT part of F-redfix-4's clearing condition; recorded so it is not silently dropped).
In `warm_reconcile.py` there are **two** such sites, both after `abra.undeploy(domain)` and both
outside the `try/except` that guards the upgrade:
**(a) upgrade path** `:512-514``abra.undeploy``wait_undeployed``warmsnap.snapshot(...)`;
**(b) rollback path** `:534-536``abra.undeploy``wait_undeployed``warmsnap.restore(...)`
`deploy_version(last_good)` at `:537`.
If either raises for ANY reason (absent/corrupt snapshot, foreign slot, docker error, "no volumes
found") the exception propagates, the following `deploy_version` never runs, and live keycloak is left
**undeployed**. Site (a) is on the *normal* upgrade path — it does not need a rollback to fire.
**Reachability, corrected (wake #42):** F-redfix-4's fix does not remove the trigger, it *adds* one —
`_assert_slot_not_foreign()` is a new raise inside BOTH `snapshot()` (`warmsnap.py:158`) and
`restore()` (`:209`). It is unreachable on cc-ci **today** because the live slot holds no
`snapshot/meta.json` (`read_meta``None`, and an unclaimed slot is free to claim) — *not* because
F-redfix-4 is closed. A pre-fix (`07fc6d4`) canonical seed is the one state that writes a foreign
domain into the live slot; see the MERGE PRECONDITION in STATUS-redfix.md.
Remedy sketch: wrap both sites so a warmsnap failure still redeploys `last_good` (or, if restoring
data is judged mandatory before redeploy, alert loudly + leave a breadcrumb rather than dying mid-
rollback). Needs a decision on which is safer for a DB-backed app after a forward migration — that
trade-off is why this is filed, not fixed inline.
- [ ] **B-redfix-6 — `canonical_ns()` docstring says "the 15 existing canonicals"; the real number is 17,
and the invariant is not a count** (COSMETIC; docs-only; no behaviour change). At
`redfix-m2-harness`@`b5f2b10`, `runner/harness/canonical.py:52` reads "zero blast radius on the 15