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>
This commit is contained in:
+55
-6
@@ -1,5 +1,7 @@
|
||||
package hub
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// HostReport is the wire contract shared with the hub's ingest
|
||||
// (felhom.eu TASK-slice3-hub-ingest). Field NAMES must match the hub
|
||||
// field-for-field. Encoding is ordinary encoding/json (no canonicalization —
|
||||
@@ -233,15 +235,62 @@ type PBSSnapshot struct {
|
||||
|
||||
type AuditEntry struct{} // audit-log tail entry fields TBD
|
||||
|
||||
// ControlEnvelope is the hub's 200 response to a host-report. This slice the agent
|
||||
// adopts ONLY PollIntervalSeconds; the rest are reserved/forward-compat fields it
|
||||
// logs at most and never acts on (reconcile, slice 4, consumes them).
|
||||
// ControlEnvelope is the hub's 200 response to a host-report — the "Down" channel (slice 10A).
|
||||
// It is a cheap change-notification on every heartbeat: the agent adopts PollIntervalSeconds,
|
||||
// and when DesiredGeneration ADVANCES past its cached one it fetches the full desired-state from
|
||||
// GET /hosts/{id}/desired-state (the heavy state moves only on change). HasSignedOps flags a
|
||||
// non-empty signed-jobs queue (the agent fetches/executes them in 10B). Blocked stays reserved.
|
||||
type ControlEnvelope struct {
|
||||
Status string `json:"status"`
|
||||
// PollIntervalSeconds is a pointer so a missing field (keep current interval) is
|
||||
// distinguishable from an explicit 0.
|
||||
PollIntervalSeconds *int `json:"poll_interval_seconds"`
|
||||
Blocked bool `json:"blocked"` // reserved — ignored (slice 4)
|
||||
DesiredGeneration int64 `json:"desired_generation"` // reserved — ignored (slice 4)
|
||||
HasSignedOps bool `json:"has_signed_ops"` // reserved — ignored (slice 4)
|
||||
Blocked bool `json:"blocked"` // reserved — ignored
|
||||
DesiredGeneration int64 `json:"desired_generation"` // slice 10A: the cached-vs-current change signal
|
||||
HasSignedOps bool `json:"has_signed_ops"` // slice 10A: signed-jobs queue non-empty (exec 10B)
|
||||
}
|
||||
|
||||
// DesiredStateResponse is GET /hosts/{host_id}/desired-state (slice 10A — the "Down" channel's
|
||||
// heavy payload, fetched only when the envelope's generation advances). Generation is the
|
||||
// generation this state corresponds to, so the agent caches state+generation atomically. This is
|
||||
// a cross-repo wire contract (DUPLICATED in felhom.eu/hub until a shared module exists); the
|
||||
// desired-state golden stays byte-identical across the two repos.
|
||||
type DesiredStateResponse struct {
|
||||
Generation int64 `json:"generation"`
|
||||
DesiredState WireDesiredState `json:"desired_state"`
|
||||
}
|
||||
|
||||
// WireDesiredState is the hub's authoritative per-host target (slice 10A). The agent reconciles the
|
||||
// parts it can today (guests: benign deltas reconciled, an explicit decommission gated
|
||||
// pending_signature); the rest are FORWARD-COMPAT — carried + cached, NOT acted on in 10A. The
|
||||
// restore_directive is consumed in 10D (host/guest-loss DR); storage_manifest / backup_policy /
|
||||
// pbs_namespace are placeholders kept opaque so the wire is stable as those land.
|
||||
type WireDesiredState struct {
|
||||
Guests []WireDesiredGuest `json:"guests"`
|
||||
|
||||
StorageManifest json.RawMessage `json:"storage_manifest,omitempty"`
|
||||
BackupPolicy json.RawMessage `json:"backup_policy,omitempty"`
|
||||
PBSNamespace string `json:"pbs_namespace,omitempty"`
|
||||
RestoreDirective *WireRestoreDirective `json:"restore_directive,omitempty"` // slice 10D (forward-compat)
|
||||
}
|
||||
|
||||
// WireDesiredGuest is one guest's target (slice 10A). Every field is optional ("unmanaged"); the
|
||||
// agent's planner acts only on the fields that are set. Run is running|stopped|""; Spec reuses
|
||||
// GuestSpec (cores/memory_bytes/disk_bytes); Decommission is the EXPLICIT destructive delta (gated
|
||||
// pending_signature in 10A — executor is 10B).
|
||||
type WireDesiredGuest struct {
|
||||
VMID int `json:"vmid"`
|
||||
Run string `json:"run,omitempty"`
|
||||
Spec *GuestSpec `json:"spec,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Decommission bool `json:"decommission,omitempty"`
|
||||
}
|
||||
|
||||
// WireRestoreDirective is the forward-compat restore directive (slice 10D — host/guest-loss DR).
|
||||
// Defined now so the wire contract is stable; 10A carries it through to the cache but does NOT
|
||||
// consume it (no restore is initiated from desired-state in 10A).
|
||||
type WireRestoreDirective struct {
|
||||
Mode string `json:"mode,omitempty"` // guest_loss | host_loss (10D vocabulary)
|
||||
Archive string `json:"archive,omitempty"` // source archive/snapshot to restore from
|
||||
VMID int `json:"vmid,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user