Homelab¶
A different shape of connector: instead of pulling messages into cap_items,
the homelab agent watches your self-hosted infrastructure via Grafana,
proposes fixes, and — only once you approve — applies them over SSH. It is
its own module (modules/homelab), not part of capture, and every part of
it that touches the LAN or your SSH keys is host-only: it runs under
launchd on the owner's Mac, never in the container, and there is deliberately
no module.yaml schedule for the executor or the updates loop.
What it does¶
- Gather — reads firing Grafana alerts plus per-alert PromQL evidence
from VictoriaMetrics into a source-agnostic health snapshot
(
homelab_agent/gather.py,GrafanaHealthcheck). - Propose — an LLM turns a degraded snapshot into a remediation proposal (commands to run, on which host), gated at plan time so it never proposes an unsafe command.
- Approve — nothing runs until you review the proposal and approve it.
- Execute — on approval, the executor re-checks every stored command against the allowlist immediately before running it, applies the commands over SSH in order (stopping on first failure), then re-gathers from Grafana to verify the fix actually took.
Everything is recorded in four collections: hl_runs (gather runs),
hl_proposals (remediation proposals), hl_llm_log (audited LLM calls), and
hl_updates (monthly image-update proposals). A proposal (in either
hl_proposals or hl_updates) moves through the states
proposed → approved → applied (or skipped if you decline it).
Grafana access¶
The gather step needs read access to your Grafana instance:
| Env var | Required | Purpose |
|---|---|---|
JARVIS_GRAFANA_URL |
yes | Base URL of your Grafana instance |
JARVIS_GRAFANA_TOKEN |
yes | Bearer token for the Grafana/Prometheus API |
JARVIS_GRAFANA_DS_UID |
no | Datasource UID used to fetch per-alert PromQL evidence; without it, alerts are reported without evidence |
SSH targets¶
Proposals carry a logical host label (e.g. vm101), not a raw IP. The
executor resolves that label to an ssh alias through
homelab_agent/hosts.py's HOST_ALIASES map. Edit that map to match your own
hosts — each key is a Grafana instance label, each value is the ssh alias to
reach it (they can be identical, or the value can be a real hostname):
# Illustrative — replace with your own hosts (values are placeholders).
HOST_ALIASES = {
"proxmox": "proxmox",
"vm101": "<your-ssh-alias-or-hostname>",
"vm103": "vm103",
"servarrct": "servarrct",
"gssh": "gssh",
}
Each alias resolves to a real host and key through your own
~/.ssh/config — no IPs or keys are committed anywhere, and none are ever
written to an hl_* row. SSH runs non-interactively:
BatchMode=yes, ConnectTimeout=10, StrictHostKeyChecking=accept-new.
Approve → execute flow¶
Review the pending card in the Home inbox (or the hl_proposals row
directly), then approve or skip it:
python3 -m homelab_agent.approve <proposal_id>
python3 -m homelab_agent.approve <proposal_id> --skip
This flips hl_proposals.state proposed → approved (or skipped) and
marks the inbox card done. Applying is a separate step, run by the
com.jarvis.homelab.execute.plist LaunchAgent every 5 minutes:
python3 -m homelab_agent.execute --host
--host is a required marker — the executor refuses to run without it, so
it can never accidentally run container-side. Full install and shakedown
steps live in modules/homelab/agent/host/HOST_EXECUTE.md.
Monthly updates flow¶
A parallel loop scans your docker stacks for available image updates (moved registry digests on floating tags) and, on approval, redeploys just that service:
python3 -m homelab_agent.update_scan --host
python3 -m homelab_agent.approve <update_id> --collection hl_updates
python3 -m homelab_agent.update_execute --host
Two LaunchAgents drive this: com.jarvis.homelab.scan-updates.plist runs the
scan on the 1st of each month at 04:00 (under caffeinate, so a sleeping Mac
still wakes for it), and com.jarvis.homelab.update-execute.plist polls for
approved updates every 10 minutes. Full install and rollback steps live in
modules/homelab/agent/host/HOST_UPDATES.md.
Gotchas¶
Host-label alignment
Each proposal's host value comes from the LLM, which reads it off the
Grafana alert's instance/host label (port stripped). The keys in
HOST_ALIASES must match your real Grafana instance labels
exactly. If they don't, resolve_alias fail-closes on every proposal —
safe, but the executor silently "blocks" everything instead of applying
anything. After your first live run, confirm the proposal was actually
applied over SSH, not left approved with a "blocked at apply: no ssh
alias for host label …" card; if you see that, edit HOST_ALIASES to
match your labels.
Double-apply race
Neither executor has an atomic claim on approved rows. Hand-running
execute --host or update_execute --host while its LaunchAgent is
loaded risks the same proposal being applied twice. Run
launchctl unload ~/Library/LaunchAgents/com.jarvis.homelab.execute.plist
(or the update-execute equivalent) before hand-running, or only hand-run
before installing the LaunchAgent in the first place.
Rollback is not automated
If an applied update verifies unhealthy, nothing rolls it back for
you. The outcome card carries the recorded previous_image
(<repo>@<olddigest>); pin that digest in the service's compose file
and redeploy by hand:
docker compose -f <stack_path> up -d <service>