Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.6 KiB
REPORT — slice 8A (agent half): local-API server + provisioning back-half (v0.10.0) (2026-06-10)
Overwrite-latest report (most recent significant work only). Cumulative history lives in CHANGELOG.md. Implements the host-agent half of
TASK — Slice 8A. The in-guest controller half is infelhom-controllerv0.35.0. No hub change. Grounded byfelhom.eu/documentation/tests/slice8a-channel-deploy-spike-findings.md(commit4a81a96).
Outcome
The agent now exposes the per-guest local API (doc 03 §6) the in-guest controller calls over the bridge, and runs the provisioning back-half that follows the slice-7 bring-up front half. With the slice-7 front half, this completes the full provisioning chain: provision a guest → deploy its (golden-baked) controller → controller comes up configured and talks to the agent — validated live end-to-end on the demo.
What landed
internal/localapi— the HTTPS local-API server, the per-guest authorization gate.- Serves a persisted self-signed leaf with a stable SHA-256 fingerprint (generated once; a fresh cert each boot would invalidate every baked bootstrap pin). The fingerprint is what the controller pins (leaf-cert SHA-256 — consistency with the agent's PVE/PBS pinning).
- 7 §6 endpoints, all self-scoped:
GET /storage(this guest's mpN mounts + fast/slow class),POST /snapshot,POST /rollback,POST /backup(enqueued, crash-consistent — quiesce is 8B),GET /backup/due(thin in 8A),GET /backup/status,GET /restore-test/status. - Token store: durable, crash-safe per-guest token→guest map persisting only a SHA-256 hash (plaintext exists transiently mint→write-to-mount, then discarded); last-write-wins; fsync'd JSONL.
- Self-scoping: VMID is resolved ONLY from the token; an explicit
vmidthat disagrees → 403, op never issued for the other guest; absent/unknown → 401.
internal/provision— the back-half: mint per-guest token → render the stablebootstrap.json(schema felhom.bootstrap/v1; no registry credential — controller image is golden-baked) → write0600→chown 100000:100000(mapped guest-root, spike gotcha 1) → attach a read-only bind mount viapct set. Host-side only (F3 — nopct exec). The token is never logged/returned.--selftest=provision— full chain on-demand (front half + back half; keeps the guest).config.LocalAPIConfig(local_api) — optional 6th daemon goroutine; disabled cleanly when unconfigured or on a token-store/cert failure (the daemon still reports/reconciles).configs/build-golden.shbakes the controller image (pulled once on the trusted build host, thendocker logout— no cred baked) + a controller-bootstrap unit that deploys the baked image from the config mount on boot. Plusconfigs/felhom-localapi-firewall.exampleand a narrowFELHOM_PROVISIONsudoers alias.
Design note — the back-half's host-root surface
The back-half's chown + bind-mount attach are host-root ops that the API token cannot do (bind
mounts are root@pam-only). They are NOT added to proxmox.Privileged (fenced to its 3
exceptions, "do not add"); they live in internal/provision and run through the shared Runner
(direct as root, or sudo -n with the new sudoers alias), confined to the agent-owned
/var/lib/felhom-agent/guests/* path. This is the per-guest provisioning host-root surface —
host-side and F3-compliant.
Tests
go test ./... green; -race green on the build server (pure-Go parts). localapi: self-scoping
(cross-guest snapshot/rollback/backup → 403, op never issued; own-guest uses the token's VMID), 401
paths, /storage class mapping, /backup enqueue, thin /backup/due, status scoping; token store
persists only the hash (plaintext never on disk), last-write-wins, survives reopen, uniqueness; the
leaf fingerprint is stable across reload. provision: writes 0600 + chowns + attaches with the right
args; the token never appears in the Result; the cross-repo bootstrap.json key-set is pinned.
Live validation (demo-felhom)
Built a new golden with the controller v0.35.0 baked (build-golden.sh). --selftest=provision
into guest 8201: front half up (fresh MAC, 13s) → back half minted the token, wrote bootstrap.json
(0600, chown 100000:100000, verified root:root 0600 inside the guest) and pct set-attached the
read-only mount (mp9). The golden's baked unit deployed the controller (no registry pull); the
controller ingested the bootstrap → came up CONFIGURED, not setup mode → reached the real
local-API GET /storage → 200, saw its mount. Self-scoping against the live server: own→200,
cross-guest ?vmid=9999→403, bogus→401, absent→401. Firewall narrowing demonstrated (source
gating: an off-bridge stand-in blocked, the guest allowed). Secret discipline: token plaintext
absent from the agent log, store holds only the hash, bootstrap 0600+chowned, selftest never prints
the token. The standing demo agent service was upgraded to v0.10.0 (local-API live on
192.168.0.162:8443); the test guest was torn down.
Deferred (stated, not built)
Quiesced app-consistent backup (/backup/due quiesce loop) → 8B. Controller de-privileging + customer
disk endpoints → 8C. The BringUpSpec/provision source (hub desired-state) + deterministic
attach-before-start sequencing → slice 10 (in 8A the back-half follows the front-half's start; the
golden bootstrap unit's After=docker.service network-online.target reliably sequences it after the
host-side mount attach). No secrets committed.