Files
felhom-agent/REPORT.md
T
admin 8ecf8929fb slice 10A: activate the control envelope (Down channel) + hub-backed desired provider (v0.15.0)
The control envelope becomes live: the agent caches the hub's desired-state +
generation and re-fetches GET /hosts/{id}/desired-state only when the
generation advances. A new internal/desired Syncer maps the wire shape into a
reconcile.CachingProvider feeding the engine; benign deltas reconcile, an
explicit guest decommission is gated pending_signature (exec is 10B). Adds the
DesiredStateResponse/WireDesiredState wire types + Client.FetchDesiredState +
the loop EnvelopeObserver seam. Cross-repo golden (envelope + desired-state)
byte-identical with the hub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:02:59 +02:00

3.8 KiB

REPORT — slice 10A (agent half): hub desired-state serving — the "Down" channel (v0.15.0) (2026-06-10)

Overwrite-latest report. Cumulative history: CHANGELOG.md.

What was implemented

The agent half of slice 10A: activate the control envelope as the live Down channel and feed a hub-backed desired-state into the reconcile engine. Pairs with hub v0.9.0.

The control loop (now live)

report (heartbeat) → control envelope → (DesiredGeneration advanced past cache? fetch desired-state) → reconcile benign / gate destructive → report. The heartbeat stays light; the heavy desired-state is fetched only on a generation advance.

internal/reconcile

  • DesiredGuest.Decommission — the canonical destructive desired-state delta (an EXPLICIT flag, never "absent from the list", so a partial hub list can't mass-destroy). Planner emits ActionDecommissionClassDecommission → Destructive → the gate refuses pending_signature.
  • Reconcile now counts a pending_signature gate refusal as Result.Pending (expected, INFO-logged), not a failure; any other refusal stays a real failure. ActionDecommission has no executor (10B) — a defensive guard refuses to run it.
  • CachingProvider — thread-safe DesiredState + generation cache (Desired/Update/ Generation); the production provider, replacing EmptyProvider in the daemon engine. Empty until the hub serves intent → cold-start is a live no-op (unchanged behaviour).

internal/hub

  • ControlEnvelope fields are now active. New wire types DesiredStateResponse + WireDesiredState (guests + forward-compat restore_directive (10D) / pbs_namespace / opaque storage_manifest+backup_policy) + WireDesiredGuest. New Client.FetchDesiredState (GET /api/v1/hosts/{host_id}/desired-state, self-scoped to the client's own host). New EnvelopeObserver loop seam + SetEnvelopeObserver (hub does not import reconcile/desired).

internal/desired (new) + wiring

  • Syncer — implements hub.EnvelopeObserver; fetches on a generation advance, maps wire→domain, updates the CachingProvider. Caches the fetched generation (race-robust); a fetch failure keeps the last-known state. restore_directive carried + logged, not acted on (10D). Wired in cmd/felhom-agent: provider → engine, syncer → loop.

Tests (all green)

  • reconcile: benign applied + destructive decommission gated pending (not executed); Plan decommission-only + classifies Destructive; CachingProvider update/isolation.
  • desired: fetch-once-on-advance / no-refetch-on-unchanged / fetch-failure-keeps-cache / caches-the-fetched-generation.
  • hub: FetchDesiredState path+auth+decode (incl. restore_directive) + typed 403; loop notifies the observer + adopts PollIntervalSeconds, skips the observer on a report error.
  • cross-repo golden (desired-state + control-envelope) decode + key-set guard, byte-identical with felhom.eu/hub. go test ./... green.

Versioning / docs

  • Version 0.14.0 → 0.15.0; CHANGELOG.md updated. Doc 03 §4 (control loop live) + §9 (slice table: 10A done, 10B/10C/10D pending) updated.

Out of scope (per the task)

  • Signed-op execution (verify + run the gated destructive op) → 10B (10A marks it pending only).
  • Restore-mode / re-enroll consumption (a new box's first directive) → 10D; 10A serves already-authenticated hosts only.

Pending

  • Live validation on the demo: build+deploy agent v0.15.0 + hub v0.9.0; admin-set a desired-state with a benign + a decommission delta → generation bumps → agent fetches → reconciles benign + gates the decommission; change poll_interval_seconds; confirm a host can't fetch another host's state.