Skip to content

Commit d2b5b44

Browse files
itcmsgrclaude
andauthored
docs(v1.100 PR-24): contract seed v1 — authority restoration policy engine (#493)
Locks PR-24 design surface before any implementation code lands. Policy only — no execution logic, no mutation, no implementation scaffolding. Lattice v2 with top-down precedence. Three outputs only: PROCEED / REFUSE / REQUIRE_EXPLICIT_INTENT. Auditor review APPROVE + two wording-only clarifications applied: - §6 Group 5: panel-auto handling explicitly spans Groups 3 and 4 - §6 Group 4: precedence clarifier for 4.1/4.2 vs 4.3 flag matching Neither edit changed lattice behavior. Implementation is a separate PR gated by separate authorization. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 78c4248 commit d2b5b44

1 file changed

Lines changed: 345 additions & 0 deletions

File tree

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
# PR-24 — Authority Restoration Policy Engine Contract (Seed v1)
2+
3+
**Status:** Seed (approved 2026-04-20, pre-implementation)
4+
**Authorization basis:** lattice v2 + locked amendments (NoRecord + --restore, legacy ActiveAtInstall, 365-day staleness fixed)
5+
**Scope:** Authority restoration **policy engine** — decision only. No execution.
6+
7+
---
8+
9+
## Pinned sentence
10+
11+
> PR-24 decides whether restoration is allowed — not how to do it. It
12+
> is a pure decision engine over four input axes producing exactly one
13+
> of three outputs: PROCEED, REFUSE, REQUIRE_EXPLICIT_INTENT. It spawns
14+
> no external process, mutates no kernel, service, or filesystem state,
15+
> and writes no history entry. Refusal is a valid and expected outcome.
16+
17+
---
18+
19+
## 1. Purpose
20+
21+
PR-24 introduces a pure decision engine that resolves whether the installer is allowed to attempt authority restoration after PR-23 has released authority. It produces one of three outcomes and performs no mutation. Execution of any allowed outcome is the responsibility of a later PR (PR-25+).
22+
23+
## 2. Scope (locked)
24+
25+
### Allowed in PR-24
26+
27+
- Decision engine — pure function over the four input axes
28+
- CLI surface for `--restore` and `--panel-auto-takeover`
29+
- Refusal and intent-required message surfaces
30+
- Structured logging of the decision path (axes read, rule matched, output)
31+
- New state-machine terminals (`StateRestoreRefused`, `StateRestoreIntentRequired`)
32+
- Non-terminal policy-handoff marker (`StateRestoreDecided`) — see §7 prohibitions
33+
- New exit codes (`ExitRefused`, `ExitIntentRequired`)
34+
- Preflight-error surface for classifier reduction failures
35+
36+
### Forbidden (hard, CI-enforced)
37+
38+
- Kernel mutation (`nft`, `iptables`, `ip`, etc.) — zero process spawns
39+
- Service mutation (`systemctl`, service file writes) — zero process spawns
40+
- Filesystem mutation of any config, state, or log file
41+
- History-schema changes (`update-history.json` untouched)
42+
- Any "best effort" / "fallback" / "silent takeover" path
43+
- Any fourth output
44+
- Any path that auto-upgrades `REQUIRE_EXPLICIT_INTENT` to `PROCEED`
45+
- Any restoration-execution code (belongs to PR-25+)
46+
47+
## 3. Inputs — four axes
48+
49+
### A. Classifier state
50+
51+
From `uninstall/authority.Classify`, reused. No parallel detection path.
52+
53+
- `AuthorityNone`
54+
- `AuthorityNFTBan`
55+
- `AuthorityExternal`
56+
- `AuthorityAmbiguous` + sub-kind:
57+
- `AmbiguityOrphanNFTBan`
58+
- `AmbiguityConflictExternal`
59+
60+
### B. Prior-authority record
61+
62+
From `internal/installer/prior` (PR-P2-1 hardened schema).
63+
64+
- `NoRecord` — no prior-authority record on disk
65+
- `Complete` — record present, all required fields parseable, **including** `ActiveAtInstall` ∈ {true, false}
66+
- `Incomplete` — record present but one or more required fields missing
67+
- **Legacy records missing `ActiveAtInstall` are classified as `Incomplete`.** No defaulting to true, no defaulting to false, no inference.
68+
- `Stale` — record present and complete but exceeds freshness window.
69+
- **Freshness window is fixed at 365 days in PR-24. Configurability is deferred to a later PR.** The implementer has no latitude to choose a different value.
70+
71+
### C. Operator intent (flags)
72+
73+
- `none` (neither flag)
74+
- `--restore`
75+
- `--panel-auto-takeover`
76+
77+
### D. Panel context
78+
79+
From existing panel detection.
80+
81+
- `None`
82+
- `DirectAdmin`
83+
- `cPanel`
84+
- `Plesk`
85+
86+
## 4. Outputs — three only (locked)
87+
88+
- `PROCEED` — policy permits restoration; PR-25+ may execute
89+
- `REFUSE` — policy forbids restoration; no PR-25+ execution permitted
90+
- `REQUIRE_EXPLICIT_INTENT` — policy cannot decide; operator must supply additional intent
91+
92+
No fourth state. No "soft proceed." No default.
93+
94+
## 5. Precedence rule (locked, load-bearing)
95+
96+
Lattice is evaluated top-down in exactly this order:
97+
98+
1. Classifier hard-stops
99+
2. Input / flag validity
100+
3. Prior-record integrity gates
101+
4. Panel context gates
102+
5. Proceed decisions
103+
104+
**Invariant:** no later rule may override an earlier refusal. Earlier-rule output is final.
105+
106+
## 6. Decision lattice (normative)
107+
108+
### Group 1 — Classifier hard-stops
109+
110+
| Classifier | Prior | Flags | Panel | Output |
111+
|---|---|---|---|---|
112+
| `AuthorityNFTBan` | * | * | * | **REFUSE** |
113+
| `AuthorityExternal` | * | * | * | **REFUSE** |
114+
| `AmbiguityConflictExternal` | * | * | * | **REFUSE** |
115+
116+
Absolute. No flag, no panel may override.
117+
118+
### Group 2 — Input / flag validity
119+
120+
| Condition | Output |
121+
|---|---|
122+
| `--panel-auto-takeover` with `Panel=None` | **REFUSE** |
123+
| `--restore` AND `--panel-auto-takeover` both set | **REFUSE** |
124+
125+
Operator input errors, not policy ambiguity.
126+
127+
### Group 3 — `AuthorityNone`
128+
129+
#### 3.1 Strong prior (`Complete` + `ActiveAtInstall=true`)
130+
131+
| Flags | Panel | Output |
132+
|---|---|---|
133+
| none | any | REQUIRE_EXPLICIT_INTENT |
134+
| `--restore` | any | **PROCEED** |
135+
| `--panel-auto-takeover` | panel present | **PROCEED** |
136+
137+
#### 3.2 Complete-but-inactive (`Complete` + `ActiveAtInstall=false`)
138+
139+
| Flags | Panel | Output |
140+
|---|---|---|
141+
| any | any | REQUIRE_EXPLICIT_INTENT |
142+
143+
Rationale: restoring a firewall the operator had deliberately disabled is an implicit re-enablement. Operator must specify target explicitly.
144+
145+
#### 3.3 Weak / absent prior
146+
147+
| Prior | Flags | Panel | Output |
148+
|---|---|---|---|
149+
| `NoRecord` | none | any | REQUIRE_EXPLICIT_INTENT |
150+
| `NoRecord` | `--restore` | any | **REQUIRE_EXPLICIT_INTENT** |
151+
| `NoRecord` | `--panel-auto-takeover` | panel present | **PROCEED** |
152+
| `Incomplete` | any | any | REQUIRE_EXPLICIT_INTENT |
153+
| `Stale` | any | any | REQUIRE_EXPLICIT_INTENT |
154+
155+
Rationale for `NoRecord + --restore`: `--restore` carries an implicit target (the recorded prior firewall). With `NoRecord`, that target does not exist. Panel-auto-takeover is the only flag whose target is independent of the record.
156+
157+
### Group 4 — `AmbiguityOrphanNFTBan`
158+
159+
Group 4 sub-rules are evaluated top-down: 4.1 and 4.2 match on prior state for flags {`none`, `--restore`}; 4.3 matches `--panel-auto-takeover` regardless of prior.
160+
161+
#### 4.1 Strong prior (`Complete` + `ActiveAtInstall=true`)
162+
163+
| Flags | Output |
164+
|---|---|
165+
| none | REQUIRE_EXPLICIT_INTENT |
166+
| `--restore` | **PROCEED** |
167+
168+
#### 4.2 Weak / inactive / absent prior
169+
170+
| Prior | Flags | Output |
171+
|---|---|---|
172+
| `Complete` + `ActiveAtInstall=false` | any | REQUIRE_EXPLICIT_INTENT |
173+
| `NoRecord` | none | REQUIRE_EXPLICIT_INTENT |
174+
| `NoRecord` | `--restore` | **REQUIRE_EXPLICIT_INTENT** |
175+
| `Incomplete` | any | REQUIRE_EXPLICIT_INTENT |
176+
| `Stale` | any | REQUIRE_EXPLICIT_INTENT |
177+
178+
#### 4.3 Orphan + panel-auto
179+
180+
| Flags | Output |
181+
|---|---|
182+
| `--panel-auto-takeover` | **REFUSE** |
183+
184+
Panel-auto must never fire over nftban residue, regardless of recoverability.
185+
186+
### Group 5 — Panel context
187+
188+
Panel context is **inert by default**. Panel-auto-takeover is handled inline in Groups 3 and 4 — as a specialized proceed case under `AuthorityNone`, and as an absolute refusal under `AmbiguityOrphanNFTBan`. It is not a standalone override.
189+
190+
## 7. State-machine integration
191+
192+
Two new `InstallState` terminals (added to `internal/installer/state/machine.go`):
193+
194+
- **`StateRestoreRefused`** — policy-determined refusal.
195+
- `IsTerminal()` returns true
196+
- `IsFailed()` returns false (refusal is not failure)
197+
- Excluded from `IsApplyTerminal()` (no mutation was attempted)
198+
- Excluded from `update-history.json` (Option A discipline continues)
199+
200+
- **`StateRestoreIntentRequired`** — policy-determined intent-required.
201+
- `IsTerminal()` returns true
202+
- `IsFailed()` returns false
203+
- Excluded from `IsApplyTerminal()`
204+
- Excluded from `update-history.json`
205+
206+
One non-terminal marker:
207+
208+
- **`StateRestoreDecided`** — marks that PR-24's decision was `PROCEED`.
209+
210+
**`StateRestoreDecided` is constrained as follows (all enforced by this contract):**
211+
212+
1. **Policy-only.** It records that the decision engine said `PROCEED`; nothing more.
213+
2. **Non-terminal for apply semantics.** `IsApplyTerminal()` returns false. `IsTerminal()` returns false.
214+
3. **Excluded from `update-history.json`.** No history row, no schema change. Option A discipline continues.
215+
4. **Not evidence that restoration happened.** No kernel, service, or filesystem change is implied. PR-25+ execution would change state further; in PR-24, `PROCEED` is a handoff outcome only.
216+
217+
*(Name is a placeholder pending bikeshed at implementation time. The semantic role above is locked regardless of final name.)*
218+
219+
## 8. Exit-code contract (extends PR-23 table)
220+
221+
| State | Exit code | Constant |
222+
|---|---|---|
223+
| `StateRestoreDecided` (PROCEED handoff) | 0 | `ExitCommitted` |
224+
| `StateRestoreRefused` | 5 | `ExitRefused` *(new)* |
225+
| `StateRestoreIntentRequired` | 6 | `ExitIntentRequired` *(new)* |
226+
227+
Rationale: distinct codes enable scriptability. Operators and automation must distinguish "engine said no" (5) from "engine said you need to clarify" (6) from "engine failed" (2, unchanged).
228+
229+
## 9. Preflight error boundary (pre-policy)
230+
231+
Any condition that prevents the classifier from producing one of the five supported classifier states is a **preflight error**, not a lattice output. Examples:
232+
233+
- classifier probe command failed
234+
- prior-record file malformed beyond `Incomplete` reduction (e.g., JSON parse failure)
235+
- internal invariant violation (`Ambiguous` without a sub-kind)
236+
237+
Handling: exits with `ExitFatal` (4) and a distinct log marker. Does **not** emit `PROCEED` / `REFUSE` / `REQUIRE_EXPLICIT_INTENT`. This keeps the lattice output space closed and testable.
238+
239+
## 10. Forbidden surfaces — enforcement mechanisms
240+
241+
| Forbidden | Enforcement mechanism |
242+
|---|---|
243+
| Kernel mutation | Exec-trace CI gate — zero `nft` / `iptables` / `ip` process spawns in restore code paths |
244+
| Service mutation | Exec-trace CI gate — zero `systemctl` process spawns in restore code paths |
245+
| Filesystem mutation | Static source scan for write APIs (`os.Create`, `os.WriteFile`, `os.Rename`, `os.Remove*`, `io.Copy` to file targets, template rendering) in the restore package — must be empty. Exec-trace separately proves zero external-process mutation paths. **No syscall-level enforcement is claimed by this seed.** |
246+
| History schema change | Diff check on `update-history.json` schema version + file unchanged across a PR-24 invocation |
247+
| Fourth output | Type system: output is a closed Go enum of three values |
248+
| Auto-upgrade from `REQUIRE_EXPLICIT_INTENT` to `PROCEED` | Source-grep for illegal state transitions in decision engine |
249+
250+
## 11. Proof model
251+
252+
PR-24 correctness is proven on two tiers:
253+
254+
### Tier 1 — Fixture tests (primary proof)
255+
256+
Exhaustive matrix over the 5 × 4 × 3 × 4 input space (= 240 cells), collapsed by the lattice into ~30 distinct rule paths. One test fixture per rule path, asserting exact output. Fixture tests own the dangerous branches (`AuthorityExternal`, `AmbiguityConflictExternal`, `AmbiguityOrphanNFTBan`, weak-record, panel-auto) — these are not real-host branches.
257+
258+
### Tier 2 — Real-host decision tests (secondary proof)
259+
260+
Run the engine on `lab2` and `lab4`, both in clean `AuthorityNone` post-PR-23. Assert:
261+
262+
- bare invocation → `REQUIRE_EXPLICIT_INTENT` (NoRecord + none flags)
263+
- `--restore``REQUIRE_EXPLICIT_INTENT` (NoRecord + `--restore`; per locked amendment)
264+
- Zero kernel / service interaction observed in either run (exec-trace)
265+
266+
Real-host proof is deliberately minimal. Simulating dangerous branches at kernel level would violate the no-mutation gate via the test harness itself, which is not acceptable.
267+
268+
## 12. CI gate requirements
269+
270+
Four new gates in the `G4-RESTORE-*` namespace:
271+
272+
| Gate | Assertion |
273+
|---|---|
274+
| `G4-RESTORE-DECISION-CORRECTNESS` | Fixture matrix: input → exact output, one assertion per rule path. Fails if any rule path is untested. |
275+
| `G4-RESTORE-REFUSAL-INTEGRITY` | When output = `REFUSE` or `REQUIRE_EXPLICIT_INTENT`, assert zero execution branches reached in the same invocation. Proven via exec-trace + call-count assertions. |
276+
| `G4-RESTORE-NO-IMPLICIT-EXEC` | Static: grep-based scan of `internal/installer/restore/` for any `exec.`, `nft`, `systemctl`, `os.Create`, `os.WriteFile`, `os.Rename`, `os.Remove*` references. Must be empty. |
277+
| `G4-RESTORE-DETERMINISM` | Same fixture inputs on two back-to-back runs produce identical outputs. No env-variable, time-of-day, or random-seed dependency. |
278+
279+
**Carry-forward from PR-23:** none of `G3-UN-SHIM-LOCK`, `G3-UN-NO-MUTATION`, `G3-EXEC-TRACE`, `G3-KS-SNAPSHOT` are weakened. They continue to apply to uninstall scope unchanged.
280+
281+
## 13. Reviewer checklist (merge-blocking)
282+
283+
### Policy correctness
284+
285+
- [ ] Every classifier state handled (no default branch, no fallthrough)
286+
- [ ] Every prior-record state handled
287+
- [ ] Group 1 hard-stops dominate all flag/panel inputs
288+
- [ ] Panel context never causes proceed without `--panel-auto-takeover`
289+
- [ ] `NoRecord + --restore` returns `REQUIRE_EXPLICIT_INTENT`
290+
- [ ] Legacy records missing `ActiveAtInstall` classify as `Incomplete`
291+
- [ ] Staleness window is fixed at 365 days (not configurable in PR-24)
292+
293+
### Safety
294+
295+
- [ ] `AuthorityExternal` never overridden
296+
- [ ] `AmbiguityConflictExternal` never overridden
297+
- [ ] Orphan + `--panel-auto-takeover``REFUSE`
298+
- [ ] No auto-upgrade path from `REQUIRE_EXPLICIT_INTENT` to `PROCEED`
299+
300+
### Purity
301+
302+
- [ ] Zero kernel interaction
303+
- [ ] Zero service interaction
304+
- [ ] Zero filesystem writes in the restore package (static scan green)
305+
- [ ] `update-history.json` schema unchanged
306+
- [ ] Exec-trace gate shows zero external-process spawns in refusal paths
307+
308+
### Output discipline
309+
310+
- [ ] Output type is a closed enum of three (`PROCEED` / `REFUSE` / `REQUIRE_EXPLICIT_INTENT`)
311+
- [ ] State-machine terminals added for refuse / intent-required
312+
- [ ] `StateRestoreDecided` excluded from `IsApplyTerminal` and from history
313+
- [ ] Exit codes distinct: `ExitRefused=5`, `ExitIntentRequired=6`
314+
315+
## 14. Merge-blocking real-host matrix
316+
317+
| Host | OS / family | Required evidence |
318+
|---|---|---|
319+
| `lab2` | Ubuntu 24.04 / DEB | `AuthorityNone + NoRecord`: bare → `REQUIRE_EXPLICIT_INTENT`; `--restore``REQUIRE_EXPLICIT_INTENT`; exec-trace clean |
320+
| `lab4` | AlmaLinux 9 / RPM | same as lab2 |
321+
322+
**Not merge-blocking** (optional extended evidence): `monitor`, `srv1`.
323+
324+
**Not real-host** (fixture-only): `AuthorityExternal`, `AmbiguityConflictExternal`, `AmbiguityOrphanNFTBan`, panel-driven proceed, all weak-record branches.
325+
326+
## 15. Follow-up items (tracked, not blocking)
327+
328+
1. `ActiveAtInstall` capture in new prior-record writes. If not already landed in PR-P2-1, new records written after PR-24 should populate the field. Legacy records without it continue to flow through `Incomplete``REQUIRE_EXPLICIT_INTENT`; this is intentional and truthful.
329+
2. Staleness window configurability. PR-24 locks the window at 365 days fixed. A configurability knob is deferred to a later PR.
330+
3. Uninstall-history schema decision (carry-forward from PR-23 follow-up).
331+
4. Panel-auto prior-firewall-identity consistency: when `--panel-auto-takeover` is used and prior record exists but names a non-panel-native firewall, this seed has no opinion — panel-auto target is panel-native regardless of record. Worth revisiting in PR-25.
332+
333+
---
334+
335+
## Amendment history
336+
337+
- **2026-04-20 v1 (seed)** — first committed seed. Lattice v2 + three locked corrections:
338+
1. `§10` filesystem purity enforcement clarified: static source scan for write APIs + exec-trace for external processes. No syscall-level enforcement claim.
339+
2. `§7` / `§8` `StateRestoreDecided` explicitly constrained as policy-only / non-terminal-for-apply / excluded-from-history / not-evidence-of-restoration.
340+
3. `§3` / `§15` staleness window locked at 365 days fixed; configurability deferred.
341+
- **2026-04-20 v1 (auditor wording)** — two non-semantic wording clarifications before merge, per auditor review of PR #493:
342+
1. `§6` Group 5 wording updated: panel-auto handling spans Groups 3 and 4 (proceed under `AuthorityNone`; refuse under `AmbiguityOrphanNFTBan`), not only Group 3.
343+
2. `§6` Group 4 precedence clarifier added: 4.1 / 4.2 match on prior state for flags {`none`, `--restore`}; 4.3 matches `--panel-auto-takeover` regardless of prior.
344+
345+
Neither edit changes lattice behavior (§5 precedence already produces the correct outcome).

0 commit comments

Comments
 (0)