Files
stem-launcher/doc/architecture-plan.md

19 KiB

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/<instance>;
  • 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:

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:

stemctl test hazard3-sim inf bss 1
stemctl deploy rp2350 inf bss 1 --target rp2350-arm

Launcher wykonuje następujący pipeline:

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:

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:

.stem/results/<profile>/<target>/<task>/<run-id>/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:

┌────────────────────────────────────────────────────────────┐
│ 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:

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

$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/<cid12>/t.sock
$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/<cid12>/n.sock
$XDG_RUNTIME_DIR/stem/<thread-key>/<instance-key>/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:

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.