# Plan architektury STEM Launcher i środowisk wykonawczych Status: wdrożony fundament P0/P1; `fast-dm`, BSP FreeRTOS symulatora i pełne bramki sprzętowe pozostają kolejnymi etapami Data: 2026-07-14 Role w procesie: - Codex — architekt i wykonawca planu; - Claude — senior reviewer; - użytkownik — właściciel produktu i programu nauczania. ## 1. Decyzja architektoniczna System składa się z jednego launchera uruchamianego na hoście, jednego wspólnego stage'a bazowego Dockerfile oraz dokładnie trzech kontenerów roboczych: | Profil launchera | Target Dockerfile | Rola | | --- | --- | --- | | `native-amd64` | `native-amd64-final` | szybkie testy algorytmów C/C++ na AMD64 | | `hazard3-sim` | `hazard3-sim-final` | RV32 bare metal i FreeRTOS na rzeczywistym RTL Hazard3 w Verilatorze | | `rp2350` | `rp2350-final` | fizyczny Pico 2/Pico 2 W: RISC-V Hazard3 i ARM Cortex-M33 | Stage `dev-ui-base` jest wspólną warstwą budowania, a nie czwartym stale uruchomionym kontenerem. Hoverboard nie otrzymuje teraz osobnego kontenera. Dodamy go dopiero po ustaleniu mikrokontrolera, BSP i interfejsu probe. Nie tworzymy osobnych kontenerów dla: - bare metal i FreeRTOS; - RP2350 RISC-V i RP2350 ARM; - C i C++/STL; - Termdebug, nvim-dap, tmuxa ani MCP. Są to targety, tryby albo narzędzia wewnątrz jednego z trzech profili. ## 2. Granice odpowiedzialności ### Host i `stem-launcher` Launcher działa jako zwykły użytkownik hosta i odpowiada za: 1. synchronizację publicznego manifestu serii; 2. listowanie serii, kart i zadań; 3. pobieranie kodu karty z Git do workspace na hoście; 4. przygotowanie repo odpowiedzi, brancha ucznia i remotes; 5. wybór profilu oraz targetu; 6. lokalny build właściwego targetu ze źródłowego Dockerfile albo użycie jego lokalnego cache; 7. utworzenie, wznowienie, zatrzymanie i usunięcie kontenera; 8. wywołanie `build`, `test`, `run`, `debug` i `deploy`; 9. rejestrację artefaktów, logów, wyników i socketów MCP; 10. wykrywanie probe i przekazanie wyłącznie potrzebnego urządzenia do profilu `rp2350`. Klucze SSH, tokeny Gitea i konfiguracja Git pozostają na hoście. Prywatnego klucza SSH nie montujemy do kontenera. Kontenery nie otrzymują socketu Podmana ani Dockera i nie zarządzają innymi kontenerami. ### Kontener roboczy Kontener odpowiada wyłącznie za powtarzalne środowisko kompilacji, uruchamiania i debugowania. Otrzymuje: - repo karty pod `/workspace` jako bind mount; - katalog stanu instancji pod `/workspace/.stem/instances/`; - deklarację targetu i akcji; - opcjonalnie konkretne urządzenie USB dla RP2350; - katalog wymiany socketów MCP widoczny również na hoście. Kod źródłowy i artefakty pozostają na hoście. Usunięcie kontenera nie usuwa pracy ucznia. ## 3. Przepływ ucznia Docelowy przepływ jest jeden dla domu, pracowni i zdalnego komputera: ```bash stemctl workspace sync stemctl series list stemctl series cards fetch inf bss stemctl env ensure native-amd64 stemctl test native-amd64 stemctl run native-amd64 stemctl env ensure hazard3-sim stemctl test hazard3-sim stemctl debug hazard3-sim stemctl env ensure rp2350 stemctl probe list stemctl deploy rp2350 inf bss 1 --target rp2350-rv --device /dev/bus/usb/001/006 stemctl debug rp2350 inf bss 1 --target rp2350-rv --device /dev/bus/usb/001/006 ``` Pełne selektory pozostają dostępne, na przykład: ```bash stemctl test hazard3-sim inf bss 1 stemctl deploy rp2350 inf bss 1 --target rp2350-arm ``` Launcher wykonuje następujący pipeline: ```text workspace-info │ ▼ wybór serii/karty/zadania │ ▼ clone/fetch Git na hoście ──► branch/remotes odpowiedzi │ ▼ wybór profilu i targetu │ ▼ rootless Podman + bind mount repo │ ├──► build ─► test ─► run/debug │ └──► artefakt ─► deploy, tylko gdy target to obsługuje ``` ### Znaczenie akcji | Akcja | `native-amd64` | `hazard3-sim` | `rp2350` | | --- | --- | --- | --- | | `build` | binarka ELF hosta | ELF/BIN RV32 | ELF/UF2 dla RV albo ARM | | `test` | golden tests, ASan/UBSan | te same wektory, RV32/RTL | testy na płytce, gdy karta je deklaruje | | `run` | proces natywny | program w Verilatorze | uruchomienie wcześniej wgranego programu i serial/RTT | | `debug` | GDB/LLDB | obecnie `fast-rsp`, docelowo `fast-dm`, kontrolnie JTAG | GDB przez OpenOCD/probe | | `deploy` | nieobsługiwane | nieobsługiwane; artefakt ładuje `run`/`debug` | flash przez probe | Komenda nie może udawać sukcesu dla niewspieranej możliwości. Launcher czyta capabilities profilu i zwraca czytelny komunikat, na przykład `deploy is not supported by native-amd64`. `run rp2350` nie wykonuje ukrytego flashowania. Wymaga rekordu, że wybrany artefakt został wcześniej wdrożony na wybraną płytkę; w przeciwnym razie odmawia startu albo wymaga jawnego `--allow-existing-firmware`. Artefakt powinien zawierać build ID możliwy do potwierdzenia przez UART/RTT lub metadata RP2350, aby nie testować przypadkiem starego firmware. ## 4. Kontrakt karty pracy Karta nie może zależeć od zaszytych w launcherze nazw plików. Jej wersjonowany manifest powinien deklarować co najmniej: ```yaml schema: 1 card: inf/bss default_task: task1 profiles: native-amd64: default_target: native hazard3-sim: default_target: hazard3-baremetal rp2350: default_target: rp2350-rv targets: native: profile: native-amd64 actions: [build, test, run, debug] hazard3-baremetal: profile: hazard3-sim runtime: baremetal actions: [build, test, run, debug] hazard3-freertos: profile: hazard3-sim runtime: freertos tick_hz: 1000 actions: [build, test, run, debug] rp2350-rv: profile: rp2350 arch: riscv actions: [build, test, run, debug, deploy] requires: debug: [probe] deploy: [probe] rp2350-arm: profile: rp2350 arch: arm actions: [build, test, run, debug, deploy] requires: debug: [probe] deploy: [probe] actions: build: ./tools/card build test: ./tools/card test run: ./tools/card run debug: ./tools/card debug deploy: ./tools/card deploy artifacts: directory: .stem/artifacts ``` Nazwa pliku manifestu i dokładny schemat zostaną zatwierdzone w implementacji. Ważny jest podział: `workspace-info` mówi, skąd pobrać kartę, manifest karty mówi, jak ją wykonać, a profil środowiska dostarcza narzędzia. Każda akcja zapisuje maszynowo czytelny wynik do: ```text .stem/results/////result.json ``` Wynik zawiera co najmniej commit karty, lokalny identyfikator wyniku builda, wersję toolchainu, polecenie, kod wyjścia, czas, testy i skróty artefaktów. Dzięki temu porównanie AMD64 → Hazard3 → RP2350 nie opiera się na ręcznym oglądaniu terminala. ## 5. Trzy kontenery ### 5.1 `native-amd64` Cel: najszybsza pętla dla algorytmów z podręcznika, matury i ZPE. Narzędzia specyficzne: - GCC i Clang; - GDB i opcjonalnie LLDB; - ASan, UBSan, coverage i Valgrind; - CMake, Ninja i Make; - testy jednostkowe C/C++. Profil nie zawiera SDK RP2350 ani Verilatora. Testuje algorytm i kontrakt API, nie emuluje zachowania 32-bitowego MCU. Testy przenośności muszą dodatkowo przejść w `hazard3-sim`. ### 5.2 `hazard3-sim` Cel: praca domowa bez płytki i dydaktyczny wgląd w procesor. Narzędzia specyficzne: - przypięty toolchain `riscv64-unknown-elf`; - przypięty Verilator; - Hazard3 RTL oraz testbench; - runtime bare-metal i osobny BSP symulatora dla FreeRTOS; - szybki RSP oraz planowany transport `fast-dm` do wbudowanego DM Hazard3; - OpenOCD/remote-bitbang wyłącznie jako okresowy gate zgodności; - opcjonalne VCD i narzędzia do zajęć z Veriloga. Dla FreeRTOS 1 kHz timer symulatora zwiększa `mtime` dokładnie raz na cykl. Przy deklarowanym zegarze 150 MHz tick przypada co 150 000 cykli RTL. Jest to 1 kHz czasu gościa, nie obietnica czasu rzeczywistego hosta. Kod aplikacyjny i wektory testowe są wspólne z RP2350, natomiast BSP, startup i linker są osobne. Bieżący port FreeRTOS RP2350 ustawia `SIO MTIME` w tryb `FULLSPEED`, dlatego przy zegarze systemowym 150 MHz również używa 150 000 zliczeń na tick. Nie traktujemy jednak tej stałej jako wspólnego API BSP: profil sam wylicza ją z rzeczywistego zegara i trybu timera. Testy czasowe mają dwa poziomy. Smoke test PR wykonuje co najmniej 100 ticków z produkcyjnym dzielnikiem 150 000. Długi test schedulera wykonuje 10 000 ticków na jawnie przyspieszonym zegarze testowym i nie jest dowodem częstotliwości. Nightly/release może wykonać dłuższy test produkcyjnego dzielnika w ustalonym budżecie czasu. ### 5.3 `rp2350` Cel: pracownia z fizycznym Pico 2/Pico 2 W. Narzędzia specyficzne: - przypięty Pico SDK i FreeRTOS Kernel; - toolchain RISC-V dla Hazard3; - toolchain ARM dla Cortex-M33; - CMake/Ninja; - OpenOCD zgodny z RP2350, picotool i obsługa wybranego probe; - serial/RTT oraz reguły diagnostyczne USB. Target `rp2350-rv` i `rp2350-arm` wybiera konfigurację CMake, startup, linker, GDB i backend OpenOCD. Nie wymaga kolejnego Dockerfile. Bare metal i FreeRTOS są wariantami manifestu projektu, nie oddzielnymi kontenerami. Kontener działa rootless, bez `--privileged`. Launcher przekazuje tylko jawnie wybrane urządzenia. Host otrzymuje regułę udev lub ACL, która pozwala grupie laboratoryjnej korzystać z probe bez `sudo`. Debug probe i natywne USB płytki to dwie różne capabilities. Domyślny debug/deploy przez zewnętrzny probe używa stabilnego urządzenia probe; reset targetu nie powinien go odłączać. Opcjonalny BOOTSEL/picotool powoduje re-enumerację targetu i zmianę adresu USB. Taki deploy jest osobną, krótkotrwałą akcją z tego samego profilu `rp2350`: launcher ponownie wykrywa urządzenie po każdej fazie i uruchamia akcję z aktualnym węzłem. Nie przekazujemy całego `/dev`; jeżeli backend wymaga dostępu na poziomie portu/busa USB, zakres jest jawnie raportowany i zatwierdzany dla tej instancji. ## 6. Wspólny kontrakt UI Każdy profil dziedziczy z `dev-ui-base`: - tmux z prefiksem `Ctrl-s`; - Neovim i identyczny zestaw skrótów; - Termdebug, nvim-dap i dashboard GDB; - serwery/bridge MCP dla tmuxa i Neovima; - `ripgrep`, `fd`, Git, Python i podstawowe narzędzia diagnostyczne; - użytkownika bez roota i ten sam katalog roboczy `/workspace`. Domyślny układ jednego widocznego okna tmuxa: ```text ┌────────────────────────────────────────────────────────────┐ │ Neovim — jeden pane tmuxa │ │ ┌─ Termdebug + GDB dashboard ─┬─ edytor/nvim-dap ───────┐ │ │ └──────────────────────────────┴──────────────────────────┘ │ ├────────────────────────────────────────────────────────────┤ │ bash, cwd=/workspace — drugi pane tmuxa │ └────────────────────────────────────────────────────────────┘ ``` Termdebug i edytor są oknami jednego Neovima. Hazard3 uruchamia symulator w drugim, domyślnie ukrytym oknie tmuxa `tb`. Termdebug i nvim-dap są dwiema nakładkami na ten sam opis targetu GDB. Karta nie przechowuje osobnych adresów i poleceń dla każdego frontendu. ## 7. Tożsamość instancji i MCP Każda sesja ma stabilną tożsamość logiczną, na przykład: ```text hazard3-sim-inf-bss-t1 ``` Kontener otrzymuje label `edu.stem.instance`. Aktualny identyfikator Podmana jest tożsamością procesu. Pełne nazwy pozostają w labels/registry, natomiast ścieżka Unix socketu używa krótkich kluczy, aby zmieścić się w limicie `sun_path` (maksymalnie 107 użytecznych bajtów na Linuksie): ```text $XDG_RUNTIME_DIR/stem////t.sock $XDG_RUNTIME_DIR/stem////n.sock $XDG_RUNTIME_DIR/stem///current ``` `thread-key` i `instance-key` są 12-znakowymi skrótami kryptograficznymi, a `cid12` to jednoznaczny na danym hoście prefiks bieżącego container ID. Registry mapuje je na pełne wartości i jest weryfikowane przez `podman inspect`. Ta sama krótka ścieżka jest widoczna na hoście i w kontenerze. Test startowy odrzuca konfigurację przekraczającą budżet 100 bajtów. Wrapper MCP wybiera najpierw jawną instancję, przez `podman inspect` sprawdza jej aktualny container ID, a dopiero potem otwiera socket. `current` jest wskaźnikiem pomocniczym ograniczonym do wątku agenta, nie globalnym wyborem dla wszystkich kontenerów. Stary socket nie może zostać użyty po odtworzeniu kontenera. Agent widzi dokładnie tę samą sesję tmuxa i Neovima co uczeń. MCP nie jest osobnym kontenerem. ## 8. Rootless Podman i SSH Podman jest domyślnym runtime. Docker może pozostać przejściowym backendem dla istniejących komputerów, ale kontrakt nie może wymagać członkostwa ucznia w grupie `docker`. Na zdalnym komputerze ucznia nauczyciel lub agent: 1. łączy się wyłącznie przez SSH z parą kluczy; 2. uruchamia `stemctl` na tym zdalnym hoście; 3. tamtejszy launcher zarządza lokalnym rootless Podmanem i lokalnym workspace; 4. w razie potrzeby sockety MCP/GDB są udostępniane przez tunel SSH, nigdy przez nasłuch na `0.0.0.0`. Nie montujemy zdalnego katalogu przez SSHFS jako podstawowego modelu i nie przekazujemy prywatnego klucza do kontenera. Repo i kontener mają znajdować się na tym samym hoście wykonawczym. ## 9. Budowanie kontenerów ze źródeł i cache Repo przechowuje jeden wieloetapowy `Dockerfile` i pliki Compose. Nie przechowuje ani nie publikuje gotowych obrazów. `dev-ui-base` zawiera przypięte binaria i zależności UI, ale nie zawiera często zmienianych `configs/` ani entrypointów. Graf stage'ów: ```text os-base └─ dev-ui-base (tmux/nvim/MCP i przypięte zależności, bez configs/) ├─ ui-node-deps (przypięte zależności serwerów MCP) ├─ native-toolchain │ └─ native-amd64-final + COPY configs/ i entrypointów ├─ hazard3-toolchain │ └─ hazard3-sim-final + COPY configs/ i entrypointów └─ rp2350-toolchain └─ rp2350-final + COPY configs/ i entrypointów ``` Duże toolchainy, SDK, Verilator, Hazard3 i OpenOCD są instalowane w stage'ach toolchain przed skopiowaniem konfiguracji UI. Zmiana lockfile zależności UI może celowo przebudować potomków; zwykła zmiana mapowania klawisza, dashboardu albo entrypointu nie może tego zrobić. Kod karty nigdy nie jest kopiowany do warstw kontenera. Zmiana konfiguracji końcowej nie może ponownie pobierać toolchainów. W CI sprawdzamy drugi build bez zmian i raportujemy wykorzystanie cache. Uczeń wykonuje `stemctl env ensure`, które używa lokalnego cache lub buduje profil ze źródłowego Dockerfile. Nic nie jest pobierane z rejestru obrazów. ## 10. Migracja `rv-launcher` do `stem-launcher` Nazwa RV jest zbyt wąska, ponieważ launcher obsługuje serie informatyczne, fizyczne, AMD64, RISC-V i ARM. Docelowe nazwy to: - repo: `edu-tools/stem-launcher`; - publiczne CLI: `stemctl`; - workspace: `~/dev/workspace/stem`; - zmienne: `STEM_*`; - stan karty: `.stem/`. Migracja musi zachować działające workspace uczniów: 1. dodać `stemctl` jako nowy entrypoint do obecnej implementacji; 2. akceptować `rvctl` jako wrapper kompatybilności z ostrzeżeniem; 3. czytać nowe `STEM_*`, a następnie stare `RV_*` jako fallback; 4. domyślnie używać `~/dev/workspace/stem`, ale automatycznie wykrywać istniejące `~/dev/workspace/rv`; 5. zmigrować `.rv/` do `.stem/` bez usuwania stanu; 6. dopiero wtedy zmienić nazwę repo na Gitea i zaktualizować manifest; 7. pozostawić alias lub repo informacyjne pod starą nazwą na co najmniej jeden cykl zajęć; 8. usunąć kompatybilność dopiero po telemetrycznym/audytowym potwierdzeniu, że materiały nie wywołują starych nazw. Szczegóły i kryteria wycofania opisuje `doc/migration-stem-launcher.md`. ## 11. Kolejność wdrożenia ### P0 — kontrakt i bezpieczeństwo 1. Zatwierdzić nazwy profili, capabilities i schemat manifestu karty. 2. Dodać `stemctl` oraz zgodność `rvctl`. 3. Przenieść runtime z Docker Compose na abstrakcję z rootless Podmanem jako backendem domyślnym. 4. Rozdzielić sekrety hosta od kontenerów i usunąć tokeny z URL-i remotes po operacji Git. 5. Ustabilizować tożsamość instancji, labels i sockety MCP. ### P1 — trzy targety jednego Dockerfile 1. Wyodrębnić `dev-ui-base`. 2. Przenieść istniejący profil `host` do `native-amd64`. 3. Przenieść istniejący `rv32i` do `hazard3-sim` i dodać simulatorowy BSP FreeRTOS 1 kHz. 4. Zbudować `rp2350` z targetami RV i ARM oraz bezpiecznym przekazywaniem probe. 5. Ujednolicić layout tmuxa, Termdebug, DAP i MCP. ### P2 — pełny pipeline karty 1. Zaimplementować manifest karty i akcje `build/test/run/debug/deploy`. 2. Zapisywać wyniki i metadane artefaktów. 3. Porównywać golden tests pomiędzy trzema profilami. 4. Dodać `fast-dm` oraz okresowy gate JTAG. 5. Dodać zdalny smoke test przez SSH bez otwierania portów debuggera. ## 12. Bramki akceptacyjne | Obszar | Kryterium | | --- | --- | | Fetch | czysty host pobiera manifest i kartę wyłącznie przez `stemctl` | | Persistence | usunięcie kontenera nie zmienia ani nie usuwa repo ucznia | | Native | golden tests, ASan i UBSan przechodzą na AMD64 | | Emulator | te same wektory przechodzą jako RV32 bare metal i FreeRTOS | | FreeRTOS | co najmniej 100 ticków z produkcyjnym dzielnikiem 150 000 oraz 10 000 ticków przyspieszonego testu schedulera bez zgubionego cyklu | | RP2350 RV | build, flash, reset, UART i debug przez probe przechodzą bez `sudo` | | RP2350 ARM | ten sam zestaw operacji przechodzi w tym samym obrazie | | USB | probe pozostaje dostępny po resecie; BOOTSEL/picotool przechodzi kontrolowaną re-enumerację bez `--privileged` | | UI | layout tmux/Nvim, skróty i dashboard są identyczne w trzech profilach | | MCP | agent steruje wskazaną instancją i nie łączy się ze starym socketem | | Isolation | brak socketu runtime, prywatnych kluczy i `--privileged` w kontenerze | | Reproducibility | wynik zapisuje commit karty, lokalny ID builda i wersje toolchainów | | SSH | praca zdalna działa przez klucze i tunele na loopback, bez publicznego GDB | | Cache | zmiana entrypointu lub konfiguracji UI nie powoduje ponownego pobrania toolchainów | | Compatibility | stare `rvctl` i workspace `/rv` działają w okresie migracji | ## 13. Poza zakresem tej iteracji - czwarty kontener dla hoverboardu; - orkiestracja klastra albo Kubernetes; - przechowywanie pracy ucznia wewnątrz warstwy kontenera; - udostępnianie GDB, UART lub MCP na publicznym interfejsie; - automatyczne podawanie agentowi prywatnych kluczy i tokenów; - gwarancja działania symulacji RTL w czasie rzeczywistym.