GL-5: DR bring-up structural bind overrides + 4d real-bind swap

ModeDRGuestLoss now passes restore-time MountOverrides for the two
platform-constant structural binds (mp8 parent, mp9 bootstrap) via the
shared throwaway-volume format helper - without them a customer-archive
restore under the privsep token fails outright ("restoring 'mp8' to bind
mount is only possible for root"). New post-restore step 4d swaps the real
binds in via the host runner (root pct set, one slot per call), deletes the
displaced unusedN volumes (API config PUT; a scoped-token refusal logs the
residue loudly instead of widening privileges), and respects the
committed/launched rollback envelope. Provision passes nil overrides -
byte-identical behavior (regression contract test).

Engine grows an optional HostRunner + StateDir seam (DR refuses up front
without a runner); selftest bring-up wires the ExecRunner + cleans the
scratch mp9 host dir on teardown; proxmox.GuestConfig.Unused() added.
6 new tests incl. C2 mid-swap rollback + C3 older-archive + 403-warn paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-08 08:48:11 +02:00
parent 4c40846769
commit c12b512316
6 changed files with 441 additions and 16 deletions
+20
View File
@@ -32,6 +32,15 @@ type Engine struct {
hostID string
logger *slog.Logger
// hostRun executes host-root commands for the DR structural-bind swap (bring-up 4d): a
// bind-mount `pct set` is root@pam-only, so it cannot go through the API token (the same
// constraint the provision back-half documents). nil on API-only engines — RunBringUp
// refuses ModeDRGuestLoss then (GL-5).
hostRun proxmox.Runner
// stateDir is the agent state dir the mp9 bootstrap host dir lives under
// (<stateDir>/guests/<vmid>/bootstrap); "" → /var/lib/felhom-agent (mirrors provision.NewBackHalf).
stateDir string
opSeq uint64 // atomic; makes each op id unique per attempt
}
@@ -46,6 +55,11 @@ type EngineOptions struct {
Gate *Gate
HostID string
Logger *slog.Logger
// HostRunner enables the DR structural-bind swap (bring-up 4d, root pct ops). Optional —
// engines that never run ModeDRGuestLoss may leave it nil.
HostRunner proxmox.Runner
// StateDir is the agent state dir ("" → /var/lib/felhom-agent); only the 4d swap reads it.
StateDir string
}
// NewEngine builds an Engine. The Queue is shared (the single §10 choke point); the
@@ -69,6 +83,10 @@ func NewEngine(opts EngineOptions) *Engine {
// the common slice-4 daemon state (no signers pinned, no desired state).
gate = NewGate(nil, opts.HostID, nil, logger)
}
stateDir := opts.StateDir
if stateDir == "" {
stateDir = "/var/lib/felhom-agent"
}
return &Engine{
api: opts.API,
queue: opts.Queue,
@@ -78,6 +96,8 @@ func NewEngine(opts EngineOptions) *Engine {
gate: gate,
hostID: opts.HostID,
logger: logger,
hostRun: opts.HostRunner,
stateDir: stateDir,
}
}