Conversation
…+ 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>
… 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>
…tural 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>
…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>
Contributor
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
…STORE-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>
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.
PR-26-code-C: cron backup manifest + A.4 manifest restore
This PR is split internally into 4 reviewable commits on the same branch:
96162f69switchop.disarmCSFArtifacts+ new sharedcron_manifest.go(sha256-pinned manifest types,ComputeCronBackupSHA256,WriteCronBackupManifest,ReadCronBackupManifest,VerifyCronBackupEntry) + read-only typedexecutor.Stat(per §51.5-A2 outside the mutation cap).c5767f45restore_deps_csf.go::mutateToCSFTargetstep 3. Two new typed sentinels:ErrCSFRestoreCronManifestCorrupt,ErrCSFRestoreCronTargetExists.93e86e25G4-RESTORE-CRON-MANIFEST-INTEGRITY— pins writer + reader symbols, allow-list cron-target literals, §46.1 line-skipping discipline.f7be0c49Authority
contract.mdPart IV §§37–504e98ff5645fc63efEvidence model
ErrCSFRestoreCronManifestCorruptErrCSFRestoreCronManifestCorruptErrCSFRestoreCronManifestCorruptErrCSFRestoreCronManifestCorruptErrCSFRestoreCronManifestCorruptErrCSFRestoreCronTargetExistsRationale: absence = migration tolerance for pre-PR-26 hosts (graceful soft-skip with operator warning, A.5 continues). Corruption = untrusted evidence (HARD refuse before A.5 — the existing §32 step-3 failure path retains the safety net; operator must inspect). This alignment was applied per the focused-audit verdict on
f7be0c49.Manifest schema (locked at §42.2)
/var/lib/nftban/state/csf-cron-backup/csf-cron,lfd-cron(the sha256-pinned content copies)manifest.json—schema_version+captured_at+ per-entry{path, backup_name, sha256, mode, uid, gid, size}1.0.0(any drift is rejected withErrCronManifestSchemaMismatch)/etc/cron.d/csf-cronAND/etc/cron.d/lfd-cronONLY. Unknown-entry paths refused.Pre-PR-26 hosts without manifest
By design: graceful soft-skip with operator warning. A.5 continues so csf can still start. The migration is purely additive — pre-PR-26 hosts do not require manifest creation.
Out of scope (not touched)
IptablesRuleExists/ iptables introspection (Option B lock)main.go/ state machine / exit codes / history gate (INV-PR25-HISTORY-GATE retained)TargetAuthority/ PR-24 latticecontract.md(no amendment needed; §42 lock satisfied as-is)Files changed (8)
internal/installer/executor/executor.goFileMeta+Stat(read-only)internal/installer/executor/real.goRealExecutor.Statvia os.Stat + syscall.Stat_tinternal/installer/executor/mock.goMockExecutor.Stat+FileStatsmapinternal/installer/switchop/cron_manifest.gointernal/installer/switchop/cron_manifest_test.gointernal/installer/switchop/takeover.godisarmCSFArtifactswrites manifest before rmcmd/nftban-installer/restore_deps_csf.gocmd/nftban-installer/restore_deps_csf_test.goseedCronManifesthelper.github/workflows/ci-restore-canonization.ymlG4-RESTORE-CRON-MANIFEST-INTEGRITYstructural gateLab2 verification (head
f7be0c49)go build ./...cleango test ./...PASS (full repo, 64 packages)go test -race -count=1cmd + restore + state + switchop PASSgo vet ./...cleango mod tidyno-opG4-RESTORE-CRON-MANIFEST-INTEGRITYlocal gate replay:FAIL=0Test plan
Restore Canonization Gatematrix (ubuntu-24.04 + almalinux-9 + summary) greenG4-RESTORE-EXEC-NO-OUT-OF-TARGETgreen (no new mutation symbols outside the bounded-3 cap)G4-RESTORE-CRON-MANIFEST-INTEGRITY(NEW) greenG4-RESTORE-NO-IMPLICIT-EXECgreenArchitecture Policy / Policy GatesgreenGo Build & Test+ race + full DEB+RPM matrix + CodeQL / Semgrep / Secure Go / OSV / Gitleaks / GitGuardian greenShellCheck/Bash Validation/Docs QualitygreenAfter CI completes, audit the gate results before merge.
🤖 Generated with Claude Code