Etap 0 truth cleanup (recon B5/B6/F20), all facts re-verified 2026-07-30 via
read-only ssh (docker ps, world/services.json, events store):
- vps: add stability-agent, npm, outline, joplin-server, umami (all running
and healthy in world state). joplin keyed joplin-server to match the
world-state key — an aggregate 'joplin' entry would drift as
missing_service forever. humanai-mailer/humanai-landing documented as
unmanaged: true + monitor: false (hand-run, no compose labels — supervisor
must not queue dead-end redeploys for them). ai-cluster deliberately gets
NO entry: legacy, retiring — comment points at ai-cluster-LEGACY.md.
- piha: comment block enumerating the ~28 known unmanaged containers plus the
host systemd mosquitto; bringing them in is a later stage (recon OQ5).
- saturn: create services.yaml with explicit empty list (dev workstation /
orchestrator, nothing monitored — closes recon F20.4).
- lustro: add node-exporter (hyphenated key = actual container name) and
piper-tts (runs locally on the Pi, no services/ dir); watchtower noted as
deliberately unmanaged.
All YAMLs parse (yaml.safe_load); control-plane suite 147 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
node-agent runs as uid 1000 (homelab) since the base compose sets
user "1000:1000"; ssh in _ship_events_to_vps() has no -i flag and looks
for keys in $HOME/.ssh = /home/homelab/.ssh. The old mount target
/root/.ssh was never consulted, so rsync to VPS failed with
'Permission denied'. uid match (pi=1000 on RPi OS) keeps OpenSSH strict
ownership checks happy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Push-based deploy step for LUSTRO (git_control=false): rsync
services/node-agent/ and the host override to /opt/homelab/deploy/node-agent/
on the remote, then docker compose up --build via SSH.
Guard by effect: skip push+build+up if node-agent container already running
(docker ps filter, not command -v). Verify: container running + events appear
in /opt/homelab/events/lustro/ within 90 s (confirms agent write path).
Override (hosts/lustro/runtime/node-agent/docker-compose.override.yml):
- group_add: ["991"] (docker GID on LUSTRO; 999 from base concatenated — harmless)
- mem_limit: 256m (MagicMirror ~1.9 GiB; agent must be bounded)
- /home/pi/.ssh:/root/.ssh:ro (not /home/oskar/.ssh — pi user)
- /opt/homelab/deploy/node-agent:/repo:ro (no repo checkout on push-based node)
- NODE_NAME=lustro, NODE_TYPE=sd_card, VPS_EVENTS_HOST=100.95.58.48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three idempotent stages with guards (probe-before-mutate), rrun() for all
remote mutations, rprobe() for unconditional state queries. Reads
hardware.swap.mb from node.yaml (default 2048 MB). Adds swap.mb: 2048
to hosts/lustro/node.yaml so the value is declarative.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
00-access.sh implements a 3-stage idempotent access bootstrap:
1. ensure_ssh_key — ssh-copy-id to first_contact (pi@pimirror2.local),
skips if BatchMode key-auth already passes
2. ensure_tailscale — install via install.sh if missing, then tailscale up
--hostname=lustro; prints interactive auth URL to operator, blocks until
authenticated; skips if BackendState already Running
3. verify — SSH over Tailscale to pi@lustro, asserts 'ok' + arch=aarch64
Reads first_contact and tailscale.hostname from node.yaml.
Respects --dry-run. No NOPASSWD or /opt/homelab mutations.
hosts/lustro/node.yaml: fill known hardware facts (arm64, 4096 MB RAM,
zram swap, docker_present, mm_runtime=systemd:magicmirror.service),
add ssh_user=pi, first_contact=pi@pimirror2.local,
services.node-agent.runtime engine=docker mem_limit=256m.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>