---
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/<node>/<action_id>.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/<node>/` 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:
<hash>_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/<svc>/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/<svc>/.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)*

