Skip to main content

max / mountaineer

chore: move todo.md to private layer (gitignored)
Author: Max Johnson <me@maxj.phd> · 2026-06-05 00:22 UTC
Commit: 83e6e750c2820c864c5c5efb94ae342d5650a7ca
Parent: 0426a1a
2 files changed, +3 insertions, -306 deletions
A .gitignore +3
@@ -0,0 +1,3 @@
1 +
2 + # Private working files — live in _private/, synced via Syncthing
3 + todo.md
D docs/todo.md -306
@@ -1,306 +0,0 @@
1 - # Mountaineer — todo
2 -
3 - ## Key Paths
4 -
5 - - `MANIFESTO.md` — principles, three purposes, "Dull Edge" channel, refusal-to-default carve-out
6 - - `STACK.md` — every default Mountaineer ships, defended; includes the compatibility contract at the top
7 - - `PROFILES.md` — what server/laptop/bastion/homelab actually mean
8 - - `AESTHETICS.md` — committed picks: surfaces-and-`sysop` doctrine, flatwhite palette, Iosevka, ratatui conventions, yambar segments, river defaults, swaylock, TTY login
9 - - `ROUTES.md` — recipe library; first-boot, mesh, bastion, Caddy+podman, restic drill landed
10 - - `BUILD-TOOLS.md` — orientation map for the upstream toolchain (apk, mkimage, mkinitfs, apkovl, lbu, ZFS, zfsbootmenu, etc.) plus Mountaineer-authored components and open toolchain decisions
11 - - `INSTALL.md` — defended spec for install mechanics: two ISOs from one source, disk layout, ZFS pool/datasets, ARC sizing, zfsbootmenu, apkovl shape and source tree, installer flow, v0.1 dogfood gate
12 - - `references/` — third-party material we want to keep alongside the spec
13 - - `../build/` — build pipeline (apkovl-src, mkimg profile, installer, QEMU iteration loop)
14 - - `../sysop/` — Cargo workspace: CLI binary + sysop-tui primitives crate
15 - - `/Users/max/Code/CONTRIBUTING.md` — cross-cutting rules (no Docker, no PRs, latest deps, file size, todo format)
16 - - `/Users/max/Code/CLAUDE.md` — repo-wide LLM instructions
17 -
18 - ## Spec
19 -
20 - - [x] Manifesto: principles, thesis, three purposes, refusal-to-default, what-we-won't-do, channel name
21 - - [x] STACK foundation: apk, busybox ash, busybox coreutils, OpenSSH
22 - - [x] STACK time/DNS: chrony, unbound
23 - - [x] STACK ingress: Caddy
24 - - [x] STACK secrets/backup: age, sops, restic
25 - - [x] STACK interactive userland: ripgrep, fd, sd, bat, eza, dust, procs, bottom, zoxide, lnav, starship
26 - - [x] STACK spicy picks: s6+s6-rc, helix, zellij, nushell, podman, ZFS on root, Vector, nftables, msmtp, ifupdown-ng+iwd, Nebula
27 - - [x] STACK Sysadmin (graphical): river, foot, yambar, fuzzel+cliphist, fnott, grim+slurp, swaylock/idle/bg, pipewire+wireplumber, seatd, brightnessctl, tlp, browser-refusal
28 - - [x] Compatibility contract section in STACK.md
29 - - [x] Cross-check MANIFESTO against STACK.md (filesystem ext4→ZFS, added chrony/msmtp/ifupdown-ng+iwd/OpenSSH/graphical-layer summary/sd/lnav/starship)
30 - - [x] Review PROFILES.md against the current stack (added `sd`, removed redundant docker-alias claim)
31 - - [ ] Decide whether any foundation pick needs promotion to spicy (Caddy? nftables?) — defer until ROUTES surfaces operational stress
32 -
33 - ## ROUTES.md
34 -
35 - - [x] First-boot checklist (keys, hostname, time, firewall, resolver verification)
36 - - [x] Stand up the mesh (Nebula) — promoted to STACK as default overlay mesh
37 - - [x] Bastion + ProxyJump (non-mesh floor)
38 - - [x] Reverse-proxy a podman container with Caddy auto-HTTPS
39 - - [x] Restic restore drill
40 - - [ ] sops + age workflow for service secrets in a repo
41 - - [ ] WireGuard peer setup via `sysop wg` (raw point-to-point, distinct from mesh)
42 - - [ ] Mail relay upgrade path (msmtp → OpenSMTPD)
43 - - [ ] ZFS snapshot/rollback walkthrough for a bad upgrade
44 - - [ ] Nebula cert rotation + CRL distribution
45 - - [ ] Reconcile `sysop` subcommands assumed by routes (`hostname set`, `net status`, `resolver verify`, `ssh keys list/add`, `ssh hostkeys`, `fw status/apply`, `mesh ca init`, `mesh sign`, `mesh bundle`, `mesh join`, `mesh status`, `mesh reload`, `mesh revoke`) with the `sysop` subsystem when it opens
46 - - [ ] Resolve overlap between `sysop ssh keys` and per-host age keys used as bundle recipients in mesh route — resolved structurally (both go under `sysop id host`), but only SSH half ships v1
47 - - [ ] Confirm `nebula` apk repo placement (community expected) — install mechanics work
48 -
49 - ## Install / overlay mechanics — SPEC
50 -
51 - - [x] apkovl layout (spec'd in INSTALL.md)
52 - - [x] Install script: ZFS pool creation, dataset layout, zfsbootmenu integration (spec'd in INSTALL.md)
53 - - [x] `zfs_arc_max` install-time sizing table (spec'd in INSTALL.md)
54 - - [x] Sysadmin vs Server build artifacts (two ISOs, one source) — strict subset enforced by build structure
55 - - [x] First-boot flow: hostname, ssh keys, network, browser-pick prompt (spec'd)
56 -
57 - ## Install / overlay mechanics — IMPLEMENTATION
58 -
59 - Lives in `../build/`. **Full installer runs end-to-end in QEMU as of build 16**; testable ISO at `../build/out/mountaineer-server-v3.23-x86_64.iso`. Next: Framework 13 hardware verification. See `../build/build_notes.md` for traps and gotchas.
60 -
61 - ### Done
62 -
63 - - [x] apkovl source tree — `apkovl-src/common/` (target/shared) + `apkovl-src/live/` (live-only overlay). manifest.toml profiles: `server` (common+live + extras) and `server-target` (common only).
64 - - [x] apkovl builder (`tools/build-apkovl.sh`) — reproducible-best-effort, GNU tar required, supports `--extra <dir>` for dynamic payload merge
65 - - [x] `mkimg.mountaineer_server.sh` — `arch=x86_64`, `profile_base` parent, `profile_abbrev="mountaineer"` (31-char volid), `apkovl="genapkovl-mountaineer-server.sh"`
66 - - [x] `genapkovl-mountaineer-server.sh` — builds target apkovl, stages installer + target apkovl + docs into extras, emits live apkovl
67 - - [x] Installer (`installer/install.sh`) — 17-step shell+dialog, `--dry-run` AND `--config <yaml>` modes, auto-detects config at `/media/cdrom/install.yaml` etc.
68 - - [x] Top-level `build.sh` — explicitly wipes `.work/apkovl_*` and `.work/image-*` before mkimage (the apkovl section caches by name only; see build_notes.md)
69 - - [x] QEMU iteration loop (`tools/qemu/`):
70 - - [x] `vm-up.sh` — cloud-image-based builder VM, cloud-init provisioning ~60s, zero-touch `setup-alpine` skipped
71 - - [x] `vm-build.sh` — rsync repo → build inside VM → copy artifacts back
72 - - [x] `vm-test-boot.sh` — boot ISO in fresh target VM, no install config, wait for sshd
73 - - [x] `vm-test-install.sh` — boot ISO with unattended install config attached (FAT image labeled CIDATA via hdiutil)
74 - - [x] ISO produced — 660 MB at `out/mountaineer-server-v3.23-x86_64.iso`
75 - - [x] ISO boots in QEMU — Mountaineer apkovl applied, custom `/etc/issue` rendered at login prompt
76 - - [x] Cloud image: `generic_alpine-3.23.4-x86_64-uefi-cloudinit-r0.qcow2` (192 MB) downloaded and resized to 30 GiB
77 - - [x] Mountaineer SSH keypair at `.vms/keys/builder_ed25519` (gitignored, ed25519, doesn't collide with operator keys)
78 -
79 - ### Build 16 — testable ISO (May 17, 13:15 local)
80 -
81 - - [x] Builds 11–16: full apkovl → mkimage → ISO pipeline works
82 - - [x] ISO boots in UEFI QEMU; sshd reachable on 2223; `mountaineer-install` at `/usr/local/sbin/`
83 - - [x] Installer runs all 17 steps end-to-end against `/dev/vda` (incl. ZBM v3.1.0 EFI fetch+verify+install)
84 - - [x] Reboot into installed disk lands in UEFI → BOOTX64.EFI (ZBM) → Mountaineer kernel; sshd listens on guest:22
85 - - [x] Build 16 has every fix from #11–#22 baked into the ISO (no live-patching needed)
86 -
87 - ### Next steps (resume here)
88 -
89 - - [ ] **Framework 13 hardware test**: `dd` `out/mountaineer-server-v3.23-x86_64.iso` to USB, boot F13 in UEFI mode, run installer against `/dev/nvme0n1` (triggers the `p1`/`p2` partition-naming branch in `installer/install.sh`). If F13 needs wifi setup before install: `iwctl` in the live env.
90 - - [ ] Re-verify the install → reboot → ssh-in cycle in QEMU with build 16 (the new ISO carries the kcl-property + pubkey fixes that haven't yet been exercised end-to-end together).
91 - - [ ] First-boot route walk-through: ROUTES.md "first boot" beyond hostname (network up, operator key works, services up under s6-rc) — verify each.
92 - - [ ] Reproducible builds: static audit landed in `../build/build_notes.md` (apkovl half looks reproducible by construction; ISO half has 8 candidate non-determinism sources). Next concrete step is the apkovl-only diff (`build-apkovl.sh` twice, compare sha) before spinning the VM. Requires GNU tar on the build host.
93 -
94 - ### Build history — fixes burned through (institutional memory)
95 -
96 - Originating builds 1–10 (pre-ISO-boots era):
97 - - [x] **#1** Invented `--apkovl` flag for mkimage → mkimage uses `apkovl=<script.sh>` in profile + `build_apkovl()` runs it
98 - - [x] **#2** Hyphen in profile name (`mountaineer-server`) → shell function `profile_$NAME` requires underscore
99 - - [x] **#3** Made up `profile_abuild` parent → real parent is `profile_base` in mkimg.base.sh
100 - - [x] **#4** Used `profile_arch=` → real var is `arch=` (silent skip if wrong via `list_has $ARCH $arch || return 0`)
101 - - [x] **#5** Listed `zfs-utils` in world → doesn't exist in Alpine; userland is in `zfs` package
102 - - [x] **#6** mtools missing from builder → grub-efi uses `mformat` to format ESP image; added to cloud-init packages
103 - - [x] **#7** Volume label 32-char truncation w/ trailing space broke grub → `profile_abbrev="mountaineer"` keeps volid under 32 chars without trailing space
104 - - [x] **#8** Genapkovl produced wrong shape → refactored to two profiles (server, server-target) + extras; live apkovl embeds target+installer+docs
105 - - [x] **#9** Live env had no ssh access → `apkovl-src/live/root/.ssh/authorized_keys`, `sshd_config.d/10-live.conf` (PermitRootLogin prohibit-password), `etc/runlevels/default/sshd` symlink
106 - - [x] **#10** sshd doesn't start despite runlevel symlink → apkovl applies after default runlevel reached; `/etc/local.d/sshd.start` retroactively starts it
107 -
108 - Builds 11–16 (ISO-boots-and-installs era — full session details in `../build/build_notes.md`):
109 - - [x] **#11** `tools/build-apkovl.sh`: `find ... -exec touch -d '@0'` followed symlinks into root-owned `/etc/init.d/*` and silently broke the live apkovl. Fix: `find ! -type l`.
110 - - [x] **#12** `apkovl-src/common/etc/ssh/sshd_config`: removed `PrintLastLog yes` — unsupported by Alpine's OpenSSH.
111 - - [x] **#13** `apkovl-src/live/etc/network/interfaces` missing → networking failed in live env. Added lo + eth0 DHCP.
112 - - [x] **#14** `build.sh` source-tree-hash cache-busting was ineffective — mkimage's apkovl section caches by name only. Replaced with explicit `rm -rf .work/apkovl_* .work/image-*`.
113 - - [x] **#15** `installer/install.sh` step 7: busybox `mount` can't auto-detect FAT32 on freshly-formatted ESP → explicit `-t vfat`.
114 - - [x] **#16** `installer/install.sh` step 9: world-file parser kept inline `# comments` as package tokens → `sed -e 's/#.*//' | xargs`.
115 - - [x] **#17** `installer/install.sh` step 9: apk on new root reports `UNTRUSTED signature` → seed `/mnt/etc/apk/keys/` from live env.
116 - - [x] **#18** `installer/install.sh` step 13: `chroot /mnt ssh-keygen -A` failed (`/dev/null` missing) → `bind_target_dev` for `/dev /proc /sys` before chroot ops; unbind in finalize.
117 - - [x] **#19** `apkovl-src/common/etc/apk/world`: `dialog` missing — installer's interactive prompts need it.
118 - - [x] **#20** `installer/install.sh` step 12: ZBM placeholder → fetch v3.1.0 `zfsbootmenu-release-x86_64-linux6.18.EFI`, sha256-pinned, installed to `/EFI/BOOT/BOOTX64.EFI`.
119 - - [x] **#21** `tools/qemu/vm-test-install.sh`: `--daemonize` mode for scripted runs; FAT config image step gracefully skipped if host lacks `dosfstools`/`mtools`.
120 - - [x] **#22** `installer/install.sh` step 12: kernel cmdline silent on serial → set `org.zfsbootmenu:commandline='ro quiet console=tty0 console=ttyS0,115200'` on root dataset (bypass `run` — its `$@` unquoted word-splits).
121 -
122 - ### Iteration tips (current)
123 -
124 - - Builder VM persists across sessions: `ps -p $(cat .vms/builder.pid)` or `ssh -p 2222 ... true`. Restart: `./tools/qemu/vm-up.sh`.
125 - - `vm-build.sh` does rsync + build.sh + pull. `build.sh` now auto-nukes `.work/apkovl_*` and `.work/image-*` so apkovl-src edits propagate without manual cache stomping.
126 - - Full nuke (rare; restores ~30 min of TCG kernel/modloop build): `ssh -p 2222 ... 'rm -rf /home/build/mountaineer-build/.work'`.
127 - - VM install test cycle: `./tools/qemu/vm-test-install.sh --daemonize` → `ssh-keygen -R [127.0.0.1]:2223 -f .vms/known_hosts` → wait for port → `ssh ... 'cat > /usr/local/sbin/mountaineer-install' < installer/install.sh` (live ISO has no scp/sftp) → `ssh ... mountaineer-install --config /tmp/install.yaml`.
128 - - After install: standalone boot of installed disk uses same OVMF setup, `-drive file=.vms/disks/target.qcow2,if=virtio,format=qcow2`, no `-cdrom`, port-forward 2224. See `../build/build_notes.md` for the full incantation.
129 -
130 - ### Package availability — v3.23 main+community (verified, don't re-test)
131 -
132 - - [x] **IN**: nebula 1.9.7, caddy 2.11.2, helix 25.07.1, zellij 0.42.2, nushell 0.104.1, podman 5.7.0, s6 2.13, s6-rc 0.5.6, execline 2.9.7, ifupdown-ng 0.12.1, iwd 3.10, msmtp 1.8.32, restic 0.18.1, age 1.2.1, sops 3.11.0, chrony 4.8, unbound 1.24.2, nftables 1.1.5, all userland (rg, fd, sd, bat, eza, dust, procs, bottom, zoxide, lnav, starship), `zfs` + `zfs-lts`
133 - - [x] **NOT IN ANY repo** (main, community, edge/testing checked): zfsbootmenu, vector
134 - - [x] **Action**: vendor zfsbootmenu — installer fetches v3.1.0 release EFI from `github.com/zbm-dev/zfsbootmenu/releases` (sha256-pinned)
135 - - [ ] **Action**: vendor vector from `vector.dev/releases` (static binary) — Mountaineer overlay candidate
136 - - [ ] **Future**: ZBM version pump discipline — update `ZBM_VERSION`+`ZBM_EFI_SHA256` in `installer/install.sh` from upstream `sha256.txt`; pin the linux6.x variant matching the running kernel's major.minor
137 -
138 - ### Open install-mechanics decisions
139 -
140 - - [x] Where the installer's helper binary lives — MVP: shell-only; ratatui v0.2 goal
141 - - [ ] Kernel/zfs-lts pairing discipline at upgrade time
142 - - [ ] ESP redundancy on multi-disk hosts (out of v0.1 scope; layout shouldn't block it)
143 - - [ ] Installer log routing during install (tmpfs only, serial console, post-install capture)
144 - - [ ] Non-interactive `--config` schema for unattended installs — basic version implemented; need to formalize
145 - - [ ] Signing keys: ISO, apkovl, and overlay-repository APKs — three keys probably, lifecycle TBD
146 - - [x] zfsbootmenu install path in installer step 12 — pinned v3.1.0 release EFI, sha256-verified, written to `/EFI/BOOT/BOOTX64.EFI`. Boot entry registration via `efibootmgr` is best-effort (fallback works without it on hardware whose firmware respects the default removable-media path).
147 - - [ ] mkimg bootloader name — `boot_efi="grub-efi"` works; verify token names hold across aports updates
148 - - [ ] OpenRC vs s6-rc on the live ISO — installed system is s6-rc; live ISO inherits Alpine's default (OpenRC) for MVP; v0.2 cleanup
149 -
150 - ### v0.1 install dogfood gate (8 criteria in INSTALL.md)
151 -
152 - - [x] ISO builds (660 MB produced)
153 - - [x] ISO boots in UEFI QEMU
154 - - [ ] Reproducible ISO builds (same source + same builder → same bytes)
155 - - [ ] Boots on physical x86_64 hardware (Framework 13)
156 - - [x] Installer completes end-to-end on a clean disk in under 15 minutes (QEMU/TCG; faster on real hw)
157 - - [~] Installed system boots, comes up on network, accepts operator SSH key, passes first-boot route unmodified — *boots and listens on :22; banner exchange + pubkey-login flow still to verify*
158 - - [ ] Caddy + podman route works on fresh Server install
159 - - [ ] Nebula mesh route works between Server (lighthouse) and Sysadmin (laptop)
160 - - [ ] Restic restore drill route works
161 - - [ ] Reboot leaves the system in the same state (idempotent)
162 -
163 - ## `sysop` CLI
164 -
165 - - [x] Subcommands committed in spec: `sysop wifi`, `sysop net`, `sysop wg`, `sysop upgrade`, `sysop info <role>`, `sysop notifications mute`, `sysop service enable/disable <name>`, `sysop podman-s6`, `sysop motd on/off` (from AESTHETICS)
166 - - [x] ratatui-based; tool-agnostic backends per principle 14
167 - - [x] **`sysop id` decided as first-class identity subsystem** — replaces "`sysop ssh keys` for authorized_keys"; operators have plural identity (personal vs client work, multiple meshes, hardware-backed keys)
168 - - [x] **Scopes**: named bundles of operator-owned identity material (one for solo operator, many for contractor)
169 - - [x] **Per-host identity stays fixed slot**: one SSH host key per host (plus later one age recipient); host can be enrolled in multiple scopes
170 - - [x] **v1 scope: SSH only** — age and Nebula CA bindings deferred
171 - - [x] **Signing-as-a-service is v1 design requirement** — operations split: *use* (sign/decrypt/derive — works on all backends including hardware) vs *extract* (read bytes — best-effort, fails on hardware-backed scopes). v1 must model this even though it only ships software-backed scopes
172 - - [x] **SSH-specific shape decided**: `sysop id agent <scope>` returns ssh-agent socket, backends plug behind it (`file://`, `agent://`, `yubikey://` v2). Consumers set `SSH_AUTH_SOCK`. `sysop id sign` for non-ssh-agent cases. `sysop id export` is extract verb (always succeeds on `file://` in v1)
173 - - [ ] Final subcommand list spec'd (sketch: `sysop id list/show/new/import/rotate/authorize/revoke/agent/sign/export`, `sysop id host show/rotate`)
174 - - [ ] Reconcile route-assumed subcommands when sysop section opens
175 - - [ ] STACK.md: dedicated `sysop` section (currently buried in ifupdown-ng paragraph)
176 - - [ ] First-boot route step 6 re-scoped from "SSH keys" to "Identity"
177 -
178 - ### `sysop id` open design questions (v1, SSH-only)
179 -
180 - - [ ] Scope storage location: `~/.config/mountaineer/scopes/<name>/`? Backup/portability story?
181 - - [ ] Active-scope mechanism: shell env var, on-disk state file, per-command flag, or all three?
182 - - [ ] `~/.ssh/config` interaction: write `IdentityFile`/`Match` snippets, or stay out of operator's config?
183 - - [ ] SSH CA model: support operator-side SSH CAs (sign user certs, push to `TrustedUserCAKeys`)? Leaning out-of-scope for v1
184 - - [ ] Hardware key bootstrap: how does `sysop id import --yubikey` work — discover-and-claim, or operator-supplied handle?
185 - - [ ] Cross-host scope sync (two laptops): operator's problem (rsync/git) but needs defended recommendation in docs
186 -
187 - ### Per-subcommand implementation
188 -
189 - Status: MVP slice complete plus the follow-ups previously deferred. `sysop info` now ships version display + server/sysadmin profile filter. `sysop net` shows default-route + resolver liveness. `sysop wifi` auto-detects the interface, pre-scans, rescans on `r`, and has a `disconnect` verb. `sysop storage` has a pool detail screen (`zpool status -P` parsing) and a dataset view (`d`). All `--summary` outputs unchanged. Active: TBD — natural next is `sysop id` (large) or live-system verification on a Mountaineer install.
190 -
191 - **sysop-tui primitives**
192 -
193 - - [ ] Re-verify on a real Mountaineer system (foot terminal, Iosevka, flatwhite-configured 24-bit color). Host-side macOS eyeball passed; values may still need tuning once foot/helix/zellij/yambar ship the matching theme
194 - - [ ] `Footer` right-align option (deferred until a subcommand needs a left status segment)
195 - - [ ] Help overlay primitive (`?` action classifies; no overlay renderer yet)
196 -
197 - **sysop info**
198 -
199 - - [x] Version field on the detail screen — lazy per-tool `<bin> --version` lookup with per-session cache; populated for 18 of 30 roles via `version_cmd` in roles.toml
200 - - [x] Server vs Sysadmin profile filter — `--server` / `--sysadmin` flags filter via the `profile` field on Role (12 graphical roles tagged `sysadmin`)
201 - - [ ] Runtime override: layer `/etc/mountaineer/roles.toml` on top of the compiled-in defaults (principle 2 — operator can see/edit). Currently only `include_str!` source
202 - - [ ] Fill `version_cmd` for the remaining roles (busybox, s6, ifupdown-ng, iwd, cliphist, swaybg, swappy, seatd, tlp) — most need a tool-specific incantation
203 - - [ ] Replace synchronous Command::output with a bounded-timeout variant if any version_cmd is found to hang on a real Mountaineer system
204 -
205 - **sysop motd**
206 -
207 - - [x] `/etc/profile.d/sysop-motd.sh` glue script — calls `sysop motd print` on login (silent no-op if sysop missing). Lives in `../build/apkovl-src/common/`
208 - - [ ] Tip set worth revisiting once more `sysop` verbs exist — currently 6, drawn from AESTHETICS
209 - - [ ] Decide whether `sysop motd print` should respect a "first-login-of-the-day" suppression to avoid tip spam in tmux-heavy sessions
210 -
211 - **sysop services**
212 -
213 - - [ ] Detail view per service: dependencies, log path, time-in-state. Needs `s6-svstat` shell-out
214 - - [ ] Enable / disable / restart verbs. Requires real s6-rc; can't be meaningfully mocked, gate on live-system testing
215 - - [ ] Reflect transient state from `s6-rc-update` runs (currently only Up/Down are derived from `s6-rc -a list` + `-d list`)
216 - - [ ] Backend mismatch indicator — if mock is active on what looks like a Mountaineer system, surface that as a fault
217 -
218 - **sysop storage**
219 -
220 - - [x] Detail view: vdev tree, error counters (read/write/cksum), scrub progress — `zpool status -P <name>` parsing with indent-preserving vdev tree, scan/errors lines, per-row red error counts
221 - - [x] Dataset view: `zfs list -H -o name,used,avail,mountpoint` — `d` key from pool list opens it; `Esc` returns
222 - - [ ] Snapshot management — read-only first (list), write later
223 - - [ ] Scrub trigger verb. Same caveat as services enable/disable — gate on live system
224 -
225 - **sysop wifi**
226 -
227 - - [x] Pre-scan: `iwctl station <dev> scan` + 1.5s settle before `get-networks`; `r` rescans from the list view with "scanning…" indicator
228 - - [x] Auto-detect interface name from `iwctl device list` (mode=station), fall back to `wlan0`
229 - - [x] Disconnect verb — `sysop wifi disconnect`
230 - - [ ] Enterprise (8021x) flow — out of scope for MVP, currently a no-op on Enter; needs identity + cert/PEAP plumbing
231 - - [ ] iwctl `get-networks` text parsing is fragile; verify on a real Mountaineer install and adjust. D-Bus interface is the upgrade path if the text format proves too noisy
232 - - [ ] Promote the password input to a `sysop-tui` primitive once a second consumer exists (probably `sysop id` for passphrase entry)
233 -
234 - **sysop net**
235 -
236 - - [x] Default route / gateway display — `ip -json route show default` parsing, rendered as a header row above the interface list
237 - - [x] Resolver liveness check — 200 ms TCP probe to 127.0.0.1:53 (`reachable` / `unreachable`), rendered alongside default-route
238 - - [ ] SSID enrichment for wireless: read `/var/lib/iwd/` or `iwctl station <iface> show` to populate `ssid` on the real backend (mock already returns it)
239 - - [ ] Yambar `net` segment should reflect the active default-route interface, not just wired-first. Currently wired-first heuristic
240 - - [ ] Detail view: per-interface MAC, MTU, link speed (ethtool for wired)
241 -
242 - ### Deferred subcommands (post-MVP)
243 -
244 - - [ ] `sysop logs <service>` — lnav front
245 - - [ ] `sysop upgrade`, `sysop wg`, `sysop notifications mute`, `sysop podman-s6`
246 -
247 - ### sysop tooling deferred (post-MVP)
248 -
249 - - [ ] Tab completion (bash/zsh/nushell)
250 - - [ ] Man pages (mdoc; generated from clap?)
251 - - [ ] CI / reproducible builds story
252 - - [ ] Release artifact shape (static-linked musl binary in Alpine apk)
253 -
254 - ### `sysop id` deferred (decided but not v1)
255 -
256 - - [ ] **Age slot in scopes** + per-host age recipient. Mesh route assumes age-encrypted bundles addressed to per-host recipients; v1-compatible variant needed (operator-decrypted-locally or scp over existing ssh path)
257 - - [ ] **Nebula CA slot in scopes**. v1: mesh CA management is its own thing under `~/.config/mountaineer/nebula-ca/`, age-encrypted independently. Reunified when scope model is mature
258 - - [ ] **Multi-backend broker design** (URI-shaped refs to ssh-agent, yubikey/PKCS#11, Bitwarden/`bw`, pass/gopass, native `file://`). Constraints agreed: native is default, supported set caps at five backends, proprietary `op` is documented-not-supported, per-backend capability matrix in docs. v1 URI shape reserved in config.toml so v2 doesn't break format
259 -
260 - ### Nebula external-signer gap (record for when Nebula CA slot ships)
261 -
262 - - [ ] `nebula-cert sign` upstream takes CA key as file; no signer-plugin interface today. When scopes back Nebula CAs: (a) materialize CA key to tmpfs briefly (OK for software, not for hardware), (b) upstream a signer interface, (c) ship Mountaineer-patched `nebula-cert`. Raise with upstream before painting ourselves in
263 -
264 - ## podman-s6 integration
265 -
266 - - [ ] Service-template library: quadlet analog for s6-rc
267 - - [ ] Restart policy, dependency declarations, log routing into Vector
268 - - [ ] One canonical example (Caddy container) checked in
269 -
270 - ## v0.1 release dogfood gate
271 -
272 - - [ ] Mountaineer project infra runs on s6-rc before v0.1 ships
273 - - [ ] Decide what infra: build server, docs site, package mirror
274 - - [ ] Decide where the infra lives, who pays for it
275 -
276 - ## UX / aesthetics
277 -
278 - - [x] Lives in `AESTHETICS.md` with source material at `references/classic-high-affordance.md`
279 - - [x] First pass: open questions turned into defended picks (palette, font, ratatui, yambar, river, swaylock, TTY)
280 - - [x] Doctrine: surfaces (bar, lock screen, motd, TTY) display; `sysop` operates. Every non-ok state visible on a surface has a `sysop <role>` invocation.
281 -
282 - ### Implementation follow-ups
283 -
284 - - [x] `sysop-tui` crate: frame, footer keybind renderer, flatwhite color constants, selection widget, reserved globals — lives in `../sysop/crates/sysop-tui/`
285 - - [x] `sysop info <role>` — first subcommand, role→tool registry shipped (33 roles in `../sysop/crates/sysop/data/roles.toml`)
286 - - [x] `sysop motd on/off/print` — opt-out tip rotation, time-bucketed; config at `/etc/mountaineer/sysop.toml` (`$SYSOP_CONFIG` override for host dev)
287 - - [x] `sysop services` (read-only) — s6-rc backend with mock fallback for host dev; `--summary` emits `ok`/`N down` for the yambar `svc` segment to consume. Enable/disable verbs deferred to live-system phase
288 - - [x] `sysop storage` — zfs backend with mock fallback; pool list + detail (vdev tree, error counters, scrub line) + dataset view (`d` from pool list). `--summary` emits `ok`/`degraded` for the yambar `zfs` segment. Scrub trigger and snapshot ops deferred
289 - - [x] `sysop wifi` — iwctl backend with mock fallback. Auto-iface, pre-scan, rescan, disconnect, selection + masked password + connect. Enterprise (8021x) deferred
290 - - [x] `sysop net` — iproute2 backend with mock fallback. Default-route + resolver liveness header rows; interface list with state + addresses + SSID; `--summary` for the yambar `net` segment. SSID enrichment, detail view deferred
291 - - [x] Overlay configs in flatwhite + Iosevka: foot, helix (theme), zellij (theme), fuzzel, fnott, swaylock — `../build/apkovl-src/sysadmin/`
292 - - [x] yambar config: tag + clock + bat + net + vol + svc + zfs segments; svc/zfs/net feed scripts at `usr/local/libexec/yambar/{svc,zfs,net}.sh` calling `sysop <subcmd> --summary`. Conditional `vpn`/`dnd` still to add
293 - - [x] `/etc/issue` content set to spec form (`Mountaineer 3.23 (Dull Edge) · \n · \l`); version literal needs a bump when Alpine base moves
294 - - [x] `/etc/profile.d/sysop-motd.sh` — shim calling `sysop motd print`; silent no-op if sysop missing
295 - - [ ] river init: minimal active bind set + commented-available blocks from upstream example — apkovl-src path TBD
296 - - [ ] Activate the `sysadmin` manifest profile (`sources = ["common", "live", "sysadmin"]`, separate `world` file) so the new overlay tree actually ships
297 - - [ ] `/etc/mountaineer/sysop.toml` seed with `[motd] enabled = true|false` written by installer based on Y/n prompt
298 - - [ ] Installer prompt: "Show a `sysop` tip on each login? [Y/n]" → writes motd-enable choice into the seed file above
299 - - [ ] Conditional yambar segments: `vpn wg0` and `dnd` (notifications muted) — need state probes
300 - - [ ] Version-pump discipline for `/etc/issue` — emit from build.sh into apkovl-src as a generated file, or accept hand-bump per Alpine release
301 -
302 - ## Project / community
303 -
304 - - [ ] Contribution workflow: mailing list vs GitHub — undecided
305 - - [ ] License pick for the overlay + scripts
306 - - [ ] Where the project itself is hosted (sourcehut / Codeberg / self-hosted Forgejo)