slice 10A: hub desired-state serving + signed-jobs queue (Down channel) (hub v0.9.0)
Serve operator intent to authenticated hosts: PUT /admin/hosts/{id}/desired-state
(global key) bumps desired_generation; GET /hosts/{id}/desired-state + /jobs are
per-host self-scoped; the host-report envelope now carries the real generation +
has_signed_ops. New signed_jobs table + store methods. Desired-state stored/served
opaquely (agent owns the schema). Cross-repo golden (envelope + desired-state)
byte-identical with felhom-agent; doc 03 §4/§9 updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,44 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.9.0 — slice 10A: desired-state serving + signed-jobs queue (the "Down" channel) (2026-06-10)
|
||||
|
||||
The hub half of slice 10A: the hub now **serves operator intent** down to already-authenticated
|
||||
hosts. The control envelope (the host-report response) stops returning placeholder
|
||||
`desired_generation:0 / has_signed_ops:false` and carries the host's **real** generation + a
|
||||
signed-jobs flag — the cheap change-notification the agent (v0.15.0) acts on. The heavy
|
||||
desired-state moves only on a dedicated, self-scoped fetch.
|
||||
|
||||
### Added
|
||||
- **`PUT /api/v1/admin/hosts/{host_id}/desired-state`** (global/operator key only) — sets a host's
|
||||
desired-state and **atomically bumps `desired_generation`**. The body is JSON the hub stores +
|
||||
serves **opaquely** (it validates only that it is well-formed JSON; the agent/CLI owns the
|
||||
schema). Unknown host → 404; malformed JSON → 400. Minimal admin path; rich editing UX is later.
|
||||
- **`GET /api/v1/hosts/{host_id}/desired-state`** (per-host key, **self-scoped** — a host reads
|
||||
only its own; the global key may read any) — returns `{generation, desired_state}`. The agent
|
||||
fetches it when the envelope's generation advances past its cache.
|
||||
- **`GET /api/v1/hosts/{host_id}/jobs`** (per-host key, self-scoped) — serves the host's pending
|
||||
**opaque** signed-op blobs (oldest first). The hub never forged, opened, or executes them
|
||||
(verify + run is slice 10B; this only serves the queue).
|
||||
- **`POST /api/v1/admin/hosts/{host_id}/jobs`** (global key only) — enqueues a pre-signed opaque
|
||||
job blob. The minimal operator path to seed the queue; the hub holds no signing key.
|
||||
- **Store**: a new `signed_jobs` table (per-host opaque blob queue); `SetHostDesired` (set + bump
|
||||
generation, atomic), `EnqueueSignedJob` / `GetSignedJobs` / `CountSignedJobs`. The `hosts` table's
|
||||
previously-inert `desired_json` / `desired_generation` columns are now live.
|
||||
|
||||
### Changed
|
||||
- The host-report **control envelope** now reports the host's actual `desired_generation` and
|
||||
`has_signed_ops` (queue non-empty), both degrading safely to their old defaults on a store error
|
||||
(a heartbeat never fails on the control channel). `poll_interval_seconds` / `blocked` unchanged.
|
||||
|
||||
### Tests
|
||||
- admin-set bumps the generation each write + the served state reflects the latest body; admin-set
|
||||
is global-key-only (per-host → 403, malformed → 400, unknown host → 404).
|
||||
- `GET /desired-state` is **self-scoped** (host A's key → host B → 403; global → any; no token → 401).
|
||||
- the envelope carries the current generation + `has_signed_ops` flips on enqueue; `GET /jobs` is
|
||||
self-scoped + serves the blobs oldest-first; admin enqueue is global-key-only.
|
||||
- cross-repo golden round-trip: `testdata/desired-state.golden.json` set → fetched back unchanged
|
||||
(the opaque pass-through), **byte-identical** with felhom-agent's copy.
|
||||
|
||||
## (no version bump) — slice 9 cross-repo wire-contract: `host.cpu_temp_c` (2026-06-10)
|
||||
|
||||
Slice 9 adds a nullable **`cpu_temp_c`** field to the shared `HostMetrics` wire struct (the agent's
|
||||
|
||||
Reference in New Issue
Block a user