Files
homelab-updaterandClaude Opus 5.5 51121729f7
ci / check (push) Successful in 1m26s
ci / publish (push) Successful in 1m13s
docs(design): Arcane with compose in git replaces Dockge
The owner's decision and what the Arcane docs confirm: pull-synced projects,
local .env, a trigger webhook, an API key REST API, its own updater off.
homelab-upgrade changes compose only by commit, rolls back with git revert
and still decides success with its own verify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 11:41:19 +02:00

27 KiB
Raw Permalink Blame History

Projekt: kolejka kroków do nocnych aktualizacji homelabu

Od 2026-09-23 łańcuch kroków jest wycofany (decyzja właściciela): aktualizacje, bramki, harmonogramy (cron per workflow) i pushe należą do silnika workflow (sekcja „Rozszerzalny silnik workflow”). Sekcje „Mechanizm”, „Dane”, „Powód STOP” i „Kroki” opisują historyczny łańcuch; jego tabele (runs, steps, reminders) zostają w bazie nieużywane. Zadania Agent i tryb demo usunięto. Stan na 2026-09-20: fundamenty, tryb „na sucho”, łańcuch prawdziwej aktualizacji przez executor oraz audytowalne zadania Agent. Projekt uzgodniony z właścicielem 2026-09-18, decyzje o zapisie 2026-09-19 (sekcja „Executor”). Diagramy: artifact „Kolejka decyzji STOP” (https://claude.ai/artifact/5WdgWwxHGFa9qM5N5c8cpR, czytaj przez Artifact read). Ten plik jest streszczeniem bez diagramów.

Cel

Nocna aktualizacja aplikacji Docker Compose na VM103 (192.168.0.139, manifesty /opt/homelab/compose/<projekt>/compose.yaml) bez nadzoru. STOP tylko wtedy, gdy właściciel naprawdę musi zdecydować. Decyzję podejmuje w appce (LAN), po pushu z Home Assistant. Zastępuje: nightly-update.sh, agentów homelab-orchestrator/homelab-updater, docs/log.md, docs/costs.tsv ze starego repo.

Mechanizm

  • Jedna generyczna pętla: driver bierze najstarszy krok queued, wykonuje go (skrypt/kod albo model), zapisuje wynik i dokłada następny krok. STOP i error to zwykły status kroku, nie osobna ścieżka.
  • Wznowienie po STOP = nowy krok queued wstawiony z appki. Ten sam driver podejmie go przy następnym sprawdzeniu (pętla co ~60 s).
  • Jedna instancja drivera (blokada), jeden krok naraz, jedna usługa naraz. Piszę do steps tylko driver.
  • Krok, który utknął: limit czasu per typ kroku; po restarcie running starszy niż limit → error. apply i rollback nigdy nie są powtarzane automatycznie.
  • Kroki z odroczeniem (not_before): ponowienie po rate_limited, przypomnienie po 24 h.
  • Wersje krok po kroku: po verify = ok driver dokłada świeży image_tags dla tej aplikacji. Nowsza wersja w progu Auto → kolejny łańcuch; powyżej progu → STOP; nic nowszego → koniec.
  • Dwa procesy z jednego kodu: driver (uprawnienia wykonawcze) i server (appka w LAN, bez uprawnień wykonawczych: tylko baza i compose do odczytu). Wspólna baza SQLite w trybie WAL na jednym hoście. Decyzja w appce to wstawienie wiersza queued.

Dane (SQLite)

  • runs: round_id PK, type (nightly|manual), started_at, finished_at, cost_usd (suma kroków).
  • steps: id PK, round_id FK, seq, app (null = krok roundu), step, status (queued|running|ok|stop|error), executor (script|sonnet|opus), reason_code, detail, summary (nullable dla starych rekordów, nowe zakończone kroki: zamaskowane 1–500 znaków), resolution (≤10 słów), resolved_at, input, result, progress (schemat v7: tylko dla kroku running, czyszczone przy finish; {at, actions, last}), cost_usd, not_before, started_at, finished_at.
  • app_config: app PK, enabled, position, service, image_repo, tag_pattern, auto (none|patch|minor|major), http_url, expect_status, release_notes_executor, migrate_executor (null = autonomiczne migracje wyłączone), post_upgrade_executor (schemat v8: null = po aktualizacji tylko skrypty; źródła notatek wydań to jedna globalna lista, patrz release_notes), prompt_hint, notes (≤10 słów), apply (0/1: prawdziwe aktualizacje przez executor; domyślnie 0 = tryb na sucho), project (projekt compose; wiersz = składnik, patrz „Grupy”).
  • Status aplikacji = jej ostatni wiersz w steps. Kolejka decyzji: status IN ('stop','error') AND resolved_at IS NULL.
  • Retencja 12 miesięcy (czyszczenie skryptem miesięcznie). Baza jest objęta nocnym vzdump VM103.
  • docs/log.md i docs/costs.tsv znikają; historia i koszt są w bazie.

Powód STOP

reason_code (znane przypadki) + detail (konkretny tekst: polecenie, kod wyjścia, pierwsza linia stderr, komunikat wyjątku albo powód modelu ≤10 słów; po maskowaniu sekretów). Nieznany przypadek = unexpected z prawdziwym tekstem, nigdy pusty „error”. stop = potrzebna decyzja właściciela (znany powód); error = coś się zepsuło. Wstępna lista: no_backup, no_space, version_above_auto, version_unknown, migration_required, schema_change, forbidden_change, regression, verify_failed, rolled_back, confirm_apply (celowy STOP przed apply; używany do 2026-09-19, teraz aktualizacja w progu auto idzie sama), no_skill, no_tool, model_unparseable, rate_limited, auth_failed, timeout, unexpected.

Kroki (mapowanie starej pętli z /root/homelab-modernization/AGENTS.md)

Krok Wykonawca Stary odpowiednik
plan kod wszystkie enabled z app_config wg position, każdej wstawia status
status kod status.sh <p> → manifest_sha256, obraz
image_tags kod image_tags.sh <repo> --pattern <tag_pattern> --current <tag> → candidate, version_change; bramka: wyżej niż auto albo unknown → STOP
release_notes model (tylko WebSearch i WebFetch) tylko dla skoku powyżej progu auto: image_tags kolejkuje go zamiast gołego STOP. Model sam znajduje notatki wydań między działającą wersją a kandydatem, preferując globalną listę zaufanych źródeł właściciela (Models → Release notes sources, tabela settings; strona http(s) albo github:owner/repo), a gdy żadne nie pasuje — oficjalne strony producenta. Notatki + skill + wersje reszty projektu → ocena AgentReport (clear/attention/blocked, kroki „Before update:”/„After update:”) w decyzji STOP version_above_auto; właściciel dalej decyduje. Błąd modelu = zwykły STOP z powodem, nigdy zablokowany łańcuch
migrate (repair) model z narzędziami nowe
image_resolve kod image_resolve.sh <repo>:<candidate> → pinned (tag@digest)
backup_check kod kopia vzdump VM103 młodsza niż 48 h: timer na Proxmoksie co godzinę zapisuje najnowszą (pvesh … storage/vm-storage/content) do /srv/appdata/homelab-upgrade/host/vzdump.json, driver czyta ją tylko do odczytu
preflight kod preflight.sh → storage.sufficient (≥ 5368709120 B wolnego w /var/lib/docker)
backup kod (executor) backup.sh <p> → backup_id (kopia manifestu i .env na VM w /srv/appdata/homelab-upgrade/backups/<p>/<id>/: katalogi 0700, pliki 0600; wynik to same hashe)
apply_dry_run, apply kod (executor) apply.sh … --dry-run, potem to samo bez; dla update diff.changed_paths musi być tylko services.<usł>.image. W progu auto po apply_dry_run od razu apply (diff zapisany w wyniku); powyżej progu STOP już w image_tags
post_upgrade model z narzędziami (sesja repair) po udanym verify, tylko dla aplikacji z post_upgrade_executor: model czyta ocenę notatek („After update:”), skill i stan usługi, wykonuje to, na co pozwala hl exec (dokładna lista config/agent-exec.json), resztę wypisuje właścicielowi (STOP migration_required, proceed idzie do następnego skoku). Np. Nextcloud: brakujące indeksy bazy przez occ. Domyślnie wyłączony
verify kod verify.sh (running, obraz = pinned, healthy jeśli jest healthcheck, http, retry 6×5 s; odbiór ze skilla później). Kontener przez proxy (GET), HTTP z drivera. Czeka do 10 min, póki healthcheck mówi starting (migracje przy starcie), potem 24×5 s. Nie przeszedł → STOP verify_failed, bez automatycznego rollbacku: powrót manifestu nie cofa danych (2026-09-19 Nextcloud po migracji bazy nie wstał na starszym obrazie). Właściciel: retry (ponowny verify), rollback albo skip
rollback kod rollback.sh <p> --from <dir> --service <usł>
skill_gen, skill_publish model bez narzędzi / kod rejestracja aplikacji w appce: szkic → zatwierdzenie → zapis
agent_task kod + model read-only ręczny audyt całego homelabu albo jednej aplikacji; wspólna kolejka FIFO

Kody wyjścia apply.sh: 2 błędne argumenty; 3 floating_tag/missing_tag → najpierw repin, missing_digest → dodaj digest, diff szerszy niż obraz → STOP; 4 manifest zmieniony → status, nowy hash, od preflight; 5 pull/up nie przeszedł → verify, jeśli usługa nie działa → rollback i STOP. Repin (digest bez zmian, nazwanie wersji): status → image_version.sh → proposed_image (no_version_label → STOP) → backup → apply dry-run → apply → verify. W porcie image_version tylko nazywa wersję (etykieta obrazu) i przekazuje ją do image_tags; osobnego apply dla samego repinu nie ma (docs/PORT.md).

Granice (niezależne od modelu)

Absolutny STOP, także dla Opusa: sekrety, sieć, porty, UID/GID, mounty, ruszanie Proxmox/TrueNAS, kasowanie danych/wolumenów/VM, latest, „update all”, prune, down -v, --remove-orphans. Od 2026-09-23 (decyzja właściciela) traefik, mqtt, unifi i homeassistant nie są już pod absolutnym STOP: o tym, co się aktualizuje, decydują flagi enabled i apply aplikacji. Executor nadal nigdy nie zmienia własnego projektu (homelab-upgrade). Egzekwowane w kodzie, nie w prompcie: po migrate diff compose jest sprawdzany deny-listą (privileged, cap_add, devices, network_mode: host, mounty poza katalogami aplikacji, porty, sieci, sekrety). Trafienie = STOP. release_notes daje ocenę, nie wyrok: decyzję o majorze podejmuje właściciel. migrate_executor = null wyłącza autonomiczną naprawę modelem dla aplikacji, ale model wybrany przy proceed (albo retry kroku verify) naprawia nieudany verify całego skoku niezależnie od tego ustawienia (repair_by niesione w wejściu kroków łańcucha). Prompt naprawy dostaje ocenę notatek tej aktualizacji.

Wykonawcy modeli: CLI z abonamentu

  • API bezpośrednio odpada: abonament go nie obejmuje, a Anthropic zabrania routowania żądań przez dane logowania planu Pro/Max i przechowywania tokenów sesji. Dozwolone jest uruchamianie niezmodyfikowanego claude zalogowanego własnym kontem właściciela.
  • Port ModelExecutor, adaptery CLI: claude -p (start), Codex CLI później (logowanie kontem ChatGPT w automatyzacji jest odradzane; sprawdzić w praktyce). Adaptery API możliwe w przyszłości bez zmian w reszcie.
  • --bare odpada (wymaga klucza API). Uruchamiaj w pustym katalogu roboczym i z osobnym katalogiem konfiguracji (do potwierdzenia: CLAUDE_CONFIG_DIR), żeby żaden CLAUDE.md/AGENTS.md ani ustawienia użytkownika nie wchodziły do kroków.
  • Wynik: --output-format json --json-schema … → pole structured_output (walidacja zod), total_cost_usd to szacunek po stronie klienta. Prompt przez stdin (limit 10 MB).
  • Profil A (release_notes, skill_gen): bez narzędzi hosta. release_notes ma tylko WebSearch i WebFetch; strony to niezaufany tekst (dane, nie polecenia), ocena jest doradcza.
  • Profil B (migrate/repair): CLI z narzędziami (Read, Edit, ograniczony Bash) dla jednej aplikacji, po świeżej kopii. --allowedTools z wzorcami, dontAsk, hook PreToolUse odrzucający polecenia z listy zakazów. Po naprawie verify (kod) decyduje: ok albo rollback.
  • Limity planu: przy limicie krok kończy się rate_limited i wraca z not_before. Poświadczenia w boksie wygasają → auth_failed + push.
  • Wersję CLI przypinamy w obrazie i aktualizujemy świadomie.

Executor (decyzja właściciela, 2026-09-19)

Pierwsza cegła boksu. Zapisuje wyłącznie wąski sidecar executor (ten sam obraz, src/main/executor.ts); driver zostaje uid 1000 bez docker.sock.

  • Kontener: root (manifesty są root:root 600), cap_drop: ALL, no-new-privileges, read_only, bez portu. Mounty: docker.sock, /opt/homelab/compose (ta sama ścieżka co na hoście), backups (0700), /var/lib/docker:ro tylko do sprawdzenia wolnego miejsca. Sieć executor (internal) tylko z driverem.
  • Zamknięte API, zawsze jedna usługa jednego projektu: inspect, preflight, backup, apply (z dry_run), rollback. Wejście i wyjście walidowane zod (domain/executor-api.ts), odpowiedzi to same hashe, nigdy treść .env ani rozwiniętego compose.
  • Które aplikacje są aktualizowane naprawdę, decyduje jedno miejsce: przełącznik „Real updates” (app_config.apply) na stronie Apps. Executor sam z siebie nigdy nie rusza aplikacji z absolutnego STOP ani własnego projektu homelab-upgrade; EXECUTOR_PROJECTS w compose.yaml domyślnie * (wszystkie), jawna lista zawęża. Pierwotnie (rano 2026-09-19) lista była obowiązkowa jako drugi zamek; właściciel uznał ją wieczorem za nieintuicyjną („wszystko tak, jak się konfiguruje w Apps”).
  • Kopia przed zmianą: manifest i .env (krok backup) oraz vzdump VM103 młodszy niż 48 h (backup_check).
  • Początkowo (rano 2026-09-19) tylko ręcznie, ze STOP confirm_apply przed każdym apply. Wieczorem właściciel: „skrypt robi upgrade, dopóki wszystko przechodzi; kiedy STOP albo kontener nie wstał, wchodzi model, patrzy i naprawia”. Teraz aktualizacja w progu auto idzie sama, a nieudany verify przejmuje krok repair (model).

Grupy (decyzja właściciela, 2026-09-19)

Projekt compose może mieć kilka usług z własnymi obrazami (grafana + influxdb, nextcloud + mariadb + redis). Wiersz app_config to składnik: klucz app (dla głównej usługi nazwa projektu, dla pozostałych <projekt>-<usługa>), project, service. Składniki jednego projektu tworzą grupę:

  • plan kolejkuje pierwszy włączony składnik każdego projektu; następny rusza dopiero, gdy poprzedni skończy swój łańcuch w przebiegu (także po „Proceed”). STOP wstrzymuje resztę grupy, „skip” ją przepuszcza. Kolejność: aplikacja przed swoją bazą (pozycja w app_config).
  • Usługi na identycznym obrazie (nextcloud app i cron) to jeden składnik: apply zmienia je razem, diff dopuszcza dokładnie ich linie image:.
  • Bazy danych: wzorzec tagów zawężony do obecnej wersji głównej (skok postgresa 14→15 to dump/restore, nie zmiana obrazu). Ruchomy tag (postgres:14, redis:7-alpine) nie mówi, jaka wersja działa: porównanie uznałoby „14” za 14.0 i zaproponowało cofnięcie, więc taki składnik jest wyłączony, dopóki manifest nie przypnie konkretnej wersji.

Model naprawia (decyzja właściciela, 2026-09-19 wieczór)

„Skrypt robi upgrade, dopóki wszystko przechodzi; kiedy STOP albo kontener nie wstał, wchodzi model, patrzy i naprawia.”

  • Krok repair: gdy verify nie przeszedł, a aplikacja ma „Migration executor” (sonnet/opus), verify sam kolejkuje repair (jedna próba; drugi nieudany verify = STOP dla właściciela). Na dowolnym STOP właściciel może wybrać „Repair (model)”.
  • Kontener model: niezmodyfikowany claude CLI (przypięta wersja w obrazie) zalogowany kontem właściciela (CLAUDE_CONFIG_DIR=/claude). Bez docker.sock, bez plików projektów i bazy.
  • Model działa tylko przez narzędzie hl w sesji executora otwartej na jeden projekt: ps, logs, compose (sekrety jako <secret:KLUCZ>), write, pull, up, restart, exec-rules (lista komend, które exec w ogóle przyjmie: model patrzy, zamiast zgadywać), exec w kontenerach tego projektu. Egzekwowane w kodzie (domain/agent-guard.ts): zapis compose sprawdzany listą zakazów z „Granic” względem manifestu z otwarcia sesji; wyjścia maskowane wartościami sekretów projektu; hook PreToolUse przepuszcza w Bash tylko hl, a Read/Edit/Write tylko w katalogu sesji; trasy drivera w executorze i /run modelu odpowiadają tylko adresowi drivera. Każde wywołanie jest w logu sesji i w wyniku kroku.
  • Wyjątek za zgodą właściciela (2026-09-23): na bramce po nieudanej naprawie przycisk „Repair with full access” uruchamia jedną ponowną naprawę w sesji full. W niej, tylko w tym projekcie, exec przyjmuje dowolną komendę i użytkownika, run uruchamia jednorazowy kontener usługi z jej wolumenami (np. chown katalogu danych przy crash-loopie), a write sprawdza tylko poprawność compose, bez listy zakazów. Maskowanie sekretów, log sesji, kopia i rollback działają dalej. To jedna próba na run: po niej zostaje rollback albo skip.
  • Krok modelu trwa minuty, więc daje znak życia: driver (watchSession) co 5 s czyta log otwartej sesji (/session/log executora, tylko dla drivera, bez zamykania sesji) i zapisuje w kroku liczbę akcji hl oraz ostatnią z nich; appka pokazuje to w wyniku kroku „running” (SSE odświeża widok). Gdy executor jest nieosiągalny, nic nie jest raportowane, żeby nie udawać żywego modelu. Odpowiedź /run sidecara jest jednym JSON-em po końcu pracy, więc wysyła nagłówki od razu i spację co 30 s (limit 5 min fetch w Node), a rozłączenie drivera przerywa claude. Koszt nieudanego przebiegu (total_cost_usd z CLI) jest zapisywany, nie zerowany.
  • Zwykłe operacje executora i cała sesja modelu używają jednej współdzielonej blokady. Przed każdą mutującą sesją repair driver ponownie sprawdza świeżość vzdump i upewnia się, że ma kopię manifestu z .env (robi ją, jeśli krok nie niesie już backup_id). hl exec jest domyślnie całkowicie wyłączony; dopuszczone są tylko dokładne czwórki project, service, user, pełne argv z config/agent-exec.json, bez wildcardów i bez powłoki.
  • Kontekst modelu: tylko ta aplikacja (co nie przeszło, docelowy obraz, pozostałe składniki grupy z wersjami) i skill jej projektu. Zgodność wersji w grupie ocenia model (np. czy Nextcloud obsługuje daną MariaDB).
  • O wyniku decyduje kod: po repair jeszcze raz verify.

Skille

Jeden na projekt compose (tabela skills), fakty dla modelu, które nie zmieniają się z wersją: wymagania (np. wersje bazy), co jest STOP, jak sprawdzić i jak wrócić. Zaimportowane raz ze starego repo (config/skills/<projekt>.md), dopisywane do bazy, gdy ich brak, edytowane w appce (Apps → aplikacja → Skill). Do modelu trafia tylko skill naprawianego projektu.

Boks (do decyzji przed uruchomieniem modeli na prawdziwych danych)

Właściciel chce: nic nie wycieka poza boks i brak kontroli nad VM. Uwaga: docker.sock to praktycznie root na VM. Kierunek: kontener bez roota, system plików tylko do odczytu poza wolumenami, montowanie tylko katalogu compose i wolumenu SQLite; Docker przez proxy z allowlistą albo cienka nakładka na docker z kontrolą argumentów (wszystko zawężone do jednej aplikacji); ruch wychodzący tylko do API Anthropic i logowania Anthropic, rejestrów obrazów, GitHuba, HA; bez dostępu do reszty LAN (TrueNAS, Proxmox, MQTT, UniFi). Szczegóły są otwarte.

Powiadomienia i appka

  • Push HA per run (app/run-report.ts, infra/ha-notifier.ts: POST do notify.<usługa>, HA_URL, HA_TOKEN, HA_NOTIFY z .env; HA stoi na VM103): gdy run czeka na decyzję właściciela, gdy się nie powiedzie i gdy run „Real” się skończy. Wysyła driver po każdym kroku; link prowadzi do strony runu.
  • Harmonogram: cron per workflow (tabela workflow_schedules, sprawdza serwer), bez osobnego przebiegu nocnego i watchdoga.
  • Appka: tylko LAN, za traefik; logowanie do przemyślenia przed wystawieniem poza dom. Widoki: Overview (co czeka na decyzję, co trwa, co się skończyło, co następne), Apps (konfiguracja, skill, runy i harmonogram aplikacji), Runs (lista i strona runu z grafem, bramką i historią), Workflows, Functions, Models. Odświeżanie na żywo przez SSE.
  • Skille: parametry mechaniczne w app_config; SKILL.md tylko fakty dla modelu. Import 15 istniejących skilli jednorazowo (na razie sprawdzony format tylko Bazarr).

Stary kod do portu (/root/homelab-modernization, tylko odczyt)

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).

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.

Arcane i compose w git (decyzja właściciela, 2026-09-24)

Dockge zastępuje Arcane (BSD-3, ghcr.io/getarcaneapp/manager, wersja przypięta, bo wydania są co kilka dni; Portainer odrzucony jako przekombinowany i idący w płatną wersję). Pliki compose.yaml trafiają do repo na Gitea, a Arcane wdraża je sam po zmianie w git. homelab-upgrade zostaje „mózgiem” (wersje, release notes, bramki, verify, naprawa modelem), Arcane jest warstwą wdrożenia i UI do ręcznej pracy.

Co daje Arcane (dokumentacja getarcane.app, sprawdzone 2026-09-24): projekt z git w trybie Pull (repo jest źródłem prawdy, pliki compose w Arcane tylko do odczytu, osobna ścieżka compose na projekt, więc jedno repo z folderem na projekt); .env zostaje lokalny, a przy synchronizacji lokalne wartości wygrywają z .env.git; webhook POST /api/webhooks/trigger/{token} uruchamia synchronizację lub redeploy i od razu odpowiada 202 (bez wyniku); REST API z nagłówkiem X-Api-Key (specyfikacja w /api/openapi.json instancji); własne auto-aktualizacje obrazów domyślnie wyłączone, a etykieta com.getarcaneapp.arcane.updater=false blokuje je niezależnie od UI; hooki pre-deploy tylko dla Pull.

Zasady:

  • Compose tylko z git. Każda zmiana compose z homelab-upgrade (aktualizacja, docker.migrate, zapis modelu przy naprawie) to commit i push, nie zapis pliku; inaczej Arcane przy synchronizacji ją nadpisze. Rollback to git revert tego commita. .env i kopie danych bez zmian (lokalnie).
  • Każdy projekt zarządzany przez homelab-upgrade ma com.getarcaneapp.arcane.updater=false.
  • Po pushu executor sam woła webhook Arcane (bez zależności od webhooka Gitea), czeka, aż plik w katalogu projektu ma skrót commita, a o sukcesie decyduje istniejący verify (obraz i health), nie status Arcane.
  • Tryb per aplikacja (direct | git) do stopniowego przełączania; dry run nie pushuje. Integracja za portem, testy na fejkach.

Do sprawdzenia w pilocie: SSH na porcie 2244, czy Pull robi up -d tylko przy zmianie plików, zachowanie przy zmianie pliku na dysku, ścieżki REST do statusu synchronizacji, PROJECTS_DIRECTORY wskazany na istniejący /opt/homelab/compose (pierwszy Pull nadpisuje pliki projektu wersją z repo, więc repo musi mieć dokładnie obecne pliki), otwarte zgłoszenie #2731 (uprawnienia GitSync).

Historyczny plan budowy obecnego systemu

  1. Decyzje: repo, Node, klucz — zrobione. Do zrobienia przez właściciela: domena appki w traefik, logowanie claude w boksie, kształt boksu.
  2. 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.
  3. Port skryptów na TS z CommandRunner (lokalnie na VM103).
  4. Handlery kroków skryptowych + import skilli + przebieg suchy.
  5. Boks.
  6. Kroki z modelem (ClaudeCliExecutor, szablony, parser, deny-lista).
  7. Appka: pełna (rejestracja aplikacji, skill_gen).
  8. Powiadomienia i dwa małe skrypty po stronie hosta (data kopii, watchdog).
  9. Przełączenie: kilka nocy równolegle ze starym przebiegiem (tryb suchy), potem wyłączenie starego, odchudzenie AGENTS.md starego repo, usunięcie agentów i log.md/costs.tsv.

Otwarte

Kształt boksu; nakładka na docker; hosty logowania Anthropic do allowlisty; Codex przez konto ChatGPT; logowanie do appki poza LAN; uzgodnione przy szkielecie (2026-09-18): język kodu i UI angielski, dokumenty polskie; stack Fastify, Vite + Preact, zod, Vitest; sterownik SQLite to wbudowany node:sqlite (Node 24.21, SQLite 3.53, bez natywnej kompilacji); TypeScript ~6.0.3 do czasu wsparcia 7.x przez typescript-eslint. Uruchamianie i układ kodu: docs/DEV.md.