Skip to content

Commit b81fe3c

Browse files
itcmsgrclaude
andauthored
feat(v1.100 Amendment 3): code-A — G1/AmbiguityConflictExternal orphan-CSF split (#523)
Implements the §63 lattice extension + §64 evidence predicate from the Amendment 3 doc seed (#522). Decision-layer only. Lattice changes (engine.go): - 3 new rule constants: G1/AmbiguityConflictExternal/default → REFUSE (preserves pre-Amendment-3 hard-stop) G1/AmbiguityConflictExternal/OrphanProceed → PROCEED PanelNative/csf G1/AmbiguityConflictExternal/EvidenceMismatch → REFUSE (predicate any-false) - decideAmbiguityConflictExternal() function mirrors decideAuthorityNFTBan() in shape: Group 2 precedence preserved → quintuple-check (csf + DA + NoRecord + --panel-auto-takeover + --accept-orphan-nftban) → §64 predicate delegation. - Decide() now calls decideAmbiguityConflictExternal(in) for the AuthorityAmbiguous + AmbiguityConflictExternal branch (replaces the inline REFUSE). Evidence predicate (types.go): - New ExternalIndicator string field on DecisionInput (carries the classifier's external-authority string; required for §62 entry condition). - New AllTrueAmendment3() helper on OrphanEvidence: identical to AllTrue() EXCEPT row E.12 (NoConflictExternal) is omitted — the §62 entry IS AmbiguityConflictExternal so requiring "no conflict external" is incompatible by construction. - New FailedRowIDAmendment3() helper returns AMD3-E.{N} stable IDs so structured logs and Code-D evidence-records distinguish which predicate fired. Test matrix (engine_amendment3_test.go — new file): - 15-row §67 matrix (AMD3-1 through AMD3-15) including the 3 auditor- required defensive-guard rows: AMD3-13 empty external, AMD3-14 rule- label assertion ("amendment-3 orphan-intent" reason substring), AMD3-15 multi-external "csf,ufw". - TestAmd3_RuleConstants pins canonical rule strings. - TestAmd3_AllTrueAmendment3_OmitsE12 verifies E.12 is excluded. - TestAmd3_FailedRowIDAmendment3_SkipsE12 verifies row-walk skips E.12. - TestAmd3_NilEvidence_AMD3E0 verifies nil-receiver sentinel. Regression updates (engine_test.go, engine_amendment2_test.go): - 2 pre-existing fixtures asserting the old "G1/AmbiguityConflictExternal" rule string updated to RuleG1AmbConflictExtDefault. Behavior unchanged (still REFUSE); only the rule sub-classifier name shifts. Same pattern as Amendment 2's AuthorityNFTBan/default rename. - declaredRules() in engine_test.go updated to include the 3 new sub-rule constants (RuleG1AmbiguityConflictExt umbrella retained for grep parity). - 2 sentinel fixtures added to allFixtures pinning the new OrphanProceed and EvidenceMismatch rules for coverage assertion (full §67 matrix lives in engine_amendment3_test.go). NOT touched (in scope per operator): - internal/installer/uninstall/* (classifier — semantic unchanged) - internal/installer/restore/execute.go (mutation surface unchanged) - internal/installer/state/* (state machine unchanged) - cmd/nftban-installer/main.go (history gate unchanged) - cmd/nftban-installer/flags.go (flag surface unchanged) - .github/workflows/* (CI unchanged) - §32 11-step ordering (unchanged) - Amendment 2's §54 predicate (untouched; AllTrue() preserved for the AuthorityNFTBan path; AllTrueAmendment3() is a new sibling) Test results (lab4): - go test ./internal/installer/restore/... → ok - go test ./cmd/nftban-installer/... → ok - go test ./... → 64 packages ok, 0 FAIL No host action. No binary rebuild. No nftban-installer invocation. dns2 stays in canonical post-Gate-A state. Closes part of PR-26 final closure path: Gate B retry on dns2 unblocks once this PR merges + fresh Tier 1 binary is built + reachability monitor activates + pre-execution Gate B retry audit returns GO. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8869392 commit b81fe3c

5 files changed

Lines changed: 749 additions & 9 deletions

File tree

internal/installer/restore/engine.go

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ const (
5252
RuleG1NFTBanOrphanProceed = "G1/AuthorityNFTBan/OrphanProceed"
5353
RuleG1EvidenceMismatch = "G1/EvidenceMismatch"
5454

55+
// Group 1 — Amendment 3 split sub-rules (§63). These live ENTIRELY
56+
// within Group 1; no later group ever defeats a Group 1 outcome.
57+
// §5 precedence holds. Amendment 3 splits the existing
58+
// G1/AmbiguityConflictExternal row (above) into:
59+
// - default: REFUSE (unchanged behavior for every flag pattern,
60+
// external authority, panel, prior, or evidence row outside
61+
// the candidate quintuple)
62+
// - orphan-intent-candidate-csf: delegates to §64 predicate
63+
// - OrphanProceed: PROCEED PanelNative/csf when §64 all-true
64+
// - EvidenceMismatch: REFUSE when §64 any-false (distinct from
65+
// the Amendment 2 path's RuleG1EvidenceMismatch so logs and
66+
// Code-D evidence-records can attribute correctly)
67+
RuleG1AmbConflictExtDefault = "G1/AmbiguityConflictExternal/default"
68+
RuleG1AmbConflictExtOrphanProceed = "G1/AmbiguityConflictExternal/OrphanProceed"
69+
RuleG1AmbConflictExtEvidenceMismatch = "G1/AmbiguityConflictExternal/EvidenceMismatch"
70+
5571
// Group 2 — input/flag validity
5672
RuleG2PanelAutoWithoutPanel = "G2/PanelAutoTakeoverWithoutPanel"
5773
RuleG2BothRestoreFlags = "G2/RestoreAndPanelAutoBothSet"
@@ -111,11 +127,7 @@ func Decide(in DecisionInput) DecisionResult {
111127
}
112128
case uninstall.AuthorityAmbiguous:
113129
if in.Ambiguity == uninstall.AmbiguityConflictExternal {
114-
return DecisionResult{
115-
Output: OutputRefuse,
116-
Rule: RuleG1AmbiguityConflictExt,
117-
Reason: "conflicting external authority detected; operator must resolve before any restoration",
118-
}
130+
return decideAmbiguityConflictExternal(in)
119131
}
120132
// Fall through to Group 4 (AmbiguityOrphanNFTBan path). Any
121133
// other Ambiguity sub-kind is a preflight invariant violation
@@ -414,3 +426,99 @@ func decideAuthorityNFTBan(in DecisionInput) DecisionResult {
414426
Reason: "amendment-2 orphan-intent: AuthorityNFTBan + DirectAdmin + strong CSF-disabled evidence; restoring CSF per §53.1",
415427
}
416428
}
429+
430+
// decideAmbiguityConflictExternal implements Amendment 3 §63 Group 1
431+
// split for Authority=AuthorityAmbiguous + Ambiguity=AmbiguityConflictExternal.
432+
// The split is ENTIRELY within Group 1; no later group ever defeats a
433+
// Group 1 outcome. §5 precedence holds.
434+
//
435+
// Evaluation order (§63.2):
436+
//
437+
// 1. Pre-condition quintuple check — only the specific combination
438+
// `ExternalIndicator="csf" + Prior=NoRecord + Panel=DirectAdmin +
439+
// PanelAutoTakeover + AcceptOrphanNFTBan` (and Group 2 not
440+
// interfering) is the "orphan-intent-candidate-csf" sub-row.
441+
// Any other combination → REFUSE with
442+
// `G1/AmbiguityConflictExternal/default` (preserves the original
443+
// pre-Amendment-3 hard-stop semantics).
444+
//
445+
// 2. §64 evidence predicate evaluation — if AllTrueAmendment3() (every
446+
// row except E.12, which is omitted because the §62 entry condition
447+
// IS AmbiguityConflictExternal), PROCEED with
448+
// `G1/AmbiguityConflictExternal/OrphanProceed`. If any row is false,
449+
// REFUSE with `G1/AmbiguityConflictExternal/EvidenceMismatch`
450+
// (NOT REQUIRE_EXPLICIT_INTENT — same semantic as Amendment 2 §54.2).
451+
//
452+
// Group 2 precedence preserved: `--restore-prior-authority +
453+
// --panel-auto-takeover` (both set) emits `G2/RestoreAndPanelAutoBothSet`;
454+
// `--panel-auto-takeover` with `PanelPresent=false` emits
455+
// `G2/PanelAutoTakeoverWithoutPanel`. Both Group 2 rules win over
456+
// the Amendment 3 split because they're checked here before the
457+
// candidate-quintuple delegation, in that exact precedence order.
458+
//
459+
// Multi-external states (e.g. ExternalIndicator="csf,ufw") and empty
460+
// strings fall to the default REFUSE per §65 + §67 row AMD3-13/AMD3-15
461+
// defensive guards. Only single, exact-match "csf" qualifies.
462+
func decideAmbiguityConflictExternal(in DecisionInput) DecisionResult {
463+
// Group 2 precedence: both flags set is an input-validity REFUSE.
464+
if in.Flags.Restore && in.Flags.PanelAutoTakeover {
465+
return DecisionResult{
466+
Output: OutputRefuse,
467+
Rule: RuleG2BothRestoreFlags,
468+
Reason: "--restore-prior-authority and --panel-auto-takeover are mutually exclusive; pick one",
469+
}
470+
}
471+
472+
// Group 2 precedence: panel-auto without a panel is an input-
473+
// validity REFUSE.
474+
if in.Flags.PanelAutoTakeover && !in.PanelPresent {
475+
return DecisionResult{
476+
Output: OutputRefuse,
477+
Rule: RuleG2PanelAutoWithoutPanel,
478+
Reason: "--panel-auto-takeover requires a control panel on the host; none detected",
479+
}
480+
}
481+
482+
// Candidate-quintuple check for the Amendment 3 orphan-intent-
483+
// candidate-csf path. Every condition must hold, else fall through
484+
// to the default REFUSE. The quintuple is composite: classifier
485+
// already established AuthorityAmbiguous + AmbiguityConflictExternal
486+
// (caller); we additionally require ExternalIndicator=="csf"
487+
// (single-external, exact-match), Prior=NoRecord, Panel=DirectAdmin,
488+
// --panel-auto-takeover, and --accept-orphan-nftban.
489+
quintuplePresent := in.ExternalIndicator == "csf" &&
490+
in.Prior == PriorStateNoRecord &&
491+
in.Panel == detect.PanelDirectAdmin &&
492+
in.Flags.PanelAutoTakeover &&
493+
in.Flags.AcceptOrphanNFTBan
494+
495+
if !quintuplePresent {
496+
return DecisionResult{
497+
Output: OutputRefuse,
498+
Rule: RuleG1AmbConflictExtDefault,
499+
Reason: "conflicting external authority detected; operator must resolve before any restoration (default G1 hard-stop)",
500+
}
501+
}
502+
503+
// Quintuple present — delegate to the §64 predicate. The
504+
// dispatcher MUST have populated in.OrphanEvidence; nil is treated
505+
// as "evidence not gathered" → REFUSE/EvidenceMismatch with
506+
// AMD3-E.0 (defensive — should never happen if dispatcher follows
507+
// §64.3).
508+
if in.OrphanEvidence == nil || !in.OrphanEvidence.AllTrueAmendment3() {
509+
return DecisionResult{
510+
Output: OutputRefuse,
511+
Rule: RuleG1AmbConflictExtEvidenceMismatch,
512+
Reason: "amendment-3 evidence predicate did not hold; failing-row=" + in.OrphanEvidence.FailedRowIDAmendment3(),
513+
}
514+
}
515+
516+
// All §64.1 rows hold. PROCEED PanelNative/csf — same target as
517+
// the Amendment 2 path. PlanFromDecision reuses the existing
518+
// panel-static-map (§20.1: PanelDirectAdmin → "csf").
519+
return DecisionResult{
520+
Output: OutputProceed,
521+
Rule: RuleG1AmbConflictExtOrphanProceed,
522+
Reason: "amendment-3 orphan-intent: AmbiguityConflictExternal + DirectAdmin + strong CSF-disabled evidence; restoring CSF per §63.1",
523+
}
524+
}

internal/installer/restore/engine_amendment2_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,17 @@ func TestAmd2_Section56_1_SplitFixtures(t *testing.T) {
323323
wantOut: OutputRefuse,
324324
wantRule: RuleG1AuthorityExternal,
325325
},
326-
// Row 17 — AmbiguityConflictExternal + orphan flag → REFUSE/G1/AmbiguityConflictExternal (no bypass)
326+
// Row 17 — AmbiguityConflictExternal + orphan flag → REFUSE.
327+
// Amendment 2's --accept-orphan-nftban does NOT extend to
328+
// AmbiguityConflictExternal — the bypass remains scoped to
329+
// AuthorityNFTBan only. After Amendment 3 (§63) splits the
330+
// G1/AmbiguityConflictExternal row, this fixture asserts the
331+
// new sub-rule G1/AmbiguityConflictExternal/default. Note: this
332+
// fixture has no ExternalIndicator field set ("" empty), so
333+
// even if Amendment 3's quintuple-check were reached, the
334+
// empty-external defensive guard (AMD3-13) would also refuse.
335+
// The "default" sub-rule fires first (no quintuple match),
336+
// preserving the original Amendment-2-era hard-stop semantics.
327337
{
328338
name: "row17_ambiguity_conflict_no_bypass",
329339
input: DecisionInput{
@@ -335,7 +345,7 @@ func TestAmd2_Section56_1_SplitFixtures(t *testing.T) {
335345
Flags: Flags{PanelAutoTakeover: true, AcceptOrphanNFTBan: true},
336346
},
337347
wantOut: OutputRefuse,
338-
wantRule: RuleG1AmbiguityConflictExt,
348+
wantRule: RuleG1AmbConflictExtDefault,
339349
},
340350
// Row 18 — AmbiguityOrphanNFTBan + panel-auto + orphan-flag → REFUSE/G4.3 (locked by §59 Q2)
341351
{

0 commit comments

Comments
 (0)