Skip to content

feat(v1.100 PR-22): uninstall lifecycle scaffold — detect + dry-run plan only#480

Merged
itcmsgr merged 5 commits intomainfrom
feat/v1.100-pr22-uninstall-scaffold
Apr 19, 2026
Merged

feat(v1.100 PR-22): uninstall lifecycle scaffold — detect + dry-run plan only#480
itcmsgr merged 5 commits intomainfrom
feat/v1.100-pr22-uninstall-scaffold

Conversation

@itcmsgr
Copy link
Copy Markdown
Owner

@itcmsgr itcmsgr commented Apr 19, 2026

Summary

First v1.100 PR. Detect + dry-run plan only. No mutation.

Authorization

V1100_LIFECYCLE_COMPLETION_CONTRACT.md §13 frozen 2026-04-19 — all 9 open questions answered, §4.4 purge table + §10.2 verification matrix tightened per the answers.

Pinned sentence

PR-22 is detect + dry-run plan only. It must not flush any chain, remove any table, disable any service, delete any file, touch any .conf.local, or re-enable any external firewall. The dry-run output reads like a release contract preview, not a debug dump, and explicitly states that no phase beyond Plan is implemented yet.

Scope locks (per reviewer directive)

  • Detect + plan only. No mutation.
  • Prior authority = optional input (3 states: NoRecord / RecordUsable / RecordIncomplete)
  • Plan output in contract language, not debug dump
  • Explicit "not yet implemented" scope-boundary block in output
  • Authority classification in 4 states: NFTBan / External / None / Ambiguous

Current state

Seed commit only (003ea26a) — the contract doc lands first, zero behaviour change. Implementation commits land next (state constants + uninstall package + dispatcher + tests + CI gates).

Explicit non-goals

  • ❌ No kernel mutation
  • ❌ No service lifecycle
  • ❌ No filesystem writes under /etc/nftban/, /usr/lib/nftban/, /usr/sbin/nftban*
  • ❌ No .conf.local touch (read-or-write)
  • ❌ No external firewall touch
  • ❌ No prior-authority record WRITE (read-only detection only)
  • ❌ No user/group deletion
  • ❌ No package-manager transactions
  • ❌ No coupling to maintenance subsystem (Q5=B — adjacent, not phase)

Deferred to later PRs

PR Scope
PR-23 Authority release (Switch phase — flush + remove table)
PR-24 CSF / external firewall restoration decision enforcement
PR-25 Purge vs remove artifact execution
PR-26 Post-uninstall verification (G3-UN-VERIFY gate)

Implementation plan (commit-by-commit)

  1. Contract seedinternal/installer/uninstall/contract.md (this commit)
  2. State machine — add StateUninstallPlanning to internal/installer/state/machine.go
  3. uninstall package — authority classifier + prior-record detector + plan struct + renderer + unit tests
  4. Installer dispatcherrunUninstallDryRun mirroring runUpdateDryRun shape; no-mutation call-path audit
  5. CI gates — G3-UN-PLAN-RENDERS (contract-language structural check) + G3-UN-NO-MUTATION (structural grep rejecting mutation-flavored calls in uninstall package)

Status: Draft — un-drafts after CI green and scope-boundary language confirmed.

🤖 Generated with Claude Code

Authorization basis: V1100_LIFECYCLE_COMPLETION_CONTRACT.md §13 frozen
2026-04-19 (all 9 open questions answered, §4.4 purge table + §10.2
verification matrix tightened per the answers).

Pinned sentence:

  "PR-22 is detect + dry-run plan only. It must not flush any chain,
   remove any table, disable any service, delete any file, touch any
   .conf.local, or re-enable any external firewall. The dry-run output
   reads like a release contract preview, not a debug dump, and
   explicitly states that no phase beyond Plan is implemented yet."

Claim surface (strict PR-22 scope):
  - state machine: +StateUninstallPlanning
  - new package internal/installer/uninstall/ with:
    - authority.go (4-state read-only classifier)
    - prior.go (3-state read-only prior-authority record detector)
    - plan.go (Plan struct + BuildPlan + Render)
    - unit tests — no real-host mutation
  - cmd/nftban-installer: --mode=uninstall dispatch → runUninstallDryRun
    mirroring runUpdateDryRun shape
  - flag surface: --mode=uninstall, --purge, --force-delete-operator-config,
    --restore-prior-authority (all plan-only, no mutation code yet)

Authority classification — 4 states (read-only):
  AuthorityNFTBan     — ip nftban table + nftband active
  AuthorityExternal   — external firewall appears authoritative
  AuthorityNone       — no authoritative firewall detectable
  AuthorityAmbiguous  — both present / inconclusive

Prior-authority record — 3 states (read-only):
  NoRecord            — no artifact on disk
  RecordUsable        — parseable + complete
  RecordIncomplete    — exists but unusable (missing fields, malformed)

PR-22 reports; does NOT enforce. Enforcement of "refuse --restore when
record incomplete" is PR-24 scope.

Plan output reads like a release contract preview:
  requested mode / artifact policy / current authority / target
  authority / restore requested / restore authorized / prior record
  state / detected external firewall / phases that would mutate
  (with "NOT IMPLEMENTED in PR-22" markers pointing at PR-23..26) /
  explicit scope-boundary block.

The scope-boundary block is MANDATORY — it prevents the PR-21 class
confusion where operators assume broader authorization than the
contract actually provides.

Explicit non-goals (PR-22 strict):
  no kernel mutation, no service lifecycle, no filesystem writes
  under /etc/nftban or /usr/lib/nftban, no .conf.local touch, no
  external firewall touch, no prior-authority record WRITE, no
  user/group deletion, no package-manager transactions, no
  maintenance subsystem coupling.

CI gates:
  G3-UN-PLAN-RENDERS    structural check on contract-language output
  G3-UN-NO-MUTATION     structural grep on uninstall/*.go rejecting
                        mutation-flavored calls

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 19, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails
actions/actions/checkout 34e114876b0b11c390a56381ad16ebd13914f8d5 🟢 5.7
Details
CheckScoreReason
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Code-Review🟢 10all changesets reviewed
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
Packaging⚠️ -1packaging workflow not detected
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Pinned-Dependencies🟢 3dependency not pinned by hash detected -- score normalized to 3
Security-Policy🟢 9security policy file detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
SAST🟢 8SAST tool detected but not run on all commits
actions/actions/setup-go d35c59abb061a4a6fb18e82ac0862c26744d6ab5 🟢 5.7
Details
CheckScoreReason
Maintained🟢 67 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 6
Code-Review🟢 10all changesets reviewed
Binary-Artifacts🟢 10no binaries found in the repo
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 9security policy file detected
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST🟢 10SAST tool is run on all commits

Scanned Files

  • .github/workflows/ci-uninstall-canonization.yml

itcmsgr and others added 4 commits April 19, 2026 22:51
Strict PR-22 scope: detect + dry-run plan only. No mutation.

Authorization: V1100_LIFECYCLE_COMPLETION_CONTRACT.md §13 frozen
2026-04-19 (Q1..Q9 answered; §4.4 purge table + §10.2 verification
matrix tightened).

State machine:
  internal/installer/state/machine.go
    +StateUninstallPlanning — the only uninstall state PR-22 ships.
    Mutation-carrying states land in PR-23/25 so this doc stays
    literally accurate: no phase beyond Planning exists yet.

New package internal/installer/uninstall/:
  - authority.go — Classify() returns 4-state CurrentAuthority:
    AuthorityNFTBan / AuthorityExternal / AuthorityNone /
    AuthorityAmbiguous. READ-ONLY. Detects external firewall type
    (ufw / firewalld / iptables / csf) via service + iptables-save
    probes; no nft add/flush/delete, no systemctl mutation.
  - prior.go — Probe() returns 3-state PriorRecordState:
    PriorNoRecord / PriorRecordUsable / PriorRecordIncomplete. Reads
    /var/lib/nftban/state/prior_authority.json (read-only; writer
    lives in install-side PR per V1100 Q9). Schema version check +
    required-field check + known-firewall-type check. Mismatched
    schema → Incomplete (Q8=A: no migration unless demonstrated break).
  - plan.go — Plan struct + BuildPlan (pure function) + Render (io.
    Writer; contract-language output with MANDATORY scope-boundary
    block per PR-22 contract) + JSON (machine consumers). Modes:
    ModeRemove / ModePurge / ModePurgeForceDOC (§4.4 rows).
  - uninstall_test.go — 19 tests covering:
    * 4-state classifier (NFTBan / External ufw / External firewalld /
      None / Ambiguous / partial-nftban note)
    * 3-state probe (NoRecord / Usable / Incomplete malformed /
      missing field / unknown type / schema mismatch)
    * BuildPlan per mode (Remove defaults / Purge preserves
      .conf.local policy language / PurgeForceDOC describes
      .conf.local delete)
    * Restore logic (Q4=B): requires usable record + flag; warns
      on no-record or incomplete-record; §10.2 B-UN-restore-norecord
      branch covered
    * Render mandatory fields + scope-boundary block (all modes)
    * JSON round-trip key fields

Installer dispatcher:
  cmd/nftban-installer/
    +flags.go — --purge, --force-delete-operator-config,
      --restore-prior-authority (all plan-only in PR-22; no mutation
      code consumes them). --mode=uninstall accepted; forces --dry-run
      on with explicit operator-visible warning ("PR-22 ships detect +
      dry-run plan only").
    +main.go — --mode=uninstall dispatch to runUninstallDryRun.
    +uninstall_dryrun.go — orchestrator mirroring runUpdateDryRun
      shape (Detect phase via Classify+Probe → Plan phase via
      BuildPlan → Render stdout + persist plan JSON to state-dir →
      transition to StateUninstallPlanning → exit 0).

CI gate (new, blocking):
  .github/workflows/ci-uninstall-canonization.yml
  Matrix Ubuntu 24.04 + AlmaLinux 9.
    G3-UN-NO-MUTATION: structural grep on internal/installer/uninstall/
      + cmd/nftban-installer/uninstall_dryrun.go rejecting 20 mutation-
      flavored Go patterns (nft add/flush/delete, systemctl 8 verbs,
      ufw/iptables/csf, apt-get/dnf remove, WriteFileAtomic, os.Remove,
      .conf.local touch). Runs BEFORE unit tests for fast reviewer
      signal.
    G3-UN-PLAN-RENDERS: end-to-end --dry-run produces all 11 mandatory
      contract-language elements; run WITHOUT --dry-run must still
      not mutate files outside state-dir.

Explicit non-goals (all honored):
  no kernel mutation, no service lifecycle, no filesystem writes
  under /etc/nftban or /usr/lib/nftban, no .conf.local touch (read
  or write), no external firewall touch, no prior-authority record
  WRITE, no user/group deletion, no package-manager transactions,
  no maintenance subsystem coupling.

Deferred to later PRs:
  PR-23: authority release (Switch phase)
  PR-24: CSF/external firewall restoration enforcement
  PR-25: purge vs remove artifact execution
  PR-26: G3-UN-VERIFY (point-of-no-return proof gate)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the five concerns flagged in the contract-level audit of the
PR-22 scaffold:

Audit C — auto-elevation to dry-run could teach bad mental model
  Strengthened the warning: stderr banner now uses a 70-column box
  that is impossible to skim-past. Message explicitly warns that PR-23
  will change this behaviour and tells operators NOT to build habits
  around the PR-22 safety-by-default.
  Added a regression note in internal/installer/uninstall/contract.md
  documenting the PR-23 acceptance criterion: either delete the
  auto-elevation, OR replace with explicit refusal requiring
  --dry-run vs --confirm-mutation choice.

Audit D.1 — classifier must prefer ambiguous over false certainty
  Found + fixed a real bug: partial nftban (table without daemon, or
  daemon without table) + external firewall = AuthorityAmbiguous, not
  AuthorityExternal. Previous logic would have returned External
  silently, giving later PR-24 mutation decisions a poisoned input.
  New test TestClassify_PartialNFTBan_WithExternal_IsAmbiguous locks
  the behaviour.

Audit D.5 — text/JSON renderer drift
  Added two first-class fields to the Plan struct:
    ScopeBoundary string
    NoMutationPerformed bool
  Both are populated by BuildPlan and appear in BOTH Render() human
  output AND JSON serialization. Machine consumers now see the same
  "no mutation performed" guarantee the human reader sees.

Audit F — critical semantic edge case proof
  New test TestBuildPlan_Restore_IncompleteRecord_SemanticEdge_Audit_F
  proves that with --restore-prior-authority AND PriorRecordIncomplete,
  the plan satisfies ALL THREE properties:
    1. does NOT promise restoration (TargetAuthority is not prior fw)
    2. does NOT silently downgrade (RestoreRequested=true + warning
       both surface the operator's intent)
    3. explicitly reports restore requested but not authorized
  Rendered output also tested — operator audit at read-time catches
  the edge without reading code.

PR-22 scope is still detect + plan only. No mutation added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Policy Gates "Suppression comment audit" step rejects //nolint:
directives. Repo convention is //lint:ignore <CHECK> <reason>.
One-line fix in uninstall_dryrun.go plan-persist site.

No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pass

Issue 1 — TestRender_HasMandatoryFieldsAndScopeBoundary expected a
cross-line substring ("later PRs in the v1.100 track must land first").
Render wraps the sentence across two lines, so strings.Contains cannot
match. Changed the needle to a single-line fragment that's present
verbatim in the rendered output.

Fixes: Build & Test + Go Security Analysis (both failed on the same
unit-test regression).

Issue 2 — G3-UN-NO-MUTATION structural grep flagged the string
'.conf.local' in PR-22's OWN contract-language plan renderer. The
grep pattern was too aggressive: plan.go and contract.md legitimately
mention .conf.local when describing the §4.4 artifact policy. The
mutation danger is WRITE/DELETE operations, which the existing
os.Remove + os.RemoveAll + WriteFileAtomic patterns already block
regardless of which file they target. Removed the raw 'conf\.local'
pattern with an inline comment explaining why.

Fixes: Uninstall Canonization (both distros).

No behaviour change in PR-22 scope. Just unblocking CI so the audit
response can be evaluated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@itcmsgr itcmsgr marked this pull request as ready for review April 19, 2026 20:11
@itcmsgr itcmsgr merged commit 547aa08 into main Apr 19, 2026
57 checks passed
@itcmsgr itcmsgr deleted the feat/v1.100-pr22-uninstall-scaffold branch April 19, 2026 20:11
itcmsgr added a commit that referenced this pull request Apr 19, 2026
Independent audit of merged PR #480 found three concrete violations of
the PR-22 contract Stop Condition ("no writes under /var/lib/nftban/"):

  1. os.WriteFile(<stateDir>/uninstall_plan.json, …) on every dry-run
  2. sf.Transition(StateUninstallPlanning, …) persisted install_state
  3. unconditional writeHistory() recorded a successful plan preview as
     "install_fail" in update-history.json — silently poisoning every
     operator dashboard that alerts on install_fail

Plus one classifier false-negative:

  4. partial nftban (table OR daemon, not both) WITHOUT external
     firewall fell through to AuthorityNone — PR-23 release logic would
     skip kernel cleanup of the orphan table

Plus CI blind spot:

  5. G3-UN-NO-MUTATION grep missed os.WriteFile/Create/MkdirAll/Rename;
     snapshot step did not cover /var/lib/nftban/ at all

This PR is BOUNDARY REPAIR ONLY. It does not add uninstall mutation,
does not change purge/remove semantics, does not expand prior-authority
logic, does not begin PR-23. Scope locked to R1-R5 from the repair
contract seed.

R1 (uninstall_dryrun.go): remove os.WriteFile + sf.Transition. Plan
renders to stdout only. Option B — no installer-state persistence
during dry-run.

R2 (main.go): guard writeHistory with cfg.mode != "uninstall" so a
successful plan preview is not recorded as install_fail.

R3 (authority.go): explicit case nftbanPartial && !extPresent →
AuthorityAmbiguous with diagnostic note; tighten the AuthorityNone
case with !nftbanPartial. Regression test added
(TestClassify_PartialNFTBan_NoExternal_IsAmbiguous) plus symmetric
daemon-up-no-table case.

R4 (uninstall_dryrun_test.go): new falsifiable purity test calls
runUninstallDryRun with MockExecutor + real tempdir and asserts:
  - zero executor writes (WriteFileAtomic)
  - zero directory creates
  - zero mutation commands (nft add/flush/delete, systemctl lifecycle,
    ufw/firewall-cmd, package-manager removal)
  - zero files on disk under temp stateDir
Two tests: no-authority host and ambiguous host.

R5 (ci-uninstall-canonization.yml):
  - extend grep patterns: os.WriteFile/Create/MkdirAll/Mkdir/Rename,
    sf.Transition, nft create, apt-get purge, dnf erase
  - widen E2E snapshot to include /var/lib/nftban/ (was only
    /etc/nftban + /usr/lib/nftban + /usr/sbin/nftban*)
  - new G3-UN-HISTORY-PURITY gate: seeds realistic update-history.json,
    runs dry-run twice (explicit + implicit), asserts byte-identical
    hash — catches the exact class that escaped PR #480
  - run the new orchestrator purity test in CI

Acceptance criteria (all pass by construction):
  - uninstall dry-run no longer writes misleading plan/history
    artifacts under /var/lib/nftban/
  - successful uninstall dry-run does not create install_fail
  - partial nftban without external returns AuthorityAmbiguous
  - purity test exists and would fail on regression
  - CI would fail on the exact write-pattern class that escaped in
    PR #480 (os.WriteFile + history pollution + snapshot blind spot)
  - no uninstall mutation capability added
  - scope remains boundary repair only

Depends on: 547aa08 (PR #480)
Refs: V1100_LIFECYCLE_COMPLETION_CONTRACT.md §13 (frozen 2026-04-19)
Refs: internal/installer/uninstall/contract.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
itcmsgr added a commit that referenced this pull request Apr 19, 2026
… history + classifier fix (#481)

Boundary-repair PR for PR #480 (PR-22 uninstall scaffold). Closes three
contract violations found by independent audit: direct filesystem writes
under /var/lib/nftban/ during dry-run, install_state persistence during
dry-run, and unconditional history writes that recorded successful plan
previews as install_fail.

Plus classifier false-negative: partial nftban (table OR daemon, not
both) WITHOUT external firewall fell through to AuthorityNone, hiding
orphan kernel state from later release logic.

Plus CI blind spot: grep missed direct os.* writers; snapshot step
did not cover /var/lib/nftban/.

Scope-locked repair — no uninstall mutation added, no purge/remove
semantics changed, no PR-23 work begun. Extended grep catches the exact
class that escaped. New history-purity gate proves update-history.json
untouched. Filesystem snapshot extended to /var/lib/nftban/. New
orchestrator-level purity test asserts zero writes / zero mutation
commands.

Per merge approval: auto-elevation is still temporary PR-22
compatibility and must be re-decided before PR-23 mutation lands
(documented in internal/installer/uninstall/contract.md).

Refs: V1100_LIFECYCLE_COMPLETION_CONTRACT.md §13 (frozen 2026-04-19)
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