Peer Node Model + Unified Control Plane — Architecture
Status: Design, written 2026-07-26. Approved (foundation). Phased build to follow.
This design records the foundational architecture for turning the project into a plug-and-play server monitoring app — one that is genuinely useful without any paid SaaS or hosted AI, and simple enough that clustering is as close to plug-and-play as the single-node case already is. It supersedes the implicit role-enforcement model and the two-orchestrator confusion.
Vision (the design contract)
Create a plug-and-play server monitoring app that does not require expensive SaaS or hosted AI, yet is good enough to democratize server management.
This is a constraint on the architecture, not a feature:
The core loop —
check → alert → notify— must be fully valuable with zero paid services and zero hosted AI. Everything else is an optional enhancer that may improve it but must never be required for it.
Free ≠ dumb: apps/intelligence/providers/local.py (LocalRecommendationProvider) is a rule-based engine that already produces real remediation advice (e.g. for disk_linux / disk_macos) with no external API. Local intelligence is the free default enricher; hosted AI is an optional upgrade.
Core decisions (foundational — expensive to reverse)
1. Peer + credentials — roles are descriptive, not enforced
Stop modeling “role” as an exclusive enum (standalone|agent|hub|conflict). Model behaviour as composable, derived facts. Primitives:
- Every node runs checks on itself.
- Every node exposes exactly one authenticated ingest endpoint (
POST /alerts/webhook/cluster/). - A node may push its results to one or more peers it holds credentials for.
- A node stores what it receives in a first-class
Noderegistry. - A node may aggregate, route, and notify.
“Hub” and “agent” become labels for how a node is used, shown to humans and derived from config + observed traffic. They are never validated or gated. This dissolves “multiple roles”, the “conflict” state, and role-enforcement complexity — a node simply does whatever its config + credentials allow (“roles added brain-free”).
2. The API key is the single ingest gate
There is no separate accept_incoming flag and no CLUSTER_ENABLED gate.
A node accepts a push iff the caller presents a valid, active API key that permits
/alerts/webhook/cluster/. Mint a key → the node is a receiver. Revoke/delete the keys → ingest is closed (401 for everyone). Default state (no keys) = closed.
The key both authorizes and scopes (folding in APIKey.allowed_endpoints). The only invariant the diagnostics enforce: API_KEY_AUTH_ENABLED stays on (the secure default we already ship), so the key is never bypassed. This retires CLUSTER_ENABLED as an ingest control; “am I a hub?” is derived from “do I have active keys and am I receiving?”.
3. The Node registry is the data spine
Today cluster pushes land as loose Alerts tagged with an instance_id string; there is no first-class node/agent record. Introduce a Node model (identity / instance_id, hostname, address, last-seen, and links to its results / incidents). Cluster ingest upserts a Node on every accepted push, so a receiver actually knows its peers. This one model is the load-bearing “store agent and hub data perfectly” decision, and it is the queryable record that the future report/view API reads. Cheap to get right now, expensive to retrofit.
4. Optionality contract — degrade gracefully, no paid dependency
Each dependency is an optional enhancer with graceful degradation; the core works with none of them:
- Intelligence: local rule-based provider is the default; hosted AI optional.
- Notification content is sourced from the alert/check data itself (what failed, which checker, severity, host,
trace_id). Intelligence enriches when present — it is never the producer and never a gate. (TodayNotifyExecutorbuilds the message from intelligence recommendations, so no AI ⇒ empty notifications. That coupling is cut.) - Celery/Redis: optional; the existing synchronous fallback stays.
- Vendors: email / generic webhook are the zero-cost defaults; Slack / PagerDuty are optional.
5. Unified control plane — commands are the API, surfaces are projections
- Management commands are the single implementation (the “verbs”). The CLI menu, installer, admin, and
cron_setupare thin clients that call them — no parallel shell-vs-Python logic. The CLI menu is generated from / validated against the command set + node state, so it cannot drift or show broken / irrelevant items (“entrypoints uniformed and unified”). preflight≡doctor: one diagnostic authority that reads node config × registry × per-capability requirements and reports what is missing + verifies end-to-end. No two separate diagnostic systems.cron_setupwires the scheduled pipeline / push as part of setup.- One unit, few commands: a small verb set (e.g.
setup,doctor,check,push,serve) rather than the current sprawl of overlapping entrypoints (install.sh cluster,setup_instance,create_api_key, CLI menus, admin, raw.env).
What this retires or corrects
CLUSTER_ENABLEDas an ingest gate, theaccept_incomingflag, the “conflict” role, and enforced role identity — all gone; role is derived.- The misleading
checkers.I001“0 pipeline definitions” signal for the webhook path: the cluster/webhook pipeline runs the definition-freePipelineOrchestratorand never needs aPipelineDefinition. Definitions become an optional, de-surfaced enhancer. - Notify’s dependency on hosted AI for content (see §4).
Deferred — allowed by this model, deliberately not built now
Per the repo’s over-build post-mortem, we do not build multi-hop machinery without a live requirement. The spine (Node + Alert + Incident) leaves the door open for:
- Alert → pipeline routing (aggregate related alerts, route to a chosen pipeline). The router was intentionally dropped earlier; revisit only with a concrete need.
- Multi-tier forwarding (a middle node propagating a child’s alert upstream). Cycle-free composition is allowed; forwarding/dedup/loop machinery is not built.
- API extraction for report / view apps — read models over the
Nodespine. - Richer aggregation, notification templating, admin config pages, guided-setup polish.
Phased build
- Phase 1 — Data spine + aligned diagnostics (mostly non-breaking).
Noderegistry model + upsert on ingest;doctorunified withpreflight; make the API key the sole ingest gate (retireCLUSTER_ENABLEDgating). - Phase 2 — Optionality / notify decoupling. Notification content from alert/incident data; local intelligence as default enricher; verify the full loop works with no paid AI/SaaS.
- Phase 3 — Unified entrypoints. Commands-as-API consolidation; role-aware generated CLI menu;
cron_setupwires the pipeline; guidedsetup as hub/agent; de-surface definitions and fix the misleading warning.
Each phase is independently shippable and leaves the system working.
Success criteria
- A fresh node monitors itself and sends a useful Slack/email alert with no AI and no paid service configured.
- Making a node a receiver is mint a key; disabling it is revoke the key — no other toggle.
doctoron any node reports exactly which of the required pieces are missing and verifies the cluster path end-to-end.- The CLI menu shows only actions valid for the node’s current state; nothing broken or irrelevant.
- A hub has a first-class, queryable record of every agent that has pushed to it.
- No enforced roles, no
CLUSTER_ENABLED/accept_incominggate; behaviour is derived from keys + config + traffic.