agent v0.86.0: DR-tier-by-default — capability inactive state (GatedBy/GateActive, pbsdr gate via DRConfigured) + F-3 root-run provision parent ownership

Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
This commit is contained in:
2026-07-12 20:06:27 +02:00
parent bcb8dad2aa
commit c20814e6c2
11 changed files with 403 additions and 148 deletions
+26 -4
View File
@@ -53,7 +53,7 @@ import (
// version is the agent version. Overridable at build time with
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
var version = "0.63.0"
var version = "0.86.0"
// runGuestHook is the PVE hook body (`felhom-agent guest-hook <vmid> <phase>`). On pre-start it
// creates placeholder dirs for any absent bind-mount source so the guest always boots (the C1 net);
@@ -399,10 +399,12 @@ func poolReadStatus(ctx context.Context, px *proxmox.Client) capability.Status {
// logCapabilities logs the privileged-capability self-check at startup: one INFO summary, plus an
// ERROR per degraded capability naming the gated feature (so a missing grant is loud at cutover,
// not days later). It never exits — serve-degraded.
// not days later). Inactive (config-gated off, plumbing healthy — v0.86.0) is counted in the
// summary but never error-logged: disabled ≠ broken. It never exits — serve-degraded.
func logCapabilities(statuses []capability.Status, logger *slog.Logger) {
ok, total, degraded := capability.Summarize(statuses)
logger.Info("capabilities self-check", "ok", ok, "total", total, "degraded", len(degraded))
logger.Info("capabilities self-check",
"ok", ok, "total", total, "degraded", len(degraded), "inactive", total-ok-len(degraded))
for _, d := range degraded {
logger.Error("capability DEGRADED — privileged grant missing (feature impaired until fixed)",
"capability", d.Name, "feature", d.Feature, "reason", d.Reason, "critical", d.Critical)
@@ -459,13 +461,27 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
// startup (loud on any denial) and attach the snapshot to every hub report; the hub owns the
// ok→degraded alert. Serve-degraded — a missing grant never blocks startup.
capProber := capability.Prober{Runner: &proxmox.ExecRunner{Mode: proxmox.RunnerDirect}}
// DR-tier gate (v0.86.0, DR-tier-by-default): the pbsdr-* capabilities are config-gated — on a
// box whose DR tier is not configured (no descriptor ever / descriptor disabled) a HEALTHY
// probe reports "inactive (disabled by configuration)" instead of ok; broken plumbing (binary
// missing / grant denied) stays DEGRADED regardless (an un-migrated box must never look
// deliberately off). Late-bound: the pbsdr manager is constructed further down; probes run at
// report time. nil (pre-assignment) fails ACTIVE — the historical behavior.
var drConfigured func() bool
capProber.GateActive = func(gate string) bool {
if gate == capability.GatePBSDR && drConfigured != nil {
return drConfigured()
}
return true
}
// A1 (v0.62.0): compose the PVE pool-read check AROUND the sudo prober (an API read does not
// belong inside the sudo-policy probe). Non-critical: a degraded pool read means the stale-lock
// reaper fail-safes (locks stay uncleared) — visible on the hub report, no operator page.
probeAll := func(ctx context.Context) []capability.Status {
return append(capProber.Probe(ctx), poolReadStatus(ctx, px))
}
logCapabilities(probeAll(context.Background()), logger)
// (The startup self-check log runs AFTER the pbsdr manager is wired below, so its snapshot
// already carries the gated view — v0.86.0.)
collector.SetCapabilityProber(probeAll)
loop := hub.NewLoop(collector, client, time.Duration(hcfg.PollSeconds)*time.Second, logger)
interval := time.Duration(hcfg.PollSeconds) * time.Second
@@ -796,8 +812,14 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
pbsdrLoop = pbsdr.NewLoop(pdMgr, 60*time.Second, logger)
desiredSyncer.AddConsumer(pbsdrLoop) // raw desired-state → the pbs_dr block
collector.SetPBSDRReporter(pbsdrLoop)
// Capability gate wiring (v0.86.0): the prober's GatePBSDR now answers from the bridge
// (descriptor state, marker-backed across restarts) — see the capProber block above.
drConfigured = pdMgr.DRConfigured
logger.Info("pbsdr: bridge enabled (hub-driven; no-op until a pbs_dr descriptor arrives)")
}
// Startup capability self-check — after the pbsdr gate wiring so the logged snapshot matches
// what the first report will carry (inactive vs degraded is already resolved here).
logCapabilities(probeAll(context.Background()), logger)
// Run reconcile, the hub loop, the storage watchdog, the restore-test scheduler, the PBS
// verify loop, (optionally) the local-API server, and (optionally) the LAN resolver loop