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 legacy Pi ALSA driver is blacklisted at the modprobe level
(/etc/modprobe.d/kn86-blacklist.conf):
blacklist snd_bcm2835Per ADR-0017 / ADR-0054 and the
Canonical Hardware Specification in canonical-spec.md, the YM2149 PSG is
synthesized on the Pi Pico 2 coprocessor and emitted via I2S to a
WM8960 codec; loading snd_bcm2835 would only expose an unused ALSA
device. Bench phase (GWP-698): snd_soc_bcm2835_i2s is deliberately
not blacklisted — the WM8960 sits on the Pi’s header as a HAT during
bring-up and its overlay needs the Pi I2S DAI to autoload (see
device-tree-overlays.md “Pi-side audio”). The
blacklist line returns when the audio chain moves to the Pico. The kiosk
user (nosh) remains NOT in the audio Linux group, yet nOSh opens the
default PCM anyway: the tty1 autologin session grants per-device logind
seat ACLs on /dev/snd/* (verified on deckline 2026-07-08 — nOSh held
pcmC1D0p at boot). Group membership stays off so non-seated users
(SSH) get no standing audio grant.
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.