diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ea66aae..b41c3876e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] - v1.100 PR-22A + PR-22B repair cycle + +### Changed + +- **v1.100 lifecycle truth repair** (PRs #480, #481, #482). Observational + paths (install refused, update and uninstall dry-run) are now + structurally honest. `StateFile.DryRun` suppresses state-file writes; + `writeHistory` gated on `!cfg.dryRun && state.IsApplyTerminal(sf.State)`; + `authority.IsNftbanAuthoritative` is the canonical predicate used by + both `authority.Classify` and `update.Preflight`. New `Ambiguous` + authority decision for orphan-table / daemon-down hosts routes through + the emergency-SSH injection path. `--panel-auto-takeover` flag + (default off) replaces the previous implicit panel auto-approve. + Flag validation now rejects `--mode=install --dry-run`, + `--repair --dry-run`, `--takeover --dry-run`, `--rpm --deb`, and + `--force-delete-operator-config` without `--purge`. + +### Data-integrity note + +- **Lifecycle-bridge authority mapping (v1.98 — v1.99)** — the + `observePlan` and `mapAuthority` switches in + `cmd/nftban-installer/lifecycle_bridge.go` compared uppercase + `authority.Decision` values (e.g. `"TAKEOVER"`) against lowercase + string literals. The switches silently hit their `default` arms on + every real run, so lifecycle consumers saw `ActionPreserveAuthority` + and `AuthorityNone` regardless of the installer's actual decision. + Fixed in PR-22B (#482) — switches now pin to `authority.Decision` + constants; `Ambiguous` maps to a new `lifecycle.AuthorityUnknown` + owner. + + **Impact on historical records**: any lifecycle telemetry, + dashboards, or audit-trail consumers that ingested + `lifecycle.RunResult` JSON between v1.98 and the merge of PR-22B + will show `PreserveAuthority` / `AuthorityNone` on every install and + update run regardless of what actually happened on the host. This + does NOT affect install_state, update-history.json, or kernel + behavior — only the lifecycle bridge's external reporting surface. + Forensic interpretation of pre-PR-22B lifecycle output should treat + the authority decision as "unknown" rather than "preserve." + ## [1.98.2] - 2026-04-19 **Runtime correctness patch — exit-code truth, health resilience, installer payload truth.** diff --git a/internal/installer/uninstall/contract.md b/internal/installer/uninstall/contract.md index e942646e0..44816b168 100644 --- a/internal/installer/uninstall/contract.md +++ b/internal/installer/uninstall/contract.md @@ -202,3 +202,57 @@ If any PR-22 commit requires: → **STOP PR-22.** Push the work to PR-23 or later. PR-22's scope lock is the point of the contract seed — bending it collapses the evidence chain for the whole v1.100 track. + +--- + +## Standing lifecycle-truth rule (from PR-22B merge) + +**No new lifecycle code may bypass the shared authority predicate, the +history-write gate, or the dry-run contract.** + +Concretely, every new or modified lifecycle code path must: + +1. use `authority.IsNftbanAuthoritative(exec)` — not re-implement the + predicate or proxy it via `NftTableExists` alone +2. respect the `!cfg.dryRun && state.IsApplyTerminal(sf.State)` history + gate — no back-door direct writes to `update-history.json` +3. respect `StateFile.DryRun` — no direct writes to `install_state` or + any path under `/var/lib/nftban/` or `/etc/nftban/` during dry-run +4. route `authority.Ambiguous` through emergency-SSH injection before + any mutation — never silent-continue, never collapse to Fresh/None +5. surface `--panel-auto-takeover` explicitly if it needs panel consent + — no implicit panel auto-approve path + +CI gates (PR-22A + PR-22B): `G3-UN-NO-MUTATION`, `G3-UN-PLAN-RENDERS`, +`G3-UN-HISTORY-PURITY`, `G3-U3` hard-assertions, `G3-U5..U10` extended +grep, `G3-IN-REFUSE-DRY-RUN`, `G3-IN-FLAG-COMBOS`. Any lifecycle PR +that bypasses the rule above is expected to fail at least one of +these gates; if it doesn't, the gate needs extension, not the rule. + +--- + +## Pre-PR-23 blockers (tracked follow-up PRs) + +PR-23 (uninstall mutation: Switch phase + authority release) must NOT +start until all six items below have landed and been verified by a +narrow-scope audit. Each is its own PR with an explicit micro-contract +and one falsifiable proof test per PR-22B merge discipline. + +| # | PR | Purpose | Blocking because | +|---|---|---|---| +| 1 | Prior-authority record hardening | Add `recorded_at`, `installer_version`, explicit `active_at_install=false` handling to `prior.go` | PR-24 restore enforcement cannot trust under-defined `RecordUsable` | +| 2 | External-firewall detection unification | One shared function + one precedence order used by install/update/uninstall | Detection drift between modules will cause disagreement under takeover/restore | +| 3 | Kernel/service snapshot CI gate | `nft list tables` + `systemctl is-active` diff before/after every dry-run path | Filesystem snapshot alone cannot prove process-level purity | +| 4 | Exec-trace CI gate | `strace -f -e trace=execve` (or equivalent) around dry-run paths; assert no forbidden mutators spawned | Strictest purity guarantee; catches dynamically-constructed commands | +| 5 | Auto-elevate shim removal gate | CI rule: PR-23-class changes blocked while the shim block in `flags.go` still exists | Prevents scaffold-era UX semantics leaking into mutation-era behavior | +| 6 | Payload integrity minimum checks | Minimum-size / header-presence for `nftban.conf`, `nftables.conf` | Presence-only validation lets a truncated file pass | + +Phase 3 gating: once items 1–6 are merged and CI green, a focused +verification audit runs with ONLY these questions: + +1. Is dry-run still pure across install / update / uninstall? +2. Is there any history / state drift? +3. Is the authority predicate still consistent across callers? + +No exploratory scope in that audit. PR-23 starts only after it returns +clean.