From 7dcc80fde8d413dc66a0190cdedbeba37ed39e47 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 8 Jun 2026 14:41:34 +0200 Subject: [PATCH] chore(agent): add CHANGELOG, version the agent at 0.1.0 - CHANGELOG.md with the v0.1.0 (slice 1) entry - main: version var (0.1.0, ldflags-overridable) + --version flag; version shown in selftest header and startup log Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++ cmd/felhom-agent/main.go | 22 ++++++++++++---- 2 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..35e7d21 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to **felhom-agent** are recorded here. Update on every code +change that gets pushed. + +## v0.1.0 — Scaffold + `proxmox` interaction layer (slice 1) (2026-06-08) + +First slice: stand up the host-agent project and its foundation — the typed +Proxmox interaction layer every other module will call. No reconcile loop, hub +client, signing, or storage/backup orchestration yet (later slices). + +### Added +- **Project scaffold**: module `gitea.dooplex.hu/admin/felhom-agent`, binary + `felhom-agent` (`cmd/felhom-agent/`), Go 1.24, zero external dependencies + (pure stdlib). `--version` flag; `version` var overridable via + `-ldflags "-X main.version="`. +- **`internal/proxmox` — API backend (`Client`)**: hand-rolled REST client over + `https://:8006/api2/json` with `PVEAPIToken` auth. Typed read ops + (`Version`, `Nodes`, `NodeStatus`, `ListLXC`, `GuestStatus`, `GuestConfig`, + `ListStorage`, `NodeStorage`, `StorageContent`) and async mutating ops + returning a UPID (`RestoreLXC` — the primary create path, `Vzdump`, `Snapshot`, + `Rollback`, `DeleteSnapshot`, `SetConfig`, `Start`, `Stop`). +- **`WaitTask`**: polls `GET /nodes/{node}/tasks/{upid}/status` until stopped, then + asserts `exitstatus == "OK"` (authorization can surface at task execution, not + the POST — phase1-2 §1.3). Exponential backoff (1s→5s cap), context + cancellation + timeout. `*APIError` parses the offending privilege from a 403; + `*TaskError` parses it from a failed task exitstatus + log tail. +- **`internal/proxmox` — fenced root-CLI backend (`Privileged`)**: limited to the + three proven OS-root exceptions only — `CreateGoldenLXC` (keyctl `pct create`), + `MountUSBByUUID`, `SMART`, `Sensors`; each cites why it can't be the API. Fence + is structural (Client never shells out, Privileged never makes an HTTP call) and + asserted in tests. +- **TLS trust**: SHA-256 leaf-cert pinning (the host serves a self-signed cert) or + a CA file; an explicitly-named `insecure_skip_verify` that is off by default. No + blanket verification disable. +- **`internal/config`**: JSON config file + `FELHOM_AGENT_*` env overrides; the + token secret is never logged (`Redacted()`). +- **`internal/log`**: slog setup (text, stderr, configurable level). +- **`cmd/felhom-agent --selftest`**: read-only health report against a live host + (version/nodes/status/guests/storage); `--selftest=task --vmid N` exercises + `WaitTask` on a reversible snapshot→rollback→delete op (gated; default selftest + mutates nothing). +- **Tests**: unit tests with a mock HTTP transport + mock runner (UPID parse, + `WaitTask` running→OK / failed-403 / timeout / ctx-cancel, 403→privilege error, + response decoding against shapes captured live from `demo-felhom`, config + redaction, and the API-vs-root routing fence). + +### Notes +- Types are grounded in the spike findings + (`felhom.eu/documentation/proxmox-platform.md`, `tests/phase{0,1-2,3}-findings.md`) + and the exact JSON shapes captured live from `demo-felhom` (PVE 9.2.2). +- Verified: `go build/vet/test` green on Go 1.24.4 (build server) and a live + read-only `--selftest` against the demo host with TLS fingerprint pinning. +- The 16-privilege `FelhomAgent` role + privsep token (role on **both** user and + token) is provisioned out-of-band; the agent only consumes the token. diff --git a/cmd/felhom-agent/main.go b/cmd/felhom-agent/main.go index cc55f6d..bca5603 100644 --- a/cmd/felhom-agent/main.go +++ b/cmd/felhom-agent/main.go @@ -22,17 +22,28 @@ import ( "gitea.dooplex.hu/admin/felhom-agent/internal/proxmox" ) +// version is the agent version. Overridable at build time with +// -ldflags "-X main.version="; defaults to the in-repo CHANGELOG version. +var version = "0.1.0" + func main() { var ( - cfgPath string - selftest selftestFlag - vmid int + cfgPath string + selftest selftestFlag + vmid int + showVersion bool ) flag.StringVar(&cfgPath, "config", envOr("FELHOM_AGENT_CONFIG", "/etc/felhom-agent/agent.json"), "path to the agent config file (JSON)") flag.Var(&selftest, "selftest", "run a self-test and exit: bare/`read` = read-only queries; `task` = reversible mutating exercise (needs -vmid)") flag.IntVar(&vmid, "vmid", 0, "guest VMID for --selftest=task (the reversible snapshot/rollback exercise)") + flag.BoolVar(&showVersion, "version", false, "print version and exit") flag.Parse() + if showVersion { + fmt.Println("felhom-agent", version) + return + } + cfg, err := config.Load(cfgPath) if err != nil { // A missing default config file is fine if env provides the values; only a @@ -48,7 +59,8 @@ func main() { switch selftest.mode { case "": // No daemon loop yet. - logger.Info("felhom-agent slice-1 scaffold; no run loop yet", + logger.Info("felhom-agent scaffold; no run loop yet", + "version", version, "hint", "use --selftest (read-only) or --selftest=task --vmid N") // TODO: poll loop — slice 3/4. return @@ -87,7 +99,7 @@ func runSelftestRead(ctx context.Context, cfg config.Config, logger *slog.Logger ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - fmt.Println("=== felhom-agent selftest (read-only) ===") + fmt.Printf("=== felhom-agent %s selftest (read-only) ===\n", version) fmt.Printf("endpoint : %s node=%s\n", cfg.Proxmox.Endpoint, cfg.Proxmox.Node) fail := 0