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:
- synchronizację publicznego manifestu serii;
- listowanie serii, kart i zadań;
- pobieranie kodu karty z Git do workspace na hoście;
- przygotowanie repo odpowiedzi, brancha ucznia i remotes;
- wybór profilu oraz targetu;
- lokalny build właściwego targetu ze źródłowego Dockerfile albo użycie jego lokalnego cache;
- utworzenie, wznowienie, zatrzymanie i usunięcie kontenera;
- wywołanie
build,test,run,debugideploy; - rejestrację artefaktów, logów, wyników i socketów MCP;
- 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
/workspacejako 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-dmdo 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:
- łączy się wyłącznie przez SSH z parą kluczy;
- uruchamia
stemctlna tym zdalnym hoście; - tamtejszy launcher zarządza lokalnym rootless Podmanem i lokalnym workspace;
- 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:
- dodać
stemctljako nowy entrypoint do obecnej implementacji; - akceptować
rvctljako wrapper kompatybilności z ostrzeżeniem; - czytać nowe
STEM_*, a następnie stareRV_*jako fallback; - domyślnie używać
~/dev/workspace/stem, ale automatycznie wykrywać istniejące~/dev/workspace/rv; - zmigrować
.rv/do.stem/bez usuwania stanu; - dopiero wtedy zmienić nazwę repo na Gitea i zaktualizować manifest;
- pozostawić alias lub repo informacyjne pod starą nazwą na co najmniej jeden cykl zajęć;
- 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
- Zatwierdzić nazwy profili, capabilities i schemat manifestu karty.
- Dodać
stemctloraz zgodnośćrvctl. - Przenieść runtime z Docker Compose na abstrakcję z rootless Podmanem jako backendem domyślnym.
- Rozdzielić sekrety hosta od kontenerów i usunąć tokeny z URL-i remotes po operacji Git.
- Ustabilizować tożsamość instancji, labels i sockety MCP.
P1 — trzy targety jednego Dockerfile
- Wyodrębnić
dev-ui-base. - Przenieść istniejący profil
hostdonative-amd64. - Przenieść istniejący
rv32idohazard3-simi dodać simulatorowy BSP FreeRTOS 1 kHz. - Zbudować
rp2350z targetami RV i ARM oraz bezpiecznym przekazywaniem probe. - Ujednolicić layout tmuxa, Termdebug, DAP i MCP.
P2 — pełny pipeline karty
- Zaimplementować manifest karty i akcje
build/test/run/debug/deploy. - Zapisywać wyniki i metadane artefaktów.
- Porównywać golden tests pomiędzy trzema profilami.
- Dodać
fast-dmoraz okresowy gate JTAG. - 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.