agent v0.85.0 WIP: F12/F11/F10/F9/F2/F1 boot-recovery plane + appliance self-heal (pre-build)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
2026-07-12 07:48:49 +02:00
parent bec4bac076
commit bc4eda926b
18 changed files with 1249 additions and 104 deletions
+18
View File
@@ -36,11 +36,26 @@ type Config struct {
SelfUpdate SelfUpdateConfig `json:"selfupdate"`
LogLevel string `json:"log_level"` // debug|info|warn|error (default info)
// DeploymentMode gates host-service self-heal (CAMPAIGN-3 Part 6). "appliance" = a Felhom-managed
// node the agent may remediate (e.g. start networking at boot — F12-class defense in depth). Any
// other value, including absent/unknown, is treated as "byo" (a customer's own host): the self-heal
// CHECK still runs and WARNs, but the REMEDY is structurally unreachable. Fail-safe to byo — never
// touch a host we do not own. Distinct from Privileged.Mode (sudo vs direct exec) — do NOT overload.
DeploymentMode string `json:"deployment_mode,omitempty"`
// SourcePath is the file this config was loaded from ("" = all-env). Set by Load, never
// serialized — the pbsdr bridge's escrow.pbs_storage_id seed writes back to it.
SourcePath string `json:"-"`
}
// DeploymentModeAppliance is the ONLY value that unlocks host-service self-heal. Everything else,
// including "" and any typo, is byo (fail-safe — a host we do not own is never remediated).
const DeploymentModeAppliance = "appliance"
// IsAppliance reports whether this node is a Felhom-managed appliance (self-heal remedies allowed).
// Fail-safe: absent/unknown → false (byo).
func (c *Config) IsAppliance() bool { return c.DeploymentMode == DeploymentModeAppliance }
// OOBConfig configures the dedicated felhom-sshd OOB access instance + belt (TASK H1). **Enabled
// DEFAULTS TO FALSE** — a rollout to a box without explicit oob.enabled=true is a no-op (no port
// claim, no config render, no belt mutation, no oob report stanza) until the operator endpoint +
@@ -575,6 +590,9 @@ func applyEnv(cfg *Config) {
if v := os.Getenv("FELHOM_AGENT_LOG_LEVEL"); v != "" {
cfg.LogLevel = v
}
if v := os.Getenv("FELHOM_AGENT_DEPLOYMENT_MODE"); v != "" {
cfg.DeploymentMode = v
}
// hub
if v := os.Getenv("FELHOM_AGENT_HUB_URL"); v != "" {
cfg.Hub.URL = v
+38
View File
@@ -133,3 +133,41 @@ func TestLoadFileThenEnvOverride(t *testing.T) {
t.Errorf("default endpoint lost: %q", cfg.Proxmox.Endpoint)
}
}
// CAMPAIGN-3 Part 6: deployment_mode gates node self-heal, and it is FAIL-SAFE to byo — absent or any
// unknown value is byo, ONLY the exact "appliance" unlocks the remedy.
func TestIsAppliance_FailSafeToByo(t *testing.T) {
cases := []struct {
mode string
want bool
}{
{"appliance", true},
{"byo", false},
{"", false}, // absent field → byo (fail-safe)
{"Appliance", false}, // case-sensitive — a typo must not unlock the remedy
{"garbage", false},
}
for _, c := range cases {
cfg := &Config{DeploymentMode: c.mode}
if got := cfg.IsAppliance(); got != c.want {
t.Errorf("IsAppliance(mode=%q) = %t, want %t", c.mode, got, c.want)
}
}
}
// The env overlay can set deployment_mode (FELHOM_AGENT_DEPLOYMENT_MODE).
func TestDeploymentModeEnvOverlay(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "agent.json")
if err := os.WriteFile(path, []byte(`{"proxmox":{"node":"n","token":"u@pve!t=s"},"deployment_mode":"byo"}`), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("FELHOM_AGENT_DEPLOYMENT_MODE", "appliance")
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if !cfg.IsAppliance() {
t.Errorf("env overlay did not set deployment_mode: %q", cfg.DeploymentMode)
}
}