Kiosk Mode
How the KN-86 boots straight into the nOSh runtime with no login prompt, no display manager, and a read-only root filesystem — and how a developer drops out of that mode for debugging. Read this if you are configuring auto-login, changing the read-only-root layout, disabling a service, or debugging “why won’t my edit to /etc/... survive a reboot.”
boot-and-systemd.md— service ordering that this kiosk model assumes.system-image-build.md— thestage-kn86-baseandstage-kn86-runtimestages where these settings are baked in.update-system.md— how updates land on the read-only root.../hardware/build-specification.md§5 — Developer-mode vs Production-mode boot flag.../../adr/ADR-0011-device-firmware-update-system.md— partition layout, including the/home/sharedsave partition this doc references.
Auto-login
Section titled “Auto-login”There is no display manager (no LightDM, no GDM, no SDDM). The system boots to multi-user.target, then a getty override on tty1 auto-logs the kiosk user.
# /etc/systemd/system/getty@tty1.service.d/kn86-autologin.conf[Service]ExecStart=ExecStart=-/sbin/agetty --autologin nosh --noclear %I $TERMType=idleThe nosh user is created by stage-kn86-base of system-image-build.md:
useradd -m -s /bin/bash -G video,audio,input,dialout noshvideo for KMSDRM framebuffer access, audio (legacy — actual audio is via the Pico, not Linux), input for /dev/input/event*, dialout for /dev/serial0 to talk to the Pico. No sudo, no shell access in production mode (see “Recovery / dev-mode toggle” below).
The user’s ~/.profile chains directly into the nOSh runtime systemd unit:
# /home/nosh/.profile (snippet)if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$KN86_BOOTED" ]]; then export KN86_BOOTED=1 exec systemctl --user start kn86-noshfiThe combination of auto-login + the ~/.profile chain delivers the user to nOSh in <8 s from kernel handoff. The multi-user.target is the right systemd target — graphical.target would pull in display-manager assumptions we explicitly don’t want.
Read-only root filesystem
Section titled “Read-only root filesystem”The rootfs (/dev/mmcblk0p4 or p5, depending on active slot per ../../adr/ADR-0011-device-firmware-update-system.md) is mounted read-only by /etc/fstab in the slot:
/dev/disk/by-label/rootfs-A / ext4 ro,noatime,errors=remount-ro 0 1/dev/disk/by-label/bootfs-A /boot vfat ro,noatime 0 2/dev/disk/by-label/share /home/shared ext4 rw,noatime 0 2tmpfs /tmp tmpfs rw,nosuid,nodev,size=64m 0 0tmpfs /var/log tmpfs rw,nosuid,nodev,size=32m 0 0Ephemeral writes (anything that systemd, journald, or nOSh wants to scribble during a session) land on tmpfs at /tmp and /var/log. They evaporate on reboot. The only writable persistent location is /home/shared (p6) — the Universal Deck State partition.
We use pi-gen’s built-in overlayroot support (an overlay-root overlay that mounts an overlayfs over the root, backed by tmpfs) for any case where a runtime tool insists on writing to a non-tmpfs path under /. This is enabled in stage-kn86-base of system-image-build.md and handled by adding init=/init-overlay to cmdline.txt for that slot. We prefer the explicit fstab ro mount above when possible (smaller surface, no overlayfs surprise behaviour); overlayroot is the fallback for code that won’t honour ro.
Save partition layout
Section titled “Save partition layout”/home/shared (p6 from ADR-0011) is the only persistent writable location the operator’s data ever touches. It carries:
/home/shared/├── deckstate.bin # Universal Deck State (operator handle, credits,│ # reputation, cartridge_history bitfield, phase chain)├── boot-success-token # 60-second sentinel for ADR-0011 tryboot commit├── kn86-mode.txt -> /boot/kn86-mode.txt # symlink for runtime read└── (cart-side per-cart save data lives on each cart's own SD; not here)The image-build pipeline never writes p6, the flasher never writes p6, the updater image never writes p6. Only nOSh (running as user nosh) writes p6. This isolation is the load-bearing reason ADR-0011 §Risks #5 holds — operator state survives every conceivable update / re-flash / slot swap.
Per-cartridge save data lives on the cart’s own SD per ../../adr/ADR-0019-cartridge-storage-and-form-factor.md — never on /home/shared.
Disabling unused services
Section titled “Disabling unused services”stage-kn86-base of system-image-build.md masks the following systemd units (the -base flavor of pi-gen pulls some in by default that we don’t want):
| Unit | Reason |
|---|---|
getty@tty2.service … getty@tty6.service | Production mode has only tty1 (autologin into nOSh). |
bluetooth.service | Bluetooth is unused; the BT module is also disabled at the device-tree level (device-tree-overlays.md disable-bt) to free UART0. |
avahi-daemon.service | mDNS exposes kn86.local on the network; in production mode we don’t want network discoverability. Re-enabled in dev mode. |
triggerhappy.service | Pi OS Lite ships a generic input-event-to-shell-command bridge; conflicts with nOSh’s exclusive ownership of evdev. |
keyboard-setup.service | Pi OS Lite tries to apply Linux console keymap; nOSh owns the keyboard, the Linux console is never user-visible. |
systemd-resolved.service | Production mode has no DNS use case post-boot; static /etc/resolv.conf (empty) is sufficient. |
wpa_supplicant.service | Wi-Fi is dev-mode-only; production has no network use case post-boot. Re-enabled in dev mode. |
NetworkManager.service | Pi OS Lite trixie default network stack (“Network stack” below). Production has no network use case post-boot; re-enabled in dev mode. |
NetworkManager-wait-online.service | Wait-for-network would block boot in production — there is no network. Re-enabled in dev mode. |
systemd-timesyncd.service | NTP is dev-mode-only; production has no Wi-Fi to sync against. See boot-and-systemd.md “Time and clock”. Re-enabled in dev mode. |
ssh.service | Production has no SSH; the service is masked. Dev mode enables it. |
dphys-swapfile.service | Pi OS Lite enables a swapfile by default (/var/swap). Swap on the SD card amplifies write-wear and the kiosk has no large-RSS workload — masked unconditionally; companion vm.swappiness=0 lives in /etc/sysctl.d/99-kn86-no-swap.conf. |
Mask command for reference (run in stage-kn86-base/run-chroot.sh):
systemctl mask bluetooth.service avahi-daemon.service triggerhappy.service \ keyboard-setup.service systemd-resolved.service \ wpa_supplicant.service ssh.service dphys-swapfile.service \ NetworkManager.service NetworkManager-wait-online.service \ systemd-timesyncd.servicefor tty in 2 3 4 5 6; do systemctl mask "getty@tty${tty}.service"doneAudio modules
Section titled “Audio modules”The Pi-side ALSA audio path is blacklisted at the modprobe level
(/etc/modprobe.d/kn86-blacklist.conf):
blacklist snd_bcm2835blacklist snd_soc_bcm2835_i2sblacklist snd_bcm2835_i2sPer ADR-0017 and the
Canonical Hardware Specification in CLAUDE.md, the YM2149 PSG is
synthesized on the Pi Pico 2 coprocessor and emitted via I2S to a
MAX98357A DAC/amp. The Pi never touches I2S audio; loading
snd_bcm2835 would only expose an unused ALSA device. The kiosk user
(nosh) is correspondingly NOT in the audio Linux group — no
capability is granted that doesn’t have a corresponding device.
Network stack
Section titled “Network stack”Pi OS Lite trixie ships NetworkManager as the default network
stack (replacing dhcpcd). We keep that default — systemd-networkd
would be a gratuitous fork.
- Production mode:
NetworkManager.serviceandNetworkManager-wait-online.serviceare masked (see “Disabling unused services” above). The kiosk has no post-boot network use case; mass storage is via cartridge, not the network. - Dev mode: both services unmask. Wi-Fi credentials are NEVER
pre-baked into the image — the operator sets them via
nmcli device wifi connect <ssid> password <pwd>on the first dev-mode boot. The connection profile lands at/etc/NetworkManager/system-connections/and persists across reboots. - NM defaults drop-in:
/etc/NetworkManager/conf.d/99-kn86.confinstalls a small set of opinionated defaults — no auto-default connection, Wi-Fi power-save off (multi-second SSH latency stalls are intolerable on the dev loop), info-level logging.
Field updates (update-system.md) do NOT use the
network — the path is cartridge-MSC sneakernet (per ADR-0011 +
ADR-0019 + ADR-0020 surface 1). The fact that production mode has no
network is a feature, not a regression.
Recovery / dev-mode toggle
Section titled “Recovery / dev-mode toggle”There is no in-device path to drop to a shell from production mode. By design — a kiosk user cannot click their way out of the kiosk, and an attacker with physical access can’t trick the device into a shell either.
Toggling mode
Section titled “Toggling mode”Mode is set by a single file read at boot: /boot/kn86-mode.txt on p1 (the common boot region). Contents:
mode=productionor
mode=developmentstage-kn86-runtime of system-image-build.md reads this file in kn86-display-init.service (early in boot) and exports KN86_MODE for the rest of the unit graph. nOSh re-reads it once at start.
Dev mode unlocks
Section titled “Dev mode unlocks”| Concern | Production | Development |
|---|---|---|
/etc/getty@tty1 autologin | nosh user, no shell exit | nosh user, but Ctrl+C drops to bash |
getty@tty2 … tty6 | masked | enabled (alt + arrow on a USB keyboard switches; the KN-86’s mech keeb does not have alt + arrow, so this is effectively bench-keyboard-only) |
ssh.service | masked | enabled (key-only — PasswordAuthentication no, PermitRootLogin no, ChallengeResponseAuthentication no via /etc/ssh/sshd_config.d/99-kn86.conf). Authorized keys baked into /home/kn86/.ssh/authorized_keys at image-build time from the wrapper’s KN86_AUTHORIZED_KEYS_FILE env var. |
NetworkManager.service | masked | enabled — NM is the dev-mode network manager. Wi-Fi creds set via nmcli device wifi connect, never pre-baked. See “Network stack” above. |
wpa_supplicant.service | masked | masked (NetworkManager owns the Wi-Fi interface in dev mode; wpa_supplicant is only used as NM’s backend, not as a standalone service). |
avahi-daemon.service | masked | enabled (kn86.local resolvable) |
systemd-timesyncd.service | masked | enabled — NTP is reachable via the dev Wi-Fi. See boot-and-systemd.md “Time and clock”. |
dphys-swapfile.service | masked | masked — swap is bad on SD regardless of mode (vm.swappiness=0 everywhere). |
| journald | volatile (/run/log/journal, capped 32 MB / 8 MB free) | persistent (/var/log/journal, capped 100 MB total / 30 MB per service) |
| Rootfs | read-only | read-only by default; mount -o remount,rw / works since the operator is root in dev |
| nEmacs REPL filesystem access | read-only | read-write to /home/shared and mounted carts |
Switching modes
Section titled “Switching modes”- Dev → Prod: edit
/boot/kn86-mode.txt(mount p1 from any host with an SD reader, or via the updater MSC mount from ADR-0011), setmode=production, reboot. - Prod → Dev: physical SD access required. No in-device path. This is deliberate — production mode should not be re-enableable into dev mode without somebody opening the case and pulling the SD. The Pelican shell makes this a deliberate ritual, not a slip.
Legacy Terminal exception
Section titled “Legacy Terminal exception”ADR-0021 defines a single sanctioned shape for nOSh-spawned child processes: the Legacy Terminal mode. This subsection clarifies how it interacts with the kiosk contract above.
The kiosk authority is preserved. Legacy Terminal does not weaken any kiosk-mode guarantee:
- nOSh remains the only entry point. The child process is launched by nOSh via
posix_spawninside the samenoshuser session — it is not an independently reachable login path. - Auto-login is unchanged. The auto-login override on tty1 still chains into nOSh; Legacy Terminal is reachable only after nOSh has started and the operator has entered the SYS+INFO×4 gesture from Bare Deck.
- The read-only rootfs is unchanged. Legacy Terminal binaries and license materials live under
/opt/legacy-terminal/baked at image-build time — no runtime install path, noapt-get, no writes to the rootfs during a session. - The
/home/sharedisolation contract from ADR-0011 is unchanged. Per-title save state lands in/home/shared/legacy-terminal/saves/(p6), created at first-boot bysystemd-tmpfiles; it is never written by the flasher. - Dev mode is not required. Legacy Terminal is a production-mode feature.
The architectural pattern — “nOSh releases framebuffer/input/audio, spawns child, reclaims on
SIGCHLD” — is the mode-swap primitive. Future features that follow the same shape (a child
process that owns the device for a bounded session) inherit the kiosk-authority preservation
argument from ADR-0021 without requiring new kiosk-mode analysis, provided they follow the same
constraints: single child, no shell escape path, saves to /home/shared only.
Bootloader-level recovery
Section titled “Bootloader-level recovery”If the SD itself is corrupt and the system won’t boot to the point of mounting p1, the only recovery path is to re-image the SD using a fresh .img from the system-image-build.md pipeline. There is no bench-side rescue partition baked into the SD layout — ADR-0011’s six-partition table does not reserve one. /home/shared (p6) survives any re-image of slots A and B (the flasher and provisioning script never touch p6 — see update-system.md), so operator state survives bootloader-level recovery.
For total-loss situations (corrupt p1 or a totally bad card), reseed the SD from .img and on first boot of the new image nOSh will see an empty /home/shared and bootstrap a fresh deck state.