v0.90.0 — guest RAM resize (R-24) + fast-tick-until-convergence (R-28)

MinAgent coupling: felhom-controller v0.143.0 gates its guest-memory-resize UI on
this agent (FeatureGuestMemoryResize, MinAgent 0.90.0).

R-24 guest RAM resize (internal/localapi/guestmemory.go): self-scoped GET/POST
/guest/memory. Agent enforces every bound FRESH per request (min 2048, max
host_total-2048, shrink floor max(2048, usage+512)); applies via PVE SetConfig —
live cgroup apply, no reboot (Phase-0 proven on the nested demo box). Verify-after-apply
re-reads maxmem before claiming success. New narrow MemoryOps seam (GuestAPI untouched);
Options.Memory nil -> 503. Memory only.

R-28 fast-tick (internal/fasttick): while any desired-state item is unapplied -
including the pre-tunnel window a hub poke can't reach - pulse the shared out-of-band
trigger every 30s, self-disarm on convergence. Four cached sources (desired-gen==0,
reconcile Planned-Pending>0, pbsdr waiting_secret only, wgtunnel desired-not-operational);
LOUD pbsdr states + pending_signature excluded. Seams: reconcile.Engine.LastResult() +
wgtunnel.Manager.TunnelConvergence() (cached, no per-tick exec).

Guests-0/0: hypothesis REFUTED live (9201 IS a pool member; 0/0 was the pre-provision
window; PoolAddVMID re-assert already covers restore-over-existing). No code change; the
fast-tick mitigates the window.

Tests + red-proofs (i floor guard, ii max guard, iii always-pulse) all restored green.
This commit is contained in:
2026-07-17 19:09:40 +02:00
parent 9127f547f9
commit ac112c956e
10 changed files with 958 additions and 1 deletions
+10
View File
@@ -91,6 +91,9 @@ type Options struct {
// GuestAttach binds an enrolled user-data drive's felhom-data namespace into the guest (slice 10
// P2, Model A). OPTIONAL — when nil, POST /disks/guest-attach reports "not configured".
GuestAttach GuestAttacher
// Memory is the guest-RAM-resize Proxmox surface (v0.90.0, R-24). OPTIONAL — when nil, the
// /guest/memory endpoints report "not configured". Satisfied by *proxmox.Client.
Memory MemoryOps
// NetStorage is the privileged network-mount (NAS) surface (Part A1). OPTIONAL — when nil, the
// /netstorage endpoints report "not configured". Satisfied by *storage.SudoHostOps.
NetStorage NetworkStorageOps
@@ -188,6 +191,8 @@ type Server struct {
diskGate StorageGate // slice 8C (optional)
guestList GuestLister // slice 8C (optional)
guestAttach GuestAttacher // slice 10 P2 (optional)
mem MemoryOps // v0.90.0 R-24 guest RAM resize (optional)
memMu sync.Mutex // single-flight around a resize apply (one customer per host)
netStorage NetworkStorageOps // Part A1: NAS network mounts (optional)
netMountRoot string // the user-data namespace root for the network-mount role gate
smbCredsDir string // where SMB creds files are written (out-of-band, 0600)
@@ -300,6 +305,7 @@ func NewServer(o Options) (*Server, error) {
diskGate: o.DiskGate,
guestList: o.Guests2,
guestAttach: o.GuestAttach,
mem: o.Memory,
netStorage: o.NetStorage,
netMountRoot: storage.NetworkMountRoot,
smbCredsDir: o.SmbCredsDir,
@@ -365,6 +371,10 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("POST /disks/guest-attach", s.withGuest(s.handleDiskGuestAttach))
// Guest reboot (slice 10 P2 activation): user-triggered restart to activate pending drive binds.
mux.HandleFunc("POST /guest/reboot", s.withGuest(s.handleGuestReboot))
// Guest RAM resize (v0.90.0, R-24): read current allocation + bounds, and apply a bounded resize
// (live cgroup apply, no reboot). Self-scoped; the agent enforces every bound fresh per request.
mux.HandleFunc("GET /guest/memory", s.withGuest(s.handleGuestMemory))
mux.HandleFunc("POST /guest/memory", s.withGuest(s.handleGuestMemoryResize))
// Network storage (NAS) — Part A1: mount/list/remove a bulk-media NAS share host-side (automount
// idle-unmount; +100000 uid recipe). A distinct class from a drive — no enroll/eject/wipe.