docs: record workflow roadmap and deployment policy
ci / check (push) Successful in 1m30s
ci / publish (push) Successful in 45s

This commit is contained in:
homelab-updater committed 2026-09-21 00:38:18 +02:00
1 parent 96f37f8e4a
commit 54aaa6c0f0
4 files changed
+189 -4

No files matched your search

+1 -1
View File
@@ -70,7 +70,7 @@ services:
EXECUTOR_URL: http://executor:8081
HOST_BACKUP_FILE: /host/vzdump.json
MODEL_URL: http://model:8083
# From .env next to this file (never in the repo): HA_URL, HA_TOKEN, HA_NOTIFY; NIGHTLY_AT=03:00 turns the night on.
# From .env next to this file (never in the repo): HA_URL, HA_TOKEN, HA_NOTIFY; NIGHTLY_AT=04:00 turns the night on.
HA_URL: ${HA_URL:-}
HA_TOKEN: ${HA_TOKEN:-}
HA_NOTIFY: ${HA_NOTIFY:-notify.notify}
+2 -2
View File
@@ -65,7 +65,7 @@ Jedno ustawienie w Apps → aplikacja, domyślnie wyłączone:
- **Post-upgrade check executor**: `sonnet` / `opus` = po udanym `verify` model sprawdza, czy aktualizacja wymaga czynności poinstalacyjnych (czyta ocenę notatek, skill i stan usługi; co robić po aktualizacji, można dopisać w skillu). Model wykona tylko to, co dopuszcza `config/agent-exec.json` (dokładne komendy, bez wildcardów); reszta trafia do właściciela jako STOP `migration_required`. Dziś lista ma tylko komendy Nextclouda: kontrola odbiorcza ze skilla (tylko odczyt: `php occ status` z `--output=json` i bez, `php occ config:app:get core lastcron`, `php occ app:list`, `pidof crond` w usłudze `cron`) oraz to, co po upgrade brakuje w bazie (`php occ db:add-missing-indices`, `db:add-missing-columns`, `db:add-missing-primary-keys`; użytkownik `www-data`, usługa `app`). Model widzi listę przez `hl exec-rules` i nie zgaduje. Dla pozostałych aplikacji ten krok tylko wypisze, co zrobić ręcznie.
Schemat bazy przechodzi na v9 (v8 dodał `post_upgrade_executor`, v9 zdejmuje niepotrzebną `release_notes_source`); starszy obraz jej nie otworzy, więc przed wdrożeniem kopia `queue.sqlite*`.
Schemat bazy przechodzi na v10 (v8 dodał `post_upgrade_executor`, v9 zdejmuje niepotrzebną `release_notes_source`, v10 dodaje niepodłączony jeszcze katalog i instancje nowego silnika workflow); starszy obraz jej nie otworzy, więc przed wdrożeniem kopia `queue.sqlite*`.
## Tryb „na sucho”: jak testować
@@ -152,7 +152,7 @@ Ręczny powrót do dowolnej kopii: STOP w kolejce → rollback, albo z kroku z `
HA_NOTIFY=notify.<twoja_usługa>
```
`update.sh` zmienia w `.env` tylko linię `TAG=`.
4. **Noc**: `NIGHTLY_AT=03:00` w tym samym `.env` (puste = bez przebiegu nocnego). Stary `homelab-nightly-update.timer` na Proxmoksie wyłącz dopiero wtedy, gdy nowy przebieg nocny działa: `systemctl disable --now homelab-nightly-update.timer`.
4. **Noc**: `NIGHTLY_AT=04:00` w tym samym `.env` (puste = bez przebiegu nocnego). Po wdrożeniu fundamentu workflow 2026-09-21 wartość pozostaje pusta do osobnej decyzji właściciela i testów ręcznych. Stary `homelab-nightly-update.timer` na Proxmoksie wyłącz dopiero wtedy, gdy nowy przebieg nocny działa: `systemctl disable --now homelab-nightly-update.timer`.
5. `./update.sh` po każdej zmianie `.env`.
### Jak działa
+20 -1
View File
@@ -131,7 +131,26 @@ Kierunek: kontener bez roota, system plików tylko do odczytu poza wolumenami, m
`skills/homelab-compose-ops/scripts/` (≈1230 linii bash + Python): apply, apply_support.py, backup, compose_ps_json.py, guest_lib.sh, image_resolve, image_tags, image_version, plan, preflight, preflight_support.py, pve_agent_exec.py, rollback, status, verify. Skille aplikacji: `skills/*-compose-upgrade/SKILL.md` (15).
Stan docelowy: czysty TypeScript, bash jako wzorzec. Port po jednym skrypcie z testem kontraktowym (wynik TS zgodny z bashem na zapisanym JSON-ie). `guest_lib.sh` znika, bo driver działa na VM103 i `CommandRunner` wykonuje polecenia lokalnie. Kolejność od najmniej ryzykownych: status, image_tags, image_resolve, preflight, verify; potem backup, apply, rollback (mutacje, najwięcej testów).
## Plan etapów
## Rozszerzalny silnik workflow (decyzja właściciela, 2026-09-20)
Obecna kolejka kroków pozostaje działającą bazą podczas rozbudowy. Docelowy silnik ma pozwalać układać i wersjonować przepływy niezależnie od konkretnego dostawcy modelu oraz dodawać kroki systemowe bez rozbudowywania drivera o kolejne wyjątki.
- Definicja workflow przechodzi cykl `draft → validate → publish`. Opublikowane wersje są niemutowalne, a każda instancja wskazuje konkretną wersję.
- Każda instancja zapisuje pełny snapshot konfiguracji potrzebnej do odtworzenia przebiegu: dokumenty workflow, konfiguracje aplikacji, skille, profile modeli, wersje funkcji i politykę akcji uprzywilejowanych.
- Przejścia stanu są czystą, deterministyczną maszyną XState 5. Do SQLite zapisujemy własną wersjonowaną projekcję domenową i append-only eventy, a nie wewnętrzny snapshot aktora XState.
- Workflow może zawierać zagnieżdżone podworkflow i sekwencyjne `for-each`, ale nie rekursję. Globalnie wykonywany jest najwyżej jeden node. Wiele instancji może jednocześnie oczekiwać w `queued` lub `waiting`; wyłączność obejmuje tylko status `running`.
- `workflow_instance` oznacza całe uruchomienie workflow, przyszły `node_attempt` pojedynczą próbę node'a, a `action_invocation` wywołanie akcji uprzywilejowanej.
- Kroki użytkownika to wersjonowane funkcje TypeScript albo POSIX shell zapisane w SQLite. Mają obowiązkowe testy oraz ścisły wynik `{event, summary, data}`. Wykonuje je izolowany, efemeryczny runtime z jawnymi limitami i capabilities.
- Operacje wymagające wysokich uprawnień są zamkniętymi akcjami w executorze, oddzielonymi od funkcji użytkownika. Przykład docelowej reguły: po spełnieniu warunku wolnego miejsca workflow może wywołać jawnie dozwoloną akcję Docker image prune. Sam `prune` nie jest domyślnym zachowaniem i wymaga osobnej polityki oraz implementacji.
- Integracja modeli korzysta z portu `ModelExecutor` i jawnych, wersjonowanych profili. Claude i Codex działają jako osobne sidecary; błędy nie powodują cichego przełączenia dostawcy. Kolejny dostawca, np. Gemini, ma wymagać nowego adaptera i profilu, nie zmian w domenie workflow.
- Profile Codex są udostępniane przez MCP. Konfiguracja modelu i narzędzi należy do snapshotu instancji.
- Edytor webowy używa generycznego grafu Cytoscape z możliwością zmiany kolejności, przypinania pozycji oraz rozwijania zagnieżdżeń. Monaco służy do edycji funkcji. Układ wizualny jest osobną, wersjonowalną metadaną i nie zmienia semantyki workflow.
- Historia aplikacji jest append-only. Widok w stylu osi Jira pokazuje eventy, zmianę stanu, aktualny node i powiązaną instancję bez wyprowadzania prawdy z logów tekstowych.
- Obecny proces aktualizacji, jego bramki i zakazy zostaną odwzorowane jako opublikowany workflow oraz porównane w trybie shadow przed przełączeniem. Do tego czasu nowy subsystem działa obok obecnego drivera.
Szczegółowe etapy i obowiązkowy cykl pracy agentów są zapisane w `docs/IMPLEMENTATION.md`.
## Historyczny plan budowy obecnego systemu
0. Decyzje: repo, Node, klucz — zrobione. Do zrobienia przez właściciela: domena appki w traefik, logowanie `claude` w boksie, kształt boksu.
1. **Fundamenty (pierwsza sesja):** szkielet TS, domain, SQLite (schemat + migracje), driver z pętlą zdarzeń i fałszywymi handlerami, serwer HTTP + SSE, mały frontend (przebiegi, kroki, kolejka STOP, akcja rozstrzygnięcia), testy, `npm run check`.
+166
View File
@@ -0,0 +1,166 @@
# Plan rozbudowy silnika workflow
Stan na 2026-09-20. Ten dokument jest operacyjnym planem przyjętym przez właściciela. Decyzje architektoniczne pozostają w `docs/DESIGN.md`; tutaj zapisujemy etapy, kryteria odbioru i stan realizacji.
## Stały cykl pracy
Każdy etap przechodzi tę samą pętlę:
1. Czysta sesja `gpt-5.6-sol`: doprecyzowanie planu etapu, ryzyk i kryteriów odbioru.
2. Czysta sesja `gpt-5.6-terra`: implementacja wyłącznie zatwierdzonego zakresu, test-first dla `domain` i `app`.
3. Czysta sesja `gpt-5.6-sol`: niezależna weryfikacja diffu i testów, lista ewentualnych poprawek oraz plan następnego etapu.
4. Czysta sesja `gpt-5.6-terra`: poprawki wykryte w weryfikacji.
5. Powtarzamy weryfikację, aż kryteria odbioru etapu są spełnione; dopiero wtedy zaczynamy następny etap.
Każda sesja najpierw czyta `AGENTS.md`, `docs/DESIGN.md` i ten dokument. Nie wykonuje commitów ani pushy bez osobnego polecenia. Nie dotyka VM103, Proxmoxa ani działających kontenerów. Istniejące lokalne zmiany właściciela muszą pozostać nienaruszone.
## Założenia przekrojowe
- Nowy subsystem powstaje obok obecnego drivera aż do kontrolowanego przełączenia.
- XState 5 jest czystym silnikiem przejść; I/O pozostaje w adapterach. Persistujemy własne projekcje i eventy domenowe.
- Opublikowane workflow, funkcje i profile są niemutowalne. Instancja ma pełny snapshot zależności.
- W kolejce może być wiele instancji `queued` i `waiting`; globalnie istnieje najwyżej jedna `running`.
- Użytkownik może składać kroki w generyczne workflow. Funkcje użytkownika i uprzywilejowane akcje systemowe są odrębnymi mechanizmami bezpieczeństwa.
- Adapter dostawcy modelu nie przenika do domeny. Profil wybiera wykonawcę jawnie i nie ma automatycznego fallbacku.
- Event ledger jest źródłem osi czasu aplikacji oraz odtwarzalnego bieżącego stanu.
## Decyzja wdrożeniowa 2026-09-21
- Dzisiejszy przebieg nocny jest anulowany. Na VM103 `NIGHTLY_AT` został wyzerowany, odtworzono wyłącznie usługę `driver` i potwierdzono brak przebiegu `nightly-20260921*`.
- Prywatna kopia `.env` sprzed zmiany znajduje się obok manifestu jako `.env.before-disable-nightly-20260921`; nie trafia do repozytorium.
- Po zakończeniu wszystkich etapów nowy system ma zostać wdrożony na VM103, ale harmonogram nocny pozostaje wyłączony. Właściciel będzie najpierw wykonywał testy ręczne.
- `NIGHTLY_AT` wolno ponownie ustawić dopiero na osobne, jawne polecenie właściciela po odbiorze testów ręcznych.
- Końcowe wdrożenie obejmuje backup, `docker compose config --quiet`, aktualizację tylko wymaganych usług oraz weryfikację `ps`, health, logów i endpointu. Nie używamy `down -v` ani nie usuwamy danych.
## Etapy
### Etap 1 — katalog workflow i instancje wykonania
Status: **zakończony i zweryfikowany 2026-09-21**.
Cel: dodać nieużywany jeszcze przez produkcyjny driver fundament wersjonowania i deterministycznego lifecycle'u.
Zakres:
- Ścisłe, wersjonowane typy domenowe dokumentu workflow, szkicu, diagnostyki walidacji, opublikowanej wersji, instancji, eventu i pełnego snapshotu.
- Na tym etapie payload grafu pozostaje nieprzezroczystym JSON-em walidowanym przez wstrzyknięty walidator. Docelowa składnia node'ów zostanie zamknięta w etapie 2.
- Czysta maszyna XState 5 dla stanów `queued`, `running`, `waiting`, `succeeded`, `failed`, `cancelled`.
- Minimalne przejścia: `CLAIM`, `NODE_COMPLETED` do `queued`, `NODE_WAITING`, zakończenia terminalne oraz `RESUME` z `waiting` do `queued`. Stan terminalny nie przyjmuje dalszych eventów.
- Port katalogu: utworzenie workflow i szkicu, zapis z `expectedRevision`, walidacja dokładnej rewizji, atomowa publikacja tylko niezmienionej poprawnej rewizji, jawne sklonowanie wybranej wersji do nowego szkicu.
- Oddzielny port `WorkflowInstanceStore`: utworzenie instancji wskazującej konkretną wersję, odczyt historii, atomowe dopisanie eventu z oczekiwanym numerem sekwencji oraz atomowy `claimNext(now)`.
- Migracja SQLite v10 dodająca wyłącznie nowe tabele: katalog workflow, szkice, wersje, `workflow_instances` i append-only `workflow_instance_events`.
- Unikalny indeks częściowy obejmuje tylko `status = 'running'`. Dowolna liczba rekordów `queued` i `waiting` jest poprawna.
- `claimNext` używa transakcji `BEGIN IMMEDIATE`: sprawdza brak `running`, wybiera najstarszą należną instancję, wykonuje przejście `CLAIM`, razem dopisuje event i aktualizuje projekcję. Brak kandydata lub istniejący `running` zwraca `null`.
- Dokumentacja nowych reguł w `docs/DESIGN.md` i ten plan wykonawczy.
Poza zakresem:
- Integracja z obecnym driverem, `main`, HTTP, UI, handlerami i executorem.
- Ostateczny graf node'ów, interpreter, podworkflow, `for-each`, funkcje użytkownika, akcje uprzywilejowane, profile modeli oraz sidecary.
- Zmiany wdrożeniowe i jakiekolwiek prawdziwe operacje na Dockerze.
Kryteria odbioru:
- Wiele instancji może być `queued` i `waiting`; oczekująca instancja nie blokuje innej.
- Równoległe próby claimu dają najwyżej jedną instancję `running`; gdy taka istnieje, kolejny claim zwraca `null`.
- `RESUME` zwraca instancję do `queued`, nigdy bezpośrednio do `running`.
- Event i projekcja commitują się razem albo oba są wycofane; wersje i eventy są niemutowalne.
- Publikacja odrzuca nieważną albo zmienioną po walidacji rewizję; kolejne publikacje tworzą monotoniczne wersje.
- Snapshot jest pełny, wersjonowany i odczytywalny po ponownym otwarciu bazy.
- Nowe moduły nie są importowane przez istniejącą ścieżkę wykonawczą, a jej zachowanie pozostaje bez zmian.
- `npm run check` przechodzi.
### Etap 2 — graf i interpreter
Status: **zaplanowany; oczekuje na osobne uruchomienie cyklu Sol → Terra**.
Cel: zamknąć semantykę grafu oraz dodać czysty interpreter z trwałą pozycją wykonania. Nadal bez podłączania nowego subsystemu do obecnego drivera i bez funkcji użytkownika, akcji uprzywilejowanych, modeli, HTTP ani UI.
#### Dokument grafu v2
- `WorkflowDocument` staje się wersjonowaną unią. Istniejący dokument v1 pozostaje odczytywalnym, nieprzezroczystym formatem; nie wolno zmienić jego znaczenia w miejscu. Wykonywalny graf otrzymuje `schemaVersion: 2`.
- Snapshot także ewoluuje przez unię: snapshot v1 pozostaje bez zmian, a wykonywalna instancja grafu używa snapshotu v2 z dokumentami v2. Nie rozszerzamy po cichu znaczenia `schemaVersion: 1`.
- Graf v2 zawiera `entryNodeId`, stabilne i unikalne `nodeId` oraz jawne przejścia po nazwanych eventach.
- Typy core: `emit`, `wait`, `subworkflow`, `forEach`, `succeed`, `fail`. Przyszłe funkcje, akcje i modele korzystają z rejestru rozszerzeń discriminated union, ale nie są implementowane w tym etapie.
- `subworkflow` oraz body `forEach` wskazują konkretny `workflowId@version`, który musi istnieć w snapshocie instancji.
- `forEach` pobiera tablicę przez ograniczony JSON Pointer. Lista elementów jest zamrażana przy wejściu, a elementy wykonywane są ściśle sekwencyjnie.
- Graf v2 jest DAG-iem; jedyną kontrolowaną iteracją jest `forEach`. Layout edytora nie należy do dokumentu semantycznego.
#### Walidacja publikacji
- Wstrzyknięty walidator v2 sprawdza duplikaty, dangling targets, nieosiągalne node'y, brak drogi terminalnej i niejednoznaczne route'y.
- Wszystkie referencje do podworkflow są przypięte do wersji i obecne w zamrożonym zestawie dokumentów.
- Iteracyjne wykrywanie cykli obejmuje cały graf zależności workflow, w tym self-reference i cykle pośrednie. Implementacja nie używa rekursywnych wywołań JS.
- Obowiązują jawne limity liczby node'ów, głębokości ramek i liczby elementów `forEach`; wartości zostaną nazwanymi stałymi domenowymi i będą testowane na granicach.
#### Czysty interpreter i trwałe ramki
- Interpreter jest pure reducerem: przyjmuje zamrożone dokumenty, własną wersjonowaną projekcję wykonania oraz event/outcome, a zwraca nową projekcję i deklaratywne komendy. Nie wykonuje I/O.
- Ramka `workflow` przechowuje przypięty workflow, bieżący node, wejście/kontekst i continuation.
- Ramka `forEach` przechowuje owner node, zamrożone items, indeks, body ref i zebrane wyniki. Na stosie znajduje się najwyżej jeden aktualny element pętli.
- `subworkflow` dodaje child frame; terminal childa zdejmuje ramkę i wraca do continuation rodzica. Interpreter działa iteracyjnie na trwałym stosie, bez rekursji.
- Wszystkie decyzje routingu wynikają wyłącznie ze snapshotu instancji.
#### Migracja SQLite v11
- v10 jest odebraną migracją i nie jest już edytowana. v11 dodaje osobny, wersjonowany `workflow_instance_execution` z immutable inputem, frame stackiem/kontekstem, current path i ostatnim numerem replayu.
- v11 dodaje append-only, jawnie wersjonowany ledger `workflow_execution_events`. Nie reinterpretujemy eventów lifecycle v1 z `workflow_instance_events`.
- v11 dodaje `node_attempts`: monotoniczny ordinal, workflow/version/node, frame path, parent attempt, opcjonalny indeks elementu, status, attempt token, timestampy oraz input/result/error.
- FK, JSON checks, immutable eventy/próby zakończone i unikalność aktywnej próby chronią spójność. Częściowy indeks v10 na jednej instancji `running` pozostaje globalną gwarancją jednego wykonywanego node'a.
- Instancja v10 bez execution row ma jawny stan `uninitialized`. Inicjalizacja jest osobną atomową operacją; migracja SQL nie zgaduje entry node'a ani nie tworzy ramek po cichu.
#### Atomowy tick i recovery
- `CLAIM` pozostaje prywatnym eventem `claimNext`; ogólny `append` nigdy go nie przyjmuje.
- `claimNext` pod `BEGIN IMMEDIATE` wybiera jedną należną instancję. W tej samej transakcji zapisuje lifecycle `CLAIM`, projekcję `running`, execution event oraz nowy `node_attempt` z unikalnym tokenem.
- Wykonanie handlera odbywa się poza transakcją. Finalizacja z `expectedSeq` i tokenem atomowo kończy attempt, dopisuje wersjonowany execution event, aktualizuje stos i ustawia instancję na `queued`, `waiting` albo stan terminalny.
- Node'y core przechodzą przez ten sam lifecycle prób co przyszłe rozszerzenia, dzięki czemu historia nie ma niewidocznych skoków.
- `wait` utrwala bieżący node i oczekiwany sygnał. `RESUME` waliduje sygnał i zawsze wraca do `queued`, nigdy bezpośrednio do `running`.
- Po restarcie tylko wygasła próba `running` może przejść do `interrupted`, a instancja do `waiting` z powodem recovery. Potencjalnie efektowy node nie jest automatycznie ponawiany; jawne wznowienie tworzy nową próbę.
#### Replay i read modele
- Fold ledgeru od immutable inputu i snapshotu rekonstruuje frame stack, current node, execution sequence i wynik. Rozbieżność replayu z projekcją jest błędem integralności, nie cichą korektą.
- Porty read-only: `getExecutionPosition` (breadcrumbs, node, status, `forEach` index/count, aktywna próba), `listNodeAttempts` i `getReplayStatus`.
- Nie ma endpointów ani widoków w tym etapie; porty i adapter SQLite przygotowują dane pod etap 7.
Kryteria odbioru:
- Wielopoziomowy `subworkflow` i sekwencyjny `forEach` zachowują pozycję po zamknięciu i ponownym otwarciu bazy oraz dają identyczny replay.
- Cykle bezpośrednie i pośrednie, dangling refs, nieosiągalne node'y i przekroczone limity są odrzucane przed publikacją.
- Crash w każdym punkcie transakcji nie rozdziela attemptu, eventów, stosu i projekcji.
- Recovery nie ponawia node'a po cichu, a stale token nie może sfinalizować nowszej próby.
- Dwa rzeczywiście równoległe claimy nadal dają najwyżej jedną instancję `running`.
- Istniejący driver pozostaje niepodłączony, a pełny `npm run check` przechodzi.
### Etap 3 — funkcje i reguły użytkownika
Wersjonowane funkcje TypeScript/POSIX shell w SQLite, kontrakt `{event, summary, data}`, obowiązkowe przypadki testowe, port wykonania i fejki. Bez uruchamiania niezaufanego kodu poza izolowanym runtime'em.
### Etap 4 — akcje uprzywilejowane i odwzorowanie aktualizacji
Zamknięty rejestr dozwolonych akcji executora oraz polityki ich użycia. Obecny łańcuch aktualizacji zostaje zapisany jako workflow z zachowaniem wszystkich bramek, kopii, verify i decyzji STOP. Docker image prune może pojawić się dopiero jako jawna akcja z osobną regułą i testami bezpieczeństwa.
### Etap 5 — profile modeli i sidecary
Jawne, wersjonowane profile modeli; adaptery Claude i Codex za `ModelExecutor`, osobne sidecary i brak cichego fallbacku. Profile Codex wystawione przez MCP. Architektura adapterów gotowa na kolejnego dostawcę, np. Gemini.
### Etap 6 — izolowany runtime funkcji
Efemeryczne wykonanie funkcji użytkownika z limitami czasu, pamięci i wyjścia, domyślnym brakiem sieci, jawnymi capabilities, maskowaniem oraz pełnymi eventami audytowymi.
### Etap 7 — API i edytor webowy
Endpointy katalogu i instancji, Monaco dla funkcji oraz Cytoscape dla grafu. Generyczna zmiana kolejności, przypinanie pozycji, rozwijanie zagnieżdżeń, osobne metadane layoutu i diagnostyka walidacji. Oś czasu aplikacji w stylu Jira pokazuje eventy, przejścia stanów i aktualny node.
### Etap 8 — shadow mode i kontrolowane przełączenie
Uruchomienie nowego workflow w trybie dry-run/shadow, porównanie decyzji z obecną kolejką, usunięcie rozbieżności i dopiero potem przejęcie claimowania. Po odbiorze następuje kontrolowane wdrożenie nowego systemu na VM103 z pustym `NIGHTLY_AT`: usługi mają działać, ale automatyczny przebieg nie może się utworzyć. Właściciel testuje ręcznie. Włączenie nocy, prawdziwe aktualizacje oraz wycofanie starej ścieżki wymagają osobnego odbioru właściciela.
## Dziennik etapów
| Etap | Plan Sol | Implementacja Terra | Weryfikacja Sol | Stan |
|---|---|---|---|---|
| 1 | 2026-09-20 | 2026-09-20–21 | PASS 2026-09-21 | zakończony |
| 2 | 2026-09-20 | oczekuje | oczekuje | gotowy do rozpoczęcia |
| 3–8 | ramowy | oczekuje | oczekuje | zaplanowane |