docs(v1.100 PR-26): record operator locks for code-A entry#513
Merged
Conversation
Records the operator's lock signals for the §§39–43 proposed locks and the §48 hard blockers, post-merge of PR-26-doc (#512, squash cd76842). This is contract authority — the locks belong next to Part IV, not in CHANGELOG (which is for shipped user-facing changes) and not in a separate docs page (which would split the source of truth). Adds §51 (inside Part IV) + a v5 Amendment-history entry. Locked decisions: - Q1 (Verification authority) ACCEPTED — row 6 conditional on §48.1 - Q2 (Real-host destructive soak) ACCEPTED — staged DA host = code-E merge-blocker - Q3 (Target-specific predicate) ACCEPTED — mechanism §48.1-dependent - Q4 (Cron backup / A.4) ACCEPTED — manifest-only, no template recreation - Q5 (Executor hardening) ACCEPTED — typed ServiceUnmask + Rename ONLY - §48.1 Option B — exact CSF SSH-rule kernel evidence ADVISORY for PR-26-code-A; no IptablesRuleExists and no new iptables introspection in code-A - §48.2 firewallType plumbing — raw firewallType, not precomputed targetUnit; consistent with the PR-25 4B-3-pre evidence-plumbing pattern P1 acknowledgements recorded: - Option A (typed iptables introspection + row 6 BLOCKING) remains possible only through a future contract amendment. - INV-PR26-NEW-MUTATION-SURFACES-BOUNDED caps mutation surfaces only, not read-only typed introspection. - Any §22 / §32 ordering extension must be re-locked explicitly at the relevant code-* slice's lock signal before implementation. Entry criteria for PR-26-code-A locked: - code-A scope is bounded to target-specific safety predicate and inline-verification hardening for Q1 BLOCKING rows 1–5 / 8 / 9 + listener-source SSH check. - code-A is NOT authorized to do, in this slice: - cron-backup manifest work (that is PR-26-code-C) - typed ServiceUnmask / Rename work (that is PR-26-code-B) - destructive real-host CSF soak (that is PR-26-code-E) - repo hygiene / UX / GOTH / metrics / module cleanup - any addition of iptables introspection in any form Hard fence honored: - internal/installer/restore/contract.md ONLY changed - doc-only diff (+83 lines, 0 deletions) - no production code, no CI workflow, no schema, no CHANGELOG, no separate docs page - §§1–36 still byte-identical with PR-25 contract baseline - Part IV §§37–50 untouched; §51 appended; v5 Amendment-history entry added Self-check: - §51 anchors present at expected file line range (1185–1266) - §51.6 entry-criteria lists all four pre-conditions for code-A and marks PR #512 + this commit as gates-met - Amendment history v5 entry restates the Option B + firewallType locks so a fresh reader of just the history block sees the operative decisions Awaiting independent auditor pass + final GO before push. Code-A stays blocked until this commit lands on main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
6 tasks
itcmsgr
added a commit
that referenced
this pull request
Apr 28, 2026
…lType plumbing (§§51.3-51.4) (#514) PR-26-code-A — restore verification / evidence hardening, slice A. Tightens the safety-net-safe predicate from PR-25's any-external-FW heuristic to the target-specific check operator-locked at §51.3 Option B. Plumbs the resolved firewallType into the inline-verify dep per §51.4. No new mutation primitives. No iptables introspection (Option A explicitly deferred to a future amendment). Authority: - PR #512 merge cd76842 (PR-26-doc / Part IV §§37-50) - PR #513 merge 52fadcc (PR-26 operator lock record §51) Behavior delta (target-specific predicate): - PR-25 / 4B-4: IsSafetyNetRemovalSafe accepted ANY service in {csf, ufw, firewalld, iptables, netfilter-persistent} being active. - PR-26-code-A: IsSafetyNetRemovalSafe requires the RESOLVED TARGET's specific service unit (csf.service for csf restore) to be active. A non-target external firewall being active no longer satisfies CSF restore safety. The §41 looseness is closed. Files changed (5): cmd/nftban-installer/restore_deps.go - productionInlineVerifyDep struct gains read-only firewallType field (constructor-injected; never re-derived per §17.3). - IsSafetyNetRemovalSafe rewritten: * SSH port via detect.SSHPort (kept). * Defensive guard: empty firewallType → ErrInlineVerifyTargetFirewallTypeMissing. * Defensive guard: firewallType not in §18.2 set → ErrInlineVerifyUnknownFirewall. * Amendment-1 §30.2 lock: firewallType != csf → ErrInlineVerifyOnlyCSFAuthorized. * Target-specific check: only the resolved unit's ServiceActive counts. Other external FW services are no longer probed. - IsTargetFirewallActive gains defensive cross-check: caller-passed firewallType must match v.firewallType when the constructor injected one. Mismatch → ErrInlineVerifyTargetMismatch. - New sentinels: ErrInlineVerifyTargetFirewallTypeMissing, ErrInlineVerifyTargetMismatch. - Removed: var inlineVerifyExternalFirewallServices (the any-FW list is dead under §51.3 Option B). - newProductionRestoreDeps + newProductionRestoreDepsWithEvidence signatures take firewallType. Old (4-arg) call sites in production routed through the new 5-arg signature with "" for the back-compat shim; tests pass "csf" or omit per fixture intent. - restoreDepsFactory function-pointer type extended to 5 args. - New helper resolveFirewallTypeForDeps maps TargetAuthority → firewallType (§18.3 invariants + §20 panel mapping). Lives in restore_deps.go (NOT restore_decide.go) to keep TargetAuthorityKind* constants out of dispatcher source — the no-Group-Kind-mapping invariant in TestDispatcher_NoLocalGroupKindMapping stays green. cmd/nftban-installer/restore_decide.go - runRestoreExecutionFromProceed gains a single line: resolves firewallType via resolveFirewallTypeForDeps(target) BEFORE calling the factory. On resolution failure: persists StateRestoreFailedExecution + the standard exit code. - newRestoreDeps call updated to the new 5-arg signature. - No Group→Kind mapping leaks into this file. - No state-machine / exit-code / history-gate change. cmd/nftban-installer/restore_decide_test.go - withFakeDeps + withFakeDepsRecordingEvidence updated to the new 5-arg factory signature. - recordingFactoryCall struct gains firewallType field. cmd/nftban-installer/restore_deps_csf_test.go - TestCSFMutate_4B3csf_PR25NonShipping_PredicateUnwiredByDefault call to newProductionRestoreDepsWithEvidence updated to the 5-arg form. - (Test still asserts the predicate is wired non-nil per 4B-4 lock; PR-26-code-A does not change that wiring — it changes what the predicate evaluates.) cmd/nftban-installer/restore_deps_inlineverify_test.go - All existing IsSafetyNetRemovalSafe tests migrated to use the new newInlineVerifyDepWithTarget(t, mock, firewallType) helper. The tests' semantic assertions are preserved (TrueOnlyWhenTargetFWActive, FalseWhenOnlyEmergencyProtects, FalseWhenSSHPortUnknown, NoMutation, FactoryWiresSafetyNetPredicate, A7_DeletesWhenPredicateTrue, A7_RefusesWhenPredicateFalse, FullRun_ChecksThreeAssertionsOnly). - Test #6 renamed from TrueOnlyWhenExternalFWActive to TrueOnlyWhenTargetFWActive to reflect the §51.3 lock. - 10 new PR-26-code-A tests (TestInlineVerify_PR26A_*): 1. NonTargetFWDoesNotSatisfy (4 sub-cases: ufw / firewalld / iptables / netfilter-persistent active does NOT satisfy CSF restore when csf.service is inactive) 2. EmptyFirewallType_DefensiveGuard 3. FactoryWiresFirewallTypeIntoInlineVerify 4. A7GateUsesTargetSpecificPredicate (mutation-side integration) 5. TargetCSFActive_SafeToRemove (happy path under tightened rule) 6. NonCSFTarget_TypedUnsupported (3 sub-cases: ufw / firewalld / iptables → ErrInlineVerifyOnlyCSFAuthorized) 7. UnknownTarget_TypedUnknown (4 sub-cases: shorewall / pf / CSF / "csf " → ErrInlineVerifyUnknownFirewall) 8. IsTargetFirewallActive_MismatchGuard (caller-passed firewallType must match v.firewallType when injected) 9. OldExternalFWListRemoved_FileScan (compile-time + grep pin that the old any-external-FW list is gone) 10. FactorySignatureCarriesFirewallType (compile-time pin that the factory signature requires firewallType) Constraints honored (per §51.6 entry criteria + operator scope): IN scope: - target-specific safety predicate ✓ - inline verification hardening ✓ - §51.3 Option B semantics (kernel SSH-rule evidence ADVISORY) ✓ - §51.4 firewallType plumbing ✓ - tests proving non-target external FWs do not satisfy CSF restore ✓ OUT of scope (and untouched): - cron backup / A.4 (PR-26-code-C) - typed executor.ServiceUnmask / Rename (PR-26-code-B) - destructive soak (PR-26-code-E) - IptablesRuleExists / iptables introspection (Option A — needs amendment) - iptables / ip6tables / iptables-save / nft list parsing (none added) - repo hygiene / UX / GOTH / metrics / module cleanup (untouched) - service lifecycle audit (untouched) - rebuild/idempotency audit (untouched) - PR-25 state terminals or exit codes (untouched) - main.go history gate (untouched) - restore_deps_csf.go production logic (untouched — only its companion test file received the 1-line factory-signature update) - contract.md (untouched) - workflows (untouched) Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./cmd/nftban-installer/... ./internal/installer/restore/... ./internal/installer/state/... PASS - go test ./... PASS (full suite) - go test -race -count=1 ./cmd/nftban-installer ./internal/installer/restore/... ./internal/installer/state/... PASS - go vet clean - go mod tidy no-op - 10 new PR-26-code-A tests + 18 sub-tests all PASS - TestDispatcher_NoLocalGroupKindMapping still green (helper lives in restore_deps.go, not restore_decide.go) Awaiting auditor pass before push. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
6 tasks
itcmsgr
added a commit
that referenced
this pull request
Apr 28, 2026
…name + raw-Run policy tightening (#515) PR-26-code-B — restore verification / evidence hardening, slice B. Executor hardening per §43 lock + §51.5-A2 invariant. CSF restore A.1 + A.3 migrate from raw Run("systemctl","unmask",…) + Run("mv",…) indirections to typed executor methods. Authority: - PR #512 / contract.md Part IV §§37-50 - PR #513 / §51 lock record (§51.5-A2: read-only typed introspection is outside the mutation cap; this commit's ServiceUnmask + Rename ARE mutation surfaces, but already enumerated by §44 row 2) - PR #514 / code-A merge 4e98ff5 - §43 executor hardening - §46 CI gate requirements - §51 code-B authorization Behavior delta: - Before: A.1 used exec.Run("systemctl","unmask",csfServiceUnit) wrapped by helper unmaskCSFService; A.3 used exec.Run("mv",old,new) wrapped by helper renameAtomicViaExec. - After: A.1 uses m.exec.ServiceUnmask(csfServiceUnit); A.3 uses m.exec.Rename(csfBinaryDisabled, csfBinary). Both helpers REMOVED. Mutation surface is unchanged in operational meaning; the typed call shape lets the CI gate enforce per-call discipline at the symbol level instead of via fragile Run-arg parsing. Files changed (6): internal/installer/executor/executor.go - Executor interface gains: ServiceUnmask(unit string) error // inverse of ServiceMask Rename(oldpath, newpath string) error // atomic same-FS rename - Header doc updated to list both new methods. internal/installer/executor/real.go - RealExecutor.ServiceUnmask runs systemctl unmask via Run with the same typed error wrapping pattern as ServiceMask. - RealExecutor.Rename calls os.Rename directly (consistent with WriteFileAtomic's existing os.Rename usage). No process spawn. internal/installer/executor/mock.go - MockExecutor.ServiceUnmask records ("systemctl","unmask",unit) and returns m.ServiceUnmaskErr (nil by default). - MockExecutor.Rename records ("rename",oldpath,newpath), returns m.RenameErr if non-nil; otherwise simulates the rename in the in-memory Files map. - New error-injection fields: ServiceUnmaskErr, RenameErr — mirror the RunResults exit-code injection pattern. cmd/nftban-installer/restore_deps_csf.go - Helpers unmaskCSFService and renameAtomicViaExec REMOVED. Replaced by a comment block documenting the §43.2 lock. - A.1 call site: m.exec.ServiceUnmask(csfServiceUnit). - A.3 call site: m.exec.Rename(csfBinaryDisabled, csfBinary). - No raw Run("systemctl","unmask",…) and no raw Run("mv",…) remain. - Log messages preserved; error wrapping (ErrCSFRestoreUnmaskFailed + ErrCSFRestoreBinaryRestoreFailed) preserved. cmd/nftban-installer/restore_deps_csf_test.go - buildCSFFixture: unmaskFailsExit injects mock.ServiceUnmaskErr; mvBinaryFailsExit injects mock.RenameErr (the previous RunResults-based simulation is removed; the OnCommand callback that simulated the rename in the Files map is also removed — Mock.Rename does that natively now). - TestCSFMutate_4B3csf_A3_* tests updated: assertions move from CommandCalled("mv", …) to CommandCalled("rename", …) because Mock.Rename records "rename", not "mv". - HappyPath_NoOutOfTargetMutation allow-list and OrderingPin expected sequences updated: A.3's recorded shape becomes ("rename", oldpath, newpath) instead of ("mv", oldpath, newpath). - Seven new TestCSFMutate_PR26B_* tests added: 1. A1_ServiceUnmaskOnlyCSFService — pins ServiceUnmask called only on csf.service 2. A3_RenameOnlyCSFBinaryRestore — pins Rename called only with (csf.disabled → csf) 3. NoRawSystemctlUnmaskRun_FileScan — pins no raw Run("systemctl","unmask",…) remains in production source 4. NoRawMvRun_FileScan — pins no raw Run("mv",…) remains 5. A1_UnmaskFailure_TypedErrorPreserved — error contract preserved through the migration 6. A3_RenameFailure_TypedErrorPreserved — same for A.3 7. RemovedHelpersGone_FileScan — pins removal of the unmaskCSFService and renameAtomicViaExec function definitions .github/workflows/ci-restore-canonization.yml - G4-RESTORE-EXEC-NO-OUT-OF-TARGET strengthened per §43.4 + §46.1: * \bexec\.ServiceUnmask\( REMOVED from forbidden list (now the authorized typed method for A.1). * Added forbidden patterns for raw Run("systemctl",…) with any mutating verb (start/stop/enable/disable/mask/unmask/restart/ reload/daemon-reload). Read-only Run("systemctl","is-enabled",…) remains authorized. * Added forbidden pattern for raw Run("mv",…). Typed Rename is the only authorized atomic-rename path. * §46.1 line-skipping discipline applied: gate strips line-leading "//" comments before pattern matching, preventing the false-positive class that broke Policy Gates on PR #511. * Header rewritten to reflect the post-PR-26-code-B authorized mutation set (typed ServiceUnmask / typed Rename; no raw Run for mutating systemctl verbs or mv). Constraints honored (per §51.6 + operator scope): IN scope: - typed executor.ServiceUnmask ✓ - typed executor.Rename ✓ - migration of CSF restore A.1 + A.3 to typed methods ✓ - raw Run policy tightening (CI gate) ✓ - G4-RESTORE-EXEC-NO-OUT-OF-TARGET strengthened ✓ OUT of scope (and untouched): - cron backup / A.4 (PR-26-code-C) - destructive soak (PR-26-code-E) - IptablesRuleExists / iptables introspection (Option B lock) - target-specific predicate changes (already done in code-A) - inline verification behavior changes - restore decision lattice - TargetAuthority / planner - main.go history gate - state machine / exit codes - contract.md - repo hygiene / UX / GOTH / metrics / module cleanup Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./cmd/nftban-installer/... ./internal/installer/restore/... ./internal/installer/state/... ./internal/installer/executor/... PASS - go test ./... PASS (full suite) - go test -race -count=1 ./cmd/nftban-installer ./internal/installer/restore/... ./internal/installer/state/... PASS - go vet (cmd + restore + state + executor) clean - go mod tidy no-op - 7 new TestCSFMutate_PR26B_* tests all PASS - Local replay of strengthened G4-RESTORE-EXEC-NO-OUT-OF-TARGET gate: FAIL=0 against the migrated restore_deps_csf.go (only authorized NftDeleteTable("ip","nftban") + NftDeleteTable("ip6","nftban") calls; no raw mutating Run, no os.* bypass, no custombuild/iptables/rebuild/purge symbols). Awaiting auditor pass before push. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
itcmsgr
added a commit
that referenced
this pull request
Apr 28, 2026
… restore (#516) * feat(v1.100 PR-26-code-C1): install-time cron-backup manifest writer + executor.Stat PR-26-code-C is split into two reviewable sub-slices on the same branch. C1 (this commit) lands the WRITER side; C2 (next commit) lands the READER side. §50 ordering lock: writer commit BEFORE reader. Authority: - PR #512 / contract.md Part IV §§37-50 - PR #513 / §51 lock record (§51.5-A2: read-only typed introspection is OUTSIDE the bounded-3 mutation cap) - PR #514 / code-A merge 4e98ff5 - PR #515 / code-B merge 45fc63e - §42 cron backup / A.4 contract - §51.6 entry criteria (code-B merged) C1 scope (this commit): 1. Add typed executor.Stat read-only introspection method. - executor/executor.go: new FileMeta struct + Stat method on Executor interface. - executor/real.go: RealExecutor.Stat via os.Stat + syscall.Stat_t. UID/GID extracted from the platform-specific Sys() interface (Linux-only target). - executor/mock.go: MockExecutor.Stat reads from new FileStats map (path → FileMeta); falls back to (0644, 0:0, len(content)) if the path is in Files but not FileStats. Returns os.ErrNotExist if neither holds. - Per §51.5-A2 invariant: read-only introspection is OUTSIDE the bounded-3 mutation surface cap of INV-PR26-NEW-MUTATION-SURFACES-BOUNDED. Stat does NOT count against §44 row 2's mutation budget. 2. New shared cron-manifest module: internal/installer/switchop/cron_manifest.go. - Constants: CronManifestSchemaVersion = "1.0.0" CronManifestDir = "/var/lib/nftban/state/csf-cron-backup" CronManifestFile = "/var/lib/nftban/state/csf-cron-backup/manifest.json" CronCSFSrcPath = "/etc/cron.d/csf-cron" CronLFDSrcPath = "/etc/cron.d/lfd-cron" - Types: CronManifestEntry (path / backup_name / sha256 / mode / uid / gid / size) + CronManifest (schema_version / captured_at / files). - Helpers: ComputeCronBackupSHA256(content) — single source of truth shared by writer + reader; identical bytes-to-hex semantics in both directions. WriteCronBackupManifest(exec, log) — install-time writer. For each of {csf-cron, lfd-cron} that exists: read content, Stat for mode/uid/gid/size, compute sha256, copy under CronManifestDir, append manifest entry. Then write manifest.json. Files absent at capture time are skipped (no entry recorded; no fabrication). ReadCronBackupManifest(exec, log) — used by the C2 reader. Three return shapes: absent (zero, false, nil), present-but- corrupt (zero, true, ErrCronManifestParseFailed/ ErrCronManifestSchemaMismatch/ErrCronManifestUnknownEntry), present-and-valid (manifest, true, nil). VerifyCronBackupEntry(exec, entry) — sha256 integrity check against the on-disk backup. - Sentinels: ErrCronManifestSchemaMismatch, ErrCronManifestSHA256Mismatch, ErrCronManifestUnknownEntry, ErrCronManifestParseFailed. 3. Modified disarmCSFArtifacts in switchop/takeover.go to call WriteCronBackupManifest BEFORE the existing rm -f of the cron files. Writer failure is logged but non-fatal: the rm path MUST still execute (nftban-takeover correctness invariant). Hosts installed before PR-26-code-C ship without a manifest; A.4 stays soft-skip on those hosts (§42.2 graceful migration). 4. Tests in internal/installer/switchop/cron_manifest_test.go: - WriteCronBackupManifest_BothPresent_RecordsBoth - WriteCronBackupManifest_OnlyOnePresent_OnlyOneRecorded - WriteCronBackupManifest_NeitherPresent_EmptyManifest - WriteCronBackupManifest_WritesOnlyManifestDir (no writes outside CronManifestDir) - WriteCronBackupManifest_ManifestPathPinnedExact - WriteCronBackupManifest_OnlyAuthorizedSrcPaths (writer ignores non-{csf-cron, lfd-cron} cron files; never invents content) - WriteCronBackupManifest_SHA256ComputedCorrectly - ReadCronBackupManifest_AbsentReturnsFalse (graceful skip path) - ReadCronBackupManifest_ParseFailure (corrupt JSON refused) - ReadCronBackupManifest_SchemaMismatch - ReadCronBackupManifest_UnknownEntryPath (defense-in-depth) - ReadCronBackupManifest_HappyPath - CronManifest_WriteThenRead_Roundtrip - VerifyCronBackupEntry_HappyPath - VerifyCronBackupEntry_SHA256Mismatch Constraints honored (per §51.6 + operator C scope): IN scope (C1): - install-time cron-backup manifest writer ✓ - only the two §42.2-locked cron files (csf-cron, lfd-cron) ✓ - only writes under CronManifestDir ✓ - manifest records: path, sha256, mode, uid, gid, size, schema_version ✓ - no template regeneration ✓ - no DirectAdmin custombuild ✓ - no unrelated cron files ✓ - absent files cleanly skipped (no fabrication) ✓ OUT of scope (and untouched): - A.4 reader / restore path (PR-26-code-C2 in next commit on same branch) - Destructive real-host CSF soak (PR-26-code-E) - IptablesRuleExists / iptables introspection (Option B lock) - main.go / state-machine / exit codes / history gate (untouched) - Restore planner / TargetAuthority / PR-24 lattice (untouched) - contract.md (untouched) - Repo hygiene / UX / GOTH / metrics / module cleanup (untouched) Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./internal/installer/switchop/... PASS C2 lands the reader side in the next commit on this branch. Both ship in PR-26-code-C; auditor checkpoint after C1+C2 compile + tests pass before push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(v1.100 PR-26-code-C2): A.4 manifest-restore in mutateToCSFTarget step 3 PR-26-code-C2 — companion to C1. C1 lands the install-time manifest writer; C2 (this commit) flips A.4 from soft-skip to manifest-restore when the §42.2 cron-backup manifest is present + integrity-clean. Authority: - C1 commit on this branch (cron_manifest.go writer + executor.Stat) - §42.2 cron-backup contract (manifest-only restore; no template regeneration; no cron files NFTBan did not back up itself) - §51.6 entry criteria Behavior delta: - Before (C1): A.4 always soft-skipped with a generic warning. - After (C2): A.4 reads switchop.ReadCronBackupManifest. Three paths: - Manifest absent (pre-PR-26 host) → graceful soft-skip, no /etc/cron.d/* writes, A.5 runs. - Manifest present but corrupt / schema-mismatch / unknown-entry / sha256-mismatch → soft-skip with a specific operator warning, no /etc/cron.d/* writes, A.5 still runs (per §42.2-D: csf can function without cron; LFD just won't auto-restart). - Manifest present + integrity-clean → for each entry whose target is currently absent, restore via WriteFileAtomic (preserves mode) + Chown (preserves uid/gid). Targets that already exist are skipped (operator may have re-created a different version post-takeover; A.4 must not overwrite operator content). Files changed (2): cmd/nftban-installer/restore_deps_csf.go - New typed sentinel: ErrCSFRestoreCronManifestCorrupt (exported for observability + test assertion via errors.Is). Per §42.2-D, A.4 emits this informationally and continues to A.5; the overall mutation does NOT abort on cron failure. - A.4 step rewritten: calls switchop.ReadCronBackupManifest, switches on (absent / corrupt / present), per-entry sha256 verification via switchop.VerifyCronBackupEntry, restoration via exec.WriteFileAtomic + exec.Chown. - New imports: "os" (for os.FileMode), "switchop" (for the shared manifest module). - New local helper fileModeFromUint32 — single-purpose conversion for the manifest's uint32 mode bitfield to os.FileMode. Keeps os import scoped narrowly. cmd/nftban-installer/restore_deps_csf_test.go - New seedCronManifest helper writes a sha256-valid manifest + matching backup files into the mock for end-to-end A.4 tests. - 8 new TestCSFMutate_PR26C2_* tests: 1. A4_ManifestAbsent_SoftSkip — pre-PR-26 host case 2. A4_HappyPath_RestoresBothFiles — manifest present + integrity clean + targets absent 3. A4_TargetExists_SkipsRestore — operator content not overwritten 4. A4_SHA256Mismatch_SoftSkip_A5StillRuns — §42.2-D non-abort 5. A4_SchemaMismatch_SoftSkip_A5StillRuns — §42.2-D non-abort 6. A4_OnlyAuthorizedTargetPaths — no broad /etc/cron.d/* writes 7. TypedSentinelExported — ErrCSFRestoreCronManifestCorrupt visible 8. A4_UnknownEntryPath_Rejected — defense-in-depth refusal Constraints honored (per §51.6 + operator C scope): IN scope (C2): - A.4 reader / restore path enabled when manifest is present ✓ - soft-skip with warning for pre-PR-26 hosts ✓ - typed refusal (sentinel surfaced) for corrupt / hash-mismatch / ambiguous cases ✓ - restore only the two §42.2-locked cron files ✓ - preserve mode/uid/gid via WriteFileAtomic + Chown ✓ - no write outside the two backup-target paths ✓ - no cron restore unless evidence says NFTBan backed up the file ✓ OUT of scope (and untouched): - Destructive real-host CSF soak (PR-26-code-E) - IptablesRuleExists / iptables introspection (Option B lock) - main.go / state-machine / exit codes / history gate - Restore planner / TargetAuthority / PR-24 lattice - contract.md - Repo hygiene / UX / GOTH / metrics / module cleanup §42.2-D semantics preserved: A.4 corrupt-manifest does NOT abort A.5. csf can function without cron; LFD just won't auto-restart. The operator-warning log line is more specific than 4B-3-csf's generic warning (states which precondition failed). The typed sentinel is exposed for higher-layer observability. Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./cmd/nftban-installer/... ./internal/installer/restore/... ./internal/installer/state/... ./internal/installer/executor/... ./internal/installer/switchop/... PASS - 8 new TestCSFMutate_PR26C2_* tests all PASS - existing TestCSFMutate_4B3csf_A4_SoftSkip_ZeroFileWrites still passes (manifest-absent fixture takes the new soft-skip path) Awaiting C1+C2 auditor checkpoint before push. CI gate update (G4-RESTORE-CRON-MANIFEST-INTEGRITY) lands as a third commit on the same branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(v1.100 PR-26-code-C): add G4-RESTORE-CRON-MANIFEST-INTEGRITY structural gate Strengthens the Restore Canonization workflow with the §46 cron- manifest integrity gate locked at §51.6 entry criteria for code-C. Authority: - §42 cron backup / A.4 contract (manifest-only restore) - §46 CI gate requirements (structural, not loose grep) - §46.1 line-skipping discipline (production-code-only, comment-stripped) Gate scope (writer + reader cross-pin): WRITER required symbols (internal/installer/switchop/cron_manifest.go): - CronManifestSchemaVersion = "1.0.0" const - CronManifestDir / CronManifestFile constants pinned to the exact /var/lib/nftban/state/csf-cron-backup/{,manifest.json} paths - CronCSFSrcPath / CronLFDSrcPath constants pinned to the exact /etc/cron.d/{csf-cron,lfd-cron} source paths - func ComputeCronBackupSHA256(content []byte) string — single source of truth for the sha256 helper - func WriteCronBackupManifest(...), ReadCronBackupManifest(...), VerifyCronBackupEntry(...) — the three exported API points - sha256.Sum256 — proves the writer actually computes sha256 (not a no-op stub) Pattern shape: whitespace-flexible ([[:space:]]+) so the patterns don't break when gofmt re-aligns the const block. READER required symbols (cmd/nftban-installer/restore_deps_csf.go): - switchop.ReadCronBackupManifest( — A.4 reads the manifest - switchop.VerifyCronBackupEntry( — A.4 verifies sha256 BEFORE restoring (this is the integrity guarantee §42.2-D requires) - ErrCSFRestoreCronManifestCorrupt — the typed sentinel surfaced on integrity failure If any required symbol is absent, the gate fails — proves the integrity check is consumed, not just imported. WRITER + READER forbidden patterns: - \bcustombuild\b — defense-in-depth (§34: no DirectAdmin custombuild) - iptables-restore — defense-in-depth (§34: csf manages its own) - "/etc/cron.d/*" glob literal — no broad cron sweep - WriteFile to /etc/cron.d/* with non-csf-prefixed leaf (rough check) READER allow-list pin: - Every WriteFileAtomic call in restore_deps_csf.go that targets a /etc/cron.d/* literal MUST equal one of the two §42.2-locked literals: "/etc/cron.d/csf-cron" OR "/etc/cron.d/lfd-cron". - The reader uses the named constants csfCronPath / lfdCronPath, so in practice this grep returns zero matches (named-constant reference, not string-literal in WriteFileAtomic args). Defense- in-depth structural pin against accidental future literal-arg drift. §46.1 discipline applied: production-code-only files, comment- stripped before pattern matching. Avoids the false-positive class that hit Policy Gates on PR #511 (//-comment text matching forbidden substrings). Local replay against the PR-26-code-C1 + C2 source: WRITER_MISS / READER_MISS / FORBIDDEN_HIT / BAD_LITERAL: all 0 FAIL=0 Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./... PASS (64 packages) - go test -race -count=1 ./cmd/nftban-installer ./internal/installer/restore/... ./internal/installer/state/... ./internal/installer/switchop/... PASS - go vet ./... clean - go mod tidy no-op Auditor checkpoint: C1 + C2 + CI gate are now all locally compiled, tested, and gate-replayed clean. Awaiting focused auditor pass before push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(v1.100 PR-26-code-C2): A.4 corrupt-manifest is hard refusal, not soft-skip (auditor verdict) Auditor focused-audit on PR-26-code-C flagged a semantic risk in the A.4 corrupt-manifest branch: previously a corrupt / hash-mismatch / unknown-entry / parse-failure manifest was a soft-skip with an informational sentinel, and A.5 still ran. The auditor argued — correctly — that proceeding to start csf.service when restore evidence is on disk but cannot be trusted weakens the evidence chain. Locked rule (per auditor verdict): manifest absent → soft-skip warning, continue to A.5 [migration gap, kept] manifest incomplete → ErrCSFRestoreCronManifestCorrupt, stop before A.5 hash mismatch → ErrCSFRestoreCronManifestCorrupt, stop before A.5 target exists dirty → ErrCSFRestoreCronTargetExists, stop before A.5 manifest clean → restore exact files, then continue to A.5 Behavior delta (this commit only — C1 + C2 + CI gate semantics remain otherwise unchanged): - Manifest parse failure / schema mismatch / unknown-entry path → A.4 returns wrapped ErrCSFRestoreCronManifestCorrupt; A.5 does NOT run; the existing §32 step-3 failure path retains the safety net. - Per-entry sha256 mismatch → same hard refusal. - Operator-content collision (target /etc/cron.d/<name> already exists) → A.4 returns wrapped ErrCSFRestoreCronTargetExists; A.5 does NOT run. - Manifest absent (pre-PR-26 host) → unchanged: graceful soft-skip with operator warning, control falls through to A.5. - Manifest clean → unchanged: restore both files, fall through to A.5. Files changed: cmd/nftban-installer/restore_deps_csf.go - ErrCSFRestoreCronManifestCorrupt docstring rewritten: now documents hard-refusal semantics (was: informational soft-skip). Wording updated: "refusing before A.5 (operator must inspect)". - New typed sentinel ErrCSFRestoreCronTargetExists for the operator-content-collision case. Distinct from ErrCSFRestoreCronManifestCorrupt for cleaner classification: a collision is an evidence conflict, not a manifest-trust failure. - A.4 step rewritten: * manifestErr branch now returns the wrapped sentinel instead of falling through. * Per-entry sha256 verify failure now returns instead of skip. * Per-entry unauthorized-Path now returns instead of skip. * Per-entry target-exists collision now returns ErrCSFRestoreCronTargetExists instead of skip. * Per-entry WriteFileAtomic failure now returns instead of skip. * Chown failure remains soft (logged warning, content already restored — partial-restore is recoverable; the integrity chain is unaffected). cmd/nftban-installer/restore_deps_csf_test.go - Renamed + retargeted three tests to assert hard-refusal: PR26C2_A4_TargetExists_SkipsRestore → PR26C2_A4_TargetExists_HardRefuses_StopsBeforeA5 + asserts errors.Is(err, ErrCSFRestoreCronTargetExists) + asserts NOT mock.CommandCalled("systemctl","start",csf.service) PR26C2_A4_SHA256Mismatch_SoftSkip_A5StillRuns → PR26C2_A4_SHA256Mismatch_HardRefuses_StopsBeforeA5 + asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt) + asserts A.5 NOT called PR26C2_A4_SchemaMismatch_SoftSkip_A5StillRuns → PR26C2_A4_SchemaMismatch_HardRefuses_StopsBeforeA5 + asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt) + asserts A.5 NOT called PR26C2_A4_UnknownEntryPath_Rejected → PR26C2_A4_UnknownEntryPath_HardRefuses_StopsBeforeA5 + asserts errors.Is(err, ErrCSFRestoreCronManifestCorrupt) + asserts A.5 NOT called - 3 new tests pinning the kept-behavior branches: PR26C2_A4_HappyPath_ContinuesToA5 — clean restore continues PR26C2_A4_ManifestAbsent_ContinuesToA5 — migration soft-skip continues PR26C2_A4_ParseFailure_HardRefuses_StopsBeforeA5 — parse failure stops Push criteria (all met as of this commit): - manifest absent = migration soft-skip ✓ (test #10 above) - manifest corrupt/hash mismatch = typed refusal before A.5 ✓ (tests #4, #5, #8, #11) - target cron path broad writes = impossible ✓ (allow-list + writer scope) - writer-before-reader invariant = tested ✓ (C1's roundtrip + C2's HappyPath_RestoresBothFiles) - G4-RESTORE-CRON-MANIFEST-INTEGRITY = PASS (local replay clean) - go test ./... + race + vet = PASS on lab2 Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./... (full repo) PASS - go test -race -count=1 cmd + restore + state + switchop PASS - go vet ./... clean - 11 TestCSFMutate_PR26C2_* tests all PASS (3 hard-refusal tests retargeted; 1 unchanged; 7 unchanged or new) - existing PR-25 / PR-26-code-A / PR-26-code-B tests all still pass - G4-RESTORE-CRON-MANIFEST-INTEGRITY local replay: FAIL=0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(v1.100 PR-26-code-C): drop stale WriteFileAtomic forbid from G4-RESTORE-EXEC-NO-OUT-OF-TARGET Classification: CI gate stale after authorized A.4 write became real, not a production-code defect. The G4-RESTORE-EXEC-NO-OUT-OF-TARGET gate was authored before A.4 became real (PR-25 commit 5 + tightened in PR-26-code-B). At that time, A.4 was a soft-skip with no legitimate file-write path, so a broad \bexec\.WriteFileAtomic\( forbid was correct. PR-26-code-C2 changed that: A.4 now legitimately writes to /etc/cron.d/csf-cron and /etc/cron.d/lfd-cron (and ONLY those two paths) when the §42.2 manifest is present and integrity-clean. The broad forbid is now stale and trips on legitimate code. Resolution per auditor verdict + operator decision: drop the \bexec\.WriteFileAtomic\( line from G4-RESTORE-EXEC-NO-OUT-OF-TARGET forbidden_patterns and rely on the dedicated G4-RESTORE-CRON-MANIFEST-INTEGRITY gate (added in commit 93e86e2) to authorize and constrain A.4 writes structurally: G4-RESTORE-EXEC-NO-OUT-OF-TARGET = forbid broad / unrelated mutation surfaces G4-RESTORE-CRON-MANIFEST-INTEGRITY = authorize and constrain the exact A.4 cron-restore writes (writer + reader symbol pin, cron-target literal allow-list, sha256-helper presence) Carving line-exceptions into the EXEC gate was rejected — that recreates the regex-brittleness class flagged at PR #515. Two gates with separate scopes is cleaner than one gate with carve-outs. Files changed: only .github/workflows/ci-restore-canonization.yml. - Removed pattern: '\bexec\.WriteFileAtomic\(' - Added explanatory comment block above the forbidden_patterns pointing at G4-RESTORE-CRON-MANIFEST-INTEGRITY for cron-write authorization. Kept (unchanged): - os.WriteFile / os.Create / os.Remove / os.Rename / exec.Command forbids - ServiceMask / ServiceDisable / DaemonReload forbids - raw mutating Run("systemctl", verb, …) forbids (9 verbs) - raw Run("mv", …) forbid - NftDeleteTable allow-list pin (ip:nftban / ip6:nftban only) - §46.1 line-skipping discipline - G4-RESTORE-CRON-MANIFEST-INTEGRITY gate (entirely) Local replay (exact CI workflow bash, against PR-26-code-C head): G4-RESTORE-EXEC-NO-OUT-OF-TARGET fail=0 G4-RESTORE-CRON-MANIFEST-INTEGRITY fail=0 No production code touched. Production semantics from C1 + C2 + the hard-refusal fix (f7be0c4) all unchanged. Pre-PR-26 hosts continue to soft-skip; A.4 hard-refuses on corrupt evidence; A.5 only runs when restore evidence is trusted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks
itcmsgr
added a commit
that referenced
this pull request
Apr 28, 2026
…6 lock) (#517) * feat(v1.100 PR-26-code-D): post-restore evidence record (§39.3 / §48.6 lock) PR-26-code-D — restore verification / evidence hardening, slice D. Adds the structured post-restore evidence-record writer per §39.3 + §48.6 operator lock. Recording-only — does NOT re-run PR-24 decisions, rebuild TargetAuthority, or add validator/module-health probes (operator design call). Authority: - PR #512 / contract.md Part IV §§37-50 - PR #513 / §51 lock record - PR #514 / code-A merge 4e98ff5 - PR #515 / code-B merge 45fc63e - PR #516 / code-C merge 6d8386d - §39 Q1 BLOCKING evidence rows - §39.3 evidence-record file requirement - §46 CI gate requirements - §48.6 (operator-locked at this commit's open): - path: /var/lib/nftban/state/restore-evidence/ - filename: restore-evidence-<UTC-RFC3339-basic>-<short-random>.json - schema: 1.0.0 - writer helper: writeRestoreEvidenceRecord(ctx, exec, record) - path constant: restoreEvidenceDir - §51.5-A2 (read-only typed introspection outside mutation cap) Files added (2): cmd/nftban-installer/restore_evidence.go - Constants: restoreEvidenceSchemaVersion = "1.0.0" restoreEvidenceDir = "/var/lib/nftban/state/restore-evidence" restoreEvidenceFilenamePrefix = "restore-evidence-" restoreEvidenceMode = 0o640 restoreEvidenceDirMode = 0o750 - Schema types: RestoreEvidenceRecord (schema_version, timestamp_utc, mode, phase, target, result, verification, history_gate, warnings) + the 4 nested structs. - Sentinels: ErrEvidenceWriteFailed, ErrEvidenceNilExecutor, ErrEvidenceNilRecord. - writeRestoreEvidenceRecord — the SINGLE helper. MkdirAll, marshal, WriteFileAtomic. Filename: prefix + UTC RFC3339-basic stamp + "-" + 8-hex random suffix + ".json". - buildRestoreEvidenceRecord — recording-only assembler. Sources: target.Kind/FirewallType/Panel, execRes.Terminal/Stage/VerifyResult, exec.NftTableExists for emergency + nftban tables, detect.SSHPortWithSource. No re-derivation; no Probe / Decide / DetectPanel calls. - evidenceShortRandom — crypto/rand-backed 8-hex suffix to avoid same-second filename collisions. cmd/nftban-installer/restore_evidence_test.go - 10 tests: 1. WriteRestoreEvidence_HappyPath — filename pattern + single write 2. WriteRestoreEvidence_RoundTripsJSON — schema_version + mode + phase + history_gate flags 3. WriteRestoreEvidence_NilExecutor — defensive guard 4. WriteRestoreEvidence_NilRecord — defensive guard 5. WriteRestoreEvidence_OnlyHelperWritesUnderEvidenceDir_FileScan — single-WriteFileAtomic invariant 6. WriteRestoreEvidence_NoForbiddenSurfaces_FileScan — recording-only invariant pin 7. BuildRestoreEvidenceRecord_RecordedPriorHappy — full happy path with ss-listener SSH port resolution 8. BuildRestoreEvidenceRecord_NftbanTablesPresent_Recorded — post-mutation kernel observation 9. BuildRestoreEvidenceRecord_AuthorityClassDivergenceWarning — ObservedAuthority diverging from AuthorityExternal surfaces in warnings 10. RestoreEvidenceConstants_LockPin — §48.6 path/version/prefix pinned exactly Files modified (4): internal/installer/detect/ssh.go - Added detect.SSHPortWithSource (read-only). Same 4-source priority chain as detect.SSHPort but also returns the source name (ss / sshd_config / state / config) — required by the §48.6 schema's ssh_port_source enum. Per §51.5-A2 outside the mutation cap. cmd/nftban-installer/restore_decide.go - runRestoreExecutionFromProceed gains a Step D (between Execute and Transition): 1. buildRestoreEvidenceRecord(target, execRes) 2. writeRestoreEvidenceRecord(ctx, exec, rec, log) - §48.6 downgrade rule: if evidence-write fails AFTER a successful StateRestoreExecuted, downgrade to StateRestoreDegraded (state.machine.go:152 already supports this terminal). The state model supports the downgrade; no contract amendment needed. - Operator-facing log line on Degraded now includes the evidence- write failure reason. - No state-machine / exit-code / history-gate change. main.go:132 mode-gate untouched. cmd/nftban-installer/restore_decide_test.go - TestRunRestoreExecutionFromProceed_FakeDeps_HappyPath_PersistsExecuted + 4 other dispatcher tests updated: pass executor.NewMockExecutor() instead of nil so the new evidence-write step succeeds and the terminal stays at StateRestoreExecuted (fake happy path). The 3 tests that pass nil exec via _ = runRestoreExecutionFromProceed do not assert on sf.State so they still pass under the downgrade. .github/workflows/ci-restore-canonization.yml - New gate G4-RESTORE-EVIDENCE-RECORD (§46). Structural — pins the named-constant + single-helper invariant: * restore_evidence.go declares restoreEvidenceDir, restoreEvidenceSchemaVersion, restoreEvidenceFilenamePrefix verbatim + locked values * restore_evidence.go declares writeRestoreEvidenceRecord + buildRestoreEvidenceRecord + RestoreEvidenceRecord struct * exactly ONE WriteFileAtomic call in restore_evidence.go (the single-helper invariant — locked by §48.6) * forbidden-symbol scan: restore.Decide / restore.PlanFromDecision / uninstall.Probe / detect.DetectPanel / writeHistory / update-history.json / mutation primitives / direct OS bypass (recording-only invariant) * dispatcher (restore_decide.go) calls BOTH writeRestoreEvidenceRecord AND buildRestoreEvidenceRecord (proves evidence is consumed, not just imported) - §46.1 line-skipping discipline applied (production-code-only, comment-stripped). Recording-only invariant (operator design call) honored: - No restore.Decide / restore.PlanFromDecision calls - No uninstall.Probe call - No detect.DetectPanel call (only detect.SSHPortWithSource — read-only typed introspection) - No validator full-sweep / module-health probe - No update-history.json write (§19.2 layer 4 / main.go:132 retained) - No new mutation primitive Constraints honored (per operator scope): IN: - evidence record type + schema ✓ (§48.6 lock) - evidence writer helper ✓ (single helper writeRestoreEvidenceRecord) - production write after restore execution path ✓ (dispatcher Step D) - structural CI gate G4-RESTORE-EVIDENCE-RECORD ✓ - tests proving all writes stay under restoreEvidenceDir ✓ - tests proving update-history is untouched ✓ (HistoryGate flags + no writeHistory references in evidence module) OUT: - destructive soak (PR-26-code-E) - A.4 cron changes (already shipped in code-C) - executor new mutation methods (Stat is read-only, shipped in code-C) - iptables introspection (Option B lock) - main.go history gate changes (untouched) - state/exit-code changes — only the existing StateRestoreDegraded is consumed, no new state added - repo hygiene / UX / GOTH / metrics / module cleanup Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./... PASS (full repo, 64 packages) - go test -race -count=1 cmd + restore + state + switchop + detect PASS - go vet ./... clean - go mod tidy no-op - 10 new TestWriteRestoreEvidence_* / TestBuildRestoreEvidenceRecord_* / TestRestoreEvidenceConstants_LockPin tests all PASS - existing 5 dispatcher fake-deps tests updated + still PASS - All 3 G4 gates (NO-OUT-OF-TARGET / CRON-MANIFEST-INTEGRITY / EVIDENCE-RECORD) local replay: FAIL=0 Awaiting auditor pass before push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(v1.100 PR-26-code-D): add 5 dispatcher-level evidence-failure semantics tests (auditor checkpoint) Auditor focused-audit on 849b372 flagged that PR-26-code-D's Step D introduces a real operator-visible terminal transition: StateRestoreExecuted + evidence write failure → StateRestoreDegraded The 10 unit tests already covered the writer + builder + recording invariants but did NOT pin the dispatcher-level downgrade semantics. This commit adds 5 dispatcher-level tests to close that gap. Tests added: cmd/nftban-installer/restore_decide_test.go 1. PR26D_ExecutedPlusEvidenceFail_DowngradesToDegraded fake deps return StateRestoreExecuted; writeFailExec wrapper forces evidence WriteFileAtomic to fail. Asserts: - sf.State == StateRestoreDegraded (downgrade fires) - exit code == StateRestoreDegraded.ExitCode() - sf.State != StateRestoreExecuted (no false claim) Note: sf.FailureReason stays empty by design (Transition only populates FailureReason on .IsFailed() states; Degraded is success-with-warnings). The downgrade reason surfaces via log.Result, which is the authoritative operator channel for Degraded outcomes. 2. PR26D_FailedExecutionPlusEvidenceFail_TerminalPreserved fake.mutateErr forces FailedExecution; writeFailExec forces evidence-write failure. Asserts: - sf.State == StateRestoreFailedExecution (terminal preserved) - exit == StateRestoreFailedExecution.ExitCode() Evidence failure is warning-only on non-Executed terminals. 3. PR26D_FailedVerificationPlusEvidenceFail_TerminalPreserved fake.activeRet=false forces inline-verify SafeToRemove=false → FailedVerification; writeFailExec forces evidence-write fail. Asserts terminal + exit code unchanged from FailedVerification. 4. PR26D_ExecutedPlusEvidenceOk_PreservesExecuted Plain MockExecutor (writes succeed). Asserts: - sf.State == StateRestoreExecuted (no downgrade on clean write) - exit == StateRestoreExecuted.ExitCode() - exactly one file written under restoreEvidenceDir - no writes outside restoreEvidenceDir 5. PR26D_NoUpdateHistoryWrite_FileScan File-scan against restore_decide.go. Strips line-leading // per §46.1; asserts no production-code reference to writeHistory( or update-history.json. Pins the §19.2 layer-4 invariant stays untouched after PR-26-code-D adds Step D. writeFailExec wrapper (test-only): Wraps *executor.MockExecutor and overrides only WriteFileAtomic to fail. Avoids changing the production MockExecutor; uses the same composition pattern as flakyCSFActiveExec (introduced in PR-25 4B-3-csf for analogous test purposes). Verified on lab2 (Ubuntu 24.04, go1.22.2): - go build ./... clean - go test ./cmd/nftban-installer/... PASS - 5 new TestRunRestoreExecutionFromProceed_PR26D_* / TestDispatcher_PR26D_* tests all PASS - go test -race -count=1 cmd + restore + state PASS - existing PR-25 + PR-26-code-A/B/C tests still PASS No production code change. No CI workflow change. No contract amendment needed. Restore semantics from §48.6 lock + §19.2 layer-4 invariant are both now structurally pinned by tests. Awaiting auditor sign-off + push signal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Doc-only follow-up to PR #512
This PR records the operator's lock signals for the §§39–43 proposed locks and the §48 hard blockers, post-merge of PR-26-doc (#512, squash
cd768424). It is contract authority for the locks — placed next to Part IV in the same file rather than inCHANGELOG.md(which is for shipped user-facing changes) or a separate docs page (which would split the source of truth).What this PR adds
internal/installer/restore/contract.md. Lines 1185–1266.firewallTypelocks so a fresh reader of just the history block sees the operative decisions.Locks recorded
Q1–Q5 lock signals
ServiceUnmask+RenameONLY.§48.1 lock — Option B selected for PR-26-code-A
IptablesRuleExistsin code-A.StateRestoreExecuted.detect.SSHPortlistener-source success.§48.2 lock —
firewallTypeplumbing selectedfirewallType string, NOT a precomputedtargetUnit string.priorRec/panelare likewise raw values, not precomputed derivatives).P1 acknowledgements (locked)
INV-PR26-NEW-MUTATION-SURFACES-BOUNDEDcaps mutation surfaces only, not read-only typed introspection.Code-A entry criteria + scope fence
PR-26-code-A may open ONLY when all of the following hold:
cd768424onmain). ✅firewallTypeplumbing recorded. ✅ (§51.4)PR-26-code-A is NOT authorized to do, in this slice:
ServiceUnmask/Renamework (PR-26-code-B).No production code, no CI/workflow/schema changes
This PR introduces no production code, no CI workflow change, no schema change, no CHANGELOG entry, no separate docs page. Code-A remains blocked until this PR merges.
Test plan
Architecture Policy / Policy Gates— must pass (doc-only diff)Documentation Validation / Docs Quality— must pass🤖 Generated with Claude Code