--- okf: "0.1" type: decision visibility: private status: active updated: 2026-08-03 links: - ../phases/backlog.md --- ## Zamknięte ### Publiczny bind `operator_ui.py:18180` na VPS bez autoryzacji — NAPRAWIONE (2026-07-22, commit `9a5c160`) **Data**: 2026-07-22 **Źródło**: sesja 2026-07-22/23 (`docs/sessions/2026-07-23-control-plane-remediation-e2e.md`) **Było**: `operator_ui.py` (port 18180) nasłuchiwał na `0.0.0.0` na publicznym VPS (`135.181.153.108`) bez żadnej autoryzacji. `curl` z zewnątrz do `/actions` zwracał HTTP 200. `do_POST /action/mutate` przenosi akcje między stanami WŁĄCZNIE z `"approved"` — dowolna osoba z internetu mogła zatwierdzić akcję remediacyjną. Jedyne co chroniło do tej pory: executor nie umiał jeszcze wykonać akcji (brak SSH, patrz wpis niżej) — przypadek, nie zabezpieczenie. **Naprawione**: dual-bind wg wzorca fleet-prometheus — `127.0.0.1:18180:8080` + `${TAILSCALE_BIND_IP}:18180:8080`, nowy `services/control-plane/env.example`. Zweryfikowane po deployu: publiczny IP → HTTP 000, Tailscale → HTTP 200. Konsumenty (node-agent VPS, materializer PIHA) nietknięte. **Footgun zapamiętany**: brak `.env` → `docker compose` tylko OSTRZEGA i po cichu wraca do bindu `0.0.0.0`, nie failuje — patrz wpis „deploy-local.sh: brak twardego checka .env" w Aktywnych. **Pozostaje osobno**: `operator_ui.py` nadal bez żadnej autoryzacji (bind zamknięty chroni przed internetem, nie przed kimkolwiek w mesh Tailscale) — patrz Aktywne. --- ### Remediacja floty bez SSH: executor→node-agent pull przez rsync — ZROBIONE (2026-07-22, commit `2dac154`), E2E potwierdzone 2026-07-23 **Data**: 2026-07-22 (implementacja), 2026-07-23 (pierwszy udany cykl E2E) **Źródło**: sesja 2026-07-22/23 (`docs/sessions/2026-07-23-control-plane-remediation-e2e.md`) **Było**: łańcuch supervisor→pending→approved→running działał, ale executor próbował `ssh oskar@{node} docker restart {container}` — kontener control-plane nie ma klienta ssh, klucza, ani rozwiązywalnych nazw węzłów. Wykonanie zawsze failowało. **Decyzja architektoniczna**: bez SSH w executorze (kontener na publicznym VPS z powłoką na całą flotę = zły blast radius). Kierunek PULL: executor zleca (zapis `actions/dispatch//.json`), node-agent na docelowym węźle wykonuje lokalnie przez własny `docker.sock`, wynik wraca eventem `action_result` przez istniejący rsync-push. VPS nigdy nie inicjuje połączenia do węzła. **Zabezpieczenia**: walidacja `node == self.node_name`; whitelist typów akcji = `{container_restart}`; guard przed restartem samego node-agenta; brak wykonywania dowolnych poleceń z payloadu; idempotencja (ponowne zlecenie = no-op). 183 testy. **Potwierdzone w boju (2026-07-23)**: cykl `test-e2e-b` — Executing → Dispatched → Completed w 31 sekund, `node_exporter` na PIHA realnie zrestartowany (uptime 30h→ minuty), zero połączeń SSH. Pierwszy w historii systemu pełny cykl remediacji. --- ### Uprawnienia `actions/` na PIHA (uid/gid oskar 1004 vs kontener 1000) — NAPRAWIONE ręcznie (2026-07-23, poza repo) **Data**: 2026-07-23 **Źródło**: sesja 2026-07-22/23 (`docs/sessions/2026-07-23-control-plane-remediation-e2e.md`) **Było**: pierwszy test E2E remediacji bez SSH padał — agent logował `[Errno 13] Permission denied: /opt/homelab/actions/dispatch` co cykl. `/opt/homelab/actions` było `oskar:oskar drwxr-xr-x` (utworzone w maju), a działający wzorzec to `/opt/homelab/events` = `oskar:pi drwxrwsr-x` (grupa `pi`, zapis grupowy, setgid). Ten sam motyw uid/gid (host oskar 1004 vs kontener 1000) uderzył już czwarty raz — patrz sekcja „Tech-debt: globalny porządek uid/gid/uprawnień we flocie". **Naprawione (ręcznie, tylko na PIHA)**: `chgrp -R pi` + `chmod -R g+w` + `chmod g+s` na `/opt/homelab/actions`. Executor zachował się poprawnie podczas awarii: po 300s timeoutu przeniósł akcję do `failed` z czytelnym powodem, nic nie zawisło. **Pozostaje osobno**: fix zastosowany TYLKO na PIHA — SOLARIA i lustro niezweryfikowane; docelowy fix systemowy to node-agent tworzący `dispatch//` z właściwymi prawami przy starcie — patrz Aktywne. --- ### Supervisor: pętla zamrożona ~24h, healthy ale nie tika — NAPRAWIONE (2026-07-16, commit `409b583`) **Data**: 2026-07-16 **Źródło**: sesja 2026-07-16 (`docs/sessions/2026-07-16.md`) — wątek control-plane **Było**: kontener supervisora `healthy`, ale pętla `reconcile()` nie tikała od ~24h, zero logów. Root cause zweryfikowany na `/proc` (`State:S hrtimer_nanosleep`, NIE deadlock): `glob` po `EVENTS_DIR` co cykl przy 358k plikach → cykl przekracza brak timeoutu → nigdy się nie kończy. Logi na DEBUG maskowały objaw. **Naprawione**: każdy cykl w `ThreadPoolExecutor` z `future.result(timeout=90s, env SUPERVISOR_RECONCILE_TIMEOUT)`; try/except owija cykl (wyjątek nie zabija pętli); tick-log co 10 cykli (`SUPERVISOR_TICK_LOG_EVERY`) na INFO; healthcheck sprawdza świeżość heartbeat, nie tylko czy proces żyje. Zweryfikowane w boju: pętla tika (cycle #340→#480), cykl #1 timeoutował ale pętla kontynuowała = odporność działa. **Lekcja**: „healthy kontener ≠ tikająca pętla" — healthcheck musi sprawdzać świeżość ostatniego cyklu, nie samo czy proces odpowiada. --- ### Event flood 358k plików + retencja martwa od fixu checkpointu — NAPRAWIONE (2026-07-16, commit `dff76ec`) **Data**: 2026-07-16 **Źródło**: sesja 2026-07-16 (`docs/sessions/2026-07-16.md`) — wątek control-plane **Było**: `EVENTS_DIR` = 358k plików, 91% to `service_healthy` emitowany co cykl per serwis (stan-jako-zdarzenie, antywzorzec). Retencja `_cleanup_control_plane_fs()` była martwa od fixu checkpointu `d5139c9` (2026-07-15, patrz „Bug: checkpoint observera po ścieżce leksykalnej" niżej) — porównanie `str(ścieżka) <= checkpoint_int` rzucało `TypeError` cicho łapany przez szeroki `except` → backlog rósł bez ograniczenia. Naprawiając checkpoint wczoraj, złamaliśmy retencję, która na nim polegała. **Naprawione**: (a) node-agent emituje `service_healthy` tylko na transition unhealthy→healthy (funkcja dla `observer.process_event` zachowana); (b) retencja naprawiona epoch-do-epoch; (c) `scripts/maintenance/cleanup_event_backlog.py` (dry-run + `--apply`). 150 testów pass. Cleanup wykonany na PIHA+VPS: 272 232 pliki usunięte, backlog 358k→12,7k. **Wynik**: reconcile supervisora przestał timeoutować. **Lekcja**: migracja typu pola (ścieżka→epoch int) musi audytować WSZYSTKICH konsumentów tego pola — szeroki `except` maskował dokładnie tę klasę regresji. --- ### Ghost kontenery w panelu (Problem B) — ZNIKNĘŁY (potwierdzone reconem 2026-07-02) > ⚠️ ZDEZAKTUALIZOWANE 2026-07-06: ghosty znów widoczne na VPS — patrz wpis > „Ghosty B WRÓCIŁY" w Aktywnych (`docs/sessions/2026-07-06.md`). **Data**: 2026-06-24 (wykryte), 2026-07-02 (zamknięte) **Źródło**: sesje 2026-06-24/25/26; recon `kb/subsystems/fleet-inventory-verify.md` **Było**: martwe kontenery ze starych project-name'ów (`8547b46c0317_control-plane-supervisor` itp.) raportowane przez observera jako `error` → `System Status ERROR` w panelu mimo zdrowego mózgu. **Zamknięte**: recon 2026-07-02 — 24 kontenery na VPS, ZERO hash-prefixed. Prawdopodobnie recreate'y z kolejnych deployów je zmiotły. Zero akcji ręcznej. Rozważenie czyszczenia obcych project-name przy deployu — już nieaktualne (fix A `3b71707` blokuje źródło divergence). --- ### brain-watchdog: poll Prometheus — POTWIERDZONY (recon 2026-07-02) **Data**: 2026-06-30 (pending), 2026-07-02 (zamknięte) **Źródło**: recon `kb/subsystems/fleet-inventory-verify.md` **Było**: log startowy nie wypisuje `PROMETHEUS_URL` → brak pewności, że polling aktywny; diagnoza opierała się na `.env` i braku błędów. **Zamknięte**: recon potwierdził — obraz zbudowany po `62d6fc0`, `PROMETHEUS_URL` w `.env` I w env kontenera, zero `poll failed` w logach. Poll aktywny. Jednolinijkowy log startowy (`polling enabled/disabled`) pozostaje opcjonalną kosmetyką. ✅ Pełne end-to-end (firing → Telegram) POTWIERDZONE 2026-07-06 testem `AlertTestEtap0` (Etap 0 cutoveru, `docs/sessions/2026-07-06.md`) — bez czekania na realną awarię. --- ### Miny #1/#2/#3 z weryfikacji inwentaryzacji — ROZBROJONE (2026-07-02) **Źródło**: `kb/subsystems/fleet-inventory-verify.md`, sesja `docs/sessions/2026-07-02.md` (tam szczegóły i lekcje). - **#1 PIHA checkout**: gałąź wciąż `task/kb-gmail-import` po resecie z 2026-06-30 (reset --hard przesuwa gałąź, nie przełącza) → `checkout master && pull`, 30 commitów nadrobione. - **#2 control-plane na SATURN**: supervisor ślepy (brak mountu repo) → `docker compose down`, wolumeny zachowane. Jedyny mózg = VPS. - **#3 owner_node**: forgejo→piha, mosquitto→vps (commit `886bc85`). --- ### 🔴 KRYTYCZNY — `deploy.sh vps` niszczył control-plane — NAPRAWIONE (commit `3b71707`) **Data**: 2026-06-24 (wykryte), 2026-06-25 (incydent w produkcji), 2026-06-26 (naprawione) **Źródło**: sesja 2026-06-24 + 2026-06-25 + 2026-06-26 (`docs/sessions/2026-06-26.md`) **Było (Problem A)**: `control-plane` w `hosts/vps/services.yaml` jako zwykły serwis pętli `deploy-node.sh`. Pętla używała innego `COMPOSE_PROJECT_NAME` niż `deploy-local.sh` (cwd=`services/control-plane`). Niezgodność → Recreate → `No such container: _control-plane-observer` → `set -e` przerywa pętlę → observer/supervisor/executor/ui znikają. Każdy `deploy.sh vps` rozkładał mózg (potwierdzone w produkcji 2026-06-25). **Naprawione**: guard w `deploy-node.sh` pomijający serwisy z własnym `services//deploy-local.sh`. `control-plane` ZOSTAJE w `services.yaml` (gate pytest+build nadal go testuje), pomijana jest tylko destrukcyjna pętla deployu. **Potwierdzone w boju**: `deploy.sh vps` wypisał `Skipping control-plane: ma własną ścieżkę deployu`, mózg `Up 25h healthy`, nietknięty. **Pozostało osobno**: ghosty z poprzednich rozjazdów (Problem B — patrz Aktywne). --- ### Flaky testy control-plane — state-leak w pytest — NAPRAWIONE (commit `992ff7c`) **Data**: 2026-06-24 (zgłoszone), 2026-06-25 (naprawione) **Źródło**: sesja 2026-06-24 + sesja 2026-06-25 (`docs/sessions/2026-06-25.md`) **Było**: `test_incident_lifecycle.py` flaky przez state-leak — `OBSERVER_STATE_FILE` wyprowadzany przy imporcie, helpery patchowały `STATE_DIR` ale nie `OBSERVER_STATE_FILE` → checkpointy pisane na realny dysk `/opt/homelab/state/` z ścieżkami otagowanymi numerem przebiegu pytest. Gate deploy.sh czerwony przy zdrowym kodzie. **Naprawione**: autouse monkeypatch fixture redirectujący WSZYSTKIE ścieżki stanu w tym `OBSERVER_STATE_FILE`; usunięto buggy `_make_observer`; posprzątano zatruty realny checkpoint. Weryfikacja: 6/6 przebiegów → 28 passed. Gate rzetelny. --- ### deploy-node.sh — brak `--env-file` (bind 0.0.0.0) — NAPRAWIONE (commit `686aca7`) **Data**: 2026-06-25 **Źródło**: sesja 2026-06-25 (`docs/sessions/2026-06-25.md`) **Było**: `deploy-node.sh` nie przekazywał `--env-file` do `compose up` → zmienne `.env` (jak `TAILSCALE_BIND_IP`) interpolowały się do pustego stringa → bind `0.0.0.0` zamiast Tailscale IP. Potencjalna dziura na publicznym VPS. **Naprawione**: guard `if [ -f "services//.env" ]` + `--env-file` per-serwis przed `docker compose up`. Worktree `task/deploy-envfile-fix`, merge ff-only. --- ### Swap 2–4 GB na VPS — ZROBIONE **Data**: 2026-06-22 (zgłoszone PENDING w sesji 2026-06-09 flota-recovery) **Źródło**: sesja 2026-06-22 (`docs/sessions/2026-06-22.md`) **Było**: VPS (3.7 GB RAM) z `swap=0` → OOM 2026-06-01. **Zrobione**: `/swapfile` 4 GB aktywny i trwały (wpis w `/etc/fstab`); `vm.swappiness=10` na żywo i w `/etc/sysctl.conf`. Weryfikacja: `free -h` → `Swap: 4.0Gi (0B used)`, `/proc/swaps` zawiera `/swapfile`. **Uwaga**: host-level one-off (nie czysto GitOps), udokumentowany jako celowy host-state w `hosts/vps/host.yaml`. Brak commitu kodu — zmiana host-side gotowcem. --- ### Observer staleness — martwy node pokazywany NOMINAL **Data**: 2026-06-08 (złapane), status: OTWARTY w sensie implementacji **Problem**: observer/supervisor trzyma ostatni znany stan; brak heartbeat TTL. Chelsty-infra milczy, ale status NOMINAL podważa zaufanie do panelu. **Fix**: heartbeat TTL → po przekroczeniu oznacz status `stale` lub `down`. **Powiązane**: brain-watchdog ślepy na per-node freshness. *(Otwarty jako TODO implementacyjny — przeniesiony z sesji 2026-06-08)*