R-812 option A (hub): the Proxmox package set — candidate, operator approval, System page

Layer pve: never auto-approved; the candidate is the Proxmox userspace set
every ring-0 box reports (kernel / boot / firmware names left out); the
operator's "Approve Proxmox set" button appears only after 2 healthy night
pve steps on every ring-0 box; an approval nudges no box (ring 1 by a signed
os_pve_step). 11 §5.10 written (BUILT, unreleased, not yet proven live); §8
step 6 split (userspace §5.10, kernel R-836).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-07 10:11:45 +02:00
parent c01ea2e7e9
commit e7fb10200e
9 changed files with 289 additions and 8 deletions
+9
View File
@@ -70,6 +70,15 @@ func (s *Server) handleOSAdmin(w http.ResponseWriter, r *http.Request, path stri
}
id, err := view.ApproveDocker()
reply(map[string]string{"release_id": id}, err)
case r.Method == http.MethodPost && path == "/os/approve-pve":
// R-812 option A (`09` §3 decision 163): the operator approves the Proxmox package set ring 0 ran.
view, ok := s.osUpdates.(OSSystemView)
if !ok {
reply(nil, fmt.Errorf("proxmox approval not available"))
return
}
id, err := view.ApprovePVE()
reply(map[string]string{"release_id": id}, err)
default:
http.Error(w, "not found", http.StatusNotFound)
}
+1
View File
@@ -59,6 +59,7 @@ var r135PostRoutes = []string{
"/os/enabled/h1",
"/os/approve-now",
"/os/approve-docker",
"/os/approve-pve",
// Not a route: the gate must refuse BEFORE routing, so even an unknown path is 403, never 404.
"/no-such-route",
}
+1
View File
@@ -57,6 +57,7 @@ type OSSystemView interface {
BundleThreshold() time.Duration
AgentThreshold() time.Duration
ApproveDocker() (string, error)
ApprovePVE() (string, error) // R-812 option A: the Proxmox package set
}
// agentCell is the "Agent" cell (R-530, hub v0.135.0): the box's agent against the vouched one, how far behind and
+28
View File
@@ -185,3 +185,31 @@ func TestHostsPage_ProxmoxKernelColumn(t *testing.T) {
t.Fatalf("hosts column missing:\n%s", b[:min(len(b), 400)])
}
}
// R-812 option A: the "Approve Proxmox set" button appears ONLY when the rule allows it (one render test per branch).
//
// COMPANION RED-PROOF (observed): drop the `(eq .Waiting "")` from the pve button's condition → "button shown after ONE night".
func TestSystemPage_PVEButtonOnlyWhenReady(t *testing.T) {
s, st, svc := systemServer(t)
if strings.Contains(getSystem(t, s), `action="/os/approve-pve"`) {
t.Fatal("button shown with no Proxmox candidate")
}
_ = st.SetOSRing("full-1", 0)
clock := time.Date(2026, 10, 7, 3, 0, 0, 0, time.UTC)
svc.Now = func() time.Time { return clock }
night := func() {
r := osupdates.Report{RunID: time.Now().String(), Layer: "pve", Trigger: "night", Mode: "apply", Outcome: "nothing",
Healthy: true, Installed: []osupdates.Package{{Name: "pve-manager", Version: "9.2.21", Origin: "Proxmox"}}}
if err := svc.Ingest("full-1", r); err != nil {
t.Fatal(err)
}
}
night()
if strings.Contains(getSystem(t, s), `action="/os/approve-pve"`) {
t.Fatal("button shown after ONE night")
}
night()
if !strings.Contains(getSystem(t, s), `action="/os/approve-pve"`) {
t.Fatal("button missing after two healthy nights")
}
}
+5
View File
@@ -67,6 +67,11 @@
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}"><input type="hidden" name="return" value="/system">
<button type="submit" class="btn btn-sm" data-confirm="Approve this Docker engine set? Ring-1 boxes take it only through a signed operator job.">Approve Docker set</button>
</form>{{end}}
{{if and (eq .Layer "pve") .Fingerprint (not .Approved) (eq .Waiting "")}}
<form method="POST" action="/os/approve-pve" style="margin-top: 0.3rem;">
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}"><input type="hidden" name="return" value="/system">
<button type="submit" class="btn btn-sm" data-confirm="Approve this Proxmox package set (no kernel)? Ring-1 boxes take it only through a signed operator job.">Approve Proxmox set</button>
</form>{{end}}
</div>
{{end}}
</div>