feat(hub): operator OOB peer + oob_peer_ip desired-state merge (H1 Part 1)

store.SetOperatorOOBPeer/GetOperatorOOBPeer (empty-host_id wg_peers row, explicit
/32, validated in-subnet/not-reserved/not-taken, last-write-wins rotation).
PUT/GET /admin/wg/operator-peer (global key). mergeWireguard adds oob_peer_ip when
an operator peer exists (absent = byte-identical). BumpAllHostGenerations forces
fleet re-fetch. Non-hollow tests both sides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-05 22:08:06 +02:00
parent a3ee93e97e
commit 0ec7555126
6 changed files with 342 additions and 1 deletions
+13
View File
@@ -1500,6 +1500,19 @@ func (s *Store) BumpHostDesired(hostID string) (int64, error) {
return gen, nil
}
// BumpAllHostGenerations advances EVERY host's desired_generation (TASK H1). Used when a FLEET-WIDE
// hub-owned served-state value changes — the operator OOB peer's /32 flows into every host's
// merge-at-read wireguard block (oob_peer_ip), so every agent must re-fetch + re-render. Returns the
// number of hosts bumped.
func (s *Store) BumpAllHostGenerations() (int64, error) {
res, err := s.db.Exec(`UPDATE hosts SET desired_generation = desired_generation + 1, updated_at = datetime('now')`)
if err != nil {
return 0, err
}
n, _ := res.RowsAffected()
return n, nil
}
// SignedJob is one OPAQUE operator-signed destructive-op blob queued for a host (slice 10A). The
// hub stores + serves the bytes; it never forges, opens, or executes them (10B owns verify+run).
type SignedJob struct {