Skip to content

Commit 07c210c

Browse files
itcmsgrclaude
andcommitted
feat(v1.100 PR-P2-5): auto-elevate shim removal gate (G3-UN-SHIM-LOCK)
Pre-PR-23 assurance blocker #5 of 2 remaining. Adds a CI gate that fails when the uninstall auto-elevate shim and uninstall mutation code coexist. This enforces that when PR-23 lands, the shim is removed in the SAME PR — preventing the scaffold-era "safe by default" UX from silently flipping meaning the moment real mutation lands. ## Rule | shim_present | mutation_present | Result | |:-:|:-:|---| | 1 | 1 | **FAIL** — shim + mutation cannot coexist | | 1 | 0 | PASS — PR-22/P2-x scaffold state | | 0 | 1 | PASS — post-PR-23, shim correctly removed | | 0 | 0 | PASS — trivially clean | ## Detection **Shim detection** (`cmd/nftban-installer/flags.go`, grep for either): - "auto-elevated to --dry-run" - "NO MUTATION WILL OCCUR (v1.100 PR-22 scope)" Two independent markers — if one is removed by refactor, the other still fires. Ensures the gate doesn't silently stop working. **Mutation detection** (`internal/installer/uninstall/*.go` + `cmd/nftban-installer/uninstall_dryrun.go`, Go only, excluding tests): - nft mutation verbs (add/create/delete/flush) - systemctl lifecycle verbs (start/stop/restart/reload/enable/ disable/mask/unmask) via exec.Run or Service* methods - External firewall binaries (ufw, firewall-cmd, iptables-restore) - Filesystem writers (WriteFileAtomic, os.WriteFile, Create, Remove, RemoveAll, MkdirAll, Rename) - State persistence (sf.Transition) ## Scope lock - NO code changes (no shim removal, no mutation added) - NO CLI redesign - Pure detection gate — fires if the coupling appears, silent otherwise ## Two acceptable shim remediations at PR-23 time 1. **Delete** the auto-elevate block entirely (`--mode=uninstall` mutates unless `--dry-run` is explicit) 2. **Convert** to explicit refusal requiring `--dry-run` or `--confirm-mutation` (no silent default either way) ## Also: tracking update Marks blocker #4 (exec-trace CI gate, PR #488) as LANDED. Remaining pre-PR-23 blockers: 2 (this PR = P2-5, plus P2-6 payload integrity). Contract doc updated with the full decision table + detection logic in internal/installer/uninstall/contract.md. Refs: internal/installer/uninstall/contract.md §"Pre-PR-23 blockers" + §"G3-UN-SHIM-LOCK (PR-P2-5) — how the gate decides" + §"Audit C regression note" (where the two remediations were originally committed to) Authorization: locked Phase 2 sequencing (2026-04-20) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4749fe1 commit 07c210c

2 files changed

Lines changed: 155 additions & 2 deletions

File tree

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

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,118 @@ jobs:
8282
# external-firewall touches into the uninstall code, CI fails BEFORE
8383
# the unit tests even run.
8484
# ------------------------------------------------------------------
85+
# ------------------------------------------------------------------
86+
# G3-UN-SHIM-LOCK (PR-P2-5) — lifecycle transition safety lock.
87+
#
88+
# The `flags.go` auto-elevate shim silently forces --dry-run when
89+
# `--mode=uninstall` is invoked without an explicit --dry-run.
90+
# This is correct ONLY during PR-22's scaffold era, when no
91+
# mutation code exists in the uninstall package. The moment
92+
# real mutation code lands (PR-23 Switch phase / authority
93+
# release), the shim MUST be removed or converted to an explicit
94+
# refusal. Leaving both in place teaches operators "--mode=
95+
# uninstall is safe by default" right up until a silent semantic
96+
# flip to mutation — the exact UX-drift attack the contract
97+
# warns against.
98+
#
99+
# This gate fires BEFORE tests so any reviewer sees the coupling
100+
# immediately. The rule:
101+
#
102+
# shim_present ∧ mutation_present → FAIL
103+
# shim_present ∧ ¬mutation_present → PASS (PR-22 scaffold state)
104+
# ¬shim_present ∧ mutation_present → PASS (post-PR-23, shim removed)
105+
# ¬shim_present ∧ ¬mutation_present → PASS (trivial)
106+
#
107+
# Contract: internal/installer/uninstall/contract.md
108+
# §"Audit C regression note" and §"Pre-PR-23 blockers"
109+
# ------------------------------------------------------------------
110+
- name: G3-UN-SHIM-LOCK — auto-elevate shim may not coexist with uninstall mutation
111+
shell: bash
112+
run: |
113+
set -Eeuo pipefail
114+
115+
# Part 1 — shim presence signature. Two independent markers,
116+
# both introduced by PR-22's flags.go block. Presence of
117+
# EITHER is sufficient — this ensures trivial renames don't
118+
# accidentally hide the shim from the gate.
119+
shim_present=0
120+
if grep -qE 'auto-elevated to --dry-run' cmd/nftban-installer/flags.go 2>/dev/null; then
121+
shim_present=1
122+
fi
123+
if grep -qE 'NO MUTATION WILL OCCUR \(v1\.100 PR-22 scope\)' cmd/nftban-installer/flags.go 2>/dev/null; then
124+
shim_present=1
125+
fi
126+
127+
# Part 2 — mutation presence in the uninstall scope. Scope is
128+
# the uninstall package + the uninstall dispatcher, Go files
129+
# only, excluding tests (which may contain mutation strings
130+
# as fixtures).
131+
mutation_scope_files=$(find \
132+
internal/installer/uninstall \
133+
cmd/nftban-installer/uninstall_dryrun.go \
134+
-type f -name '*.go' 2>/dev/null | grep -vE '_test\.go$' || true)
135+
136+
# Mutation-flavored patterns. Each targets a specific kernel /
137+
# service / filesystem / external-firewall write path. Match
138+
# any = mutation code has landed.
139+
mutation_patterns=(
140+
'exec\.Run\("nft"[^)]*add'
141+
'exec\.Run\("nft"[^)]*create'
142+
'exec\.Run\("nft"[^)]*delete'
143+
'exec\.Run\("nft"[^)]*flush'
144+
'exec\.Run\("systemctl"[^)]*stop'
145+
'exec\.Run\("systemctl"[^)]*start'
146+
'exec\.Run\("systemctl"[^)]*restart'
147+
'exec\.Run\("systemctl"[^)]*reload'
148+
'exec\.Run\("systemctl"[^)]*enable'
149+
'exec\.Run\("systemctl"[^)]*disable'
150+
'exec\.Run\("systemctl"[^)]*mask'
151+
'exec\.Run\("systemctl"[^)]*unmask'
152+
'exec\.ServiceStop\('
153+
'exec\.ServiceStart\('
154+
'exec\.ServiceEnable\('
155+
'exec\.ServiceDisable\('
156+
'exec\.ServiceMask\('
157+
'exec\.ServiceUnmask\('
158+
'exec\.Run\("ufw"'
159+
'exec\.Run\("firewall-cmd"'
160+
'exec\.Run\("iptables-restore"'
161+
'exec\.WriteFileAtomic\('
162+
'os\.WriteFile\('
163+
'os\.Create\('
164+
'os\.Remove\('
165+
'os\.RemoveAll\('
166+
'os\.MkdirAll\('
167+
'os\.Rename\('
168+
'sf\.Transition\('
169+
)
170+
mutation_present=0
171+
if [[ -n "$mutation_scope_files" ]]; then
172+
for pat in "${mutation_patterns[@]}"; do
173+
if grep -nE "$pat" $mutation_scope_files 2>/dev/null; then
174+
mutation_present=1
175+
fi
176+
done
177+
fi
178+
179+
echo "G3-UN-SHIM-LOCK state: shim_present=$shim_present mutation_present=$mutation_present"
180+
181+
# Gate rule: the coupling is the failure.
182+
if [[ "$shim_present" -eq 1 && "$mutation_present" -eq 1 ]]; then
183+
echo "::error::G3-UN-SHIM-LOCK FAIL: uninstall mutation code present AND auto-elevate shim still exists in cmd/nftban-installer/flags.go"
184+
echo "::error::The shim must be REMOVED or CONVERTED TO REFUSAL before any mutation code lands in internal/installer/uninstall/."
185+
echo "::error::See internal/installer/uninstall/contract.md §'Audit C regression note' for the two acceptable remediations."
186+
exit 1
187+
fi
188+
189+
if [[ "$mutation_present" -eq 1 ]]; then
190+
echo "G3-UN-SHIM-LOCK PASS — uninstall mutation has landed AND shim was correctly removed (post-PR-23 state)"
191+
elif [[ "$shim_present" -eq 1 ]]; then
192+
echo "G3-UN-SHIM-LOCK PASS — no uninstall mutation yet; auto-elevate shim allowed during PR-22/P2-x scaffold era"
193+
else
194+
echo "G3-UN-SHIM-LOCK PASS — neither shim nor uninstall mutation present"
195+
fi
196+
85197
- name: G3-UN-NO-MUTATION — structural audit of uninstall package
86198
shell: bash
87199
run: |

internal/installer/uninstall/contract.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ discipline.
283283
| 1 | Prior-authority record hardening | PR #484 / `3b834033` | Added `recorded_at`, `installer_version`, explicit `active_at_install=false` handling to `prior.go`; 5-state classification |
284284
| 2 | External-firewall detection unification | PR #486 / `49d98fc1` | `internal/installer/extfw` canonical detector; Option A CSF config-file signal shared across install/update/uninstall; multi-active → `Ambiguous` (no silent collapse); cross-caller consistency test locked as regression guard |
285285
| 3 | Kernel/service snapshot CI gate | PR #487 / (tracked post-merge) | `G3-KS-SNAPSHOT` added to all 3 canonization workflows; `scripts/ci-snapshot-kernel-service.sh` helper; hard-asserts kernel nft tables + firewall-adjacent service states byte-identical after every dry-run path |
286+
| 4 | Exec-trace CI gate | PR #488 / (tracked post-merge) | `G3-EXEC-TRACE` added to all 3 canonization workflows; `scripts/ci-exec-trace-assert.sh` wraps dry-runs under `strace -f -e trace=execve`; fails if any forbidden mutator (nft add/flush/delete, systemctl lifecycle verbs, ufw/firewall-cmd/iptables-restore, package-manager removal, userdel/groupdel) is spawned |
286287

287288
### Behavioral / semantic blockers (code contract changes)
288289

@@ -294,8 +295,48 @@ discipline.
294295

295296
| # | PR | Scope | Blocking because |
296297
|---|---|---|---|
297-
| 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 that source grep cannot see |
298-
| 5 | Auto-elevate shim removal gate | CI rule: PR-23-class changes blocked while the shim block in `flags.go` still exists when any mutation code lands in `internal/installer/uninstall/` | Prevents scaffold-era UX semantics leaking into mutation-era behavior |
298+
| 5 | Auto-elevate shim removal gate | `G3-UN-SHIM-LOCK` CI rule: PR-23-class changes blocked while the shim block in `flags.go` still exists when any mutation code lands in `internal/installer/uninstall/` | Prevents scaffold-era UX semantics leaking into mutation-era behavior |
299+
300+
### G3-UN-SHIM-LOCK (PR-P2-5) — how the gate decides
301+
302+
The gate lives in `ci-uninstall-canonization.yml` and runs before the
303+
structural-audit + unit tests so reviewers see the coupling first. It
304+
performs two independent detections and applies one rule:
305+
306+
- **Shim detection.** Grep `cmd/nftban-installer/flags.go` for either
307+
of two stable signature strings introduced by PR-22's uninstall
308+
auto-elevate block: `"auto-elevated to --dry-run"` or
309+
`"NO MUTATION WILL OCCUR (v1.100 PR-22 scope)"`. Presence of either
310+
sets `shim_present=1`.
311+
312+
- **Mutation detection.** Grep `internal/installer/uninstall/*.go`
313+
plus `cmd/nftban-installer/uninstall_dryrun.go` (Go files only,
314+
excluding `_test.go`) for any forbidden mutation-flavored pattern:
315+
`nft` mutation verbs, `systemctl` lifecycle verbs, `Service*`
316+
executor methods, external-firewall binaries, `os.*` filesystem
317+
writers, `sf.Transition(`. Match of any one sets `mutation_present=1`.
318+
319+
Rule table:
320+
321+
| `shim_present` | `mutation_present` | Result |
322+
|:-:|:-:|---|
323+
| 1 | 1 | **FAIL** — shim + mutation cannot coexist |
324+
| 1 | 0 | PASS — PR-22/P2-x scaffold state |
325+
| 0 | 1 | PASS — post-PR-23, shim correctly removed |
326+
| 0 | 0 | PASS — trivially clean |
327+
328+
The gate is **pure detection** — it does NOT remove the shim, does
329+
NOT add mutation code, and does NOT change CLI behavior. It only
330+
ensures that when PR-23 (or any later PR) adds uninstall mutation,
331+
the shim is removed in the SAME PR and both land together.
332+
333+
Two acceptable shim remediations at PR-23 time:
334+
335+
1. **Delete** the auto-elevate block entirely (so `--mode=uninstall`
336+
mutates unless `--dry-run` is explicit)
337+
2. **Convert** to an explicit refusal requiring the operator to choose
338+
between `--dry-run` and `--confirm-mutation` (no silent default
339+
behaviour in either direction)
299340

300341
### Later v1.100 work (preferred order, not dogmatic)
301342

0 commit comments

Comments
 (0)