restore

package
v1.133.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 26, 2026 License: MPL-2.0 Imports: 6 Imported by: 0

Documentation

Overview

============================================================================= NFTBan v1.100 PR-24 — Authority Restoration Policy Decision Engine ============================================================================= SPDX-License-Identifier: MPL-2.0 meta:name="installer-restore-engine" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-04-20" meta:description="Pure decision engine for PR-24 restoration policy (lattice per seed §6)" meta:inventory.files="internal/installer/restore/engine.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="none" =============================================================================

PR-24 scope lock (per merged contract seed, PR #493):

Pure decision. No side effects. No process spawn. No filesystem.
No kernel. No service. No history. Implements exactly the lattice
in seed §6 with the top-down precedence rule in §5.

If a cell of the input space does not match a rule, Decide panics.
This is by design: the lattice claims to cover every input, and a
fallthrough is a contract violation that the G4-RESTORE-
DECISION-CORRECTNESS CI gate catches via exhaustive fixtures. A
silent default in this file would directly violate seed §5
("no default fallthrough branch") — losing at compile-time / CI is
strictly better than losing at runtime with the wrong answer.

=============================================================================

============================================================================= NFTBan v1.100 PR-24 — Authority Restoration Policy Engine Types (Pure) ============================================================================= SPDX-License-Identifier: MPL-2.0 meta:name="installer-restore-types" meta:type="lib" meta:owner="Antonios Voulvoulis <contact@nftban.com>" meta:created_date="2026-04-20" meta:description="Closed enums + input/result types for PR-24 decision engine" meta:inventory.files="internal/installer/restore/types.go" meta:inventory.binaries="" meta:inventory.env_vars="" meta:inventory.config_files="" meta:inventory.systemd_units="" meta:inventory.network="" meta:inventory.privileges="none" =============================================================================

PR-24 scope lock (per merged contract seed, PR #493):

This package implements the restoration POLICY DECISION ENGINE only.
It spawns no external process, mutates no kernel / service / file,
writes no history entry, and does not invoke any execution code.
Output is a closed enum of three values — PROCEED / REFUSE /
REQUIRE_EXPLICIT_INTENT — and no fourth output may be added.

Execution of any PROCEED outcome belongs to PR-25+.

=============================================================================

Index

Constants

View Source
const (
	// Group 1 — classifier hard-stops
	RuleG1AuthorityNFTBan      = "G1/AuthorityNFTBan"
	RuleG1AuthorityExternal    = "G1/AuthorityExternal"
	RuleG1AmbiguityConflictExt = "G1/AmbiguityConflictExternal"

	// Group 1 — Amendment 2 split sub-rules (§53). These live ENTIRELY
	// within Group 1; no later group ever defeats a Group 1 outcome.
	RuleG1NFTBanDefault       = "G1/AuthorityNFTBan/default"
	RuleG1NFTBanOrphanProceed = "G1/AuthorityNFTBan/OrphanProceed"
	RuleG1EvidenceMismatch    = "G1/EvidenceMismatch"

	// Group 1 — Amendment 3 split sub-rules (§63). These live ENTIRELY
	// within Group 1; no later group ever defeats a Group 1 outcome.
	// §5 precedence holds. Amendment 3 splits the existing
	// G1/AmbiguityConflictExternal row (above) into:
	//   - default: REFUSE (unchanged behavior for every flag pattern,
	//     external authority, panel, prior, or evidence row outside
	//     the candidate quintuple)
	//   - orphan-intent-candidate-csf: delegates to §64 predicate
	//   - OrphanProceed: PROCEED PanelNative/csf when §64 all-true
	//   - EvidenceMismatch: REFUSE when §64 any-false (distinct from
	//     the Amendment 2 path's RuleG1EvidenceMismatch so logs and
	//     Code-D evidence-records can attribute correctly)
	RuleG1AmbConflictExtDefault          = "G1/AmbiguityConflictExternal/default"
	RuleG1AmbConflictExtOrphanProceed    = "G1/AmbiguityConflictExternal/OrphanProceed"
	RuleG1AmbConflictExtEvidenceMismatch = "G1/AmbiguityConflictExternal/EvidenceMismatch"

	// Group 2 — input/flag validity
	RuleG2PanelAutoWithoutPanel = "G2/PanelAutoTakeoverWithoutPanel"
	RuleG2BothRestoreFlags      = "G2/RestoreAndPanelAutoBothSet"

	// Group 3 — AuthorityNone
	RuleG3_1StrongPriorNoFlag    = "G3.1/StrongPrior+NoFlag"
	RuleG3_1StrongPriorRestore   = "G3.1/StrongPrior+Restore"
	RuleG3_1StrongPriorPanelAuto = "G3.1/StrongPrior+PanelAuto"
	RuleG3_2CompleteInactive     = "G3.2/CompleteInactive"
	RuleG3_3NoRecordNoFlag       = "G3.3/NoRecord+NoFlag"
	RuleG3_3NoRecordRestore      = "G3.3/NoRecord+Restore"
	RuleG3_3NoRecordPanelAuto    = "G3.3/NoRecord+PanelAuto"
	RuleG3_3Incomplete           = "G3.3/Incomplete"
	RuleG3_3Stale                = "G3.3/Stale"

	// Group 4 — AmbiguityOrphanNFTBan
	RuleG4_1OrphanStrongNoFlag     = "G4.1/OrphanStrong+NoFlag"
	RuleG4_1OrphanStrongRestore    = "G4.1/OrphanStrong+Restore"
	RuleG4_2OrphanCompleteInactive = "G4.2/OrphanCompleteInactive"
	RuleG4_2OrphanNoRecordNoFlag   = "G4.2/OrphanNoRecord+NoFlag"
	RuleG4_2OrphanNoRecordRestore  = "G4.2/OrphanNoRecord+Restore"
	RuleG4_2OrphanIncomplete       = "G4.2/OrphanIncomplete"
	RuleG4_2OrphanStale            = "G4.2/OrphanStale"
	RuleG4_3OrphanPanelAuto        = "G4.3/OrphanPanelAuto"
)

Rule identifiers. Stable strings; fixtures and CI gates assert on these. Keep in sync with seed §6 group numbering.

View Source
const (
	StagePreflight = "preflight" // §23.1
	StageInsert    = "insert"    // §23.2
	StageMutate    = "mutate"    // §23.3
	StageVerify    = "verify"    // §23.4 (inline verification)
	StageRemove    = "remove"    // §23.5
	StageComplete  = "complete"  // §23.6 success terminal selected
)

Stage names — §23 step labels for the Stage field above.

View Source
const StalenessWindowDays = 365

StalenessWindowDays locks the freshness window at 365 days per seed §3.B and amendment history (auditor-approved). Configurability is a seed §15 follow-up item; PR-24 has no configurability knob.

Variables

View Source
var (
	// ErrExecuteNilDeps is returned when any of the four deps is nil.
	ErrExecuteNilDeps = errors.New("restore: Execute requires non-nil ExecuteDeps with all four members")

	// ErrExecuteRefusedNoneTarget is returned when Execute is called
	// with TargetAuthority{} (zero value) or TargetNone(). PR-25
	// must not run on a None target (contract §24).
	ErrExecuteRefusedNoneTarget = errors.New("restore: Execute refused — TargetAuthority kind is None (§24)")

	// ErrExecutePreflightRefused is returned when the dep's
	// preflight check returns false (logical refusal) or an error.
	// Either way, no mutation occurred.
	ErrExecutePreflightRefused = errors.New("restore: Execute refused — preflight (§23.1)")

	// ErrExecuteInsertFailed is returned when safety-net insertion
	// fails. No mutation occurred.
	ErrExecuteInsertFailed = errors.New("restore: Execute aborted — safety-net insert (§23.2)")

	// ErrExecuteMutateFailed is returned when target mutation fails.
	// Safety net is still in place; no removal.
	ErrExecuteMutateFailed = errors.New("restore: Execute mutation failed (§23.3)")

	// ErrExecuteVerifyFailed is returned when inline verification
	// either errors or returns SafeToRemove=false. Safety net
	// retained per §21.3 / §22.
	ErrExecuteVerifyFailed = errors.New("restore: Execute inline verification failed (§21.1, §23.4)")

	// ErrExecuteRemoveFailed is returned when safety-net removal
	// fails after verification passed. The system is left in a
	// state where mutation succeeded but the safety net was not
	// cleaned up — caller must surface this for operator inspection.
	ErrExecuteRemoveFailed = errors.New("restore: Execute safety-net removal failed (§23.5)")
)

Sentinel errors returned by Execute.

View Source
var (
	// ErrInlineVerifyNilDep is returned when dep is nil.
	ErrInlineVerifyNilDep = errors.New("restore: inline-verify dep is nil")

	// ErrInlineVerifyDepFailed wraps an underlying dep error from
	// any of the three §21.1 assertion calls.
	ErrInlineVerifyDepFailed = errors.New("restore: inline-verify dep call failed")

	// ErrInlineVerifyInvalidAuthority is returned when the caller
	// passes an expectedAuthority value that is not one of the
	// uninstall.CurrentAuthority constants. Defensive guard against
	// caller-side typos.
	ErrInlineVerifyInvalidAuthority = errors.New("restore: inline-verify expectedAuthority is invalid")

	// ErrInlineVerifyEmptyFirewallType is returned when targetFirewall
	// is the empty string. Per §18.2, an empty FirewallType is only
	// valid for PanelNative kind; the caller (Execute, commit 3C)
	// must resolve the panel to a concrete firewallType via
	// ResolvePanelFirewall BEFORE calling InlineVerify.
	ErrInlineVerifyEmptyFirewallType = errors.New("restore: inline-verify targetFirewall must be a known firewall type, got empty")
)

Sentinel errors returned by InlineVerify wrapped in VerifyResult.Err.

View Source
var (
	// ErrPlanNotProceed is returned when the caller passes a non-PROCEED
	// DecisionResult. PR-24 REFUSE / REQUIRE_EXPLICIT_INTENT must NOT
	// reach the planner; they short-circuit at the dispatcher.
	ErrPlanNotProceed = errors.New("restore: PlanFromDecision called with non-PROCEED decision")

	// ErrPlanMissingPriorRecord is returned when a Flags.Restore (or
	// implicit-restore-on-strong-prior) PROCEED rule reached the planner
	// but the supplied priorRec is nil. This is a caller invariant
	// violation — PR-24's strong-prior PROCEED rules require a usable
	// record on disk.
	ErrPlanMissingPriorRecord = errors.New("restore: PROCEED rule requires a prior record but none was supplied")

	// ErrPlanInvariantViolation is returned when the caller's inputs
	// disagree with PR-24's PROCEED preconditions (e.g. both flags set,
	// or panel-auto without panel). PR-24 would never have returned
	// PROCEED for these combinations, so reaching the planner with them
	// means the caller corrupted the inputs between Decide and
	// PlanFromDecision.
	ErrPlanInvariantViolation = errors.New("restore: planner inputs violate PR-24 PROCEED preconditions")
)

Sentinel errors returned by PlanFromDecision. Callers may use errors.Is() to discriminate. None of these errors carry runtime data beyond what the caller already supplied.

View Source
var (
	// ErrSafetyNetInsertFailed wraps an underlying SafetyNetDep
	// failure during InsertSafetyNet.
	ErrSafetyNetInsertFailed = errors.New("restore: safety-net insertion failed")

	// ErrSafetyNetRemoveFailed wraps an underlying SafetyNetDep
	// failure during RemoveSafetyNet.
	ErrSafetyNetRemoveFailed = errors.New("restore: safety-net removal failed")

	// ErrSafetyNetRemoveBeforeVerification is returned by
	// RemoveSafetyNet when the caller passed verifiedSafe=false.
	// Per contract §21.3: PR-25 MUST NOT remove the safety net until
	// inline verification (§21.1) passes. The primitive enforces
	// this with an explicit boolean argument so callers cannot omit
	// the gate.
	ErrSafetyNetRemoveBeforeVerification = errors.New("restore: safety-net removal refused — verification has not asserted safety (§21.3)")

	// ErrSafetyNetNilDep is returned when the caller passes a nil
	// SafetyNetDep. The primitive does not invent a default — see
	// §20.3 / §17.2 (no implicit takeover, no default firewall surface).
	ErrSafetyNetNilDep = errors.New("restore: safety-net dep is nil")
)

Sentinel errors returned by safety-net primitives.

View Source
var ErrPanelMappingInvalid = errors.New("restore: panel mapping output is not a known firewall type (§20.1 output validation)")

ErrPanelMappingInvalid is returned when the static map contains an output that is not a member of knownFirewallTypes (the §18.2 set used for RecordedPrior). This is a code-time invariant: every entry in panelToFirewall must validate. ErrPanelMappingInvalid surfaces a programmer error in the map itself, not a runtime input problem.

View Source
var ErrPanelNoneForPanelNative = errors.New("restore: PanelNone is not a valid PanelNative target")

ErrPanelNoneForPanelNative is returned by TargetPanelNative when the panel argument is detect.PanelNone (§18.2).

View Source
var ErrPanelNoneNotMappable = errors.New("restore: PanelNone has no firewall mapping (§20.1 key invariant)")

ErrPanelNoneNotMappable is returned by ResolvePanelFirewall when the caller passes detect.PanelNone. PanelNone is not a valid panel target and must never reach this resolver — the planner already refuses it (see TargetPanelNative in target_authority.go).

View Source
var ErrUnknownFirewallType = errors.New("restore: unknown firewall type")

ErrUnknownFirewallType is returned by TargetRecordedPrior when the firewallType argument is not a member of the known set (§18.2).

View Source
var ErrUnmappedPanel = errors.New("restore: panel has no PR-25 firewall mapping; refusal required (§20.2)")

ErrUnmappedPanel is returned by ResolvePanelFirewall when the supplied panel has no authoritative entry in panelToFirewall. Callers must refuse before any mutation per §20.2 — there is no fallback.

Functions

func InsertSafetyNet

func InsertSafetyNet(ctx context.Context, dep SafetyNetDep) error

InsertSafetyNet calls dep.InsertEmergencySSH. It is intentionally a thin wrapper: the only added value over a direct call is uniform error wrapping (so callers can use errors.Is(err, ErrSafetyNetInsertFailed) to discriminate).

The primitive MUST NOT do any other firewall edit. It MUST NOT touch services, files, or the broader ruleset. Verified by the file-scan test (no service/firewall manipulation strings in safety_net.go).

Ordering relative to the rest of restore execution is enforced by the caller (Execute, commit 3C). This primitive does not gate on any external state.

func RemoveSafetyNet

func RemoveSafetyNet(ctx context.Context, dep SafetyNetDep, verifiedSafe bool) error

RemoveSafetyNet calls dep.RemoveEmergencySSH but ONLY if the caller explicitly asserts verifiedSafe == true.

The verifiedSafe boolean is the §21.3 hard invariant rendered as an argument: a caller that has not run inline verification — or that ran it and got a non-pass result — must pass false, and the primitive then returns ErrSafetyNetRemoveBeforeVerification without touching the kernel.

This is deliberately strict. The primitive does NOT call inline verification itself; that coupling belongs in Execute (commit 3C), which orchestrates the §23 sequence: verify → ask, then call this primitive with the verifier's result.

On verifiedSafe=true, the primitive delegates to dep and wraps any underlying error in ErrSafetyNetRemoveFailed.

func ResolvePanelFirewall

func ResolvePanelFirewall(panel detect.PanelType) (string, error)

ResolvePanelFirewall returns the authorized native firewall for a given panel per the PR-25 §20 mapping. There is exactly one path:

  1. panel == detect.PanelNone → ErrPanelNoneNotMappable
  2. panel ∈ panelToFirewall → return mapped value (validated against §18.2)
  3. panel ∉ panelToFirewall → ErrUnmappedPanel

No fallback. No default. No heuristic. No live runtime discovery.

The resolver does not mutate, does not call any external command, does not read any file or service state. It is a pure function over the static map.

Types

type DecisionInput

type DecisionInput struct {
	// Authority is the classifier state. Taken verbatim from
	// uninstall.ClassifyResult.State; AmbiguityKind is carried in the
	// dedicated field below so the engine does not have to re-import
	// or re-derive sub-classification.
	Authority uninstall.CurrentAuthority

	// Ambiguity is the sub-classification of AuthorityAmbiguous.
	// MUST be uninstall.AmbiguityNone for any Authority !=
	// AuthorityAmbiguous. Violations are treated as a preflight
	// invariant error in the dispatcher, not a lattice output.
	Ambiguity uninstall.AmbiguityKind

	// Prior is the normalized prior-record state (see PriorState).
	Prior PriorState

	// Flags captures operator intent.
	Flags Flags

	// PanelPresent is a single boolean rather than the raw panel type.
	// The lattice treats panel context as "panel or no panel"; any
	// specific panel (DA / cPanel / Plesk / Hestia / etc.) is
	// equivalent for policy purposes for §6 Groups 1–5.
	PanelPresent bool

	// Panel is the typed panel identity. Required by Amendment 2 §54
	// evidence row E.1 (the orphan-intent PROCEED path activates only
	// for Panel == detect.PanelDirectAdmin). Outside the Amendment 2
	// G1/AuthorityNFTBan split, the lattice ignores this field —
	// PanelPresent remains the sole panel input for §6 Groups 3 and 4.
	Panel detect.PanelType

	// OrphanEvidence carries the §54.1 read-only evidence predicate
	// result, populated by the dispatcher only when the candidate
	// triple (AuthorityNFTBan + Prior=NoRecord + Panel=DirectAdmin +
	// Flags.PanelAutoTakeover + Flags.AcceptOrphanNFTBan) is present.
	// Nil for every other input pattern — the engine MUST NOT
	// dereference it outside the Amendment 2 split.
	//
	// For the Amendment 3 path (G1/AmbiguityConflictExternal split,
	// §62-§69), the same OrphanEvidence struct is reused with
	// AllTrueAmendment3() / FailedRowIDAmendment3() — they evaluate
	// the same rows EXCEPT E.12 (omitted because AmbiguityConflictExternal
	// is the §62 entry condition, incompatible with E.12 by construction).
	// E.2 is reframed semantically by the dispatcher (the row-bool now
	// represents AuthorityAmbiguous + AmbiguityConflictExternal +
	// external=csf rather than AuthorityNFTBan); the predicate
	// implementation remains shared.
	OrphanEvidence *OrphanEvidence

	// ExternalIndicator is the classifier's external-authority string
	// (e.g. "csf", "ufw", "firewalld"; "csf,ufw" for multi-external;
	// empty when classifier did not flag a conflict). Required for the
	// Amendment 3 §62 entry condition: the orphan-intent-candidate-csf
	// sub-row activates only when ExternalIndicator == "csf" (single,
	// case-sensitive). For every other lattice path, this field is
	// ignored. Multi-external strings (e.g. "csf,ufw") and empty strings
	// fall through to G1/AmbiguityConflictExternal/default REFUSE per
	// §65 (multi-external out of scope) and §67 row AMD3-13 (empty-external
	// defensive guard).
	ExternalIndicator string
}

DecisionInput is the pure-function input to Decide. Every axis is pre-reduced (no raw probe results, no executor references, no file system). This keeps the engine side-effect-free by construction.

type DecisionResult

type DecisionResult struct {
	// Output is one of OutputProceed / OutputRefuse /
	// OutputRequireExplicitIntent. No fourth value.
	Output Output

	// Rule identifies the lattice rule that matched. The string is
	// stable (contract-level); tests and CI gates assert on it. Format:
	// "G{group}.{subgroup}/{flag-or-condition}", e.g. "G1/AuthorityNFTBan",
	// "G3.3/NoRecord+Restore", "G4.3/PanelAutoOnOrphan". See engine.go
	// for the canonical list.
	Rule string

	// Reason is a short human-readable explanation of the output. Not
	// contract-stable; kept for operator-facing output and logging.
	Reason string
}

DecisionResult is the pure-function output of Decide. Contains the closed-enum output plus the matched rule identifier for logging, testing, and CI-gate coverage assertions.

func Decide

func Decide(in DecisionInput) DecisionResult

Decide evaluates the lattice (seed §6) with the top-down precedence rule (seed §5). Returns a DecisionResult whose Output is exactly one of OutputProceed / OutputRefuse / OutputRequireExplicitIntent.

Precedence (evaluated in this order; earlier match wins and is final — no later rule may override):

  1. Classifier hard-stops
  2. Input / flag validity
  3. Prior-record integrity gates
  4. Panel context gates (handled inline in 3 and 4 per seed §6 G5)
  5. Proceed decisions

Every possible input falls into exactly one rule; see tests for exhaustive coverage. A panic means a contract regression.

type ExecuteDeps

type ExecuteDeps struct {
	Preflight    PreflightDep
	SafetyNet    SafetyNetDep
	Mutation     MutationDep
	InlineVerify InlineVerifyDep
}

ExecuteDeps bundles the four dependency seams Execute needs. Constructed by the dispatcher (commit 4) with production implementations; tests in this commit construct fakes.

type ExecuteResult

type ExecuteResult struct {
	// Terminal is the §22 terminal state the caller should record.
	// One of:
	//   StateRestoreExecuted              — full success
	//   StateRestoreFailedExecution       — preflight refusal,
	//                                       insert failure, mutate
	//                                       failure
	//   StateRestoreFailedVerification    — inline verification
	//                                       failed (assertion or dep
	//                                       error); safety net retained
	// StateRestoreDegraded is NOT returned by this commit; per the
	// locked plan, it is used only if the contract defines a
	// degraded-but-executed condition, and no such condition is
	// implemented in commit 3C. Adding it requires a contract
	// amendment.
	Terminal state.InstallState

	// Stage names the §23 step that produced the terminal state.
	// Useful for logging and ordering tests. Values are constants
	// declared below (StagePreflight, StageInsert, StageMutate,
	// StageVerify, StageRemove, StageComplete).
	Stage string

	// Err is non-nil when a dep call returned an error or an input
	// invariant was violated. nil on the success path.
	Err error

	// VerifyResult is captured when verification ran (Stage ==
	// StageVerify or later). Empty otherwise.
	VerifyResult VerifyResult
}

ExecuteResult carries the outcome of one Execute call.

func Execute

Execute runs the §23 six-step ordered sequence for a frozen TargetAuthority. INV-PR25-AUTHORITY-IMMUTABILITY (§17.3): t MUST be the value the planner returned; Execute does not re-derive it and the deps must not refresh it mid-flight.

Step order (no reordering, no skipping):

  1. Preflight target validation (§23.1)
  2. Safety net insertion (§23.2)
  3. Target-specific mutation (§23.3)
  4. Inline verification (§23.4 / §21.1)
  5. Safety net removal (§23.5 / §21.3)
  6. Terminal state selection (§23.6)

Failure modes (truthful terminal mapping):

  • Kind=None / zero-value → ErrExecuteRefusedNoneTarget (Stage="" — refused before §23.1; no mutation)
  • Preflight refusal / error → StateRestoreFailedExecution (Stage=preflight; no mutation, no safety net inserted)
  • Safety-net insert failure → StateRestoreFailedExecution (Stage=insert; no mutation)
  • Target mutation failure → StateRestoreFailedExecution (Stage=mutate; safety net retained; verification skipped; remove skipped)
  • Inline verification failure → StateRestoreFailedVerification (Stage=verify; safety net retained per §21.3)
  • Safety-net removal failure → StateRestoreFailedVerification (Stage=remove; mutation succeeded but safety net could not be removed; non-success terminal so the caller never reports success)
  • All steps pass → StateRestoreExecuted (Stage=complete)

StateRestoreDegraded is NOT returned by this commit — no degraded- but-executed condition is defined in commit 3C. Adding one requires a contract amendment.

Execute does NOT call any of the live re-detection APIs (detect.DetectPanel, uninstall.Probe, uninstall.Classify, restore.Decide). It does NOT write history. It does NOT touch dispatcher state. All mutation flows through the four injected deps.

type Flags

type Flags struct {
	// Restore corresponds to --restore-prior-authority in the CLI
	// surface. Semantic: "restore the recorded prior firewall."
	Restore bool
	// PanelAutoTakeover corresponds to --panel-auto-takeover.
	// Semantic: "install the panel-native firewall." Target is the
	// panel, not the prior record — this asymmetry is the policy
	// hinge in seed §3.3 / §4.2.
	PanelAutoTakeover bool
	// AcceptOrphanNFTBan corresponds to --accept-orphan-nftban
	// (Amendment 2, locked 2026-04-28). Semantic: "explicit operator
	// intent to restore CSF on a DirectAdmin host where NFTBan is the
	// current authority and no prior-authority record exists." Combined
	// with PanelAutoTakeover + DirectAdmin + strong CSF-disabled
	// evidence, this activates the §53 G1 split orphan-intent path.
	// Standalone (without PanelAutoTakeover, or under any other
	// classifier) the flag has no effect — REFUSE remains.
	//
	// MUST be supplied via CLI argv only. No env-var, no config-file,
	// no implicit default — see Amendment 2 §55.
	AcceptOrphanNFTBan bool
}

Flags captures the two operator-intent flags the engine considers. Exactly one of Restore / PanelAutoTakeover may be set at lattice evaluation time; the "both set" case is an input-validity REFUSE and is caught by Group 2 of the lattice (never reaches the proceed decisions).

type InlineVerifyDep

type InlineVerifyDep interface {
	// IsTargetFirewallActive reports whether the panel-auto-mapped or
	// recorded-prior firewallType is currently active on the host.
	// firewallType is one of the §18.2 known values
	// ({ufw, firewalld, iptables, csf}); callers MUST NOT pass any
	// other value. Returns (active, err).
	//
	// Active means: the firewall's runtime presence is observable
	// (e.g. service running, kernel rules loaded). The exact
	// observability mechanism is the production implementation's
	// concern; it is NOT defined here.
	IsTargetFirewallActive(ctx context.Context, firewallType string) (bool, error)

	// CurrentAuthorityClass returns the host's current authority
	// class as classified after the PR-25 mutation has completed.
	// Returns the same uninstall.CurrentAuthority enum that PR-22 /
	// PR-23 / PR-24 use, so callers can compare the post-mutation
	// classification against an expected value.
	//
	// This MUST reflect a fresh classification, but the *call* is
	// allowed to be expensive — production implementations may
	// shell out. It is NOT a hot-path primitive.
	CurrentAuthorityClass(ctx context.Context) (uninstall.CurrentAuthority, error)

	// IsSafetyNetRemovalSafe reports whether removing the
	// emergency-SSH safety net at this moment is safe. Production
	// implementations must check, at minimum, that:
	//
	//   - SSH connectivity remains observable on the post-mutation
	//     ruleset, OR
	//   - an equivalent operator-recovery path exists that does not
	//     depend on the safety-net rule.
	//
	// The exact predicate is the production implementation's concern.
	// This primitive only carries the boolean result.
	IsSafetyNetRemovalSafe(ctx context.Context) (bool, error)
}

InlineVerifyDep is the dependency-injection seam for inline verification queries. Each method answers ONE of the three minimum-sufficient §21.1 questions and nothing more. Production implementations are NOT in this commit (3B is primitives only); production wiring is a commit-3C concern, tests in this commit use a fake.

Crucially: this interface is NOT the full nftban validator surface. It is a narrow query interface tailored to §21.1's three assertions. Adding methods here for unrelated module health is forbidden — that belongs to the PR-26 verification gate, not PR-25.

type MutationDep

type MutationDep interface {
	// MutateToTarget performs the kernel + run-state mutation
	// required to make firewallType the authoritative firewall.
	// Returns nil on success, non-nil error on any failure.
	// Implementations MUST leave the safety net (inserted by Execute
	// step 2 prior to this call) in place even on failure — Execute
	// chooses the terminal state and decides safety-net handling.
	MutateToTarget(ctx context.Context, firewallType string) error
}

MutationDep performs the §23.3 minimal target-specific kernel + service-run-state changes. The interface is intentionally narrow — the operation is "switch authority to this firewall and start its service run-state". No unit-file edits, no enable/disable, no filesystem cleanup.

The dep is responsible for translating the firewallType string into the appropriate native operations. Execute does not unpack the mapping further.

type OrphanEvidence

type OrphanEvidence struct {
	E1PanelDirectAdmin    bool // detect.DetectPanel == PanelDirectAdmin
	E2AuthorityNFTBan     bool // uninstall.Classify == AuthorityNFTBan
	E3PriorNoRecord       bool // uninstall.Probe == PriorNoRecord
	E4PanelAutoTakeover   bool // --panel-auto-takeover present
	E5AcceptOrphanNFTBan  bool // --accept-orphan-nftban present (CLI argv only)
	E6CSFServiceDisabled  bool // csf.service exists AND not active AND is-enabled in {masked, disabled}
	E7CSFDisabledExists   bool // /usr/sbin/csf.disabled exists
	E8CSFAbsent           bool // /usr/sbin/csf does NOT exist
	E9NftIPNftbanPresent  bool // nft table ip nftban present
	E10NftIP6NftbanPres   bool // nft table ip6 nftban present
	E11NftbandActive      bool // nftband.service active
	E12NoConflictExternal bool // classifier did not return AmbiguityConflictExternal
	E13NoAmbiguous        bool // classifier did not return AuthorityAmbiguous
}

OrphanEvidence is the §54.1 evidence predicate result. All 13 rows are read-only observations of the live host state. The engine consumes a pre-evaluated struct; the dispatcher (`gatherOrphanEvidence`) does the live reads.

`AllTrue()` returns true iff every row is satisfied; `FailedRowID()` returns the first failing row's stable ID (e.g. "AMD2-E.6") for structured logging. The struct is value-type so a nil pointer at DecisionInput is unambiguous ("evidence not gathered").

func (*OrphanEvidence) AllTrue

func (e *OrphanEvidence) AllTrue() bool

AllTrue reports whether every §54.1 row is satisfied. Read failures in the dispatcher MUST set the corresponding row to false — never REQUIRE_EXPLICIT_INTENT — per Amendment 2 §54.2.

func (*OrphanEvidence) AllTrueAmendment3

func (e *OrphanEvidence) AllTrueAmendment3() bool

AllTrueAmendment3 reports whether every §54/§64 combined-predicate row is satisfied for the Amendment 3 G1/AmbiguityConflictExternal orphan-intent-candidate-csf path (§64.1). Identical to AllTrue() EXCEPT row E.12 (NoConflictExternal) is omitted: the §62 entry condition IS AmbiguityConflictExternal, so requiring "no AmbiguityConflictExternal" is incompatible by construction.

E.2's row-bool is reframed by the dispatcher (see DecisionInput. OrphanEvidence doc): in the Amendment 3 path the bool represents AuthorityAmbiguous + AmbiguityConflictExternal + ExternalIndicator == "csf" rather than AuthorityNFTBan. The predicate implementation is unchanged.

Per Amendment 3 §66.2 regression requirement, AllTrue() (Amendment 2) and AllTrueAmendment3() (Amendment 3) produce identical results when E.12 is true, and only diverge when E.12 is false.

func (*OrphanEvidence) FailedRowID

func (e *OrphanEvidence) FailedRowID() string

FailedRowID returns the stable ID of the first false row (e.g. "AMD2-E.6"), or empty string if all rows are true. Nil receiver returns "AMD2-E.0" so structured logs distinguish "evidence not gathered" from "every row evaluated false".

func (*OrphanEvidence) FailedRowIDAmendment3

func (e *OrphanEvidence) FailedRowIDAmendment3() string

FailedRowIDAmendment3 returns the stable ID of the first false row in the Amendment 3 evaluation order (§64.2). Skips E.12 per AllTrueAmendment3 semantics. Returns "AMD3-E.0" for nil receiver to distinguish "evidence not gathered" from "every row evaluated false". Returns "AMD3-E.{N}" rather than "AMD2-E.{N}" so structured logs and Code-D evidence-record consumers can distinguish which predicate fired.

type Output

type Output string

Output is the closed enum of the three (and only three) outputs of the PR-24 decision engine. Per seed §4 (merged), no fourth value exists, and adding one is a contract violation caught by the G4-RESTORE-DECISION-CORRECTNESS CI gate.

const (
	// OutputProceed — policy permits restoration. PR-25+ may execute.
	OutputProceed Output = "PROCEED"
	// OutputRefuse — policy forbids restoration. No execution permitted.
	OutputRefuse Output = "REFUSE"
	// OutputRequireExplicitIntent — policy cannot decide. Operator must
	// supply additional intent (e.g. switch the flag being used, or
	// accept that there is no valid restoration target).
	OutputRequireExplicitIntent Output = "REQUIRE_EXPLICIT_INTENT"
)

type PreflightDep

type PreflightDep interface {
	// PreflightTarget reports whether the resolved firewallType is
	// still a valid execution target right now. Returns nil error +
	// true on go. Returns nil error + false on logical refusal.
	// Returns non-nil error on dep failure (treated as a hard
	// refusal: §23.1 refusal must be non-mutating).
	PreflightTarget(ctx context.Context, firewallType string) (bool, error)
}

PreflightDep answers the §23.1 pre-mutation question:

"Is the resolved TargetAuthority still valid right now?"

Implementations check, at minimum, that the authorized target's runtime preconditions hold (e.g. the panel-mapped firewall package is still installed; the recorded-prior firewall service unit still exists). Production implementations are NOT in this commit.

The interface deliberately does NOT take a TargetAuthority by value to discourage re-derivation — it carries only the resolved firewallType the planner produced. Callers that need authority Kind for branching read it from Execute's TargetAuthority argument directly.

type PriorState

type PriorState string

PriorState is the normalized prior-authority-record state consumed by the decision engine. It is derived from uninstall.ProbeResult + freshness window check in the dispatcher layer; the engine itself takes the reduced state only.

Mapping from uninstall.PriorRecordState (see dispatcher):

PriorNoRecord             → PriorStateNoRecord
PriorRecordMalformed      → preflight error (NOT a lattice input)
PriorRecordIncomplete     → PriorStateIncomplete
PriorRecordUsableActive   + recorded_at ≤ 365d → PriorStateCompleteActive
PriorRecordUsableActive   + recorded_at > 365d → PriorStateStale
PriorRecordUsableInactive + recorded_at ≤ 365d → PriorStateCompleteInactive
PriorRecordUsableInactive + recorded_at > 365d → PriorStateStale

Legacy records that lack the ActiveAtInstall field are classified by uninstall.Probe as PriorRecordIncomplete (see uninstall/prior.go PR-P2-1 hardening). They therefore flow through PriorStateIncomplete here → REQUIRE_EXPLICIT_INTENT, per seed §3.B.

const (
	PriorStateNoRecord         PriorState = "no_record"
	PriorStateCompleteActive   PriorState = "complete_active"
	PriorStateCompleteInactive PriorState = "complete_inactive"
	PriorStateIncomplete       PriorState = "incomplete"
	PriorStateStale            PriorState = "stale"
)

type SafetyNetDep

type SafetyNetDep interface {
	// InsertEmergencySSH inserts the single emergency-SSH allow rule.
	// It MUST NOT perform any other firewall edit. Returns an error
	// describing the failure on any non-success outcome.
	InsertEmergencySSH(ctx context.Context) error

	// RemoveEmergencySSH removes the previously-inserted rule.
	// It MUST NOT perform any other firewall edit. It MUST NOT
	// re-flush, rebuild, or otherwise touch unrelated rules. Returns
	// an error describing the failure on any non-success outcome.
	RemoveEmergencySSH(ctx context.Context) error
}

SafetyNetDep is the dependency-injection seam for safety-net kernel operations. Every mutation-capable call goes through this interface so production code uses a real implementation and tests use a fake.

The interface is intentionally narrow:

  • InsertEmergencySSH adds the single emergency-SSH allow rule into the kernel ruleset. No broader firewall edits, no service manipulation, no file writes.
  • RemoveEmergencySSH removes the same rule. The caller is responsible for not calling this until inline verification said it is safe; the package-level RemoveSafetyNet primitive enforces that with an explicit verifiedSafe argument.

Implementations of this interface are NOT in this package and NOT in this commit (3B is primitives only). Production wiring is a commit-3C concern; tests in this commit use a fake.

type TargetAuthority

type TargetAuthority struct {
	// contains filtered or unexported fields
}

TargetAuthority is the authorized execution target for PR-25.

Per contract §18: struct with unexported fields; construction is constrained to one of three paths (TargetNone, TargetRecordedPrior, TargetPanelNative).

Zero value is equivalent to TargetNone() (contract §18.2). This is intentional. PR-25 execution must reject a None target before any mutation; the type alone does not enforce that — the Execute path does (contract §24, enforced in commit 3).

INV-PR25-AUTHORITY-IMMUTABILITY (contract §17.3): once constructed, a TargetAuthority value is read-only. There are no exported mutators.

func PlanFromDecision

func PlanFromDecision(
	decision DecisionResult,
	input DecisionInput,
	priorRec *uninstall.PriorRecord,
	panel detect.PanelType,
) (TargetAuthority, error)

PlanFromDecision resolves the PR-25 TargetAuthority exactly once from the PR-24-approved inputs.

Per contract §24, the planner consumes:

  • decision — must be PROCEED. Anything else returns ErrPlanNotProceed.
  • input — the same restore.DecisionInput that PR-24's Decide() evaluated. The planner reads the lattice axes (Flags, Authority, Prior, PanelPresent) verbatim. The planner does NOT re-run Decide and does NOT re-detect.
  • priorRec — the parsed prior-authority record from uninstall.Probe. Required for RecordedPrior PROCEED rules (G3.1+NoFlag/Restore, G4.1+NoFlag/Restore). Ignored for PanelNative rules (Q4 §20.3 forbids consulting it).
  • panel — the panel type from detect.DetectPanel. Required for PanelNative rules (Q4 §20). Ignored for RecordedPrior.

Mapping (strict):

input.Flags.PanelAutoTakeover == true   → TargetPanelNative(panel)
input.Flags.PanelAutoTakeover == false  → TargetRecordedPrior(priorRec.FirewallType)

All PROCEED rules in PR-24's engine.go fall into one of these two classes. PR-24 already rejected:

  • Both flags set (REFUSE / G2/RestoreAndPanelAutoBothSet)
  • panel-auto without panel (REFUSE / G2/PanelAutoTakeoverWithoutPanel)
  • --restore on NoRecord (REQUIRE_EXPLICIT_INTENT / G3.3)
  • --restore on Incomplete / Stale (REQUIRE_EXPLICIT_INTENT)

so they cannot legitimately reach the planner. The planner still asserts these defensively and returns ErrPlanInvariantViolation if a caller corrupted the input chain.

Side-effect contract:

  • No filesystem read, no kernel probe, no service inspection, no live re-classification, no fresh detect.Panel call, no fresh prior-record read.
  • Only operates on the values already passed in.

Return contract:

  • Returns a constructed TargetAuthority of kind RecordedPrior or PanelNative. Kind=None is unreachable from PlanFromDecision — enforced by exhaustive flag handling + invariant checks.
  • Returns errors, never panics.

Once this function returns nil error, the returned TargetAuthority is frozen for the entire PR-25 execution window (INV-PR25-AUTHORITY-IMMUTABILITY, contract §17.3).

func TargetNone

func TargetNone() TargetAuthority

TargetNone returns the None-kind target. Equivalent to the zero value.

func TargetPanelNative

func TargetPanelNative(panel detect.PanelType) (TargetAuthority, error)

TargetPanelNative constructs a TargetAuthority of kind PanelNative. panel must not be detect.PanelNone or ErrPanelNoneForPanelNative is returned.

Per code-phase guardrail 3, this public constructor returns an error rather than panicking on invalid input.

func TargetRecordedPrior

func TargetRecordedPrior(firewallType string) (TargetAuthority, error)

TargetRecordedPrior constructs a TargetAuthority of kind RecordedPrior. The firewallType argument must be a member of the known set (§18.2) or ErrUnknownFirewallType is returned.

Per code-phase guardrail 3, this public constructor returns an error rather than panicking on invalid input.

func (TargetAuthority) FirewallType

func (t TargetAuthority) FirewallType() string

FirewallType returns the firewall-type string. Empty for None and PanelNative per the §18.3 payload invariants.

func (TargetAuthority) Kind

Kind returns the authority kind. Zero-value TargetAuthority returns TargetAuthorityKindNone.

func (TargetAuthority) Panel

func (t TargetAuthority) Panel() detect.PanelType

Panel returns the panel type. PanelNone for None and RecordedPrior per the §18.3 payload invariants.

type TargetAuthorityKind

type TargetAuthorityKind string

TargetAuthorityKind is the closed enum of authority kinds PR-25 can act on.

Per contract §18.4: adding a new variant requires a §12-style review.

const (
	// TargetAuthorityKindNone is the zero value. PR-25 must not execute on a
	// None target; the dispatcher refuses before any mutation (contract §24).
	TargetAuthorityKindNone TargetAuthorityKind = ""

	// TargetAuthorityKindRecordedPrior names a target whose firewall identity
	// came from a recorded prior-record on disk and was already validated and
	// approved by PR-24's PROCEED path.
	TargetAuthorityKindRecordedPrior TargetAuthorityKind = "recorded_prior"

	// TargetAuthorityKindPanelNative names a target whose firewall identity
	// is determined statically by panel detection (PR-25 §20 mapping).
	TargetAuthorityKindPanelNative TargetAuthorityKind = "panel_native"
)

type VerifyResult

type VerifyResult struct {
	TargetFirewallActive  bool
	AuthorityClassCorrect bool
	SafetyNetRemovalSafe  bool

	// SafeToRemove is true ONLY when all three assertions are true
	// AND no dep error occurred. It is the §21.3 gate value the
	// caller passes to RemoveSafetyNet.
	SafeToRemove bool

	// ObservedAuthority is the value returned by
	// dep.CurrentAuthorityClass — included in the result for caller
	// logging and terminal-state selection (commit 3C). Empty on
	// dep-error path.
	ObservedAuthority uninstall.CurrentAuthority

	// Err is non-nil only when a dep call returned an error.
	// Verification logical-FAIL (one or more assertions returning
	// false) does NOT produce an Err — it produces SafeToRemove=false.
	Err error
}

VerifyResult captures the outcome of one inline-verification call.

Three semantic outcomes:

  • All three assertions PASS → safe-to-remove, all observed flags true, Err == nil.
  • One or more assertions FAIL deterministically → safe-to-remove == false, the corresponding observed flag is false, Err == nil. Per §21.3 the caller MUST NOT remove the safety net on this path.
  • Underlying dep error → all observed flags are zero values, safe-to-remove == false, Err != nil. Caller treats this as a verification HARD-FAIL (per §22 "FailedVerification" semantics in commit 3C).

func InlineVerify

func InlineVerify(
	ctx context.Context,
	dep InlineVerifyDep,
	targetFirewall string,
	expectedAuthority uninstall.CurrentAuthority,
) VerifyResult

InlineVerify performs the §21.1 minimum-sufficient three-assertion check. It is the ONLY public entry point in this file.

Inputs:

  • ctx — cancellation context.
  • dep — verification dep, must be non-nil.
  • targetFirewall — the firewall the operator authorized (RecordedPrior.FirewallType OR the ResolvePanelFirewall(panel) output for PanelNative). Must be a §18.2 known firewall type; empty string is rejected.
  • expectedAuthority — the post-mutation authority class the caller expects. For RecordedPrior / PanelNative restoration, this is uninstall.AuthorityExternal (the panel or recorded firewall is now authoritative). Empty / unknown values are rejected.

Outputs (carried in VerifyResult):

  • TargetFirewallActive — assertion 1
  • AuthorityClassCorrect — assertion 2
  • SafetyNetRemovalSafe — assertion 3
  • SafeToRemove — true iff all three assertions are true AND no dep error occurred
  • ObservedAuthority — the actual class returned by the dep
  • Err — non-nil on dep failure / invalid input

The function does NOT call any other restore-package mutation primitive. It does NOT remove the safety net. It does NOT decide a terminal state. It does NOT interpret SafeToRemove for the caller. All of that belongs to Execute (commit 3C).

The function does NOT implement the PR-26 verification gate — see §21.4.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL