[ norboten ]

Open questions

Things the build plan does not settle, with the decision taken so work could continue. Each entry is Open (needs the maintainer) or Decided (a working answer, revisit if wrong).


Q1 — How does a learner reach a VM that has no SSH? · Decided

Lab 3 boots into emergency mode and Lab 5 needs rd.break at the bootloader. Lima reaches guests over SSH, which is not up in either state.

Decision: the lab console (k, and b for the bootloader) attaches to the guest's serial console (QEMU chardev socket that Lima creates per instance). Golden images put GRUB and the kernel on the serial console and set a known root password (norboten) so the emergency shell's sulogin accepts it — on RHEL-family systems a locked root account refuses the emergency shell entirely, which would make Lab 3 unsolvable. Lab 5 then changes the root password as one of its faults.

Q2 — VZ or QEMU on macOS? · Decided

Lima defaults to Apple's Virtualization.framework (vmType: vz) on macOS. Norboten needs qcow2 disks it can snapshot with qemu-img (a lab reset, < 15 s) and the serial console sockets Lima creates only for QEMU.

Decision: vmType: qemu on every host. Doctor requires QEMU and prints the install command (brew install qemu, apt install qemu-system, …). Lima itself is downloaded and pinned by the CLI, QEMU is not — it is a system package with kernel-module dependencies on Linux.

Q3 — Windows · Open

Lima on Windows uses the WSL2 driver, which has no snapshots and no serial console, so doctor reports Windows as unsupported for local labs. Running Norboten inside a WSL2 distribution with nested KVM may work and should be tested before claiming support. There is no hosted fallback (see Q5).

Q4 — What the hosted side costs · Decided (2026-09-13)

The first plan put the server side on AWS with an EKS cluster for hosted labs; the EKS control plane alone cost more than a $30 budget. Decision: no cloud provider services at all. One server — a netcup VPS, or any Ubuntu machine — runs the whole Docker Compose project: Caddy, the API, PostgreSQL, Redis, Ollama, Prometheus and Grafana (n8n was removed in P15, 2026-09-14). The server calls no hosted model (decided 2026-09-15), so there is no other running cost.

Q5 — Hosted lab mode · Dropped (2026-09-13)

Hosted mode would have run a learner's lab VM on the server. A cloud VPS does not expose /dev/kvm, so QEMU there would run without acceleration — unusably slow for a reboot check — and Kubernetes added an operator's worth of machinery for one feature. Decision: labs run only on the learner's machine. Hosted labs remain possible on a bare-metal server with KVM, and are not built.

Q6 — "Also the runtime for the two labs that do not need a kernel" · Decided (2026-09-14)

Section 4 of the plan says Docker is the runtime for two kernel-free labs; none existed and runtime: container was a field nothing implemented. Now both exist: linux-05-shared-folder-locks-people-out (groups, modes, setgid, umask, default ACLs) and linux-06-log-that-never-rotates (logrotate's refusals, su, create versus copytruncate). They run on ubuntu-26.04-container, an image of kind: container built locally from a Dockerfile, through norboten.containers.Container, which gives the engine, the grader and the gate the same interface a Lima VM does. Neither has a reboot check, because neither has a boot: the persistence a VM lab proves by rebooting is, here, a property of files. Each starts in about a second, resets in under one, and passes the gate in 2–3 s; in CI the gate needs Docker, not KVM.

Q7 — Redis and Packer · Decided (2026-09-13)

Redis is load-bearing now: server-sent events for live sessions subscribe to Redis pub/sub instead of polling the database, rate limits count there, and the leaderboard is cached there — all state that must be shared between API workers and may be lost without harm (docs/data-model.md). Packer is not used: the golden image builder boots the upstream image with Lima (already a dependency), runs the Ansible baseline and exports the disk with qemu-img; Packer's QEMU builder would duplicate that path.

Q8 — Section references in the plan are off by one · Decided (2026-09-14)

Section 4 of the plan pointed to "section 6" for the agents (they are in 7) and "section 7" for n8n (it is 8); the solvability gate is Phase 6 in section 11, not "section 8". All three now point where they should; the other references in the plan (base images and theory questions in 6, the gate per base in 11, the site in 12) were checked and were already right.

Q9 — Podman in EX200 for RHEL 10 · Decided

Checked on 2026-09-11 against the official EX200 page (redhat.com/en/services/training/ ex200-red-hat-certified-system-administrator-rhcsa-exam): the exam is based on RHEL 10 and lists no container or Podman objectives. New in the RHEL 10 list: Flatpak repositories and packages. No Podman lab. RHCSA lab objectives: quote the official wording verbatim.

Q10 — GHCR owner · Decided (provisional)

Images and labs publish to ghcr.io/northelks/…. Change images/registry.yaml and the publish workflows together if the project moves to an organisation.

Q11 — The intro track · Decided

The plan binds rhcsa to Rocky and linux to Ubuntu/Alpine, but hello must run on all three images. The spec adds a third track, intro, served by every image and used only by hello.

Q12 — min_memory per image · Decided (2026-09-14)

images/registry.yaml carried first estimates. images/memory.py now measures them: every lab runs through the solvability gate while a sampler in the guest records MemTotal, MemAvailable and swap once a second, labelled by gate step, collected before each reboot and before the VM is deleted, with the kernel's out-of-memory count; --memory image=size gives a lab less, which is how a smaller value is proven.

Image min_memory Peak used (lab, step) Also passes at Below that
alpine 256 MiB 52 MiB (linux-01, boot) 224 MiB 192 MiB hangs in UEFI; 128 MiB kernel panic
ubuntu-26.04 † 512 MiB 106 MiB (linux-02, after the reboot) 384 MiB
ubuntu-26.04-devops † 1 GiB 324 MiB (docker-01, broken state) 768 MiB
ubuntu-26.04-automation † 3 GiB 1061 MiB (ollama-02, broken state; re-measured 2026-09-15 after n8n left, 7 labs) not re-probed; 2 GiB passed with n8n
rocky-10 1536 MiB 425 MiB (rhcsa-04, the solution) not probed: the RHCSA labs pin 1536 MiB

All 29 lab × image pairs passed at the registry's values with no swap and no OOM kill. The estimates stay: each keeps at least twice the reference solution's peak, and a learner does more than the solution does — editors, man, a Compose stack of their own, a second model loaded in Ollama. Lowering them would buy a few hundred megabytes on the host at the price of labs that fail for a reason the lab is not about. One-second sampling can miss a short spike, which is why the "also passes at" column, and not the peak, is the evidence for headroom.

† Measured on the image each one replaced when the images moved to Ubuntu 26.04 (2026-09-15); the values stay until images/memory.py has measured the Ubuntu images.

Q13 — Memory snapshots are broken on Apple Silicon · Decided

Measured on QEMU 11.0.1 with HVF (macOS, M-series): after savevm — which is what limactl snapshot create does on a running VM — the next guest reboot hangs in UEFI at 100% CPU, and starting QEMU with -loadvm aborts (cpu_pre_load: assertion failed). A plain pause/resume does not trigger it.

Decision: snapshots are disk-only (qemu-img snapshot, VM stopped) and a lab reset is a cold boot from the restored disk. Golden images are tuned to make that fast — GRUB timeout 1 s, cloud-init limited to NoCloud, and on Alpine no blocking chronyd initstepslew (15 s) or dhcpcd ARP probe (5 s). Measured reset: Alpine 9.8 s, Rocky 9.9 s. Revisit memory snapshots on KVM hosts, where they may work, once there is a Linux test machine.

Q14 — The gate and boot_after_break · Decided

For a lab whose broken boot never reaches the network (Lab 3's emergency mode), the reference solution cannot run over SSH after the extra boot. The gate therefore applies the faults and runs the solution without the post-break boot; the grading reboot that follows still proves the fix survives a boot. The learner-facing broken boot is exercised by hand when a lab is reviewed — which missed two faults; Q18 closed that gap with a second gate phase.

Q15 — Upstream mirrors are slow · Decided

dl.rockylinux.org delivered ~0.4 MB/s and stalled a Lima download mid-file. The golden image builder downloads upstream images itself — resumable, retried, digest-verified — into ~/.norboten/cache/upstream/ and hands Lima a local path.

Q16 — A vector database · Decided (2026-09-13)

The plan listed pgvector for retrieval and question dedup. Nothing in the stack needed embeddings: the consultant ranks with BM25 (exact technical terms beat similarity here), question dedup is textual (difflib against the topic's accepted prompts), and relations between labs, topics, question banks and journals are computed with TF-IDF, cosine similarity and a networkx graph — deterministic, explainable, no model. Decision: no vector database. pgvector in the same PostgreSQL stays the next step if semantic similarity ever earns its place; the image would change from postgres:17-alpine to pgvector/pgvector:pg17 and nothing else.

Q17 — Which models verify a drafted question · Decided (2026-09-15)

Questions are drafted locally, never on the server. By default the writer is claude-code/opus and the blind solvers claude-code/sonnet and claude-code/haiku, on the drafter's own Claude Code subscription. The pipeline requires at least two verifiers that are not the writer, so one vendor still gets independent solves (a different model, a fresh context, no key in the prompt); exported OpenAI or Gemini keys add another family, the stronger check. The vendor is inferred from the id prefix.

Q18 — Faults that do not survive the boot after the break · Decided (2026-09-14)

The consequence of Q14, found by replaying every journal on a real machine (2026-09-13): a fault that holds right after the break scripts run but not after one more boot passes the gate and is already gone for the learner. Two labs shipped that way. In rhcsa-03 the held, deleted spool file died with its process at the boot (fixed: the spooler takes its spool again in a new boot until it has been restarted). In rhcsa-05 the Storage=volatile drop-in was named 50-… and lost to the golden image's 90-norboten.conf, and the deleted /var/log/journal is recreated by systemd-tmpfiles at boot (fixed: 99-retention.conf).

Decision: a second gate phase for boot_after_break labs (docs/lab-spec.md §9, steps 5–6). After the normal gate passes, a fresh VM gets the faults, boots once more exactly as a learner's does, and every check must still fail. The checks run over SSH when the broken machine has a network. When the boot stops at a maintenance prompt they run over a serial port: before that boot the gate enables systemd's debug-shell.service on Lima's spare PCI serial port (found by writing a marker to each guest tty and seeing which one reaches serialp.log), which gives a root shell in emergency mode without any password — rhcsa-05 changes the root password, so sulogin with the image's password is only the fallback. The runner and the checks cross in acknowledged base64 chunks (cli/src/norboten/lima/serial_shell.py).

Verified on rocky-10: rhcsa-03 and rhcsa-05 both stop at the maintenance prompt and pass the phase; a copy of rhcsa-05 with the old 50-retention.conf passes every earlier step of the gate and fails this phase on exactly 12_journal_persistent. One check needed a declared exemption: rhcsa-03's 05_held_space_released cannot fail in emergency mode, where the spooler is not running; it is marked maintenance_pass: true with the reason, and the exemption lapses if a boot ever reaches the network.

Q19 — Should the consultant add an embedding rerank? · Decided (2026-09-15)

Retrieval was measured on two held-out sets of learner-style questions. The first set scores 15/16 in the top three passages, but the corpus has since been edited with it in mind. The second set, which nothing was tuned for, scores 6/16: BM25 finds a question's exact terms (lvextend, fstab) and misses the same question in other words. An embedding model would help with rewording — all-minilm through Ollama gave 0.621 to "disk full" against "no space left on device", which share no words, and 0.228 to an unrelated phrase — at the cost of a second model on the server, an index to rebuild with the content, and rankings that are harder to explain. Decision (the maintainer): keep BM25 alone, as Q16 and the architecture describe. The 6/16 is a known limit, the test keeps its floor under it, and the consultant's widget still falls back to the same passages in the browser. Revisit with a third held-out set if learners' real questions show the misses matter.